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 dayjs from "dayjs";
import Chart from "react-apexcharts";
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);
return {
date: new Date(dayData.date).toLocaleDateString(),
date: dayData.date,
categories,
series: [
{
@@ -113,15 +114,11 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
}
const formatDate = (dateString) => {
const date = new Date(dateString);
const monthNames = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
];
const month = monthNames[date.getMonth()];
const day = date.getDate();
return { month, day };
const d = dayjs(dateString, ["YYYY-MM-DD", "YYYY-MM-DDTHH:mm:ssZ"]);
return { month: d.format("MMM"), day: d.format("D") };
};
return (
<div className={`${styles.chartItemContainer} ${selectedIndex !== -1 ? styles.expanded : ''}`}>
@@ -142,27 +139,37 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
key={indexx}
className={`${styles.dateSelector} ${index === indexx ? styles.dateSelectorActive : styles.dateSelectorInactive
}`}
style={{ position: 'relative' }}
style={{ position: 'relative', width: 'calc(100% / 7)' }}
onClick={() =>
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
style={{ color: index === indexx ? 'black' : 'transparent' }}>
{indexx !== chartData.length - 1 ? (
<>
{day}{" "}
{(indexx === 0 || (formatDate(chartData[indexx - 1].date).month !== month && type != 'weekly')) && month}
</>
<p style={{ fontSize: '13px' }}>{day}{" "}
{(
indexx === 0 ||
(indexx > 0 &&
dayjs(chartData[indexx - 1].date).month() !== dayjs(item.date).month() &&
type !== "weekly")
) && month}
</p>
) : (
<>
{type != 'weekly' ? 'Hari ini' : day}
</>
<p style={{ fontSize: '13px' }}>
{type != 'weekly' ? 'Hari ini' : day + ' ' + month}
</p>
)}
</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'
? chartData[indexx].totalValue
: formatRupiah(chartData[indexx].totalValue)}

View File

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

View File

@@ -103,6 +103,17 @@ function CafePage({
// };
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(() => {
const handleResize = () => {
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)
return (
@@ -297,6 +372,7 @@ function CafePage({
)}
<div style={{ width: isTablet ? "60%" : "100%" }}>
<div className="App-header">
{isTablet && !isFullscreen && <FullscreenButton onClick={handleFullscreen} />}
<Header
HeaderText={"Menu"}
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) => {
setIsPaymentLoading(true);
@@ -300,9 +369,16 @@ export default function Invoice({
tableNumber,
textareaRef.current.value
);
if (pay) window.location.reload();
} else if (deviceType == "guestSide") {
if (pay) {
handlePrint(pay.transaction);
localStorage.removeItem("cart");
localStorage.removeItem("lastTransaction");
setCartItems([]);
setTotalPrice(0);
window.dispatchEvent(new Event("localStorageUpdated"));
}
}
else if (deviceType == "guestSide") {
const pay = await handlePaymentFromGuestSide(
shopId,
email,

View File

@@ -1,13 +1,12 @@
import React, { useState, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import qs from 'qs';
import '../print.css'; // Kamu bisa pakai styling temanmu atau buat file baru
import '../print.css';
export default function PrintPage() {
const location = useLocation();
const [orientation, setOrientation] = useState('portrait');
// Parse data dari query string
const data = useMemo(() => {
try {
const query = qs.parse(location.search, { ignoreQueryPrefix: true });
@@ -19,8 +18,57 @@ export default function PrintPage() {
if (!data) return <div>Invalid data</div>;
const handlePrint = () => {
window.print();
const formatWaktu = (() => {
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 (
@@ -41,85 +89,14 @@ export default function PrintPage() {
Landscape
</button>
</div>
<button className="print-button" onClick={handlePrint}>
Cetak Struk ({orientation})
<button className="print-button" onClick={handlePrintBluetooth}>
🖨 Print ke Bluetooth
</button>
</div>
<div className="print-area">
<h2>Struk Pembayaran</h2>
<p><strong>Transaction ID:
</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>
<pre className="print-area">
{getReceiptText()}
</pre>
</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);
}, 0);
};
const formatRupiah = (number) => {
return 'Rp' + number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.');
};
const calculateAllTransactionsTotal = (transactions) => {
return transactions
.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) => {
// Pilih data yang ingin dikirim
const printableData = {
transactionId: transaction?.transactionId,
items: transaction?.DetailedTransactions.map(dt => ({
name: dt.Item.name,
qty: dt.qty,
price: dt.promoPrice || dt.price,
})),
total: calculateTotalPrice(transaction.DetailedTransactions),
date: transaction.createdAt,
payment_type: transaction.payment_type,
table: transaction.Table?.tableNo || "N/A",
const formatWaktu = (() => {
const date = new Date(transaction.createdAt);
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 = 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)
return (
<div className="Loader">
@@ -179,7 +222,7 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
return (
<div className={styles.Transactions}>
<h2 className={styles["Transactions-title"]}>
Transaksi selesai Rp {calculateAllTransactionsTotal(transactions)}
Transaksi selesai {formatRupiah(calculateAllTransactionsTotal(transactions))}
</h2>
<input
@@ -208,7 +251,7 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
</ul>
<div className={styles.TotalContainer}>
<span>Total:</span>
<span>Rp {item.totalPrice}</span>
<span>{formatRupiah(item.totalPrice)}</span>
</div>
</div>
))}
@@ -329,8 +372,11 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
<ul>
{transaction.DetailedTransactions.map((detail) => (
<li key={detail.detailedTransactionId}>
<span>{detail.Item.name}</span> - {detail.qty < 1 ? 'tidak tersedia' : `${detail.qty} x Rp
${detail.promoPrice ? detail.promoPrice : detail.price}`}
<span>{detail.Item.name}</span> - {detail.qty < 1
? 'tidak tersedia'
: `${detail.qty} x ${formatRupiah(detail.promoPrice || detail.price)}`
}
</li>
))}
</ul>
@@ -375,7 +421,7 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
<div className={styles.TotalContainer}>
<span>Total:</span>
<span>
Rp {calculateTotalPrice(transaction.DetailedTransactions)}
{formatRupiah(calculateTotalPrice(transaction.DetailedTransactions))}
</span>
</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' &&
<ButtonWithReplica
paymentUrl={paymentUrl}
price={
"Rp" + calculateTotalPrice(transaction.DetailedTransactions)
}
price={formatRupiah(calculateTotalPrice(transaction.DetailedTransactions))}
disabled={isPaymentLoading}
isPaymentLoading={isPaymentLoading}
handleClick={() => handleConfirm(transaction.transactionId)}

View File

@@ -3,13 +3,12 @@
flex-direction: column;
align-items: center;
padding: 20px;
min-height: 100vh;
background-color: #f5f5f5;
font-family: Arial, sans-serif;
font-family: monospace;
}
.controls {
background-color: #2c3e50;
background-color: #333;
color: white;
padding: 20px;
border-radius: 10px;
@@ -19,121 +18,67 @@
margin-bottom: 30px;
}
.controls h1 {
margin-top: 0;
}
.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;
.orientation-selector button,
.print-button {
margin: 10px;
padding: 10px 20px;
font-weight: bold;
cursor: pointer;
border: none;
border-radius: 5px;
}
.orientation-selector button.active {
background-color: #21a9c7;
background-color: #007bff;
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 {
background-color: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
width: 90%;
max-width: 800px;
background: white;
white-space: pre-wrap;
font-family: monospace;
font-size: 12px;
line-height: 1.4;
padding: 10px;
width: 48mm;
max-width: 48mm;
color: black;
box-shadow: none;
border: 1px solid #ccc;
}
/* Orientation styles */
.print-test.portrait .print-area {
max-width: 800px;
}
.print-test.landscape .print-area {
max-width: 1100px;
}
/* Print specific styles */
/* Print media query */
@media print {
@page {
margin: 58mm;
}
.print-test.portrait @page {
size: portrait;
}
.print-test.landscape @page {
size: landscape;
}
body {
background-color: white;
margin: 0;
padding: 0;
}
.controls {
display: none;
}
.print-area {
box-shadow: none;
width: 100%;
max-width: 100%;
padding: 15mm;
border: none;
box-shadow: none;
padding: 0;
font-size: 12px;
}
body * {
visibility: hidden;
}
.print-area, .print-area * {
visibility: visible;
}
.print-area {
position: absolute;
left: 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;
}
}