Compare commits

...

3 Commits

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

89
package-lock.json generated
View File

@@ -18,9 +18,11 @@
"crypto-js": "^4.2.0",
"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",

View File

@@ -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",

View File

@@ -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={
@@ -811,8 +819,10 @@ function App() {
welcomePageConfig={shop.welcomePageConfig}
onClose={closeModal}
setModal={setModal}
setIsModalOpen={setIsModalOpen}
onModalCloseFunction={onModalCloseFunction}
onModalYesFunction={onModalYesFunction}
shopIdentifier={shopIdentifier}
/>
</div>
);

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect } from "react";
import dayjs from "dayjs";
import Chart from "react-apexcharts";
import styles from "./BarChart.module.css"; // Import the CSS module
@@ -7,7 +8,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 +37,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;
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;
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;
Array.isArray(transactions) ? transactions.reduce((acc, t) => acc + (t.materialOutcome || t.price * t.stockDifference), 0) : transactions?.outcome || 0;
let seriesData = []
if (graphFilter == 'transactions') {
if (graphFilter === 'transactions') {
seriesData = [
sumSold(dayData?.hour0To3Transactions),
sumSold(dayData?.hour3To6Transactions),
@@ -45,7 +60,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 +72,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),
@@ -69,23 +84,26 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
sumOutcome(dayData?.hour21To24MaterialIds),
];
}
let totalValue = seriesData.reduce((acc, val) => acc + val, 0);
return {
date: new Date(dayData.date).toLocaleDateString(),
date: dayData.date,
categories,
series: [
{
name: `Transactions on ${new Date(dayData.date).toLocaleDateString()}`,
name: graphFilter === 'transactions' ? 'Transaksi' : (graphFilter === 'income' ? 'Pemasukan' : 'Pengeluaran'),
data: seriesData,
},
],
totalValue, // ⬅️ Tambahkan ini
};
});
};
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 +111,17 @@ 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 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 d = dayjs(dateString, ["YYYY-MM-DD", "YYYY-MM-DDTHH:mm:ssZ"]);
return { month: d.format("MMM"), day: d.format("D") };
};
return (
<div className={`${styles.chartItemContainer} ${selectedIndex !== -1 ? styles.expanded : ''}`}>
{chartData &&
chartData.map((data, index) => (
<div
@@ -128,33 +139,78 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
key={indexx}
className={`${styles.dateSelector} ${index === indexx ? styles.dateSelectorActive : styles.dateSelectorInactive
}`}
style={{ position: 'relative' }}
style={{ position: 'relative', width: 'calc(100% / 7)' }}
onClick={() =>
type == 'yesterday' && selectedIndex == -1 || type != 'yesterday' && selectedIndex !== index ? setSelectedIndex(index) : setSelectedIndex(-1)
}
// 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
style={{ color: index === indexx ? 'black' : 'transparent' }}>
{indexx !== chartData.length - 1 ? (
<>
{day}{" "}
{(indexx === 0 || (formatDate(chartData[indexx - 1].date).month !== month && type != 'weekly')) && month}
</>
<p style={{ fontSize: '13px' }}>{day}{" "}
{(
indexx === 0 ||
(indexx > 0 &&
dayjs(chartData[indexx - 1].date).month() !== dayjs(item.date).month() &&
type !== "weekly")
) && month}
</p>
) : (
'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 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: '7px',
},
offsetY: -10,
background: {
enabled: (index == chartData.length - 1 || selectedIndex != -1) ? true : false
}
},
chart: {
id: `chart-${index}`,
type: "area",
@@ -169,7 +225,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 +237,9 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
style: {
colors: "transparent",
},
formatter: function (val) {
return formatRupiah(val);
},
},
},
grid: {
@@ -198,7 +257,6 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
/>
</div>
</div>
))}
</div>
);

View File

@@ -35,7 +35,7 @@ import CreateCoupon from "../pages/CreateCoupon";
import CheckCoupon from "../pages/CheckCoupon";
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 [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 === "edit_tables" && <TablesPage shop={shop} />}
{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" && (
<Transaction propsShopId={shop.cafeId} />
<Transaction propsShopId={shop.cafeId} setIsModalOpen={setIsModalOpen} cafeIdentityName={shopIdentifier} />
)}
{modalContent === "transaction_pending" && <Transaction_pending deviceType={deviceType} setModal={setModal}/>}
{modalContent === "transaction_item" && <Transaction_item />}

