Compare commits
3 Commits
dcf0455772
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f75b8658a | ||
|
|
222169be74 | ||
|
|
026813d1e0 |
89
package-lock.json
generated
89
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
10
src/App.js
10
src/App.js
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 />}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
</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}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|
||||||
|
|||||||
@@ -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
102
src/pages/PrintPage.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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
84
src/print.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user