ok
This commit is contained in:
89
package-lock.json
generated
89
package-lock.json
generated
@@ -18,9 +18,11 @@
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"html-to-image": "^1.11.11",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jsqr": "^1.4.0",
|
||||
"qr-scanner": "^1.4.2",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"qs": "^6.14.0",
|
||||
"react": "^18.3.1",
|
||||
"react-apexcharts": "^1.7.0",
|
||||
"react-bootstrap": "^2.10.4",
|
||||
@@ -6951,6 +6953,15 @@
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/batch": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
|
||||
@@ -7051,6 +7062,21 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"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": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz",
|
||||
@@ -7865,6 +7891,15 @@
|
||||
"postcss": "^8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/css-line-break": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/css-loader": {
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz",
|
||||
@@ -10015,6 +10050,21 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"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": {
|
||||
"version": "3.1.3",
|
||||
"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": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
|
||||
@@ -17681,12 +17744,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.6"
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
@@ -20482,6 +20545,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/text-segmentation": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
@@ -21024,6 +21096,15 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/utrie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
|
||||
@@ -14,9 +14,11 @@
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"html-to-image": "^1.11.11",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jsqr": "^1.4.0",
|
||||
"qr-scanner": "^1.4.2",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"qs": "^6.14.0",
|
||||
"react": "^18.3.1",
|
||||
"react-apexcharts": "^1.7.0",
|
||||
"react-bootstrap": "^2.10.4",
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "react-router-dom";
|
||||
import socket from "./services/socketService";
|
||||
|
||||
import Print from "./pages/PrintPage.js";
|
||||
import Dashboard from "./pages/Dashboard";
|
||||
import ScanMeja from "./pages/ScanMeja";
|
||||
import CafePage from "./pages/CafePage";
|
||||
@@ -45,6 +46,7 @@ import {
|
||||
import Modal from "./components/Modal"; // Import your modal component
|
||||
|
||||
import { requestNotificationPermission } from "./services/notificationService"; // Import the notification service
|
||||
import PrintPage from "./pages/PrintPage.js";
|
||||
|
||||
function App() {
|
||||
const location = useLocation();
|
||||
@@ -674,6 +676,12 @@ function App() {
|
||||
<div className="App">
|
||||
<header className="App-header" id="header">
|
||||
<Routes>
|
||||
<Route
|
||||
path="/:shopIdentifier/print"
|
||||
element={
|
||||
<PrintPage />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
|
||||
@@ -7,7 +7,21 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
|
||||
|
||||
useEffect(() => {
|
||||
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) => {
|
||||
if (!graphData) return null;
|
||||
@@ -22,18 +36,18 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
|
||||
"18-21",
|
||||
"21-24",
|
||||
];
|
||||
console.log(dayData)
|
||||
const sumSold = (transactions) =>
|
||||
Array.isArray(transactions) ? transactions.reduce((acc, t) => acc + t.sold, 0) : transactions.transaction || 0;
|
||||
const sumSold = (transactions) =>
|
||||
Array.isArray(transactions) ? transactions.reduce((acc, t) => acc + t.sold, 0) : transactions?.transaction || 0;
|
||||
|
||||
const sumTotal = (transactions) =>
|
||||
Array.isArray(transactions) ? transactions.reduce((acc, t) => acc + t.totalPrice, 0) : transactions.income || 0;
|
||||
const sumTotal = (transactions) =>
|
||||
Array.isArray(transactions) ? transactions.reduce((acc, t) => acc + t.totalPrice, 0) : transactions?.income || 0;
|
||||
|
||||
const sumOutcome = (transactions) =>
|
||||
Array.isArray(transactions) ? transactions.reduce((acc, t) => acc + (t.materialOutcome || t.price), 0) : transactions?.outcome || 0;
|
||||
|
||||
const sumOutcome = (transactions) =>
|
||||
Array.isArray(transactions) ? transactions.reduce((acc, t) => acc + t.materialOutcome || t.price, 0) : transactions.outcome || 0;
|
||||
|
||||
let seriesData = []
|
||||
if (graphFilter == 'transactions') {
|
||||
if (graphFilter === 'transactions') {
|
||||
seriesData = [
|
||||
sumSold(dayData?.hour0To3Transactions),
|
||||
sumSold(dayData?.hour3To6Transactions),
|
||||
@@ -45,7 +59,7 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
|
||||
sumSold(dayData?.hour21To24Transactions),
|
||||
];
|
||||
}
|
||||
else if (graphFilter == 'income') {
|
||||
else if (graphFilter === 'income') {
|
||||
seriesData = [
|
||||
sumTotal(dayData?.hour0To3Transactions),
|
||||
sumTotal(dayData?.hour3To6Transactions),
|
||||
@@ -57,7 +71,7 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
|
||||
sumTotal(dayData?.hour21To24Transactions),
|
||||
];
|
||||
}
|
||||
else if (graphFilter == 'outcome') {
|
||||
else if (graphFilter === 'outcome') {
|
||||
seriesData = [
|
||||
sumOutcome(dayData?.hour0To3MaterialIds),
|
||||
sumOutcome(dayData?.hour3To6MaterialIds),
|
||||
@@ -74,7 +88,7 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
|
||||
categories,
|
||||
series: [
|
||||
{
|
||||
name: `Transactions on ${new Date(dayData.date).toLocaleDateString()}`,
|
||||
name: graphFilter === 'transactions' ? 'Transaksi' : (graphFilter === 'Pemasukan' ? 'Pemasukan' : 'Pengeluaran'),
|
||||
data: seriesData,
|
||||
},
|
||||
],
|
||||
@@ -82,10 +96,10 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
|
||||
});
|
||||
};
|
||||
|
||||
const chartData = processData(graphFilter != 'outcome' ? transactionGraph : materialGraph);
|
||||
const chartData = processData(graphFilter !== 'outcome' ? transactionGraph : materialGraph);
|
||||
|
||||
let globalMax = null;
|
||||
if (chartData)
|
||||
if (chartData) {
|
||||
globalMax = chartData.reduce(
|
||||
(max, data) => {
|
||||
const localMax = Math.max(...data.series[0].data);
|
||||
@@ -93,24 +107,21 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
|
||||
},
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
const date = new Date(dateString); // Parse the date string
|
||||
|
||||
// Create an array of month names (use the same names you had earlier)
|
||||
const date = new Date(dateString);
|
||||
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
|
||||
const month = monthNames[date.getMonth()];
|
||||
const day = date.getDate();
|
||||
return { month, day };
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${styles.chartItemContainer} ${selectedIndex !== -1 ? styles.expanded : ''}`}>
|
||||
|
||||
{chartData &&
|
||||
chartData.map((data, index) => (
|
||||
<div
|
||||
@@ -132,7 +143,6 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
|
||||
onClick={() =>
|
||||
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
|
||||
@@ -145,7 +155,6 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
|
||||
) : (
|
||||
'Hari ini'
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -154,7 +163,34 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
|
||||
<div className={styles.chartWrapper}>
|
||||
<Chart
|
||||
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: '10px',
|
||||
},
|
||||
offsetY: -10,
|
||||
background: {
|
||||
enabled: (index == chartData.length - 1 || selectedIndex != -1) ? true : false
|
||||
}
|
||||
},
|
||||
chart: {
|
||||
id: `chart-${index}`,
|
||||
type: "area",
|
||||
@@ -169,7 +205,7 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
|
||||
categories: data.categories,
|
||||
labels: {
|
||||
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 +217,9 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
|
||||
style: {
|
||||
colors: "transparent",
|
||||
},
|
||||
formatter: function (val) {
|
||||
return formatRupiah(val);
|
||||
},
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
@@ -198,10 +237,9 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DailyCharts;
|
||||
export default DailyCharts;
|
||||
@@ -1,62 +1,92 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
const CircularDiagram = ({ segments }) => {
|
||||
const radius = 70; // Radius of the circle
|
||||
const strokeWidth = 20; // Width of each portion
|
||||
const circumference = 2 * Math.PI * (radius - strokeWidth / 2);
|
||||
const HorizontalBarDiagram = ({ segments, width = 300 }) => {
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
|
||||
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 (
|
||||
<svg
|
||||
width={radius * 2}
|
||||
height={radius * 2}
|
||||
viewBox={`0 0 ${radius * 2} ${radius * 2}`}
|
||||
style={svgStyles}
|
||||
>
|
||||
<circle
|
||||
cx={radius}
|
||||
cy={radius}
|
||||
r={radius - strokeWidth / 2}
|
||||
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;
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
style={{ display: "block", margin: "0 auto" }}
|
||||
>
|
||||
{visibleSegments.map((segment, index) => {
|
||||
const { name, value, color } = segment;
|
||||
const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0;
|
||||
const barWidth = (width * percentage) / 100;
|
||||
|
||||
startOffset += segmentLength;
|
||||
return (
|
||||
<g
|
||||
key={index}
|
||||
transform={`translate(0, ${index * (barHeight + gap)})`}
|
||||
>
|
||||
{/* Background bar */}
|
||||
<rect
|
||||
x={0}
|
||||
y={0}
|
||||
width={width}
|
||||
height={barHeight}
|
||||
fill="#eee"
|
||||
rx={8}
|
||||
ry={8}
|
||||
/>
|
||||
|
||||
return (
|
||||
<circle
|
||||
key={index}
|
||||
cx={radius}
|
||||
cy={radius}
|
||||
r={radius - strokeWidth / 2}
|
||||
stroke={color}
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
strokeDasharray={`${segmentLength} ${
|
||||
circumference - segmentLength
|
||||
}`}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
strokeLinecap="round" // Rounds the edges of each segment
|
||||
transform={`rotate(-90 ${radius} ${radius})`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
{/* 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" }}
|
||||
>
|
||||
{`${name}: ${value} (${percentage}%)`}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</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;
|
||||
|
||||
125
src/pages/PrintPage.js
Normal file
125
src/pages/PrintPage.js
Normal file
@@ -0,0 +1,125 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import qs from 'qs';
|
||||
import '../print.css'; // Kamu bisa pakai styling temanmu atau buat file baru
|
||||
|
||||
export default function PrintPage() {
|
||||
const location = useLocation();
|
||||
const [orientation, setOrientation] = useState('portrait');
|
||||
|
||||
// Parse data dari query string
|
||||
const data = useMemo(() => {
|
||||
try {
|
||||
const query = qs.parse(location.search, { ignoreQueryPrefix: true });
|
||||
return JSON.parse(query.data);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}, [location.search]);
|
||||
|
||||
if (!data) return <div>Invalid data</div>;
|
||||
|
||||
const handlePrint = () => {
|
||||
window.print();
|
||||
};
|
||||
|
||||
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={handlePrint}>
|
||||
Cetak Struk ({orientation})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="print-area">
|
||||
<h2>Struk Pembayaran</h2>
|
||||
<p><strong>Transaction ID:
|
||||
</strong> {data.transactionId}</p>
|
||||
<p><strong>Waktu:</strong> {
|
||||
(() => {
|
||||
const date = new Date(data.date);
|
||||
const options = { day: '2-digit', month: 'long', year: 'numeric' };
|
||||
const tanggal = date.toLocaleDateString('id-ID', options);
|
||||
const jam = date.toLocaleTimeString('id-ID', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
return `${tanggal}, ${jam}`;
|
||||
})()
|
||||
}</p>
|
||||
|
||||
{/* <p><strong>Table:
|
||||
</strong> {data.table}</p> */}
|
||||
<p><strong>Metode Pembayaran:
|
||||
</strong> {data.payment_type}</p>
|
||||
<div>
|
||||
{data.items.map((item, idx) => (
|
||||
<p key={idx} style={{ marginBottom: '12px' }}>
|
||||
<div>{item.name} x {item.qty} Rp{item.price.toLocaleString('id-ID')}</div>
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
<h3>Total: Rp{data.total.toLocaleString('id-ID')}</h3>
|
||||
<div
|
||||
style={{
|
||||
marginTop: '24px',
|
||||
fontStyle: 'italic',
|
||||
fontSize: '12px',
|
||||
lineHeight: '1.5',
|
||||
}}
|
||||
>
|
||||
<p style={{ margin: 0 }}>Terima kasih atas kunjungan Anda!</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
fontStyle: 'italic',
|
||||
fontSize: '12px',
|
||||
lineHeight: '1.5',
|
||||
|
||||
textAlign: 'center'
|
||||
|
||||
}}
|
||||
>
|
||||
<strong style={{
|
||||
marginLeft: '-60px'
|
||||
|
||||
}}>~ Cafe Horee ~</strong>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: '24px',
|
||||
fontStyle: 'italic',
|
||||
fontSize: '12px',
|
||||
lineHeight: '1.5',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
<p style={{
|
||||
fontSize: '9px',
|
||||
marginBottom: '30px',
|
||||
marginLeft: '60px',
|
||||
|
||||
}}>www.kedaimaster.com</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -55,6 +55,9 @@ const RoundedRectangle = ({
|
||||
? "rgb(85 85 85)"
|
||||
: !isChildren && !children && backgroundColor,
|
||||
color: loading ? "transparent" : color,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
};
|
||||
|
||||
const valueAndPercentageContainerStyle = {
|
||||
@@ -91,19 +94,17 @@ const RoundedRectangle = ({
|
||||
|
||||
return (
|
||||
<div style={containerStyle} onClick={onClick}>
|
||||
<div style={titleStyle}>{title}</div>
|
||||
<div style={titleStyle}>
|
||||
{title}
|
||||
<div style={percentageStyle}>
|
||||
{loading ? "" : percentage}
|
||||
{percentage !== undefined && !loading && "%"}
|
||||
</div>
|
||||
</div>
|
||||
{!children && (
|
||||
<div style={valueAndPercentageContainerStyle}>
|
||||
<div style={valueStyle}>{loading ? "Loading..." : value}</div>
|
||||
<div style={percentageStyle}>
|
||||
{loading ? "" : percentage}
|
||||
{percentage !== undefined && !loading && "%"}
|
||||
{percentage !== undefined && !loading && (
|
||||
<span style={arrowStyle}>
|
||||
{percentage > 0 ? "↗" : percentage === 0 ? "-" : "↘"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
{children && <div>{children}</div>} {/* Properly render children */}
|
||||
@@ -118,7 +119,7 @@ const App = ({ forCafe = true, cafeId = -1,
|
||||
const [selectedCafeId, setSelectedCafeId] = useState(cafeId);
|
||||
const [analytics, setAnalytics] = useState({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filter, setFilter] = useState("monthly");
|
||||
const [filter, setFilter] = useState("yesterday");
|
||||
const [circularFilter, setCircularFilter] = useState("item");
|
||||
const [graphFilter, setGraphFilter] = useState("income");
|
||||
|
||||
@@ -195,102 +196,81 @@ const App = ({ forCafe = true, cafeId = -1,
|
||||
// Define a color palette or generate colors dynamically
|
||||
const colorPalette = colors;
|
||||
|
||||
// Ensure that each segment gets a unique color
|
||||
let colorIndex = 0;
|
||||
|
||||
console.log(filteredItems)
|
||||
let segments = (selectedCafeId == 0 || selectedCafeId == -1) ? filteredItems.flatMap((cafe) => {
|
||||
const cafeItems = cafe.report?.itemSales || [];
|
||||
console.log(cafeItems); // Log all items for the cafe
|
||||
// Segment penjualan item
|
||||
let segments =
|
||||
selectedCafeId == 0 || selectedCafeId == -1
|
||||
? filteredItems.flatMap((cafe) => {
|
||||
const cafeItems = cafe.report?.itemSales || [];
|
||||
return cafeItems.map((item) => {
|
||||
const color = colorPalette[colorIndex % colorPalette.length];
|
||||
colorIndex++;
|
||||
return {
|
||||
name: item.itemName,
|
||||
value: item.sold,
|
||||
color,
|
||||
};
|
||||
});
|
||||
})
|
||||
: filteredItems.map((item) => {
|
||||
const color = colorPalette[colorIndex % colorPalette.length];
|
||||
colorIndex++;
|
||||
return {
|
||||
name: item.itemName,
|
||||
value: item.sold,
|
||||
color,
|
||||
};
|
||||
});
|
||||
|
||||
return cafeItems.map((item, index) => {
|
||||
const percentage = totalSoldAcrossAllCafes > 0
|
||||
? ((item.sold / totalSoldAcrossAllCafes) * 100).toFixed(2)
|
||||
: 0;
|
||||
// Urutkan descending berdasarkan value
|
||||
segments.sort((a, b) => b.value - a.value);
|
||||
|
||||
console.log(`${item.itemName}: ${(percentage)}%`); // Log item name and percentage
|
||||
// Reset color index untuk material
|
||||
colorIndex = 0;
|
||||
|
||||
// Assign a unique color from the color palette
|
||||
const color = colorPalette[colorIndex % colorPalette.length]; // Use modulo to cycle through colors
|
||||
// Segment pengeluaran material
|
||||
let materialSegments =
|
||||
selectedCafeId == 0 || selectedCafeId == -1
|
||||
? filteredItems.flatMap((cafe) => {
|
||||
const cafeItems = cafe.report?.materialSpend || [];
|
||||
return cafeItems.map((item) => {
|
||||
const color = colorPalette[colorIndex % colorPalette.length];
|
||||
colorIndex++;
|
||||
return {
|
||||
name: item.materialName,
|
||||
value: item.spend,
|
||||
color,
|
||||
};
|
||||
});
|
||||
})
|
||||
: filteredItems.map((item) => {
|
||||
const color = colorPalette[colorIndex % colorPalette.length];
|
||||
colorIndex++;
|
||||
return {
|
||||
name: item.materialName,
|
||||
value: item.spend,
|
||||
color,
|
||||
};
|
||||
});
|
||||
|
||||
colorIndex++; // Increment to ensure a new color for the next item
|
||||
// Urutkan descending berdasarkan value
|
||||
materialSegments.sort((a, b) => b.value - a.value);
|
||||
|
||||
return {
|
||||
itemName: item.itemName,
|
||||
sold: item.sold,
|
||||
percentage: percentage,
|
||||
color: color,
|
||||
};
|
||||
});
|
||||
}) : filteredItems.map((item, index) => {
|
||||
const color = colorPalette[colorIndex % colorPalette.length]; // Use modulo to cycle through colors
|
||||
colorIndex++; // Increment to ensure a new color for the next item
|
||||
return {
|
||||
itemName: item.itemName,
|
||||
percentage: item.percentage,
|
||||
color: color,
|
||||
};
|
||||
});
|
||||
|
||||
segments.sort((a, b) => b.sold - a.sold);
|
||||
|
||||
|
||||
let materialSegments = (selectedCafeId == 0 || selectedCafeId == -1) ? filteredItems.flatMap((cafe) => {
|
||||
const cafeItems = cafe.report?.materialSpend || [];
|
||||
console.log(cafeItems); // Log all items for the cafe
|
||||
|
||||
return cafeItems.map((item, index) => {
|
||||
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 {
|
||||
itemName: item.materialName,
|
||||
sold: item.spend,
|
||||
percentage: percentage,
|
||||
color: color,
|
||||
};
|
||||
});
|
||||
}) : filteredItems.map((item, index) => {
|
||||
const color = colorPalette[colorIndex % colorPalette.length]; // Use modulo to cycle through colors
|
||||
colorIndex++; // Increment to ensure a new color for the next item
|
||||
return {
|
||||
itemName: item.materialName,
|
||||
percentage: item.percentage,
|
||||
color: color,
|
||||
};
|
||||
});
|
||||
|
||||
materialSegments.sort((a, b) => b.spend - a.spend);
|
||||
|
||||
|
||||
console.log(selectedCafeId)
|
||||
console.log(segments)
|
||||
|
||||
const formatIncome = (amount) => {
|
||||
if (amount >= 1_000_000_000) {
|
||||
// Format for billions
|
||||
const billions = amount / 1_000_000_000;
|
||||
return billions.toFixed(0) + "m"; // No decimal places for billions
|
||||
} else if (amount >= 1_000_000) {
|
||||
// Format for millions
|
||||
const millions = amount / 1_000_000;
|
||||
return millions.toFixed(2).replace(/\.00$/, "") + "jt"; // Two decimal places, remove trailing '.00'
|
||||
} else if (amount >= 1_000) {
|
||||
// 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();
|
||||
}
|
||||
if (amount == null) return "0";
|
||||
|
||||
const formatter = new Intl.NumberFormat("id-ID", {
|
||||
style: "currency",
|
||||
currency: "IDR",
|
||||
minimumFractionDigits: 0,
|
||||
});
|
||||
|
||||
return formatter.format(amount);
|
||||
};
|
||||
|
||||
function roundToInteger(num) {
|
||||
@@ -478,7 +458,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>}
|
||||
<div style={{ marginTop: '10px' }}>
|
||||
{!forCafe &&
|
||||
<div className={styles.dateSelectorWrapper} style={{ fontSize: '12px' }}>
|
||||
<div className={styles.dateSelectorWrapper} style={{ fontSize: '16px', textTransform: 'uppercase' }}>
|
||||
{texts.map((item, indexx) => {
|
||||
return (
|
||||
<div
|
||||
@@ -531,7 +511,7 @@ const App = ({ forCafe = true, cafeId = -1,
|
||||
<RoundedRectangle
|
||||
title="Pendapatan"
|
||||
fontSize="12px"
|
||||
value={!loading && "Rp" + formatIncome(analytics?.currentTotals?.income)}
|
||||
value={!loading && formatIncome(analytics?.currentTotals?.income)}
|
||||
percentage={roundToInteger(analytics?.growth?.incomeGrowth)}
|
||||
invert={false}
|
||||
loading={loading}
|
||||
@@ -543,7 +523,7 @@ const App = ({ forCafe = true, cafeId = -1,
|
||||
<RoundedRectangle
|
||||
title="Pengeluaran"
|
||||
fontSize="12px"
|
||||
value={!loading && "Rp" + formatIncome(analytics?.currentTotals?.outcome)}
|
||||
value={!loading && formatIncome(analytics?.currentTotals?.outcome)}
|
||||
percentage={roundToInteger(analytics?.growth?.outcomeGrowth)}
|
||||
invert={true}
|
||||
loading={loading}
|
||||
@@ -614,8 +594,8 @@ const App = ({ forCafe = true, cafeId = -1,
|
||||
7 hari terakhir dengan 7 hari sebelumnya, dengan penghitungan dimulai dari data kemarin.`
|
||||
:
|
||||
(filter == 'yesterday') ? `Data dihitung dengan membandingkan antara hari ini dan kemarin.`
|
||||
:
|
||||
(filter == 'monthly') ? `Data dihitung dengan membandingkan antara awal hingga akhir bulan ini dan bulan lalu, dengan penghitungan berakhir pada data kemarin.` : `Data dihitung dengan membandingkan antara awal hingga akhir tahun ini dan tahun lalu, dengan penghitungan berakhir pada data kemarin.`}
|
||||
:
|
||||
(filter == 'monthly') ? `Data dihitung dengan membandingkan antara awal hingga akhir bulan ini dan bulan lalu, dengan penghitungan berakhir pada data kemarin.` : `Data dihitung dengan membandingkan antara awal hingga akhir tahun ini dan tahun lalu, dengan penghitungan berakhir pada data kemarin.`}
|
||||
</h6>
|
||||
</div>
|
||||
</div>
|
||||
@@ -653,29 +633,6 @@ const App = ({ forCafe = true, cafeId = -1,
|
||||
<div style={{ flex: 1 }}>
|
||||
<CircularDiagram segments={circularFilter == 'item' ? segments : materialSegments} />
|
||||
</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 className={styles.filterSelectorWrapper}>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
import qs from 'qs';
|
||||
|
||||
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 {
|
||||
getTransaction,
|
||||
@@ -25,6 +27,9 @@ export default function Transactions({ propsShopId, sendParam, deviceType, handl
|
||||
|
||||
const [transactionRefreshKey, setTransactionRefreshKey] = useState(0);
|
||||
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const transactionId = searchParams.get("transactionId") || "";
|
||||
|
||||
@@ -115,10 +120,29 @@ export default function Transactions({ propsShopId, sendParam, deviceType, handl
|
||||
autoResizeTextArea(noteRef.current);
|
||||
}
|
||||
}, [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
|
||||
navigate(`/print?${queryString}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={transactionRefreshKey} className={styles.Transaction}>
|
||||
|
||||
@@ -316,7 +340,7 @@ export default function Transactions({ propsShopId, sendParam, deviceType, handl
|
||||
{transaction.confirmed > 1 && (
|
||||
<h5
|
||||
className={styles.DeclineButton}
|
||||
onClick={() => handlePrint()}
|
||||
onClick={() => handlePrint(transaction)}
|
||||
>
|
||||
Cetak struk
|
||||
</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 { useParams } from "react-router-dom";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { ColorRing } from "react-loader-spinner";
|
||||
import {
|
||||
getMyTransactions,
|
||||
@@ -30,6 +31,9 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setMatchedItems(searchAndAggregateItems(transactions, searchTerm));
|
||||
}, [searchTerm, transactions]);
|
||||
@@ -58,51 +62,51 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
|
||||
return total + dt.qty * (dt.promoPrice ? dt.promoPrice : dt.price);
|
||||
}, 0);
|
||||
};
|
||||
const calculateAllTransactionsTotal = (transactions) => {
|
||||
return transactions
|
||||
.filter(transaction => transaction.confirmed > 1) // Filter transactions where confirmed > 1
|
||||
.reduce((grandTotal, transaction) => {
|
||||
return grandTotal + calculateTotalPrice(transaction.DetailedTransactions);
|
||||
}, 0);
|
||||
};
|
||||
const searchAndAggregateItems = (transactions, searchTerm) => {
|
||||
if (!searchTerm.trim()) return [];
|
||||
const calculateAllTransactionsTotal = (transactions) => {
|
||||
return transactions
|
||||
.filter(transaction => transaction.confirmed > 1) // Filter transactions where confirmed > 1
|
||||
.reduce((grandTotal, transaction) => {
|
||||
return grandTotal + calculateTotalPrice(transaction.DetailedTransactions);
|
||||
}, 0);
|
||||
};
|
||||
const searchAndAggregateItems = (transactions, searchTerm) => {
|
||||
if (!searchTerm.trim()) return [];
|
||||
|
||||
const normalizedTerm = searchTerm.trim().toLowerCase();
|
||||
// Map with key = `${itemId}-${confirmedGroup}` to keep confirmed groups separate
|
||||
const aggregatedItems = new Map();
|
||||
const normalizedTerm = searchTerm.trim().toLowerCase();
|
||||
// Map with key = `${itemId}-${confirmedGroup}` to keep confirmed groups separate
|
||||
const aggregatedItems = new Map();
|
||||
|
||||
transactions.forEach(transaction => {
|
||||
// Determine confirmed group as a string key
|
||||
const confirmedGroup = transaction.confirmed >= 0 && transaction.confirmed > 1 ? 'confirmed_gt_1' : 'confirmed_le_1';
|
||||
transactions.forEach(transaction => {
|
||||
// Determine confirmed group as a string key
|
||||
const confirmedGroup = transaction.confirmed >= 0 && transaction.confirmed > 1 ? 'confirmed_gt_1' : 'confirmed_le_1';
|
||||
|
||||
transaction.DetailedTransactions.forEach(detail => {
|
||||
const itemName = detail.Item.name;
|
||||
const itemNameLower = itemName.toLowerCase();
|
||||
transaction.DetailedTransactions.forEach(detail => {
|
||||
const itemName = detail.Item.name;
|
||||
const itemNameLower = itemName.toLowerCase();
|
||||
|
||||
if (itemNameLower.includes(normalizedTerm)) {
|
||||
// Combine itemId and confirmedGroup to keep them separated
|
||||
const key = `${detail.itemId}-${confirmedGroup}`;
|
||||
if (itemNameLower.includes(normalizedTerm)) {
|
||||
// Combine itemId and confirmedGroup to keep them separated
|
||||
const key = `${detail.itemId}-${confirmedGroup}`;
|
||||
|
||||
if (!aggregatedItems.has(key)) {
|
||||
aggregatedItems.set(key, {
|
||||
itemId: detail.itemId,
|
||||
name: itemName,
|
||||
totalQty: 0,
|
||||
totalPrice: 0,
|
||||
confirmedGroup, // Keep track of which group this belongs to
|
||||
});
|
||||
if (!aggregatedItems.has(key)) {
|
||||
aggregatedItems.set(key, {
|
||||
itemId: detail.itemId,
|
||||
name: itemName,
|
||||
totalQty: 0,
|
||||
totalPrice: 0,
|
||||
confirmedGroup, // Keep track of which group this belongs to
|
||||
});
|
||||
}
|
||||
|
||||
const current = aggregatedItems.get(key);
|
||||
current.totalQty += detail.qty;
|
||||
current.totalPrice += detail.qty * (detail.promoPrice || detail.price);
|
||||
}
|
||||
|
||||
const current = aggregatedItems.get(key);
|
||||
current.totalQty += detail.qty;
|
||||
current.totalPrice += detail.qty * (detail.promoPrice || detail.price);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
console.log(aggregatedItems.values())
|
||||
return Array.from(aggregatedItems.values());
|
||||
};
|
||||
console.log(aggregatedItems.values())
|
||||
return Array.from(aggregatedItems.values());
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -142,6 +146,27 @@ console.log(aggregatedItems.values())
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
navigate(`/print?${queryString}`);
|
||||
};
|
||||
if (loading)
|
||||
return (
|
||||
<div className="Loader">
|
||||
@@ -172,8 +197,8 @@ console.log(aggregatedItems.values())
|
||||
|
||||
{matchedItems.length > 0 && matchedItems.map(item => (
|
||||
<div
|
||||
key={`${item.itemId}-${item.confirmedGroup}`}
|
||||
className={styles.RoundedRectangle}
|
||||
key={`${item.itemId}-${item.confirmedGroup}`}
|
||||
className={styles.RoundedRectangle}
|
||||
style={{ overflow: "hidden" }}
|
||||
>
|
||||
<ul>
|
||||
@@ -356,26 +381,37 @@ console.log(aggregatedItems.values())
|
||||
|
||||
<div className={styles.TotalContainer}>
|
||||
{(deviceType == 'clerk' && !transaction.is_paid && (transaction.confirmed == 0 || transaction.confirmed == 1 || transaction.confirmed == 2)) &&
|
||||
<button
|
||||
className={styles.PayButton}
|
||||
onClick={() => handleConfirm(transaction.transactionId)}
|
||||
disabled={isPaymentLoading}
|
||||
>
|
||||
{
|
||||
isPaymentLoading ? (
|
||||
<ColorRing height="50" width="50" color="white" />
|
||||
) : transaction.confirmed === 1 ? (
|
||||
"Konfirmasi Telah Bayar"
|
||||
) : transaction.confirmed === 2 ? (
|
||||
"Confirm item is ready"
|
||||
) : (
|
||||
"Confirm availability"
|
||||
)
|
||||
}
|
||||
<>
|
||||
<button
|
||||
className={styles.PayButton}
|
||||
onClick={() => handleConfirm(transaction.transactionId)}
|
||||
disabled={isPaymentLoading}
|
||||
>
|
||||
{
|
||||
isPaymentLoading ? (
|
||||
<ColorRing height="50" width="50" color="white" />
|
||||
) : transaction.confirmed === 1 ? (
|
||||
"Konfirmasi Telah Bayar"
|
||||
) : transaction.confirmed === 2 ? (
|
||||
"Confirm item is ready"
|
||||
) : (
|
||||
"Confirm availability"
|
||||
)
|
||||
}
|
||||
|
||||
</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' &&
|
||||
<ButtonWithReplica
|
||||
paymentUrl={paymentUrl}
|
||||
|
||||
@@ -403,48 +403,58 @@
|
||||
display: none; /* Hidden in normal view */
|
||||
}
|
||||
|
||||
/* Print-specific styles */
|
||||
|
||||
@media print {
|
||||
.Transaction {
|
||||
display: none; /* Hide everything else when printing */
|
||||
@page {
|
||||
size: portrait;
|
||||
margin: 15mm;
|
||||
}
|
||||
|
||||
.printContainer {
|
||||
display: block !important;
|
||||
position: static;
|
||||
background: white;
|
||||
color: black;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
padding: 20px;
|
||||
body * {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.receipt {
|
||||
#print-section,
|
||||
#print-section * {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
#print-section {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.center-text {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.item-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.total {
|
||||
text-align: right;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
button, .DeclineButton, .addNewItem {
|
||||
display: none !important;
|
||||
padding: 15mm;
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.receipt {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 400px;
|
||||
margin: auto;
|
||||
color: #000;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.receipt hr {
|
||||
border: none;
|
||||
border-top: 1px dashed #aaa;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.receipt .center-text {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.receipt .item-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.receipt .total {
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
139
src/print.css
Normal file
139
src/print.css
Normal file
@@ -0,0 +1,139 @@
|
||||
.print-test {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
.controls {
|
||||
background-color: #2c3e50;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
width: 90%;
|
||||
max-width: 800px;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.controls h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.orientation-selector {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.orientation-selector button {
|
||||
background-color: #61dafb;
|
||||
color: #282c34;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
margin: 0 10px;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.orientation-selector button.active {
|
||||
background-color: #21a9c7;
|
||||
color: white;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.orientation-selector button:hover:not(.active) {
|
||||
background-color: #4bc5e0;
|
||||
}
|
||||
|
||||
.print-button {
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
font-size: 18px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin-top: 15px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.print-button:hover {
|
||||
background-color: #2980b9;
|
||||
}
|
||||
|
||||
.print-area {
|
||||
background-color: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
width: 90%;
|
||||
max-width: 800px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
/* Orientation styles */
|
||||
.print-test.portrait .print-area {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.print-test.landscape .print-area {
|
||||
max-width: 1100px;
|
||||
}
|
||||
|
||||
/* Print specific styles */
|
||||
@media print {
|
||||
@page {
|
||||
margin: 58mm;
|
||||
}
|
||||
.print-test.portrait @page {
|
||||
size: portrait;
|
||||
}
|
||||
.print-test.landscape @page {
|
||||
size: landscape;
|
||||
}
|
||||
body {
|
||||
background-color: white;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.controls {
|
||||
display: none;
|
||||
}
|
||||
.print-area {
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 15mm;
|
||||
}
|
||||
body * {
|
||||
visibility: hidden;
|
||||
}
|
||||
.print-area, .print-area * {
|
||||
visibility: visible;
|
||||
}
|
||||
.print-area {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.print-area {
|
||||
font-size: 12px; /* lebih kecil dari default */
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.print-area h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.print-area h3 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user