Compare commits

..

3 Commits

Author SHA1 Message Date
Vassshhh
5f75b8658a ok 2025-09-06 13:37:37 +07:00
Vassshhh
222169be74 ok 2025-08-31 18:59:15 +07:00
Vassshhh
026813d1e0 ok 2025-08-29 12:43:14 +07:00
18 changed files with 1110 additions and 504 deletions

89
package-lock.json generated
View File

@@ -18,9 +18,11 @@
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"html2canvas": "^1.4.1",
"jsqr": "^1.4.0", "jsqr": "^1.4.0",
"qr-scanner": "^1.4.2", "qr-scanner": "^1.4.2",
"qrcode.react": "^3.1.0", "qrcode.react": "^3.1.0",
"qs": "^6.14.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-apexcharts": "^1.7.0", "react-apexcharts": "^1.7.0",
"react-bootstrap": "^2.10.4", "react-bootstrap": "^2.10.4",
@@ -6951,6 +6953,15 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT" "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": { "node_modules/batch": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
@@ -7051,6 +7062,21 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/body-parser/node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/bonjour-service": { "node_modules/bonjour-service": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz",
@@ -7865,6 +7891,15 @@
"postcss": "^8.4" "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": { "node_modules/css-loader": {
"version": "6.11.0", "version": "6.11.0",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz",
@@ -10015,6 +10050,21 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/express/node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -11137,6 +11187,19 @@
} }
} }
}, },
"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": { "node_modules/htmlparser2": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
@@ -17681,12 +17744,12 @@
} }
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.13.0", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"side-channel": "^1.0.6" "side-channel": "^1.1.0"
}, },
"engines": { "engines": {
"node": ">=0.6" "node": ">=0.6"
@@ -20482,6 +20545,15 @@
"node": ">=8" "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": { "node_modules/text-table": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -21024,6 +21096,15 @@
"node": ">= 0.4.0" "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": { "node_modules/uuid": {
"version": "8.3.2", "version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",

View File

@@ -14,9 +14,11 @@
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"html2canvas": "^1.4.1",
"jsqr": "^1.4.0", "jsqr": "^1.4.0",
"qr-scanner": "^1.4.2", "qr-scanner": "^1.4.2",
"qrcode.react": "^3.1.0", "qrcode.react": "^3.1.0",
"qs": "^6.14.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-apexcharts": "^1.7.0", "react-apexcharts": "^1.7.0",
"react-bootstrap": "^2.10.4", "react-bootstrap": "^2.10.4",

View File

@@ -10,6 +10,7 @@ import {
} from "react-router-dom"; } from "react-router-dom";
import socket from "./services/socketService"; import socket from "./services/socketService";
import Print from "./pages/PrintPage.js";
import Dashboard from "./pages/Dashboard"; import Dashboard from "./pages/Dashboard";
import ScanMeja from "./pages/ScanMeja"; import ScanMeja from "./pages/ScanMeja";
import CafePage from "./pages/CafePage"; import CafePage from "./pages/CafePage";
@@ -45,6 +46,7 @@ import {
import Modal from "./components/Modal"; // Import your modal component import Modal from "./components/Modal"; // Import your modal component
import { requestNotificationPermission } from "./services/notificationService"; // Import the notification service import { requestNotificationPermission } from "./services/notificationService"; // Import the notification service
import PrintPage from "./pages/PrintPage.js";
function App() { function App() {
const location = useLocation(); const location = useLocation();
@@ -674,6 +676,12 @@ function App() {
<div className="App"> <div className="App">
<header className="App-header" id="header"> <header className="App-header" id="header">
<Routes> <Routes>
<Route
path="/:shopIdentifier/print"
element={
<PrintPage />
}
/>
<Route <Route
path="/" path="/"
element={ element={
@@ -811,8 +819,10 @@ function App() {
welcomePageConfig={shop.welcomePageConfig} welcomePageConfig={shop.welcomePageConfig}
onClose={closeModal} onClose={closeModal}
setModal={setModal} setModal={setModal}
setIsModalOpen={setIsModalOpen}
onModalCloseFunction={onModalCloseFunction} onModalCloseFunction={onModalCloseFunction}
onModalYesFunction={onModalYesFunction} onModalYesFunction={onModalYesFunction}
shopIdentifier={shopIdentifier}
/> />
</div> </div>
); );

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
@@ -7,7 +8,21 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
useEffect(() => { useEffect(() => {
setSelectedIndex(-1); setSelectedIndex(-1);
}, [transactionGraph]); }, [transactionGraph, graphFilter]);
// Helper function to format numbers to Indonesian Rupiah
const formatRupiah = (number) => {
if (number === null || number === undefined) {
return "Rp 0";
}
const formatter = new Intl.NumberFormat("id-ID", {
style: "currency",
currency: "IDR",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
return formatter.format(number);
};
const processData = (graphData) => { const processData = (graphData) => {
if (!graphData) return null; if (!graphData) return null;
@@ -22,18 +37,18 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
"18-21", "18-21",
"21-24", "21-24",
]; ];
console.log(dayData)
const sumSold = (transactions) => const sumSold = (transactions) =>
Array.isArray(transactions) ? transactions.reduce((acc, t) => acc + t.sold, 0) : transactions.transaction || 0; Array.isArray(transactions) ? transactions.reduce((acc, t) => acc + t.sold, 0) : transactions?.transaction || 0;
const sumTotal = (transactions) => const sumTotal = (transactions) =>
Array.isArray(transactions) ? transactions.reduce((acc, t) => acc + t.totalPrice, 0) : transactions.income || 0; Array.isArray(transactions) ? transactions.reduce((acc, t) => acc + t.totalPrice, 0) : transactions?.income || 0;
const sumOutcome = (transactions) => const sumOutcome = (transactions) =>
Array.isArray(transactions) ? transactions.reduce((acc, t) => acc + t.materialOutcome || t.price, 0) : transactions.outcome || 0; Array.isArray(transactions) ? transactions.reduce((acc, t) => acc + (t.materialOutcome || t.price * t.stockDifference), 0) : transactions?.outcome || 0;
let seriesData = [] let seriesData = []
if (graphFilter == 'transactions') { if (graphFilter === 'transactions') {
seriesData = [ seriesData = [
sumSold(dayData?.hour0To3Transactions), sumSold(dayData?.hour0To3Transactions),
sumSold(dayData?.hour3To6Transactions), sumSold(dayData?.hour3To6Transactions),
@@ -45,7 +60,7 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
sumSold(dayData?.hour21To24Transactions), sumSold(dayData?.hour21To24Transactions),
]; ];
} }
else if (graphFilter == 'income') { else if (graphFilter === 'income') {
seriesData = [ seriesData = [
sumTotal(dayData?.hour0To3Transactions), sumTotal(dayData?.hour0To3Transactions),
sumTotal(dayData?.hour3To6Transactions), sumTotal(dayData?.hour3To6Transactions),
@@ -57,7 +72,7 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
sumTotal(dayData?.hour21To24Transactions), sumTotal(dayData?.hour21To24Transactions),
]; ];
} }
else if (graphFilter == 'outcome') { else if (graphFilter === 'outcome') {
seriesData = [ seriesData = [
sumOutcome(dayData?.hour0To3MaterialIds), sumOutcome(dayData?.hour0To3MaterialIds),
sumOutcome(dayData?.hour3To6MaterialIds), sumOutcome(dayData?.hour3To6MaterialIds),
@@ -69,23 +84,26 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
sumOutcome(dayData?.hour21To24MaterialIds), sumOutcome(dayData?.hour21To24MaterialIds),
]; ];
} }
let totalValue = seriesData.reduce((acc, val) => acc + val, 0);
return { return {
date: new Date(dayData.date).toLocaleDateString(), date: dayData.date,
categories, categories,
series: [ series: [
{ {
name: `Transactions on ${new Date(dayData.date).toLocaleDateString()}`, name: graphFilter === 'transactions' ? 'Transaksi' : (graphFilter === 'income' ? 'Pemasukan' : 'Pengeluaran'),
data: seriesData, data: seriesData,
}, },
], ],
totalValue, // ⬅️ Tambahkan ini
}; };
}); });
}; };
const chartData = processData(graphFilter != 'outcome' ? transactionGraph : materialGraph); const chartData = processData(graphFilter !== 'outcome' ? transactionGraph : materialGraph);
let globalMax = null; let globalMax = null;
if (chartData) if (chartData) {
globalMax = chartData.reduce( globalMax = chartData.reduce(
(max, data) => { (max, data) => {
const localMax = Math.max(...data.series[0].data); const localMax = Math.max(...data.series[0].data);
@@ -93,24 +111,17 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
}, },
0 0
); );
}
const formatDate = (dateString) => { const formatDate = (dateString) => {
const date = new Date(dateString); // Parse the date string const d = dayjs(dateString, ["YYYY-MM-DD", "YYYY-MM-DDTHH:mm:ssZ"]);
return { month: d.format("MMM"), day: d.format("D") };
// Create an array of month names (use the same names you had earlier)
const monthNames = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
];
// Get the month and day
const month = monthNames[date.getMonth()]; // Month is 0-indexed (January = 0)
const day = date.getDate(); // Get the day of the month
return { month, day }; // Return the result
}; };
return ( return (
<div className={`${styles.chartItemContainer} ${selectedIndex !== -1 ? styles.expanded : ''}`}> <div className={`${styles.chartItemContainer} ${selectedIndex !== -1 ? styles.expanded : ''}`}>
{chartData && {chartData &&
chartData.map((data, index) => ( chartData.map((data, index) => (
<div <div
@@ -128,33 +139,78 @@ 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)
} }
// style={{ backgroundColor: index === indexx ? colors[index % colors.length] : 'transparent' }}
> >
<div style={{ position: 'absolute', bottom: 0, 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>
) : ( ) : (
'Hari ini' <p style={{ fontSize: '13px' }}>
{type != 'weekly' ? 'Hari ini' : day + ' ' + month}
</p>
)} )}
</div>
{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)}
</p>}
</div>
</div>
</div>
); );
})} })}
</div> </div>
<div className={styles.chartWrapper}> <div className={styles.chartWrapper}>
<Chart <Chart
options={{ options={{
tooltip: { enabled: false }, tooltip: {
enabled: true,
y: {
formatter: function (val) {
return graphFilter === "transactions"
? val
: formatRupiah(val);
},
},
},
dataLabels: {
enabled: true,
formatter: function (val) {
if (graphFilter === 'transactions') {
return val; // angka biasa
} else {
return formatRupiah(val); // format Rupiah
}
},
style: {
colors: [(index == chartData.length - 1 || selectedIndex != -1) ? "#000" : "transparent"],
fontSize: '7px',
},
offsetY: -10,
background: {
enabled: (index == chartData.length - 1 || selectedIndex != -1) ? true : false
}
},
chart: { chart: {
id: `chart-${index}`, id: `chart-${index}`,
type: "area", type: "area",
@@ -169,7 +225,7 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
categories: data.categories, categories: data.categories,
labels: { labels: {
style: { style: {
colors: index === 0 || index == selectedIndex || selectedIndex == 0 && index == 1 ? "#000" : "transparent", colors: index === 0 || index === selectedIndex || (selectedIndex === 0 && index === 1) ? "#000" : "transparent",
}, },
}, },
}, },
@@ -181,6 +237,9 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
style: { style: {
colors: "transparent", colors: "transparent",
}, },
formatter: function (val) {
return formatRupiah(val);
},
}, },
}, },
grid: { grid: {
@@ -198,7 +257,6 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
/> />
</div> </div>
</div> </div>
))} ))}
</div> </div>
); );

