This commit is contained in:
Vassshhh
2025-08-27 21:27:08 +07:00
parent 4fa272875f
commit dcf0455772
12 changed files with 219 additions and 101 deletions

60
package-lock.json generated
View File

@@ -18,7 +18,6 @@
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"html-to-image": "^1.11.11",
"html2canvas": "^1.4.1",
"jsqr": "^1.4.0",
"qr-scanner": "^1.4.2",
"qrcode.react": "^3.1.0",
@@ -32,6 +31,7 @@
"react-router-dom": "^6.24.0",
"react-scripts": "5.0.1",
"react-switch": "^7.0.0",
"react-to-print": "^3.1.1",
"react-youtube": "^10.1.0",
"smooth-scroll-into-view-if-needed": "^2.0.2",
"socket.io-client": "^4.7.5",
@@ -6951,15 +6951,6 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/batch": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
@@ -7874,15 +7865,6 @@
"postcss": "^8.4"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/css-loader": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz",
@@ -11155,19 +11137,6 @@
}
}
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/htmlparser2": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
@@ -18206,6 +18175,15 @@
"react-dom": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-to-print": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/react-to-print/-/react-to-print-3.1.1.tgz",
"integrity": "sha512-N0MUMhpl8nkGri13BjP7zusj3B/j+1eMOTt8N8PYuhBYGzA4PqTXqcihJ9cZw996dvhV6mBdwafIQCg3Ap5bKg==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ~19"
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
@@ -20504,15 +20482,6 @@
"node": ">=8"
}
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -21055,15 +21024,6 @@
"node": ">= 0.4.0"
}
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",

View File

@@ -14,7 +14,6 @@
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"html-to-image": "^1.11.11",
"html2canvas": "^1.4.1",
"jsqr": "^1.4.0",
"qr-scanner": "^1.4.2",
"qrcode.react": "^3.1.0",
@@ -28,6 +27,7 @@
"react-router-dom": "^6.24.0",
"react-scripts": "5.0.1",
"react-switch": "^7.0.0",
"react-to-print": "^3.1.1",
"react-youtube": "^10.1.0",
"smooth-scroll-into-view-if-needed": "^2.0.2",
"socket.io-client": "^4.7.5",

View File

@@ -377,7 +377,7 @@ function App() {
setGuestSides(connectedGuestSides.sessionDatas);
console.log("getting guest side");
setDeviceType("clerk");
checkNotifications();
// checkNotifications();
} else {
setDeviceType("guestDevice");
}

View File

