This commit is contained in:
Vassshhh
2025-09-06 13:37:37 +07:00
parent 222169be74
commit 5f75b8658a
7 changed files with 361 additions and 237 deletions

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import dayjs from "dayjs";
import Chart from "react-apexcharts"; import Chart from "react-apexcharts";
import styles from "./BarChart.module.css"; // Import the CSS module import styles from "./BarChart.module.css"; // Import the CSS module
@@ -85,7 +86,7 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
} }
let totalValue = seriesData.reduce((acc, val) => acc + val, 0); let totalValue = seriesData.reduce((acc, val) => acc + val, 0);
return { return {
date: new Date(dayData.date).toLocaleDateString(), date: dayData.date,
categories, categories,
series: [ series: [
{ {
@@ -113,15 +114,11 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
} }
const formatDate = (dateString) => { const formatDate = (dateString) => {
const date = new Date(dateString); const d = dayjs(dateString, ["YYYY-MM-DD", "YYYY-MM-DDTHH:mm:ssZ"]);
const monthNames = [ return { month: d.format("MMM"), day: d.format("D") };
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
];
const month = monthNames[date.getMonth()];
const day = date.getDate();
return { month, day };
}; };
return ( return (
<div className={`${styles.chartItemContainer} ${selectedIndex !== -1 ? styles.expanded : ''}`}> <div className={`${styles.chartItemContainer} ${selectedIndex !== -1 ? styles.expanded : ''}`}>
@@ -142,27 +139,37 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
key={indexx} key={indexx}
className={`${styles.dateSelector} ${index === indexx ? styles.dateSelectorActive : styles.dateSelectorInactive className={`${styles.dateSelector} ${index === indexx ? styles.dateSelectorActive : styles.dateSelectorInactive
}`} }`}
style={{ position: 'relative' }} style={{ position: 'relative', width: 'calc(100% / 7)' }}
onClick={() => onClick={() =>
type == 'yesterday' && selectedIndex == -1 || type != 'yesterday' && selectedIndex !== index ? setSelectedIndex(index) : setSelectedIndex(-1) type == 'yesterday' && selectedIndex == -1 || type != 'yesterday' && selectedIndex !== index ? setSelectedIndex(index) : setSelectedIndex(-1)
} }
> >
<div style={{ position: 'absolute', bottom: '28px', left: '10%', right: '10%', borderBottom: index == indexx ? `2px solid ${colors[index % colors.length]}` : 'none' }}></div> <div style={{ position: 'absolute', bottom: '21px', left: '10%', right: '10%', borderBottom: index == indexx ? `2px solid ${colors[index % colors.length]}` : 'none' }}></div>
<div <div
style={{ color: index === indexx ? 'black' : 'transparent' }}> style={{ color: index === indexx ? 'black' : 'transparent' }}>
{indexx !== chartData.length - 1 ? ( {indexx !== chartData.length - 1 ? (
<> <p style={{ fontSize: '13px' }}>{day}{" "}
{day}{" "} {(
{(indexx === 0 || (formatDate(chartData[indexx - 1].date).month !== month && type != 'weekly')) && month} indexx === 0 ||
</> (indexx > 0 &&
dayjs(chartData[indexx - 1].date).month() !== dayjs(item.date).month() &&
type !== "weekly")
) && month}
</p>
) : ( ) : (
<> <p style={{ fontSize: '13px' }}>
{type != 'weekly' ? 'Hari ini' : day} {type != 'weekly' ? 'Hari ini' : day + ' ' + month}
</> </p>
)} )}
</div> </div>
{index == indexx && <p style={{ margin: '7px 0 0 0', fontSize: '12px', color: 'black' }}> {index == indexx && <p style={{
margin: '7px 0 0 0', fontSize: '9px', color: 'black',
position: 'absolute',
width: '100%',
left: 0,
bottom: 6
}}>
{graphFilter === 'transactions' {graphFilter === 'transactions'
? chartData[indexx].totalValue ? chartData[indexx].totalValue
: formatRupiah(chartData[indexx].totalValue)} : formatRupiah(chartData[indexx].totalValue)}

View File

@@ -252,10 +252,9 @@ export const handlePaymentFromClerk = async (
); );
if (response.ok) { if (response.ok) {
// Handle success response const data = await response.json();
console.log("Transaction successful!"); console.log("Transaction successful!", data);
// Optionally return response data or handle further actions upon success return data;
return true;
} else { } else {
// Handle error response // Handle error response
console.error("Transaction failed:", response.statusText); console.error("Transaction failed:", response.statusText);

View File

@@ -103,6 +103,17 @@ function CafePage({
// }; // };
const [isTablet, setIsTablet] = useState(window.innerWidth >= 768); const [isTablet, setIsTablet] = useState(window.innerWidth >= 768);
const [isFullscreen, setIsFullscreen] = useState(!!document.fullscreenElement);
useEffect(() => {
function fullscreenChangeHandler() {
setIsFullscreen(!!document.fullscreenElement);
}
document.addEventListener("fullscreenchange", fullscreenChangeHandler);
return () => document.removeEventListener("fullscreenchange", fullscreenChangeHandler);
}, []);
useEffect(() => { useEffect(() => {
const handleResize = () => { const handleResize = () => {
setIsTablet(window.innerWidth >= 768); setIsTablet(window.innerWidth >= 768);
@@ -259,6 +270,70 @@ function CafePage({
} }
} }
}; };
const FullscreenButton = ({ onClick }) => {
return (
<div
onClick={onClick}
style={{
width: 40,
height: 40,
borderRadius: "50%",
backgroundColor: "#7272729e",
display: "flex",
justifyContent: "center",
alignItems: "center",
cursor: "pointer",
userSelect: "none",
position: 'fixed',
top: 0,
left: 0,
zIndex: 1000
}}
title="Toggle Fullscreen"
>
<span
style={{
display: "inline-block",
transform: "rotate(45deg)",
color: "white",
fontWeight: "bold",
fontSize: 24,
lineHeight: 1,
userSelect: "none",
}}
>
&lt;&gt;
</span>
</div>
);
};
const handleFullscreen = () => {
const elem = document.documentElement; // fullscreen seluruh halaman
if (!document.fullscreenElement) {
// masuk fullscreen
if (elem.requestFullscreen) {
elem.requestFullscreen();
} else if (elem.mozRequestFullScreen) { /* Firefox */
elem.mozRequestFullScreen();
} else if (elem.webkitRequestFullscreen) { /* Chrome, Safari & Opera */
elem.webkitRequestFullscreen();
} else if (elem.msRequestFullscreen) { /* IE/Edge */
elem.msRequestFullscreen();
}
} else {
// keluar fullscreen
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) { /* Firefox */
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) { /* Chrome, Safari and Opera */
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) { /* IE/Edge */
document.msExitFullscreen();
}
}
};
if (loading) if (loading)
return ( return (
@@ -297,6 +372,7 @@ function CafePage({
)} )}
<div style={{ width: isTablet ? "60%" : "100%" }}> <div style={{ width: isTablet ? "60%" : "100%" }}>
<div className="App-header"> <div className="App-header">
{isTablet && !isFullscreen && <FullscreenButton onClick={handleFullscreen} />}
<Header <Header
HeaderText={"Menu"} HeaderText={"Menu"}
showProfile={true} showProfile={true}

View File

@@ -275,6 +275,75 @@ export default function Invoice({
); );
} }
}; };
const handlePrint = (transaction) => {
console.log(transaction)
const formatWaktu = (() => {
const date = transaction?.createdAt
? new Date(transaction.createdAt)
: new Date(); // UTC now
const tanggal = date.toLocaleDateString("id-ID");
const jam = date.toLocaleTimeString("id-ID", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
return `${tanggal} ${jam}`;
})();
const itemsStr = transaction.DetailedTransactions.map((dt) => {
const name =
dt.Item.name.length > 11
? dt.Item.name.slice(0, 11)
: dt.Item.name.padEnd(11);
const qty = dt.qty.toString().padStart(3);
const total = formatRupiah(dt.qty * (dt.promoPrice || dt.price)).padStart(15);
return `${name} ${qty} ${total}`;
}).join("\n");
const totalHarga = transaction.DetailedTransactions.reduce((acc, dt) => {
return acc + dt.qty * (dt.promoPrice || dt.price);
}, 0);
const totalStr = `Total: ${formatRupiah(totalHarga)}`;
const receiptText = ` CAFE HOREE
Jl. Ahmad Yani No. 12, Kediri
Telp: 0812-1617-6963
==============================
Tanggal : ${formatWaktu}
Bayar : ${transaction.payment_type}
------------------------------
Item Qty Total
------------------------------
${itemsStr}
${totalStr}
==============================
Terima kasih atas kunjungannya!
~~
supported by kedaimaster.com
\n\n\n\n\n`;
const params = new URLSearchParams();
params.append("content", receiptText);
params.append("encode_format", "UTF-8");
const printUrl = `btprinter://print?${params.toString()}`;
window.location.href = printUrl;
};
const formatRupiah = (value) => {
if (typeof value !== "number") return value;
return value.toLocaleString("id-ID", {
style: "currency",
currency: "IDR",
minimumFractionDigits: 0,
});
};
const handlePay = async (orderMethod) => { const handlePay = async (orderMethod) => {
setIsPaymentLoading(true); setIsPaymentLoading(true);
@@ -300,9 +369,16 @@ export default function Invoice({
tableNumber, tableNumber,
textareaRef.current.value textareaRef.current.value
); );
if (pay) window.location.reload(); if (pay) {
handlePrint(pay.transaction);
} else if (deviceType == "guestSide") { localStorage.removeItem("cart");
localStorage.removeItem("lastTransaction");
setCartItems([]);
setTotalPrice(0);
window.dispatchEvent(new Event("localStorageUpdated"));
}
}
else if (deviceType == "guestSide") {
const pay = await handlePaymentFromGuestSide( const pay = await handlePaymentFromGuestSide(
shopId, shopId,
email, email,

View File

@@ -1,13 +1,12 @@
import React, { useState, useMemo } from 'react'; import React, { useState, useMemo } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import qs from 'qs'; import qs from 'qs';
import '../print.css'; // Kamu bisa pakai styling temanmu atau buat file baru import '../print.css';
export default function PrintPage() { export default function PrintPage() {
const location = useLocation(); const location = useLocation();
const [orientation, setOrientation] = useState('portrait'); const [orientation, setOrientation] = useState('portrait');
// Parse data dari query string
const data = useMemo(() => { const data = useMemo(() => {
try { try {
const query = qs.parse(location.search, { ignoreQueryPrefix: true }); const query = qs.parse(location.search, { ignoreQueryPrefix: true });
@@ -19,8 +18,57 @@ export default function PrintPage() {
if (!data) return <div>Invalid data</div>; if (!data) return <div>Invalid data</div>;
const handlePrint = () => { const formatWaktu = (() => {
window.print(); const date = new Date(data.date);
const tanggal = date.toLocaleDateString('id-ID');
const jam = date.toLocaleTimeString('id-ID', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
});
return `${tanggal} ${jam}`;
})();
const itemsStr = data.items.map((item) => {
const name = item.name.length > 11 ? item.name.slice(0, 11) : item.name.padEnd(11);
const qty = item.qty.toString().padStart(2);
const total = (item.qty * item.price).toString().padStart(6);
return `${name} ${qty} ${total}`;
}).join('\n');
const getReceiptText = () => {
return (
` CAFE HOREE
Jl. Merdeka No. 123, Jakarta
Telp: (021) 12345678
==============================
Tanggal : ${formatWaktu}
Kasir : ${data.cashier || 'UNKNOWN'}
Bayar : ${data.payment_type}
------------------------------
Item Q Total
------------------------------
${itemsStr}
------------------------------
Terima kasih atas kunjungan Anda!
~ Cafe Horee ~
www.kedaimaster.com`
);
};
const handlePrintBluetooth = () => {
const content = getReceiptText();
const params = new URLSearchParams();
params.append("content", content);
params.append("encode_format", "UTF-8");
// Optional: jika ingin spesifik printer Bluetooth
// params.append("device_address", "00:11:22:33:44:55");
const printUrl = `btprinter://print?${params.toString()}`;
window.location.href = printUrl;
}; };
return ( return (
@@ -41,85 +89,14 @@ export default function PrintPage() {
Landscape Landscape
</button> </button>
</div> </div>
<button className="print-button" onClick={handlePrint}> <button className="print-button" onClick={handlePrintBluetooth}>
Cetak Struk ({orientation}) 🖨 Print ke Bluetooth
</button> </button>
</div> </div>
<div className="print-area"> <pre className="print-area">
<h2>Struk Pembayaran</h2> {getReceiptText()}
<p><strong>Transaction ID: </pre>
</strong> {data.transactionId}</p>
<p><strong>Waktu:</strong> {
(() => {
const date = new Date(data.date);
const options = { day: '2-digit', month: 'long', year: 'numeric' };
const tanggal = date.toLocaleDateString('id-ID', options);
const jam = date.toLocaleTimeString('id-ID', {
hour: '2-digit',
minute: '2-digit',
hour12: false
});
return `${tanggal}, ${jam}`;
})()
}</p>
{/* <p><strong>Table:
</strong> {data.table}</p> */}
<p><strong>Metode Pembayaran:
</strong> {data.payment_type}</p>
<div>
{data.items.map((item, idx) => (
<p key={idx} style={{ marginBottom: '12px' }}>
<div>{item.name} x {item.qty} Rp{item.price.toLocaleString('id-ID')}</div>
</p>
))}
</div>
<h3>Total: Rp{data.total.toLocaleString('id-ID')}</h3>
<div
style={{
marginTop: '24px',
fontStyle: 'italic',
fontSize: '12px',
lineHeight: '1.5',
}}
>
<p style={{ margin: 0 }}>Terima kasih atas kunjungan Anda!</p>
</div>
<div
style={{
fontStyle: 'italic',
fontSize: '12px',
lineHeight: '1.5',
textAlign: 'center'
}}
>
<strong style={{
marginLeft: '-60px'
}}>~ Cafe Horee ~</strong>
</div>
<div
style={{
marginTop: '24px',
fontStyle: 'italic',
fontSize: '12px',
lineHeight: '1.5',
textAlign: 'center'
}}
>
</div>
<p style={{
fontSize: '9px',
marginBottom: '30px',
marginLeft: '60px',
}}>www.kedaimaster.com</p>
</div>
</div> </div>
); );
} }

View File

@@ -62,6 +62,12 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
return total + dt.qty * (dt.promoPrice ? dt.promoPrice : dt.price); return total + dt.qty * (dt.promoPrice ? dt.promoPrice : dt.price);
}, 0); }, 0);
}; };
const formatRupiah = (number) => {
return 'Rp' + number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.');
};
const calculateAllTransactionsTotal = (transactions) => { const calculateAllTransactionsTotal = (transactions) => {
return transactions return transactions
.filter(transaction => transaction.confirmed > 1) // Filter transactions where confirmed > 1 .filter(transaction => transaction.confirmed > 1) // Filter transactions where confirmed > 1
@@ -147,26 +153,63 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
}; };
const handlePrint = (transaction) => { const handlePrint = (transaction) => {
// Pilih data yang ingin dikirim const formatWaktu = (() => {
const printableData = { const date = new Date(transaction.createdAt);
transactionId: transaction?.transactionId, const tanggal = date.toLocaleDateString('id-ID');
items: transaction?.DetailedTransactions.map(dt => ({ const jam = date.toLocaleTimeString('id-ID', {
name: dt.Item.name, hour: '2-digit',
qty: dt.qty, minute: '2-digit',
price: dt.promoPrice || dt.price, hour12: false,
})), });
total: calculateTotalPrice(transaction.DetailedTransactions), return `${tanggal} ${jam}`;
date: transaction.createdAt, })();
payment_type: transaction.payment_type,
table: transaction.Table?.tableNo || "N/A", const itemsStr = transaction.DetailedTransactions.map((dt) => {
const name = dt.Item.name.length > 11
? dt.Item.name.slice(0, 11)
: dt.Item.name.padEnd(11);
const qty = dt.qty.toString().padStart(3);
const total = formatRupiah(dt.qty * (dt.promoPrice || dt.price)).padStart(15);
return `${name} ${qty} ${total}`;
}).join('\n');
const totalHarga = calculateTotalPrice(transaction.DetailedTransactions);
const totalStr = `Total: ${formatRupiah(totalHarga)}`;
const receiptText = (
` CAFE HOREE
Jl. Ahmad Yani No. 12, Kediri
Telp: 0812-1617-6963
==============================
Tanggal : ${formatWaktu}
Bayar : ${transaction.payment_type}
------------------------------
Item Qty Total
------------------------------
${itemsStr}
${totalStr}
==============================
Terima kasih atas kunjungannya!
~~
supported by kedaimaster.com
\n\n\n\n\n`
);
const params = new URLSearchParams();
params.append("content", receiptText);
params.append("encode_format", "UTF-8");
const printUrl = `btprinter://print?${params.toString()}`;
// Trigger aplikasi printer via URL scheme
window.location.href = printUrl;
}; };
// Serialize to query string
const queryString = qs.stringify({ data: JSON.stringify(printableData) });
// Navigate to /print with query string
navigate(`/${shopIdentifier}/print?${queryString}`);
};
if (loading) if (loading)
return ( return (
<div className="Loader"> <div className="Loader">
@@ -179,7 +222,7 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
return ( return (
<div className={styles.Transactions}> <div className={styles.Transactions}>
<h2 className={styles["Transactions-title"]}> <h2 className={styles["Transactions-title"]}>
Transaksi selesai Rp {calculateAllTransactionsTotal(transactions)} Transaksi selesai {formatRupiah(calculateAllTransactionsTotal(transactions))}
</h2> </h2>
<input <input
@@ -208,7 +251,7 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
</ul> </ul>
<div className={styles.TotalContainer}> <div className={styles.TotalContainer}>
<span>Total:</span> <span>Total:</span>
<span>Rp {item.totalPrice}</span> <span>{formatRupiah(item.totalPrice)}</span>
</div> </div>
</div> </div>
))} ))}
@@ -329,8 +372,11 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
<ul> <ul>
{transaction.DetailedTransactions.map((detail) => ( {transaction.DetailedTransactions.map((detail) => (
<li key={detail.detailedTransactionId}> <li key={detail.detailedTransactionId}>
<span>{detail.Item.name}</span> - {detail.qty < 1 ? 'tidak tersedia' : `${detail.qty} x Rp <span>{detail.Item.name}</span> - {detail.qty < 1
${detail.promoPrice ? detail.promoPrice : detail.price}`} ? 'tidak tersedia'
: `${detail.qty} x ${formatRupiah(detail.promoPrice || detail.price)}`
}
</li> </li>
))} ))}
</ul> </ul>
@@ -375,7 +421,7 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
<div className={styles.TotalContainer}> <div className={styles.TotalContainer}>
<span>Total:</span> <span>Total:</span>
<span> <span>
Rp {calculateTotalPrice(transaction.DetailedTransactions)} {formatRupiah(calculateTotalPrice(transaction.DetailedTransactions))}
</span> </span>
</div> </div>
@@ -415,9 +461,7 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
{deviceType == 'guestDevice' && transaction.confirmed < 2 && transaction.payment_type != 'cash' && transaction.payment_type != 'paylater/cash' && {deviceType == 'guestDevice' && transaction.confirmed < 2 && transaction.payment_type != 'cash' && transaction.payment_type != 'paylater/cash' &&
<ButtonWithReplica <ButtonWithReplica
paymentUrl={paymentUrl} paymentUrl={paymentUrl}
price={ price={formatRupiah(calculateTotalPrice(transaction.DetailedTransactions))}
"Rp" + calculateTotalPrice(transaction.DetailedTransactions)
}
disabled={isPaymentLoading} disabled={isPaymentLoading}
isPaymentLoading={isPaymentLoading} isPaymentLoading={isPaymentLoading}
handleClick={() => handleConfirm(transaction.transactionId)} handleClick={() => handleConfirm(transaction.transactionId)}

View File

@@ -3,13 +3,12 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 20px; padding: 20px;
min-height: 100vh;
background-color: #f5f5f5; background-color: #f5f5f5;
font-family: Arial, sans-serif; font-family: monospace;
} }
.controls { .controls {
background-color: #2c3e50; background-color: #333;
color: white; color: white;
padding: 20px; padding: 20px;
border-radius: 10px; border-radius: 10px;
@@ -19,121 +18,67 @@
margin-bottom: 30px; margin-bottom: 30px;
} }
.controls h1 { .orientation-selector button,
margin-top: 0; .print-button {
} margin: 10px;
padding: 10px 20px;
.orientation-selector {
margin: 15px 0;
}
.orientation-selector button {
background-color: #61dafb;
color: #282c34;
border: none;
padding: 12px 24px;
font-size: 18px;
cursor: pointer;
transition: all 0.3s;
margin: 0 10px;
border-radius: 5px;
font-weight: bold; font-weight: bold;
cursor: pointer;
border: none;
border-radius: 5px;
} }
.orientation-selector button.active { .orientation-selector button.active {
background-color: #21a9c7; background-color: #007bff;
color: white; color: white;
transform: scale(1.05);
}
.orientation-selector button:hover:not(.active) {
background-color: #4bc5e0;
}
.print-button {
background-color: #3498db;
color: white;
border: none;
padding: 12px 24px;
font-size: 18px;
border-radius: 5px;
cursor: pointer;
margin-top: 15px;
transition: background-color 0.3s;
}
.print-button:hover {
background-color: #2980b9;
} }
.print-area { .print-area {
background-color: white; background: white;
padding: 30px; white-space: pre-wrap;
border-radius: 10px; font-family: monospace;
box-shadow: 0 4px 8px rgba(0,0,0,0.1); font-size: 12px;
width: 90%; line-height: 1.4;
max-width: 800px; padding: 10px;
width: 48mm;
max-width: 48mm;
color: black; color: black;
box-shadow: none;
border: 1px solid #ccc;
} }
/* Orientation styles */ /* Print media query */
.print-test.portrait .print-area {
max-width: 800px;
}
.print-test.landscape .print-area {
max-width: 1100px;
}
/* Print specific styles */
@media print { @media print {
@page {
margin: 58mm;
}
.print-test.portrait @page {
size: portrait;
}
.print-test.landscape @page {
size: landscape;
}
body { body {
background-color: white; background-color: white;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
.controls { .controls {
display: none; display: none;
} }
.print-area { .print-area {
box-shadow: none;
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
padding: 15mm; border: none;
box-shadow: none;
padding: 0;
font-size: 12px;
} }
body * { body * {
visibility: hidden; visibility: hidden;
} }
.print-area, .print-area * { .print-area, .print-area * {
visibility: visible; visibility: visible;
} }
.print-area { .print-area {
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
width: 100%;
height: 100%;
} }
.print-area {
font-size: 12px; /* lebih kecil dari default */
line-height: 1.4;
}
.print-area h2 {
font-size: 16px;
}
.print-area h3 {
font-size: 14px;
}
} }