View File

@@ -35,7 +35,7 @@ import CreateCoupon from "../pages/CreateCoupon";
import CheckCoupon from "../pages/CheckCoupon"; import CheckCoupon from "../pages/CheckCoupon";
import CreateUserWithCoupon from "../pages/CreateUserWithCoupon"; import CreateUserWithCoupon from "../pages/CreateUserWithCoupon";
const Modal = ({ user, shop, isOpen, onClose, modalContent, deviceType, setModal, handleMoveToTransaction, depth,welcomePageConfig, onModalCloseFunction, onModalYesFunction }) => { const Modal = ({ user, shop, shopIdentifier, isOpen, onClose, modalContent, deviceType, setModal, setIsModalOpen, handleMoveToTransaction, depth,welcomePageConfig, onModalCloseFunction, onModalYesFunction }) => {
const [shopImg, setShopImg] = useState(''); const [shopImg, setShopImg] = useState('');
const [updateKey, setUpdateKey] = useState(0); const [updateKey, setUpdateKey] = useState(0);
@@ -88,10 +88,10 @@ const Modal = ({ user, shop, isOpen, onClose, modalContent, deviceType, setModal
{modalContent === "create_tenant" && <CreateTenant shopId={shop.cafeId} />} {modalContent === "create_tenant" && <CreateTenant shopId={shop.cafeId} />}
{modalContent === "edit_tables" && <TablesPage shop={shop} />} {modalContent === "edit_tables" && <TablesPage shop={shop} />}
{modalContent === "new_transaction" && ( {modalContent === "new_transaction" && (
<Transaction propsShopId={shop.cafeId} handleMoveToTransaction={handleMoveToTransaction} depth={depth} shopImg={shopImg} setModal={setModal}/> <Transaction propsShopId={shop.cafeId} setIsModalOpen={setIsModalOpen} cafeIdentityName={shopIdentifier} handleMoveToTransaction={handleMoveToTransaction} depth={depth} shopImg={shopImg} setModal={setModal}/>
)} )}
{modalContent === "transaction_canceled" && ( {modalContent === "transaction_canceled" && (
<Transaction propsShopId={shop.cafeId} /> <Transaction propsShopId={shop.cafeId} setIsModalOpen={setIsModalOpen} cafeIdentityName={shopIdentifier} />
)} )}
{modalContent === "transaction_pending" && <Transaction_pending deviceType={deviceType} setModal={setModal}/>} {modalContent === "transaction_pending" && <Transaction_pending deviceType={deviceType} setModal={setModal}/>}
{modalContent === "transaction_item" && <Transaction_item />} {modalContent === "transaction_item" && <Transaction_item />}

View File

@@ -7,175 +7,187 @@ const PeriodCharts = ({ type, graphFilter, aggregatedCurrentReports, aggregatedP
useEffect(() => { useEffect(() => {
setSelectedIndex(-1); setSelectedIndex(-1);
}, [aggregatedCurrentReports, aggregatedPreviousReports]); }, [aggregatedCurrentReports, aggregatedPreviousReports, graphFilter]);
const monthly = ["1 - 7", "8 - 14", "15 - 21", "22 - 28", "29 - 31"]; const monthly = ["1 - 7", "8 - 14", "15 - 21", "22 - 28", "29 - 31"];
const yearly = ["Kuartal 1", "Kuartal 2", "Kuartal 3", "Kuartal 4"]; const yearly = ["Kuartal 1", "Kuartal 2", "Kuartal 3", "Kuartal 4"];
const cat = type == 'monthly' ? monthly : yearly; const cat = type === "monthly" ? monthly : yearly;
// Helper Rupiah formatter
const formatRupiah = (number) => {
if (number === null || number === undefined) return "Rp 0";
return new Intl.NumberFormat("id-ID", {
style: "currency",
currency: "IDR",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(number);
};
let currentIncomeData,
currentOutcomeData,
currentTransactionData,
previousIncomeData,
previousOutcomeData,
previousTransactionData = null;
// Map the data for the current reports
let currentIncomeData, currentOutcomeData, currentTransactionData, previousIncomeData, previousOutcomeData, previousTransactionData = null;
if (aggregatedCurrentReports) { if (aggregatedCurrentReports) {
currentIncomeData = aggregatedCurrentReports.map((report) => report.income); currentIncomeData = aggregatedCurrentReports.map((r) => r.income);
currentOutcomeData = aggregatedCurrentReports.map((report) => report.outcome); currentOutcomeData = aggregatedCurrentReports.map((r) => r.outcome);
currentTransactionData = aggregatedCurrentReports.map((report) => report.transactions); currentTransactionData = aggregatedCurrentReports.map((r) => r.transactions);
if (type == 'monthly' && currentIncomeData.length === 4) { if (type === "monthly" && currentIncomeData.length === 4) {
currentIncomeData.push(null); currentIncomeData.push(null);
}
if (type == 'monthly' && currentOutcomeData.length === 4) {
currentOutcomeData.push(null); currentOutcomeData.push(null);
}
if (type == 'monthly' && currentTransactionData.length === 4) {
currentTransactionData.push(null); currentTransactionData.push(null);
} }
} }
if (aggregatedPreviousReports) {
// Map the data for the previous reports
previousIncomeData = aggregatedPreviousReports.map((report) => report.income);
previousOutcomeData = aggregatedPreviousReports.map((report) => report.outcome);
previousTransactionData = aggregatedPreviousReports.map((report) => report.transactions);
if (type == 'monthly' && previousIncomeData.length === 4) { if (aggregatedPreviousReports) {
previousIncomeData = aggregatedPreviousReports.map((r) => r.income);
previousOutcomeData = aggregatedPreviousReports.map((r) => r.outcome);
previousTransactionData = aggregatedPreviousReports.map((r) => r.transactions);
if (type === "monthly" && previousIncomeData.length === 4) {
previousIncomeData.push(null); previousIncomeData.push(null);
}
if (type == 'monthly' && previousOutcomeData.length === 4) {
previousOutcomeData.push(null); previousOutcomeData.push(null);
}
if (type == 'monthly' && previousTransactionData.length === 4) {
previousTransactionData.push(null); previousTransactionData.push(null);
} }
} }
let globalMax = null;
// cari global max untuk y-axis
let globalMax = 0;
if (aggregatedCurrentReports || aggregatedPreviousReports) { if (aggregatedCurrentReports || aggregatedPreviousReports) {
// Find the global maximum for the y-axis const dataset =
globalMax = Math.max( graphFilter === "income"
...(graphFilter === 'income' ? [...(currentIncomeData || []), ...(previousIncomeData || [])]
? [...currentIncomeData, ...previousIncomeData] : graphFilter === "outcome"
: graphFilter === 'outcome' ? [...(currentOutcomeData || []), ...(previousOutcomeData || [])]
? [...currentOutcomeData, ...previousOutcomeData] : [...(currentTransactionData || []), ...(previousTransactionData || [])];
: [...currentTransactionData, ...previousTransactionData]) globalMax = Math.max(...dataset);
);
} }
const getSeries = (isCurrent) => {
if (graphFilter === "income")
return isCurrent ? currentIncomeData : previousIncomeData;
if (graphFilter === "outcome")
return isCurrent ? currentOutcomeData : previousOutcomeData;
return isCurrent ? currentTransactionData : previousTransactionData;
};
return ( return (
<div className={`${styles.chartItemContainer} ${selectedIndex !== -1 ? styles.expanded : ''}`}> <div
{aggregatedPreviousReports && ( className={`${styles.chartItemContainer} ${selectedIndex !== -1 ? styles.expanded : ""
<div className={`${styles.chartItemWrapper} ${selectedIndex !== -1 && selectedIndex !== 0 }`}
>
{[aggregatedPreviousReports, aggregatedCurrentReports].map(
(dataset, i) =>
dataset && (
<div
key={i}
className={`${styles.chartItemWrapper} ${selectedIndex !== -1 && selectedIndex !== i
? styles.chartItemWrapperActive ? styles.chartItemWrapperActive
: styles.chartItemWrapperInactive : styles.chartItemWrapperInactive
}`}>
<div className={styles.dateSelectorWrapper}>
<div className={styles.dateSelector}
onClick={() =>
selectedIndex === -1 ? setSelectedIndex(0) : setSelectedIndex(-1)
}
style={{ color: 'black', position: 'relative' }}
>
<div style={{ position: 'absolute', bottom: 0, left: '10%', right: '10%', borderBottom: `2px solid ${colors[0]}` }}></div>
<div>{type == 'monthly' ? 'bulan lalu' : 'tahun lalu'}</div>
</div>
<div
className={`${styles.dateSelector} ${styles.dateSelectorInactive
}`} }`}
>
<div className={styles.dateSelectorWrapper}>
{[0, 1].map((idx) => (
<div
key={idx}
className={`${styles.dateSelector} ${idx === i ? styles.dateSelectorActive : styles.dateSelectorInactive
}`}
style={{ position: "relative" }}
onClick={() => onClick={() =>
selectedIndex === 0 ? setSelectedIndex(-1) : setSelectedIndex(1) selectedIndex == idx ? setSelectedIndex(-1) : setSelectedIndex(idx)
}>
<div>{type == 'monthly' ? 'bulan ini' : 'tahun ini'}</div>
</div>
</div>
<div className={styles.chartWrapper}>
<Chart
options={{
tooltip: { enabled: false },
chart: { type: "area", zoom: { enabled: false }, toolbar: { show: false } },
xaxis: {
categories: cat,
axisBorder: {
show: false, // Removes the x-axis line
},
axisTicks: {
show: false, // Removes the ticks on the x-axis
},
labels: {
style: {
colors: ['black', 'black', 'black', 'black', aggregatedPreviousReports?.length == 4 ? 'transparent' : 'black'],
} }
} >
}, {idx === i && (
yaxis: { max: globalMax, min: 0, labels: { <div
maxWidth: 20, style: { colors: "transparent" } } }, style={{
grid: { show: false }, position: "absolute",
fill: { opacity: 0.5 }, bottom: 0,
colors: [colors[0]], left: "10%",
right: "10%",
borderBottom: `2px solid ${colors[i]}`,
}} }}
series={[ ></div>
// { name: "Pemasukan", data: previousIncomeData },
// { name: "Pengaluaran", data: previousOutcomeData },
{ name: "Total transaksi", data: graphFilter == 'income' ? previousIncomeData : graphFilter == 'outcome' ? previousOutcomeData : previousTransactionData },
]}
type="area"
height={200}
width="100%"
/>
</div>
</div>
)} )}
{aggregatedCurrentReports && ( <div style={{ color: idx === i ? "black" : "transparent" }}>
<div className={`${styles.chartItemWrapper} ${selectedIndex !== -1 && selectedIndex !== 1 {type === "monthly"
? styles.chartItemWrapperActive ? idx === 0
: styles.chartItemWrapperInactive ? "bulan lalu"
}`}> : "bulan ini"
<div className={styles.dateSelectorWrapper}> : idx === 0
<div ? "tahun lalu"
className={`${styles.dateSelector} ${styles.dateSelectorInactive : "tahun ini"}
}`} </div>
onClick={() => </div>
selectedIndex === 1 ? setSelectedIndex(-1) : setSelectedIndex(0) ))}
}>
<div>{type == 'monthly' ? 'bulan lalu' : 'tahun lalu'}</div>
</div> </div>
<div className={styles.dateSelector}
onClick={() =>
selectedIndex === -1 ? setSelectedIndex(1) : setSelectedIndex(-1)
}
style={{ color: 'black', position: 'relative' }}
>
<div style={{ position: 'absolute', bottom: 0, left: '10%', right: '10%', borderBottom: `2px solid ${colors[1]}` }}></div>
<div>{type == 'monthly' ? 'bulan ini' : 'tahun ini'}</div>
</div>
</div>
<div className={styles.chartWrapper}> <div className={styles.chartWrapper}>
<Chart <Chart
options={{ options={{
tooltip: { enabled: false }, tooltip: {
chart: { type: "area", zoom: { enabled: false }, toolbar: { show: false } }, enabled: true,
y: {
formatter: (val) =>
graphFilter === "transactions" ? val : formatRupiah(val),
},
},
dataLabels: {
enabled: true,
formatter: (val) =>
graphFilter === "transactions" ? val : formatRupiah(val),
style: {
colors: ["#000"], // <- Selalu tampil hitam
fontSize: "10px",
},
offsetY: -10,
background: {
enabled: true, // <- Selalu tampil background label
},
},
chart: {
type: "area",
zoom: { enabled: false },
toolbar: { show: false },
},
xaxis: { xaxis: {
categories: cat, categories: cat,
axisBorder: {
show: false, // Removes the x-axis line
},
axisTicks: {
show: false, // Removes the ticks on the x-axis
},
labels: { labels: {
style: { style: {
colors: ['black', 'black', 'black', 'black', aggregatedCurrentReports?.length == 4 ? 'transparent' : 'black'], colors: cat.map(() =>
} i === selectedIndex || selectedIndex === -1 ? "#000" : "transparent"
} ),
},
},
},
yaxis: {
max: globalMax,
min: 0,
labels: {
maxWidth: 20,
style: { colors: "transparent" },
formatter: (val) => formatRupiah(val),
},
}, },
yaxis: { max: globalMax, min: 0, labels: { maxWidth: 20, style: { colors: "transparent" } } },
grid: { show: false }, grid: { show: false },
fill: { opacity: 0.5 }, fill: { opacity: 0.5 },
colors: [colors[1]], colors: [colors[i]],
}} }}
series={[ series={[
// { name: "Pemasukan", data: currentIncomeData }, {
// { name: "Pengeluaran", data: currentOutcomeData }, name:
{ name: "Total transaksi", data: graphFilter == 'income' ? currentIncomeData : graphFilter == 'outcome' ? currentOutcomeData : currentTransactionData }, graphFilter === "transactions"
? "Transaksi"
: graphFilter === "income"
? "Pemasukan"
: "Pengeluaran",
data: getSeries(i === 1),
},
]} ]}
type="area" type="area"
height={200} height={200}
@@ -183,6 +195,7 @@ if (aggregatedCurrentReports || aggregatedPreviousReports) {
/> />
</div> </div>
</div> </div>
)
)} )}
</div> </div>
); );

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,62 +1,92 @@
import React from "react"; import React, { useState } from "react";
const CircularDiagram = ({ segments }) => { const HorizontalBarDiagram = ({ segments, width = 300 }) => {
const radius = 70; // Radius of the circle const [showAll, setShowAll] = useState(false);
const strokeWidth = 20; // Width of each portion
const circumference = 2 * Math.PI * (radius - strokeWidth / 2);
let startOffset = -63; // Initial offset for each segment const barHeight = 20; // tinggi tiap bar
const gap = 12; // jarak antar bar
const total = segments.reduce((sum, seg) => sum + seg.value, 0);
// tentukan data yang ditampilkan
const visibleSegments = showAll ? segments : segments.slice(0, 5);
const height = visibleSegments.length * (barHeight + gap);
const svgStyles = {
display: "block",
margin: "0 auto",
};
console.log(segments)
return ( return (
<div style={{ textAlign: "center" }}>
<svg <svg
width={radius * 2} width={width}
height={radius * 2} height={height}
viewBox={`0 0 ${radius * 2} ${radius * 2}`} style={{ display: "block", margin: "0 auto" }}
style={svgStyles}
> >
<circle {visibleSegments.map((segment, index) => {
cx={radius} const { name, value, color, unit} = segment;
cy={radius} const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0;
r={radius - strokeWidth / 2} const barWidth = (width * percentage) / 100;
stroke="#eee"
strokeWidth={strokeWidth}
fill="none"
/>
{segments.map((segment, index) => {
const { percentage, color } = segment;
console.log(percentage)
let p = percentage;
if(p == 'Infinity' || isNaN(p)) p = 0;
const segmentLength = (circumference * p) / 100;
const strokeDashoffset = circumference - startOffset;
startOffset += segmentLength;
return ( return (
<circle <g
key={index} key={index}
cx={radius} transform={`translate(0, ${index * (barHeight + gap)})`}
cy={radius} >
r={radius - strokeWidth / 2} {/* Background bar */}
stroke={color} <rect
strokeWidth={strokeWidth} x={0}
fill="none" y={0}
strokeDasharray={`${segmentLength} ${ width={width}
circumference - segmentLength height={barHeight}
}`} fill="#eee"
strokeDashoffset={strokeDashoffset} rx={8}
strokeLinecap="round" // Rounds the edges of each segment ry={8}
transform={`rotate(-90 ${radius} ${radius})`}
/> />
{/* Filled bar */}
<rect
x={0}
y={0}
width={barWidth}
height={barHeight}
fill={color}
rx={8}
ry={8}
/>
{/* Name + Value + Percentage di dalam bar */}
<text
x={width - 8} // dekat ujung kanan
y={barHeight / 2}
dy=".35em"
textAnchor="end"
fill="black"
fontSize="11"
fontWeight="bold"
style={{ pointerEvents: "none", textTransform: "capitalize" }}
>
{unit ? (name + ' ( ' + value +' '+ unit+')') : (name + ' ' + value +' ('+ percentage + '%)')}
</text>
</g>
); );
})} })}
</svg> </svg>
{/* Tombol lihat lebih banyak / lebih sedikit */}
{segments.length > 5 && (
<button
onClick={() => setShowAll(!showAll)}
style={{
marginTop: "8px",
padding: "6px 12px",
fontSize: "12px",
border: "1px solid #ccc",
borderRadius: "6px",
background: "#f9f9f9",
cursor: "pointer",
}}
>
{showAll ? "Lihat lebih sedikit" : "Lihat lebih banyak"}
</button>
)}
</div>
); );
}; };
export default CircularDiagram; export default HorizontalBarDiagram;

View File

@@ -234,7 +234,7 @@ const LinktreePage = ({ user, setModal }) => {
return ( return (
<> <>
{user && user.roleId < 2 ? ( {getLocalStorage("auth") ? (
<div> <div>
<div className={styles.header}> <div className={styles.header}>

View File

@@ -180,7 +180,7 @@ const SetPaymentQr = ({ cafeId }) => {
setLatestMutation(latestMutation); setLatestMutation(latestMutation);
setCurrentQuantity(latestMutation.newStock); setCurrentQuantity(latestMutation.newStock);
setCurrentPrice(formatCurrency(latestMutation.priceAtp)); setCurrentPrice(formatCurrency(latestMutation.priceAtp));
setNewPrice(formatCurrency(latestMutation.priceAtp)); setNewPrice(formatCurrency(latestMutation.priceAtp) || 0);
} else { } else {
setCurrentQuantity(0); // Default value if no mutations exist setCurrentQuantity(0); // Default value if no mutations exist
setLatestMutation({ newStock: 0 }); setLatestMutation({ newStock: 0 });
@@ -195,12 +195,12 @@ const SetPaymentQr = ({ cafeId }) => {
const handleUpdateStock = async () => { const handleUpdateStock = async () => {
setLoading(true); setLoading(true);
console.log('aaa')
try { try {
const newprice = convertToInteger(newPrice)
const newStock = currentQuantity + quantityChange; const newStock = currentQuantity + quantityChange;
const formData = new FormData(); const formData = new FormData();
formData.append("newStock", newStock); formData.append("newStock", newStock);
formData.append("priceAtp", newprice); formData.append("priceAtp", newPrice);
formData.append("reason", "Stock update"); formData.append("reason", "Stock update");
await createMaterialMutation(materials[selectedMaterialIndex].materialId, formData); await createMaterialMutation(materials[selectedMaterialIndex].materialId, formData);

102
src/pages/PrintPage.js Normal file
View File

@@ -0,0 +1,102 @@
import React, { useState, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import qs from 'qs';
import '../print.css';
export default function PrintPage() {
const location = useLocation();
const [orientation, setOrientation] = useState('portrait');
const data = useMemo(() => {
try {
const query = qs.parse(location.search, { ignoreQueryPrefix: true });
return JSON.parse(query.data);
} catch (err) {
return null;
}
}, [location.search]);
if (!data) return <div>Invalid data</div>;
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 (
<div className={`print-test ${orientation}`}>
<div className="controls">
<h1>CAFE HOREE - Mode {orientation.charAt(0).toUpperCase() + orientation.slice(1)}</h1>
<div className="orientation-selector">
<button
className={orientation === 'portrait' ? 'active' : ''}
onClick={() => setOrientation('portrait')}
>
Portrait
</button>
<button
className={orientation === 'landscape' ? 'active' : ''}
onClick={() => setOrientation('landscape')}
>
Landscape
</button>
</div>
<button className="print-button" onClick={handlePrintBluetooth}>
🖨 Print ke Bluetooth
</button>
</div>
<pre className="print-area">
{getReceiptText()}
</pre>
</div>
);
}

View File

@@ -55,6 +55,9 @@ const RoundedRectangle = ({
? "rgb(85 85 85)" ? "rgb(85 85 85)"
: !isChildren && !children && backgroundColor, : !isChildren && !children && backgroundColor,
color: loading ? "transparent" : color, color: loading ? "transparent" : color,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}; };
const valueAndPercentageContainerStyle = { const valueAndPercentageContainerStyle = {
@@ -91,20 +94,18 @@ const RoundedRectangle = ({
return ( return (
<div style={containerStyle} onClick={onClick}> <div style={containerStyle} onClick={onClick}>
<div style={titleStyle}>{title}</div> <div style={titleStyle}>
{!children && ( {title}
<div style={valueAndPercentageContainerStyle}>
<div style={valueStyle}>{loading ? "Loading..." : value}</div>
<div style={percentageStyle}> <div style={percentageStyle}>
{loading ? "" : percentage} {loading ? "" : percentage}
{percentage !== undefined && !loading && "%"} {percentage !== undefined && !loading && "%"}
{percentage !== undefined && !loading && (
<span style={arrowStyle}>
{percentage > 0 ? "↗" : percentage === 0 ? "-" : "↘"}
</span>
)}
</div> </div>
</div> </div>
{!children && (
<div style={valueAndPercentageContainerStyle}>
<div style={valueStyle}>{loading ? "Loading..." : value}</div>
</div>
)} )}
{children && <div>{children}</div>} {/* Properly render children */} {children && <div>{children}</div>} {/* Properly render children */}
</div> </div>
@@ -118,7 +119,7 @@ const App = ({ forCafe = true, cafeId = -1,
const [selectedCafeId, setSelectedCafeId] = useState(cafeId); const [selectedCafeId, setSelectedCafeId] = useState(cafeId);
const [analytics, setAnalytics] = useState({}); const [analytics, setAnalytics] = useState({});
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState("monthly"); const [filter, setFilter] = useState("yesterday");
const [circularFilter, setCircularFilter] = useState("item"); const [circularFilter, setCircularFilter] = useState("item");
const [graphFilter, setGraphFilter] = useState("income"); const [graphFilter, setGraphFilter] = useState("income");
@@ -195,102 +196,84 @@ const App = ({ forCafe = true, cafeId = -1,
// Define a color palette or generate colors dynamically // Define a color palette or generate colors dynamically
const colorPalette = colors; const colorPalette = colors;
// Ensure that each segment gets a unique color
let colorIndex = 0; let colorIndex = 0;
console.log(filteredItems) // Segment penjualan item
let segments = (selectedCafeId == 0 || selectedCafeId == -1) ? filteredItems.flatMap((cafe) => { let segments =
selectedCafeId == 0 || selectedCafeId == -1
? filteredItems.flatMap((cafe) => {
const cafeItems = cafe.report?.itemSales || []; const cafeItems = cafe.report?.itemSales || [];
console.log(cafeItems); // Log all items for the cafe return cafeItems.map((item) => {
const color = colorPalette[colorIndex % colorPalette.length];
return cafeItems.map((item, index) => { colorIndex++;
const percentage = totalSoldAcrossAllCafes > 0
? ((item.sold / totalSoldAcrossAllCafes) * 100).toFixed(2)
: 0;
console.log(`${item.itemName}: ${(percentage)}%`); // Log item name and percentage
// Assign a unique color from the color palette
const color = colorPalette[colorIndex % colorPalette.length]; // Use modulo to cycle through colors
colorIndex++; // Increment to ensure a new color for the next item
return { return {
itemName: item.itemName, name: item.itemName,
sold: item.sold, value: item.sold,
percentage: percentage, color,
color: color,
}; };
}); });
}) : filteredItems.map((item, index) => { })
const color = colorPalette[colorIndex % colorPalette.length]; // Use modulo to cycle through colors : filteredItems.map((item) => {
colorIndex++; // Increment to ensure a new color for the next item const color = colorPalette[colorIndex % colorPalette.length];
colorIndex++;
return { return {
itemName: item.itemName, name: item.itemName,
percentage: item.percentage, value: item.sold,
color: color, color,
}; };
}); });
segments.sort((a, b) => b.sold - a.sold); // Urutkan descending berdasarkan value
segments.sort((a, b) => b.value - a.value);
// Reset color index untuk material
colorIndex = 0;
let materialSegments = (selectedCafeId == 0 || selectedCafeId == -1) ? filteredItems.flatMap((cafe) => { // Segment pengeluaran material
let materialSegments =
selectedCafeId == 0 || selectedCafeId == -1
? filteredItems.flatMap((cafe) => {
const cafeItems = cafe.report?.materialSpend || []; const cafeItems = cafe.report?.materialSpend || [];
console.log(cafeItems); // Log all items for the cafe return cafeItems.map((item) => {
const color = colorPalette[colorIndex % colorPalette.length];
return cafeItems.map((item, index) => { colorIndex++;
const percentage = totalSpendAcrossAllCafes > 0
? ((item.spend / totalSpendAcrossAllCafes) * 100).toFixed(2)
: 0;
console.log(`${item.materialName}: ${(percentage)}%`); // Log item name and percentage
// Assign a unique color from the color palette
const color = colorPalette[colorIndex % colorPalette.length]; // Use modulo to cycle through colors
colorIndex++; // Increment to ensure a new color for the next item
return { return {
itemName: item.materialName, name: item.materialName,
sold: item.spend, value: item.spend,
percentage: percentage, unit: item.unit,
color: color, color,
}; };
}); });
}) : filteredItems.map((item, index) => { })
const color = colorPalette[colorIndex % colorPalette.length]; // Use modulo to cycle through colors : filteredItems.map((item) => {
colorIndex++; // Increment to ensure a new color for the next item const color = colorPalette[colorIndex % colorPalette.length];
colorIndex++;
return { return {
itemName: item.materialName, name: item.materialName,
percentage: item.percentage, value: item.spend,
color: color, unit: item.unit,
color,
}; };
}); });
materialSegments.sort((a, b) => b.spend - a.spend); // Urutkan descending berdasarkan value
materialSegments.sort((a, b) => b.value - a.value);
console.log(selectedCafeId) console.log(selectedCafeId)
console.log(segments) console.log(segments)
const formatIncome = (amount) => { const formatIncome = (amount) => {
if (amount >= 1_000_000_000) { if (amount == null) return "0";
// Format for billions
const billions = amount / 1_000_000_000; const formatter = new Intl.NumberFormat("id-ID", {
return billions.toFixed(0) + "m"; // No decimal places for billions style: "currency",
} else if (amount >= 1_000_000) { currency: "IDR",
// Format for millions minimumFractionDigits: 0,
const millions = amount / 1_000_000; });
return millions.toFixed(2).replace(/\.00$/, "") + "jt"; // Two decimal places, remove trailing '.00'
} else if (amount >= 1_000) { return formatter.format(amount);
// Format for thousands
const thousands = amount / 1_000;
return thousands.toFixed(1).replace(/\.0$/, "") + "k"; // One decimal place, remove trailing '.0'
} else {
// Less than a thousand
if (amount != null) return amount.toString();
}
}; };
function roundToInteger(num) { function roundToInteger(num) {
@@ -478,7 +461,7 @@ const App = ({ forCafe = true, cafeId = -1,
{forCafe && <div style={{ marginTop: '49px', marginRight: '10px', display: 'flex', flexWrap: 'nowrap', alignItems: 'center', fontSize: '25px' }} onClick={handleClose}><svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 512 512"><path d="M48,256c0,114.87,93.13,208,208,208s208-93.13,208-208S370.87,48,256,48,48,141.13,48,256Zm212.65-91.36a16,16,0,0,1,.09,22.63L208.42,240H342a16,16,0,0,1,0,32H208.42l52.32,52.73A16,16,0,1,1,238,347.27l-79.39-80a16,16,0,0,1,0-22.54l79.39-80A16,16,0,0,1,260.65,164.64Z" /></svg>Laporan</div>} {forCafe && <div style={{ marginTop: '49px', marginRight: '10px', display: 'flex', flexWrap: 'nowrap', alignItems: 'center', fontSize: '25px' }} onClick={handleClose}><svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 512 512"><path d="M48,256c0,114.87,93.13,208,208,208s208-93.13,208-208S370.87,48,256,48,48,141.13,48,256Zm212.65-91.36a16,16,0,0,1,.09,22.63L208.42,240H342a16,16,0,0,1,0,32H208.42l52.32,52.73A16,16,0,1,1,238,347.27l-79.39-80a16,16,0,0,1,0-22.54l79.39-80A16,16,0,0,1,260.65,164.64Z" /></svg>Laporan</div>}
<div style={{ marginTop: '10px' }}> <div style={{ marginTop: '10px' }}>
{!forCafe && {!forCafe &&
<div className={styles.dateSelectorWrapper} style={{ fontSize: '12px' }}> <div className={styles.dateSelectorWrapper} style={{ fontSize: '16px', textTransform: 'uppercase' }}>
{texts.map((item, indexx) => { {texts.map((item, indexx) => {
return ( return (
<div <div
@@ -531,7 +514,7 @@ const App = ({ forCafe = true, cafeId = -1,
<RoundedRectangle <RoundedRectangle
title="Pendapatan" title="Pendapatan"
fontSize="12px" fontSize="12px"
value={!loading && "Rp" + formatIncome(analytics?.currentTotals?.income)} value={!loading && formatIncome(analytics?.currentTotals?.income)}
percentage={roundToInteger(analytics?.growth?.incomeGrowth)} percentage={roundToInteger(analytics?.growth?.incomeGrowth)}
invert={false} invert={false}
loading={loading} loading={loading}
@@ -543,7 +526,7 @@ const App = ({ forCafe = true, cafeId = -1,
<RoundedRectangle <RoundedRectangle
title="Pengeluaran" title="Pengeluaran"
fontSize="12px" fontSize="12px"
value={!loading && "Rp" + formatIncome(analytics?.currentTotals?.outcome)} value={!loading && formatIncome(analytics?.currentTotals?.outcome)}
percentage={roundToInteger(analytics?.growth?.outcomeGrowth)} percentage={roundToInteger(analytics?.growth?.outcomeGrowth)}
invert={true} invert={true}
loading={loading} loading={loading}
@@ -653,29 +636,6 @@ const App = ({ forCafe = true, cafeId = -1,
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<CircularDiagram segments={circularFilter == 'item' ? segments : materialSegments} /> <CircularDiagram segments={circularFilter == 'item' ? segments : materialSegments} />
</div> </div>
<div style={{ flex: 1, marginLeft: "20px" }}>
{(circularFilter === 'item' ? segments : materialSegments).map((item, index) => (
<div
key={index}
style={{
display: "flex",
alignItems: "center",
margin: "10px",
}}
>
<div
style={{
marginRight: "5px",
fontSize: "1.2em",
color: colors[index],
}}
>
</div>
<h5 style={{ margin: 0, textAlign: "left" }}>{item.itemName}</h5>
</div>
))}
</div>
</div> </div>
<div className={styles.filterSelectorWrapper}> <div className={styles.filterSelectorWrapper}>

View File

@@ -1,6 +1,8 @@
import React, { useRef, useEffect, useState } from "react"; import React, { useRef, useEffect, useState } from "react";
import qs from 'qs';
import styles from "./Transactions.module.css"; import styles from "./Transactions.module.css";
import { useParams } from "react-router-dom"; import { useParams, useNavigate } from "react-router-dom";
import { ColorRing } from "react-loader-spinner"; import { ColorRing } from "react-loader-spinner";
import { import {
getTransaction, getTransaction,
@@ -11,7 +13,7 @@ import { getTables } from "../helpers/tableHelper";
import TableCanvas from "../components/TableCanvas"; import TableCanvas from "../components/TableCanvas";
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
export default function Transactions({ propsShopId, sendParam, deviceType, handleMoveToTransaction, depth, shopImg, setModal }) { export default function Transactions({ propsShopId,setIsModalOpen,cafeIdentityName, sendParam, deviceType, handleMoveToTransaction, depth, shopImg, setModal }) {
const { shopId, tableId } = useParams(); const { shopId, tableId } = useParams();
if (sendParam) sendParam({ shopId, tableId }); if (sendParam) sendParam({ shopId, tableId });
@@ -25,6 +27,9 @@ export default function Transactions({ propsShopId, sendParam, deviceType, handl
const [transactionRefreshKey, setTransactionRefreshKey] = useState(0); const [transactionRefreshKey, setTransactionRefreshKey] = useState(0);
const navigate = useNavigate();
useEffect(() => { useEffect(() => {
const transactionId = searchParams.get("transactionId") || ""; const transactionId = searchParams.get("transactionId") || "";
@@ -115,8 +120,28 @@ export default function Transactions({ propsShopId, sendParam, deviceType, handl
autoResizeTextArea(noteRef.current); autoResizeTextArea(noteRef.current);
} }
}, [transaction?.notes]); }, [transaction?.notes]);
const handlePrint = () => {
window.print(); 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",
};
// Serialize to query string
const queryString = qs.stringify({ data: JSON.stringify(printableData) });
// Navigate to /print with query string
setIsModalOpen(false);
navigate(`/${cafeIdentityName}/print?${queryString}`);
}; };
return ( return (
@@ -316,7 +341,7 @@ export default function Transactions({ propsShopId, sendParam, deviceType, handl
{transaction.confirmed > 1 && ( {transaction.confirmed > 1 && (
<h5 <h5
className={styles.DeclineButton} className={styles.DeclineButton}
onClick={() => handlePrint()} onClick={() => handlePrint(transaction)}
> >
Cetak struk Cetak struk
</h5> </h5>

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react"; import qs from 'qs';
import styles from "./Transactions.module.css"; import styles from "./Transactions.module.css";
import { useParams } from "react-router-dom"; import { useParams, useNavigate } from "react-router-dom";
import { ColorRing } from "react-loader-spinner"; import { ColorRing } from "react-loader-spinner";
import { import {
getMyTransactions, getMyTransactions,
@@ -30,6 +31,9 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const navigate = useNavigate();
useEffect(() => { useEffect(() => {
setMatchedItems(searchAndAggregateItems(transactions, searchTerm)); setMatchedItems(searchAndAggregateItems(transactions, searchTerm));
}, [searchTerm, transactions]); }, [searchTerm, transactions]);
@@ -58,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
@@ -142,6 +152,64 @@ console.log(aggregatedItems.values())
} }
}; };
const handlePrint = (transaction) => {
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;
};
if (loading) if (loading)
return ( return (
<div className="Loader"> <div className="Loader">
@@ -154,7 +222,7 @@ console.log(aggregatedItems.values())
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
@@ -183,7 +251,7 @@ console.log(aggregatedItems.values())
</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>
))} ))}
@@ -304,8 +372,11 @@ console.log(aggregatedItems.values())
<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>
@@ -350,12 +421,13 @@ console.log(aggregatedItems.values())
<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>
<div className={styles.TotalContainer}> <div className={styles.TotalContainer}>
{(deviceType == 'clerk' && !transaction.is_paid && (transaction.confirmed == 0 || transaction.confirmed == 1 || transaction.confirmed == 2)) && {(deviceType == 'clerk' && !transaction.is_paid && (transaction.confirmed == 0 || transaction.confirmed == 1 || transaction.confirmed == 2)) &&
<>
<button <button
className={styles.PayButton} className={styles.PayButton}
onClick={() => handleConfirm(transaction.transactionId)} onClick={() => handleConfirm(transaction.transactionId)}
@@ -374,14 +446,22 @@ console.log(aggregatedItems.values())
} }
</button> </button>
</>
} }
{deviceType == 'clerk' && transaction.confirmed > 1 && (
<h5
className={styles.DeclineButton}
onClick={() => handlePrint(transaction)}
>
Cetak struk
</h5>
)}
{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

@@ -403,48 +403,58 @@
display: none; /* Hidden in normal view */ display: none; /* Hidden in normal view */
} }
/* Print-specific styles */
@media print { @media print {
.Transaction { @page {
display: none; /* Hide everything else when printing */ size: portrait;
margin: 15mm;
} }
.printContainer { body * {
display: block !important; visibility: hidden;
position: static; }
background: white;
color: black; #print-section,
font-family: 'Courier New', Courier, monospace; #print-section * {
padding: 20px; visibility: visible;
}
#print-section {
position: absolute;
left: 0;
top: 0;
width: 100%;
padding: 15mm;
background-color: white;
}
} }
.receipt { .receipt {
width: 100%; font-family: Arial, sans-serif;
max-width: 400px; max-width: 400px;
margin: 0 auto; margin: auto;
color: #000;
font-size: 14px;
} }
.center-text { .receipt hr {
border: none;
border-top: 1px dashed #aaa;
margin: 10px 0;
}
.receipt .center-text {
text-align: center; text-align: center;
font-weight: bold; font-weight: bold;
} }
.item-line { .receipt .item-line {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-bottom: 8px;
} }
.total { .receipt .total {
text-align: right;
font-size: 1.2em;
font-weight: bold; font-weight: bold;
margin-top: 20px; text-align: right;
margin-top: 10px;
} }
button, .DeclineButton, .addNewItem {
display: none !important;
}
}

84
src/print.css Normal file
View File

@@ -0,0 +1,84 @@
.print-test {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
background-color: #f5f5f5;
font-family: monospace;
}
.controls {
background-color: #333;
color: white;
padding: 20px;
border-radius: 10px;
width: 90%;
max-width: 800px;
text-align: center;
margin-bottom: 30px;
}
.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: #007bff;
color: white;
}
.print-area {
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;
}
/* Print media query */
@media print {
body {
background-color: white;
margin: 0;
padding: 0;
}
.controls {
display: none;
}
.print-area {
width: 100%;
max-width: 100%;
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;
}
}