@@ -29,10 +29,51 @@ const Item = ({
const [itemDescription, setItemDescription] = useState(initialDescription);
const fileInputRef = useRef(null);
const formatToRupiah = (value) => {
if (typeof value !== "number") return value;
return value.toLocaleString("id-ID");
// const stringValue = (value || '').toString();
// // Ambil hanya angka
// const cleaned = stringValue.replace(/[^0-9]/g, '');
// // Pastikan cleaned bukan kosong
// if (!cleaned) return 'Rp0';
// // Ubah ke number dan kalikan 1000
// const number = Number(cleaned) * 1000;
// // Kalau gagal parsing, fallback
// if (isNaN(number)) return 'Rp0';
// // Format rupiah tanpa desimal
// return number.toLocaleString('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0,
// maximumFractionDigits: 0
// });
};
const rupiahFormat = (angka, prefix = "Rp. ") => {
let number_string = angka.toString().replace(/[^,\d]/g, '');
let split = number_string.split(',');
let sisa = split[0].length % 3;
let rupiah = split[0].substr(0, sisa);
let ribuan = split[0].substr(sisa).match(/\d{3}/gi);
// tambahkan titik jika yang di input sudah menjadi angka ribuan
if(ribuan){
let separator = sisa ? '.' : '';
rupiah += separator + ribuan.join('.');
}
rupiah = split[1] != undefined ? rupiah + ',' + split[1] : rupiah;
return rupiah ? prefix + rupiah : "";
}
useEffect(() => {
console.log(imageUrl);
console.log(selectedImage);
@@ -305,7 +346,9 @@ const formatToRupiah = (value) => {
))}
{forInvoice && (
<p className={styles.itemPriceInvoice}>Rp {itemQty * (promoPrice > 0? formatToRupiah(promoPrice) : formatToRupiah(itemPrice))}</p>
<p className={styles.itemPriceInvoice}>Rp {formatToRupiah(itemQty * (promoPrice > 0? promoPrice : itemPrice))}</p>
// <p className={styles.itemPriceInvoice}>{itemQty * (promoPrice > 0? rupiahFormat(promoPrice) : rupiahFormat(itemPrice))}</p>
)}
</div>
{forCart && (

View File

@@ -119,7 +119,7 @@
.TotalContainer {
display: flex;
justify-content: space-between;
width: 80vw;
/* width: 80vw; */
margin: 0 auto;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600;

View File

@@ -217,7 +217,8 @@ export const handlePaymentFromClerk = async (
user_email,
payment_type,
serving_type,
tableNo
tableNo,
notes
) => {
try {
const token = getLocalStorage("auth");
@@ -245,6 +246,7 @@ export const handlePaymentFromClerk = async (
serving_type,
tableNo,
transactions: structuredItems,
notes
}),
}
);

View File

@@ -429,7 +429,7 @@ function CafePage({
}
/>
))}
{!isEditMode && !isTablet && (user.username || cartItemsLength > 0) && (
{!isEditMode && (user.username || cartItemsLength > 0) && (
<div
style={{
marginTop: "10px",
@@ -442,7 +442,7 @@ function CafePage({
textAlign: "center",
}}
>
{(lastTransaction != null || cartItemsLength > 0) && (
{(!isTablet &&(lastTransaction != null || cartItemsLength > 0)) && (
<div
onClick={goToCart}
style={{

View File

@@ -22,6 +22,11 @@ import { getItemsByCafeId } from "../helpers/cartHelpers.js";
import Dropdown from "./Dropdown.js";
import { useNavigationHelpers } from "../helpers/navigationHelpers";
const formatToRupiah = (value) => {
if (typeof value !== "number") return value;
return value.toLocaleString("id-ID");
};
export default function Invoice({
shopId,
setModal,
@@ -292,8 +297,11 @@ export default function Invoice({
email,
orderMethod,
orderType,
tableNumber
tableNumber,
textareaRef.current.value
);
if (pay) window.location.reload();
} else if (deviceType == "guestSide") {
const pay = await handlePaymentFromGuestSide(
shopId,
@@ -302,6 +310,8 @@ export default function Invoice({
orderType,
tableNumber
);
if (pay) window.location.reload();
} else if (deviceType == "guestDevice") {
const socketId = socket.id;
const pay = await handlePaymentFromGuestDevice(
@@ -312,6 +322,7 @@ export default function Invoice({
textareaRef.current.value,
socketId
);
if (pay) window.location.reload();
}
console.log("transaction from " + deviceType + "success");
@@ -598,7 +609,7 @@ export default function Invoice({
<span>Pesan</span>
)}
<span>Rp{totalPrice}</span>
<span>Rp{formatToRupiah(totalPrice)}</span>
</div>
)}
</button>
@@ -654,7 +665,8 @@ export default function Invoice({
<span>
Rp
{transactionData.DetailedTransactions.reduce(
{formatToRupiah(
transactionData.DetailedTransactions.reduce(
(total, transaction) => {
return (
total +
@@ -665,6 +677,7 @@ export default function Invoice({
);
},
0
)
)}
</span>
</div>
@@ -694,7 +707,7 @@ export default function Invoice({
<span>Pesan</span>
)}
<span>Rp{totalPrice}</span>
<span>Rp{formatToRupiah(totalPrice)}</span>
</div>
)}
</button>

View File

@@ -69,7 +69,7 @@
.TotalContainer {
display: flex;
justify-content: space-between;
width: 80vw;
/* width: 80vw; */
margin: 0 auto;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600;
@@ -466,7 +466,7 @@
.TotalContainer {
display: flex;
justify-content: space-between;
width: 80vw;
/* width: 80vw; */
margin: 0 auto;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600;

View File

@@ -115,6 +115,9 @@ export default function Transactions({ propsShopId, sendParam, deviceType, handl
autoResizeTextArea(noteRef.current);
}
}, [transaction?.notes]);
const handlePrint = () => {
window.print();
};
return (
<div key={transactionRefreshKey} className={styles.Transaction}>
@@ -128,7 +131,6 @@ export default function Transactions({ propsShopId, sendParam, deviceType, handl
setSelectedTable(transaction.Table || { tableId: 0 })
}
>
<div className={styles['receipt-header']}>
<div className={styles['receipt-info']}>
<h3>{transaction.payment_type == 'cash' ? 'Tunai' : transaction.payment_type == 'cashless' ? 'Non tunai' : transaction.payment_type == 'paylater' ? 'Open bill' : 'Close bill'}</h3>
@@ -311,9 +313,55 @@ export default function Transactions({ propsShopId, sendParam, deviceType, handl
batalkan
</h5>
)}
{transaction.confirmed > 1 && (
<h5
className={styles.DeclineButton}
onClick={() => handlePrint()}
>
Cetak struk
</h5>
)}
</div>
)}
</div>
{transaction &&
<div id="print-section" className={styles.printContainer}>
<div className="receipt">
<h2 className="center-text">Struk Pembayaran</h2>
<hr />
<p>ID Transaksi: {transaction.transactionId}</p>
<p>Metode: {transaction.payment_type === 'cash' ? 'Tunai' :
transaction.payment_type === 'cashless' ? 'Non Tunai' :
'Open Bill'}</p>
<p>Meja: {transaction.Table?.tableNo || '-'}</p>
<hr />
{transaction.DetailedTransactions.map((detail) => (
<div key={detail.detailedTransactionId} className="item-line">
<p>{detail.Item.name}</p>
<p>{detail.qty} x Rp{detail.promoPrice || detail.price}</p>
</div>
))}
<hr />
<p className="total">Total: Rp {calculateTotalPrice(transaction.DetailedTransactions)}</p>
{transaction.notes && (
<>
<hr />
<p>Catatan:</p>
<p>{transaction.notes}</p>
</>
)}
<hr />
<p className="center-text">Terima Kasih!</p>
</div>
</div>
}
</div>
);
}

View File

@@ -198,7 +198,7 @@ console.log(aggregatedItems.values())
<div className={styles['receipt-header']}>
{transaction.confirmed === 1 ? (
<ColorRing className={styles['receipt-logo']} />
) : transaction.confirmed === -1 && !transaction.is_paid || transaction.confirmed === -2 && !transaction.is_paid ? (
) : (transaction.confirmed == -1 && !transaction.is_paid) || (transaction.confirmed === -2 && !transaction.is_paid) ? (
<div style={{ display: 'flex', justifyContent: 'center', margin: '16px 0px' }}>
<svg
style={{ width: '60px', transform: 'Rotate(45deg)' }}

View File

@@ -396,3 +396,55 @@
color: white;
line-height: 27px;
}
/* Transactions.module.css */
.printContainer {
display: none; /* Hidden in normal view */
}
/* Print-specific styles */
@media print {
.Transaction {
display: none; /* Hide everything else when printing */
}
.printContainer {
display: block !important;
position: static;
background: white;
color: black;
font-family: 'Courier New', Courier, monospace;
padding: 20px;
}
.receipt {
width: 100%;
max-width: 400px;
margin: 0 auto;
}
.center-text {
text-align: center;
font-weight: bold;
}
.item-line {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.total {
text-align: right;
font-size: 1.2em;
font-weight: bold;
margin-top: 20px;
}
button, .DeclineButton, .addNewItem {
display: none !important;
}
}