View File

@@ -7,175 +7,187 @@ const PeriodCharts = ({ type, graphFilter, aggregatedCurrentReports, aggregatedP
useEffect(() => {
setSelectedIndex(-1);
}, [aggregatedCurrentReports, aggregatedPreviousReports]);
}, [aggregatedCurrentReports, aggregatedPreviousReports, graphFilter]);
const monthly = ["1 - 7", "8 - 14", "15 - 21", "22 - 28", "29 - 31"];
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) {
currentIncomeData = aggregatedCurrentReports.map((report) => report.income);
currentOutcomeData = aggregatedCurrentReports.map((report) => report.outcome);
currentTransactionData = aggregatedCurrentReports.map((report) => report.transactions);
currentIncomeData = aggregatedCurrentReports.map((r) => r.income);
currentOutcomeData = aggregatedCurrentReports.map((r) => r.outcome);
currentTransactionData = aggregatedCurrentReports.map((r) => r.transactions);
if (type == 'monthly' && currentIncomeData.length === 4) {
if (type === "monthly" && currentIncomeData.length === 4) {
currentIncomeData.push(null);
}
if (type == 'monthly' && currentOutcomeData.length === 4) {
currentOutcomeData.push(null);
}
if (type == 'monthly' && currentTransactionData.length === 4) {
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);
}
if (type == 'monthly' && previousOutcomeData.length === 4) {
previousOutcomeData.push(null);
}
if (type == 'monthly' && previousTransactionData.length === 4) {
previousTransactionData.push(null);
}
}
let globalMax = null;
if (aggregatedCurrentReports || aggregatedPreviousReports) {
// Find the global maximum for the y-axis
globalMax = Math.max(
...(graphFilter === 'income'
? [...currentIncomeData, ...previousIncomeData]
: graphFilter === 'outcome'
? [...currentOutcomeData, ...previousOutcomeData]
: [...currentTransactionData, ...previousTransactionData])
);
}
// cari global max untuk y-axis
let globalMax = 0;
if (aggregatedCurrentReports || aggregatedPreviousReports) {
const dataset =
graphFilter === "income"
? [...(currentIncomeData || []), ...(previousIncomeData || [])]
: graphFilter === "outcome"
? [...(currentOutcomeData || []), ...(previousOutcomeData || [])]
: [...(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 (
<div className={`${styles.chartItemContainer} ${selectedIndex !== -1 ? styles.expanded : ''}`}>
{aggregatedPreviousReports && (
<div className={`${styles.chartItemWrapper} ${selectedIndex !== -1 && selectedIndex !== 0
<div
className={`${styles.chartItemContainer} ${selectedIndex !== -1 ? styles.expanded : ""
}`}
>
{[aggregatedPreviousReports, aggregatedCurrentReports].map(
(dataset, i) =>
dataset && (
<div
key={i}
className={`${styles.chartItemWrapper} ${selectedIndex !== -1 && selectedIndex !== i
? styles.chartItemWrapperActive
: 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={() =>
selectedIndex === 0 ? setSelectedIndex(-1) : setSelectedIndex(1)
}>
<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'],
selectedIndex == idx ? setSelectedIndex(-1) : setSelectedIndex(idx)
}
}
},
yaxis: { max: globalMax, min: 0, labels: {
maxWidth: 20, style: { colors: "transparent" } } },
grid: { show: false },
fill: { opacity: 0.5 },
colors: [colors[0]],
>
{idx === i && (
<div
style={{
position: "absolute",
bottom: 0,
left: "10%",
right: "10%",
borderBottom: `2px solid ${colors[i]}`,
}}
series={[
// { 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>
></div>
)}
{aggregatedCurrentReports && (
<div className={`${styles.chartItemWrapper} ${selectedIndex !== -1 && selectedIndex !== 1
? styles.chartItemWrapperActive
: styles.chartItemWrapperInactive
}`}>
<div className={styles.dateSelectorWrapper}>
<div
className={`${styles.dateSelector} ${styles.dateSelectorInactive
}`}
onClick={() =>
selectedIndex === 1 ? setSelectedIndex(-1) : setSelectedIndex(0)
}>
<div>{type == 'monthly' ? 'bulan lalu' : 'tahun lalu'}</div>
<div style={{ color: idx === i ? "black" : "transparent" }}>
{type === "monthly"
? idx === 0
? "bulan lalu"
: "bulan ini"
: idx === 0
? "tahun lalu"
: "tahun ini"}
</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}>
<Chart
options={{
tooltip: { enabled: false },
chart: { type: "area", zoom: { enabled: false }, toolbar: { show: false } },
tooltip: {
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: {
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', 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 },
fill: { opacity: 0.5 },
colors: [colors[1]],
colors: [colors[i]],
}}
series={[
// { name: "Pemasukan", data: currentIncomeData },
// { name: "Pengeluaran", data: currentOutcomeData },
{ name: "Total transaksi", data: graphFilter == 'income' ? currentIncomeData : graphFilter == 'outcome' ? currentOutcomeData : currentTransactionData },
{
name:
graphFilter === "transactions"
? "Transaksi"
: graphFilter === "income"
? "Pemasukan"
: "Pengeluaran",
data: getSeries(i === 1),
},
]}
type="area"
height={200}
@@ -183,6 +195,7 @@ if (aggregatedCurrentReports || aggregatedPreviousReports) {
/>
</div>
</div>
)
)}
</div>
);

View File

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

View File

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

View File

@@ -70,7 +70,7 @@ export default function Invoice({
if (lastTransaction?.payment_type == "paylater")
methods.isOpenBillAvailable = false;
setPaymentMethods(methods);
} catch (err) {}
} catch (err) { }
};
if (shopId) {
@@ -275,6 +275,75 @@ export default function Invoice({
);
}
};
const handlePrint = (transaction) => {
console.log(transaction)
const formatWaktu = (() => {
const date = transaction?.createdAt
? new Date(transaction.createdAt)
: new Date(); // UTC now
const tanggal = date.toLocaleDateString("id-ID");
const jam = date.toLocaleTimeString("id-ID", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
return `${tanggal} ${jam}`;
})();
const itemsStr = transaction.DetailedTransactions.map((dt) => {
const name =
dt.Item.name.length > 11
? dt.Item.name.slice(0, 11)
: dt.Item.name.padEnd(11);
const qty = dt.qty.toString().padStart(3);
const total = formatRupiah(dt.qty * (dt.promoPrice || dt.price)).padStart(15);
return `${name} ${qty} ${total}`;
}).join("\n");
const totalHarga = transaction.DetailedTransactions.reduce((acc, dt) => {
return acc + dt.qty * (dt.promoPrice || dt.price);
}, 0);
const totalStr = `Total: ${formatRupiah(totalHarga)}`;
const receiptText = ` CAFE HOREE
Jl. Ahmad Yani No. 12, Kediri
Telp: 0812-1617-6963
==============================
Tanggal : ${formatWaktu}
Bayar : ${transaction.payment_type}
------------------------------
Item Qty Total
------------------------------
${itemsStr}
${totalStr}
==============================
Terima kasih atas kunjungannya!
~~
supported by kedaimaster.com
\n\n\n\n\n`;
const params = new URLSearchParams();
params.append("content", receiptText);
params.append("encode_format", "UTF-8");
const printUrl = `btprinter://print?${params.toString()}`;
window.location.href = printUrl;
};
const formatRupiah = (value) => {
if (typeof value !== "number") return value;
return value.toLocaleString("id-ID", {
style: "currency",
currency: "IDR",
minimumFractionDigits: 0,
});
};
const handlePay = async (orderMethod) => {
setIsPaymentLoading(true);
@@ -300,9 +369,16 @@ export default function Invoice({
tableNumber,
textareaRef.current.value
);
if (pay) window.location.reload();
} else if (deviceType == "guestSide") {
if (pay) {
handlePrint(pay.transaction);
localStorage.removeItem("cart");
localStorage.removeItem("lastTransaction");
setCartItems([]);
setTotalPrice(0);
window.dispatchEvent(new Event("localStorageUpdated"));
}
}
else if (deviceType == "guestSide") {
const pay = await handlePaymentFromGuestSide(
shopId,
email,
@@ -437,7 +513,7 @@ export default function Invoice({
>
<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>
}
}
Keranjang
</div>
@@ -452,7 +528,7 @@ export default function Invoice({
alignItems: "center",
}}
>
<div style={{ width: isTablet ? "30%":"50%" }}>
<div style={{ width: isTablet ? "30%" : "50%" }}>
<svg viewBox="0 0 32 32" style={{ fill: "#8F8787" }}>
<path d="M9.79175 24.75C8.09591 24.75 6.72383 26.1375 6.72383 27.8333C6.72383 29.5292 8.09591 30.9167 9.79175 30.9167C11.4876 30.9167 12.8751 29.5292 12.8751 27.8333C12.8751 26.1375 11.4876 24.75 9.79175 24.75ZM0.541748 0.0833435V3.16668H3.62508L9.17508 14.8679L7.09383 18.645C6.84717 19.0767 6.70842 19.5854 6.70842 20.125C6.70842 21.8208 8.09591 23.2083 9.79175 23.2083H28.2917V20.125H10.4392C10.2234 20.125 10.0538 19.9554 10.0538 19.7396L10.1001 19.5546L11.4876 17.0417H22.973C24.1292 17.0417 25.1467 16.4096 25.6709 15.4538L31.1901 5.44834C31.3134 5.23251 31.3751 4.97043 31.3751 4.70834C31.3751 3.86043 30.6813 3.16668 29.8334 3.16668H7.03217L5.583 0.0833435H0.541748ZM25.2084 24.75C23.5126 24.75 22.1405 26.1375 22.1405 27.8333C22.1405 29.5292 23.5126 30.9167 25.2084 30.9167C26.9042 30.9167 28.2917 29.5292 28.2917 27.8333C28.2917 26.1375 26.9042 24.75 25.2084 24.75Z" />
</svg>

View File

@@ -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 (
<div style={{ textAlign: "center" }}>
<svg
width={radius * 2}
height={radius * 2}
viewBox={`0 0 ${radius * 2} ${radius * 2}`}
style={svgStyles}
width={width}
height={height}
style={{ display: "block", margin: "0 auto" }}
>
<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;
startOffset += segmentLength;
{visibleSegments.map((segment, index) => {
const { name, value, color, unit} = segment;
const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0;
const barWidth = (width * percentage) / 100;
return (
<circle
<g
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})`}
transform={`translate(0, ${index * (barHeight + gap)})`}
>
{/* Background bar */}
<rect
x={0}
y={0}
width={width}
height={barHeight}
fill="#eee"
rx={8}
ry={8}
/>
{/* 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>
{/* Tombol lihat lebih banyak / lebih sedikit */}
{segments.length > 5 && (
<button
onClick={() => setShowAll(!showAll)}
style={{
marginTop: "8px",
padding: "6px 12px",
fontSize: "12px",
border: "1px solid #ccc",
borderRadius: "6px",
background: "#f9f9f9",
cursor: "pointer",
}}
>
{showAll ? "Lihat lebih sedikit" : "Lihat lebih banyak"}
</button>
)}
</div>
);
};
export default CircularDiagram;
export default HorizontalBarDiagram;

View File

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

View File

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

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

@@ -0,0 +1,102 @@
import React, { useState, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import qs from 'qs';
import '../print.css';
export default function PrintPage() {
const location = useLocation();
const [orientation, setOrientation] = useState('portrait');
const data = useMemo(() => {
try {
const query = qs.parse(location.search, { ignoreQueryPrefix: true });
return JSON.parse(query.data);
} catch (err) {
return null;
}
}, [location.search]);
if (!data) return <div>Invalid data</div>;
const formatWaktu = (() => {
const date = new Date(data.date);
const tanggal = date.toLocaleDateString('id-ID');
const jam = date.toLocaleTimeString('id-ID', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
});
return `${tanggal} ${jam}`;
})();
const itemsStr = data.items.map((item) => {
const name = item.name.length > 11 ? item.name.slice(0, 11) : item.name.padEnd(11);
const qty = item.qty.toString().padStart(2);
const total = (item.qty * item.price).toString().padStart(6);
return `${name} ${qty} ${total}`;
}).join('\n');
const getReceiptText = () => {
return (
` CAFE HOREE
Jl. Merdeka No. 123, Jakarta
Telp: (021) 12345678
==============================
Tanggal : ${formatWaktu}
Kasir : ${data.cashier || 'UNKNOWN'}
Bayar : ${data.payment_type}
------------------------------
Item Q Total
------------------------------
${itemsStr}
------------------------------
Terima kasih atas kunjungan Anda!
~ Cafe Horee ~
www.kedaimaster.com`
);
};
const handlePrintBluetooth = () => {
const content = getReceiptText();
const params = new URLSearchParams();
params.append("content", content);
params.append("encode_format", "UTF-8");
// Optional: jika ingin spesifik printer Bluetooth
// params.append("device_address", "00:11:22:33:44:55");
const printUrl = `btprinter://print?${params.toString()}`;
window.location.href = printUrl;
};
return (
<div className={`print-test ${orientation}`}>
<div className="controls">
<h1>CAFE HOREE - Mode {orientation.charAt(0).toUpperCase() + orientation.slice(1)}</h1>
<div className="orientation-selector">
<button
className={orientation === 'portrait' ? 'active' : ''}
onClick={() => setOrientation('portrait')}
>
Portrait
</button>
<button
className={orientation === 'landscape' ? 'active' : ''}
onClick={() => setOrientation('landscape')}
>
Landscape
</button>
</div>
<button className="print-button" onClick={handlePrintBluetooth}>
🖨 Print ke Bluetooth
</button>
</div>
<pre className="print-area">
{getReceiptText()}
</pre>
</div>
);
}

View File

@@ -55,6 +55,9 @@ const RoundedRectangle = ({
? "rgb(85 85 85)"
: !isChildren && !children && backgroundColor,
color: loading ? "transparent" : color,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
};
const valueAndPercentageContainerStyle = {
@@ -91,20 +94,18 @@ const RoundedRectangle = ({
return (
<div style={containerStyle} onClick={onClick}>
<div style={titleStyle}>{title}</div>
{!children && (
<div style={valueAndPercentageContainerStyle}>
<div style={valueStyle}>{loading ? "Loading..." : value}</div>
<div style={titleStyle}>
{title}
<div style={percentageStyle}>
{loading ? "" : percentage}
{percentage !== undefined && !loading && "%"}
{percentage !== undefined && !loading && (
<span style={arrowStyle}>
{percentage > 0 ? "↗" : percentage === 0 ? "-" : "↘"}
</span>
)}
</div>
</div>
{!children && (
<div style={valueAndPercentageContainerStyle}>
<div style={valueStyle}>{loading ? "Loading..." : value}</div>
</div>
)}
{children && <div>{children}</div>} {/* Properly render children */}
</div>
@@ -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,84 @@ 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) => {
// Segment penjualan item
let segments =
selectedCafeId == 0 || selectedCafeId == -1
? filteredItems.flatMap((cafe) => {
const cafeItems = cafe.report?.itemSales || [];
console.log(cafeItems); // Log all items for the cafe
return cafeItems.map((item, index) => {
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 cafeItems.map((item) => {
const color = colorPalette[colorIndex % colorPalette.length];
colorIndex++;
return {
itemName: item.itemName,
sold: item.sold,
percentage: percentage,
color: color,
name: item.itemName,
value: item.sold,
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
})
: filteredItems.map((item) => {
const color = colorPalette[colorIndex % colorPalette.length];
colorIndex++;
return {
itemName: item.itemName,
percentage: item.percentage,
color: color,
name: item.itemName,
value: item.sold,
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 || [];
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 cafeItems.map((item) => {
const color = colorPalette[colorIndex % colorPalette.length];
colorIndex++;
return {
itemName: item.materialName,
sold: item.spend,
percentage: percentage,
color: color,
name: item.materialName,
value: item.spend,
unit: item.unit,
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
})
: filteredItems.map((item) => {
const color = colorPalette[colorIndex % colorPalette.length];
colorIndex++;
return {
itemName: item.materialName,
percentage: item.percentage,
color: color,
name: item.materialName,
value: item.spend,
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(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 +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>}
<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 +514,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 +526,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}
@@ -653,29 +636,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}>

View File

@@ -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,
@@ -11,7 +13,7 @@ import { getTables } from "../helpers/tableHelper";
import TableCanvas from "../components/TableCanvas";
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();
if (sendParam) sendParam({ shopId, tableId });
@@ -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,30 @@ 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
setIsModalOpen(false);
navigate(`/${cafeIdentityName}/print?${queryString}`);
};
return (
<div key={transactionRefreshKey} className={styles.Transaction}>
@@ -316,7 +341,7 @@ export default function Transactions({ propsShopId, sendParam, deviceType, handl
{transaction.confirmed > 1 && (
<h5
className={styles.DeclineButton}
onClick={() => handlePrint()}
onClick={() => handlePrint(transaction)}
>
Cetak struk
</h5>

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useState } from "react"; import qs from 'qs';
import styles from "./Transactions.module.css";
import { 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,14 +62,20 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
return total + dt.qty * (dt.promoPrice ? dt.promoPrice : dt.price);
}, 0);
};
const calculateAllTransactionsTotal = (transactions) => {
const formatRupiah = (number) => {
return 'Rp' + number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.');
};
const calculateAllTransactionsTotal = (transactions) => {
return transactions
.filter(transaction => transaction.confirmed > 1) // Filter transactions where confirmed > 1
.reduce((grandTotal, transaction) => {
return grandTotal + calculateTotalPrice(transaction.DetailedTransactions);
}, 0);
};
const searchAndAggregateItems = (transactions, searchTerm) => {
};
const searchAndAggregateItems = (transactions, searchTerm) => {
if (!searchTerm.trim()) return [];
const normalizedTerm = searchTerm.trim().toLowerCase();
@@ -100,9 +110,9 @@ const searchAndAggregateItems = (transactions, searchTerm) => {
}
});
});
console.log(aggregatedItems.values())
console.log(aggregatedItems.values())
return Array.from(aggregatedItems.values());
};
};
@@ -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)
return (
<div className="Loader">
@@ -154,7 +222,7 @@ console.log(aggregatedItems.values())
return (
<div className={styles.Transactions}>
<h2 className={styles["Transactions-title"]}>
Transaksi selesai Rp {calculateAllTransactionsTotal(transactions)}
Transaksi selesai {formatRupiah(calculateAllTransactionsTotal(transactions))}
</h2>
<input
@@ -183,7 +251,7 @@ console.log(aggregatedItems.values())
</ul>
<div className={styles.TotalContainer}>
<span>Total:</span>
<span>Rp {item.totalPrice}</span>
<span>{formatRupiah(item.totalPrice)}</span>
</div>
</div>
))}
@@ -304,8 +372,11 @@ console.log(aggregatedItems.values())
<ul>
{transaction.DetailedTransactions.map((detail) => (
<li key={detail.detailedTransactionId}>
<span>{detail.Item.name}</span> - {detail.qty < 1 ? 'tidak tersedia' : `${detail.qty} x Rp
${detail.promoPrice ? detail.promoPrice : detail.price}`}
<span>{detail.Item.name}</span> - {detail.qty < 1
? 'tidak tersedia'
: `${detail.qty} x ${formatRupiah(detail.promoPrice || detail.price)}`
}
</li>
))}
</ul>
@@ -350,12 +421,13 @@ console.log(aggregatedItems.values())
<div className={styles.TotalContainer}>
<span>Total:</span>
<span>
Rp {calculateTotalPrice(transaction.DetailedTransactions)}
{formatRupiah(calculateTotalPrice(transaction.DetailedTransactions))}
</span>
</div>
<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)}
@@ -374,14 +446,22 @@ console.log(aggregatedItems.values())
}
</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}
price={
"Rp" + calculateTotalPrice(transaction.DetailedTransactions)
}
price={formatRupiah(calculateTotalPrice(transaction.DetailedTransactions))}
disabled={isPaymentLoading}
isPaymentLoading={isPaymentLoading}
handleClick={() => handleConfirm(transaction.transactionId)}

View File

@@ -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;
}

84
src/print.css Normal file
View File

@@ -0,0 +1,84 @@
.print-test {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
background-color: #f5f5f5;
font-family: monospace;
}
.controls {
background-color: #333;
color: white;
padding: 20px;
border-radius: 10px;
width: 90%;
max-width: 800px;
text-align: center;
margin-bottom: 30px;
}
.orientation-selector button,
.print-button {
margin: 10px;
padding: 10px 20px;
font-weight: bold;
cursor: pointer;
border: none;
border-radius: 5px;
}
.orientation-selector button.active {
background-color: #007bff;
color: white;
}
.print-area {
background: white;
white-space: pre-wrap;
font-family: monospace;
font-size: 12px;
line-height: 1.4;
padding: 10px;
width: 48mm;
max-width: 48mm;
color: black;
box-shadow: none;
border: 1px solid #ccc;
}
/* Print media query */
@media print {
body {
background-color: white;
margin: 0;
padding: 0;
}
.controls {
display: none;
}
.print-area {
width: 100%;
max-width: 100%;
border: none;
box-shadow: none;
padding: 0;
font-size: 12px;
}
body * {
visibility: hidden;
}
.print-area, .print-area * {
visibility: visible;
}
.print-area {
position: absolute;
left: 0;
top: 0;
}
}