Compare commits

...

16 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
Vassshhh
dcf0455772 ok 2025-08-27 21:27:08 +07:00
Vassshhh
4fa272875f ok 2025-08-27 10:11:42 +07:00
Vassshhh
40830ee48c ok 2025-08-27 03:14:51 +07:00
Vassshhh
ba896106d4 ok 2025-08-27 02:53:44 +07:00
Vassshhh
e039fc8acc ok 2025-08-27 02:53:30 +07:00
Vassshhh
dd0227ab80 ok 2025-08-26 01:46:40 +07:00
Vassshhh
67cf759b31 ok 2025-08-25 23:41:35 +07:00
Vassshhh
53e091d3a4 ok 2025-07-28 01:15:07 +07:00
everythingonblack
3a431b1b14 ok 2025-05-23 10:50:39 +07:00
everythingonblack
69a07be3cd ok 2025-05-22 16:43:50 +07:00
everythingonblack
b012517568 ok 2025-05-22 02:15:12 +07:00
everythingonblack
3e35468f2c ok 2025-05-21 16:52:38 +07:00
everythingonblack
df7c4f737c ok 2025-05-20 17:47:43 +07:00
45 changed files with 9915 additions and 5926 deletions

11011
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,7 @@
"jsqr": "^1.4.0", "jsqr": "^1.4.0",
"qr-scanner": "^1.4.2", "qr-scanner": "^1.4.2",
"qrcode.react": "^3.1.0", "qrcode.react": "^3.1.0",
"qs": "^6.14.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-apexcharts": "^1.7.0", "react-apexcharts": "^1.7.0",
"react-bootstrap": "^2.10.4", "react-bootstrap": "^2.10.4",
@@ -28,6 +29,7 @@
"react-router-dom": "^6.24.0", "react-router-dom": "^6.24.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-switch": "^7.0.0", "react-switch": "^7.0.0",
"react-to-print": "^3.1.1",
"react-youtube": "^10.1.0", "react-youtube": "^10.1.0",
"smooth-scroll-into-view-if-needed": "^2.0.2", "smooth-scroll-into-view-if-needed": "^2.0.2",
"socket.io-client": "^4.7.5", "socket.io-client": "^4.7.5",

View File

@@ -28,6 +28,15 @@
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>KedaiMaster</title> <title>KedaiMaster</title>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-2SKSCVFB2N"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){ dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', 'G-2SKSCVFB2N');
</script>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -3,17 +3,17 @@
"name": "jangan pernah ragukan pelanggan", "name": "jangan pernah ragukan pelanggan",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "kedai.png",
"sizes": "64x64 32x32 24x24 16x16", "sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon" "type": "image/x-icon"
}, },
{ {
"src": "logo192.png", "src": "kedai.png",
"type": "image/png", "type": "image/png",
"sizes": "192x192" "sizes": "192x192"
}, },
{ {
"src": "logo512.png", "src": "kedai.png",
"type": "image/png", "type": "image/png",
"sizes": "512x512" "sizes": "512x512"
} }

View File

@@ -3,12 +3,10 @@
@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@200;300;400;500;600;700;800&ital,wght@0,200..800;1,200..800&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@200;300;400;500;600;700;800&ital,wght@0,200..800;1,200..800&display=swap");
html, html,
body { body {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */ scrollbar-width: none; /* Firefox */
} }
.App { .App {
/* overflow-x: hidden; */
} }
.Cafe { .Cafe {

View File

@@ -10,7 +10,7 @@ import {
} from "react-router-dom"; } from "react-router-dom";
import socket from "./services/socketService"; import socket from "./services/socketService";
import Print from "./pages/PrintPage.js";
import Dashboard from "./pages/Dashboard"; import Dashboard from "./pages/Dashboard";
import ScanMeja from "./pages/ScanMeja"; import ScanMeja from "./pages/ScanMeja";
import CafePage from "./pages/CafePage"; import CafePage from "./pages/CafePage";
@@ -27,7 +27,7 @@ import { getTableByCode } from "./helpers/tableHelper.js";
import { import {
getTransactionsFromCafe, getTransactionsFromCafe,
checkIsMyTransaction checkIsMyTransaction,
} from "./helpers/transactionHelpers"; } from "./helpers/transactionHelpers";
import { import {
getConnectedGuestSides, getConnectedGuestSides,
@@ -45,7 +45,8 @@ import {
} from "./helpers/subscribeHelpers.js"; } from "./helpers/subscribeHelpers.js";
import Modal from "./components/Modal"; // Import your modal component import Modal from "./components/Modal"; // Import your modal component
import { requestNotificationPermission } from './services/notificationService'; // Import the notification service import { requestNotificationPermission } from "./services/notificationService"; // Import the notification service
import PrintPage from "./pages/PrintPage.js";
function App() { function App() {
const location = useLocation(); const location = useLocation();
@@ -71,31 +72,34 @@ function App() {
const [depth, setDepth] = useState(-1); const [depth, setDepth] = useState(-1);
const [queue, setQueue] = useState([]); const [queue, setQueue] = useState([]);
const [newTransaction, setNewTransaction] = useState({});
const queryParams = new URLSearchParams(location.search);
const tokenParams = queryParams.get("token");
if (tokenParams) localStorage.setItem("auth", tokenParams);
const validTransactionStates = [ const validTransactionStates = [
'new_transaction', "new_transaction",
'transaction_canceled', "transaction_canceled",
'transaction_pending', "transaction_pending",
'transaction_confirmed', "transaction_confirmed",
'payment_claimed', "payment_claimed",
'transaction_success', "transaction_success",
'transaction_end', "transaction_end",
'transaction_failed', "transaction_failed",
]; ];
const calculateTotalsFromLocalStorage = () => { const calculateTotalsFromLocalStorage = () => {
const { totalCount, totalPrice } = calculateTotals(shopId); const { totalCount, totalPrice } = calculateTotals(shopId);
setTotalItemsCount(totalCount); setTotalItemsCount(totalCount);
setTotalPrice(totalPrice); setTotalPrice(totalPrice);
// If 'lastTransaction' exists, proceed // If 'lastTransaction' exists, proceed
const lastTransaction = JSON.parse(localStorage.getItem("lastTransaction")); const lastTransaction = JSON.parse(localStorage.getItem("lastTransaction"));
console.log(lastTransaction); console.log(lastTransaction);
if (lastTransaction != null) { if (lastTransaction != null) {
console.log(lastTransaction) console.log(lastTransaction);
setLastTransaction(lastTransaction); setLastTransaction(lastTransaction);
} }
}; };
@@ -109,6 +113,10 @@ function App() {
const handleStorageChange = () => { const handleStorageChange = () => {
calculateTotalsFromLocalStorage(); calculateTotalsFromLocalStorage();
if (!localStorage.getItem("lastTransaction")) {
setLastTransaction(null);
}
}; };
window.addEventListener("localStorageUpdated", handleStorageChange); window.addEventListener("localStorageUpdated", handleStorageChange);
@@ -135,8 +143,13 @@ function App() {
return; return;
} }
const myLastTransaction = await checkIsMyTransaction(lastTransaction.transactionId); setModal("transaction_confirmed", {
console.log(myLastTransaction) transactionId: lastTransaction.transactionId,
});
const myLastTransaction = await checkIsMyTransaction(
lastTransaction.transactionId
);
console.log(myLastTransaction);
if (myLastTransaction.isMyTransaction) { if (myLastTransaction.isMyTransaction) {
setLastTransaction(lastTransaction); setLastTransaction(lastTransaction);
} else { } else {
@@ -144,7 +157,6 @@ function App() {
} }
}; };
const handleSetParam = async ({ shopIdentifier, tableCode }) => { const handleSetParam = async ({ shopIdentifier, tableCode }) => {
setShopIdentifier(shopIdentifier); setShopIdentifier(shopIdentifier);
@@ -159,12 +171,14 @@ function App() {
const fetchData = async () => { const fetchData = async () => {
console.log("gettingItems"); console.log("gettingItems");
try { try {
const { response, cafe, data } = await getItemTypesWithItems(shopIdentifier); const { response, cafe, data } = await getItemTypesWithItems(
shopIdentifier
);
if (response.status === 200) { if (response.status === 200) {
setShopId(cafe.cafeId) setShopId(cafe.cafeId);
setShop(cafe); setShop(cafe);
setShopItems(data); setShopItems(data);
console.log(data) console.log(data);
// Filter out unavailable items // Filter out unavailable items
const filteredData = data const filteredData = data
.map((itemType) => ({ .map((itemType) => ({
@@ -219,7 +233,7 @@ function App() {
}); });
} else { } else {
socket.emit("checkUserToken", { socket.emit("checkUserToken", {
token: getLocalStorage("auth"), token: getLocalStorage("auth") || tokenParams,
shopId, shopId,
}); });
} }
@@ -230,33 +244,34 @@ function App() {
// Call `setModal` with content and parameters // Call `setModal` with content and parameters
setModal("transaction_pending", data); setModal("transaction_pending", data);
localStorage.setItem('cart', []); localStorage.setItem("cart", []);
calculateTotalsFromLocalStorage(); calculateTotalsFromLocalStorage();
}); });
socket.on("transaction_confirmed", async (data) => { socket.on("transaction_confirmed", async (data) => {
console.log("transaction notification: " + data); console.log(JSON.stringify(data));
setModal("transaction_confirmed", data); setModal("transaction_confirmed", data);
localStorage.setItem('cart', []); localStorage.setItem("cart", []);
const startTime = Date.now(); // Capture the start time // const startTime = Date.now(); // Capture the start time
const timeout = 10000; // 10 seconds timeout in milliseconds // const timeout = 10000; // 10 seconds timeout in milliseconds
calculateTotalsFromLocalStorage(); calculateTotalsFromLocalStorage();
while (localStorage.getItem("lastTransaction") === null) { // while (localStorage.getItem("lastTransaction") === null) {
if (Date.now() - startTime > timeout) { // if (Date.now() - startTime > timeout) {
return; // Exit the function and don't proceed further // return; // Exit the function and don't proceed further
} // }
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
}
// await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
// }
// If 'lastTransaction' exists, proceed // If 'lastTransaction' exists, proceed
const lastTransaction = JSON.parse(localStorage.getItem("lastTransaction")); const lastTransaction = JSON.parse(
localStorage.getItem("lastTransaction")
);
console.log(lastTransaction); console.log(lastTransaction);
if (lastTransaction != null) { if (lastTransaction != null) {
@@ -264,23 +279,28 @@ function App() {
} }
}); });
socket.on("transaction_success", async (data) => { socket.on("transaction_success", async (data) => {
console.log("transaction notification"); console.log("transaction notification");
setModal("transaction_success", data); setModal("transaction_success", data);
// If 'lastTransaction' exists, proceed // If 'lastTransaction' exists, proceed
localStorage.removeItem("lastTransaction"); if (localStorage.getItem("lastTransaction")) {
if (lastTransaction != null) {
setLastTransaction(null); setLastTransaction(null);
console.log('remove last transaction') localStorage.removeItem("lastTransaction");
window.dispatchEvent(new Event("localStorageUpdated"));
} }
}); });
socket.on("transaction_end", async (data) => { socket.on("transaction_end", async (data) => {
console.log("transaction notification"); console.log("transaction notification");
setModal("transaction_end", data); setModal("transaction_end", data);
// If 'lastTransaction' exists, proceed
if (localStorage.getItem("lastTransaction")) {
setLastTransaction(null);
localStorage.removeItem("lastTransaction");
window.dispatchEvent(new Event("localStorageUpdated"));
}
}); });
socket.on("payment_claimed", async (data) => { socket.on("payment_claimed", async (data) => {
@@ -289,10 +309,16 @@ function App() {
}); });
socket.on("transaction_failed", async (data) => { socket.on("transaction_failed", async (data) => {
console.log("transaction notification"); console.log(JSON.stringify(data));
setModal("transaction_failed", data); setModal("transaction_failed", data);
});
// If 'lastTransaction' exists, proceed
if (localStorage.getItem("lastTransaction")) {
setLastTransaction(null);
localStorage.removeItem("lastTransaction");
window.dispatchEvent(new Event("localStorageUpdated"));
}
});
const handleNotificationClick = async () => { const handleNotificationClick = async () => {
const permission = await requestNotificationPermission(); const permission = await requestNotificationPermission();
@@ -302,7 +328,7 @@ function App() {
// Set up notifications or show a success modal // Set up notifications or show a success modal
} else { } else {
console.error("Notification permission denied."); console.error("Notification permission denied.");
setModal('blocked_notification'); // Show modal for blocked notifications setModal("blocked_notification"); // Show modal for blocked notifications
} }
}; };
@@ -311,10 +337,21 @@ function App() {
// Check current permission // Check current permission
const searchParams = new URLSearchParams(location.search); const searchParams = new URLSearchParams(location.search);
let searchModal = searchParams.get("modal") || ''; // Get transactionId or set it to empty string let searchModal = searchParams.get("modal") || ""; // Get transactionId or set it to empty string
if (permission !== "granted" && searchModal == '') { if (permission !== "granted" && searchModal == "") {
setModal("message", { captMessage: 'Notifikasi tidak aktif', descMessage: 'Aktifkan notifikasi supaya kamu tetap dapat info pesanan, meski sedang buka aplikasi lain.', yesText: 'Aktifkan', noText: 'Tutup' }, null, handleNotificationClick); setModal(
"message",
{
captMessage: "Notifikasi tidak aktif",
descMessage:
"Aktifkan notifikasi supaya kamu tetap dapat info pesanan, meski sedang buka aplikasi lain.",
yesText: "Aktifkan",
noText: "Tutup",
},
null,
handleNotificationClick
);
} }
}; };
@@ -323,10 +360,15 @@ function App() {
removeLocalStorage("auth"); removeLocalStorage("auth");
setDeviceType("guestDevice"); setDeviceType("guestDevice");
} else { } else {
console.log(data) console.log(data);
if(data.data.user.cafeId != null) navigate(`/${data.data.user.cafeIdentityName}`, { replace: true }); if (data.data.user.cafeId != null)
navigate(`/${data.data.user.cafeIdentityName}`, { replace: true });
setUser(data.data.user); setUser(data.data.user);
if (data.data.latestOpenBillTransaction != null) localStorage.setItem('lastTransaction', JSON.stringify(data.data.latestOpenBillTransaction)) if (data.data.latestOpenBillTransaction != null)
localStorage.setItem(
"lastTransaction",
JSON.stringify(data.data.latestOpenBillTransaction)
);
if ( if (
data.data.user.password == "unsetunsetunset" && data.data.user.password == "unsetunsetunset" &&
localStorage.getItem("settings") localStorage.getItem("settings")
@@ -337,7 +379,7 @@ function App() {
setGuestSides(connectedGuestSides.sessionDatas); setGuestSides(connectedGuestSides.sessionDatas);
console.log("getting guest side"); console.log("getting guest side");
setDeviceType("clerk"); setDeviceType("clerk");
checkNotifications(); // checkNotifications();
} else { } else {
setDeviceType("guestDevice"); setDeviceType("guestDevice");
} }
@@ -370,7 +412,6 @@ function App() {
socket.on("updateQueue", ({ queue }) => { socket.on("updateQueue", ({ queue }) => {
setQueue(queue); // Only set the queue if it's a valid non-empty array setQueue(queue); // Only set the queue if it's a valid non-empty array
console.log("Updated Queue:", queue); // Log the valid queue console.log("Updated Queue:", queue); // Log the valid queue
}); });
return () => { return () => {
@@ -378,30 +419,31 @@ function App() {
}; };
}, [socket, shopId]); }, [socket, shopId]);
async function checkIfStillViewingOtherTransaction() { async function checkIfStillViewingOtherTransaction(data) {
console.log("transaction notification"); console.log("transaction notification");
console.log(modalContent); console.log(modalContent);
let response; let response;
response = await getTransactionsFromCafe(shopId, 0, true); response = await getTransactionsFromCafe(shopId, 0, true);
transactionList.current = response;
console.log(response); console.log(response);
// Get current URL's search parameters inside the socket event handler // Get current URL's search parameters inside the socket event handler
const searchParams = new URLSearchParams(location.search); const searchParams = new URLSearchParams(location.search);
let transaction_info = searchParams.get("transactionId") || ''; // Get transactionId or set it to empty string let transaction_info = searchParams.get("transactionId") || ""; // Get transactionId or set it to empty string
console.log(transaction_info); // Log the updated transaction_info
if (response[0].transactionId != transaction_info)
transactionList.current = response;
let depthh = transactionList.current.findIndex( let depthh = transactionList.current.findIndex(
item => item.transactionId.toString() === transaction_info.toString() (item) => item.transactionId.toString() === transaction_info.toString()
); );
if(depthh == 0 && transaction_info.toString() != '') depthh = 1;
setDepth(depthh); if (transaction_info != response[0].transactionId) setDepth(depthh);
console.log(transaction_info == response[0].transactionId) else setModal("new_transaction", data);
console.log(transaction_info == response[0].transactionId);
// If transaction_info is an empty string, set the modal // If transaction_info is an empty string, set the modal
if (transaction_info.toString() == '' || transaction_info.toString() == response[0].transactionId) return false; if (transaction_info.toString() == "") return false;
else return true; else return true;
} }
@@ -409,12 +451,16 @@ function App() {
// This will ensure that searchParams and transaction_info get updated on each render // This will ensure that searchParams and transaction_info get updated on each render
socket.on("transaction_created", async (data) => { socket.on("transaction_created", async (data) => {
console.log("transaction notification"); console.log("transaction notification");
const isViewingOtherTransaction = await checkIfStillViewingOtherTransaction(); setNewTransaction(data);
if (!location.pathname.endsWith("/transactions")) {
const isViewingOtherTransaction =
await checkIfStillViewingOtherTransaction(data);
// If transaction_info is an empty string, set the modal // If transaction_info is an empty string, set the modal
if (!isViewingOtherTransaction) { if (!isViewingOtherTransaction) {
setModal("new_transaction", data); setModal("new_transaction", data);
} }
}
// Show browser notification // Show browser notification
let permission = Notification.permission; let permission = Notification.permission;
if (permission !== "granted") return; if (permission !== "granted") return;
@@ -429,12 +475,16 @@ function App() {
socket.on("transaction_canceled", async (data) => { socket.on("transaction_canceled", async (data) => {
console.log("transaction notification"); console.log("transaction notification");
const isViewingOtherTransaction = await checkIfStillViewingOtherTransaction(); setNewTransaction(data);
if (location.pathname != "/transactions") {
const isViewingOtherTransaction =
await checkIfStillViewingOtherTransaction(data);
// If transaction_info is an empty string, set the modal // If transaction_info is an empty string, set the modal
if (!isViewingOtherTransaction) { if (!isViewingOtherTransaction) {
setModal("new_transaction", data); setModal("new_transaction", data);
navigate(`?transactionId=${data.transactionId}`, { replace: true }); navigate(`?transactionId=${data.transactionId}`, { replace: true });
} }
}
}); });
// Clean up the socket event listener on unmount or when dependencies change // Clean up the socket event listener on unmount or when dependencies change
@@ -444,43 +494,34 @@ function App() {
}; };
}, [socket, shopId, location]); // Ensure location is in the dependencies to respond to changes in the URL }, [socket, shopId, location]); // Ensure location is in the dependencies to respond to changes in the URL
// useEffect(() => {
// if (user.cafeId != null && user.cafeId !== shopId) {
// // Preserve existing query parameters
// const currentParams = new URLSearchParams(location.search).toString();
// // Navigate to the new cafeId while keeping existing params
// navigate(`/${user.cafeId}?${currentParams}`, { replace: true });
// }
// }, [user, shopId]);
function handleMoveToTransaction(direction, from) { function handleMoveToTransaction(direction, from) {
console.log(direction); console.log(direction);
console.log(from); console.log(from);
// Find the current index based on the 'from' transactionId // Find the current index based on the 'from' transactionId
const currentIndex = transactionList.current.findIndex(item => item.transactionId == from); const currentIndex = transactionList.current.findIndex(
(item) => item.transactionId == from
);
// Determine the new transactionId based on the direction // Determine the new transactionId based on the direction
let newTransactionId; let newTransactionId;
if (direction === 'next') { if (direction === "next") {
// If we're not at the last transaction, get the next transactionId // If we're not at the last transaction, get the next transactionId
newTransactionId = currentIndex < transactionList.current.length - 1 newTransactionId =
currentIndex < transactionList.current.length - 1
? transactionList.current[currentIndex + 1].transactionId ? transactionList.current[currentIndex + 1].transactionId
: from; // If already at the end, stay on the current transactionId : from; // If already at the end, stay on the current transactionId
} else if (direction === 'previous') { } else if (direction === "previous") {
setDepth(currentIndex - 1);
setDepth(currentIndex -1);
// If we're not at the first transaction, get the previous transactionId // If we're not at the first transaction, get the previous transactionId
newTransactionId = currentIndex > 0 newTransactionId =
currentIndex > 0
? transactionList.current[currentIndex - 1].transactionId ? transactionList.current[currentIndex - 1].transactionId
: from; // If already at the beginning, stay on the current transactionId : from; // If already at the beginning, stay on the current transactionId
} }
// Log the new transactionId // Log the new transactionId
console.log('New Transaction ID:', newTransactionId); console.log("New Transaction ID:", newTransactionId);
// Update the URL with the new transactionId using navigate // Update the URL with the new transactionId using navigate
navigate(`?transactionId=${newTransactionId}`, { replace: true }); navigate(`?transactionId=${newTransactionId}`, { replace: true });
@@ -490,7 +531,6 @@ function App() {
// setModalContent({ cafeId: shopId, transactionId: newTransactionId }); // setModalContent({ cafeId: shopId, transactionId: newTransactionId });
} }
const handleModalFromURL = () => { const handleModalFromURL = () => {
const queryParams = new URLSearchParams(location.search); const queryParams = new URLSearchParams(location.search);
const modal = queryParams.get("modal"); const modal = queryParams.get("modal");
@@ -522,8 +562,8 @@ function App() {
document.body.style.overflow = "hidden"; document.body.style.overflow = "hidden";
setIsModalOpen(true); setIsModalOpen(true);
setModalContent(content) setModalContent(content);
console.log(onCloseFunction) console.log(onCloseFunction);
if (onCloseFunction) { if (onCloseFunction) {
setOnModalCloseFunction(() => onCloseFunction); // Store the close function setOnModalCloseFunction(() => onCloseFunction); // Store the close function
@@ -540,7 +580,8 @@ function App() {
const closeModal = (closeTheseContent = []) => { const closeModal = (closeTheseContent = []) => {
if ( if (
Array.isArray(closeTheseContent) && Array.isArray(closeTheseContent) &&
(closeTheseContent.length === 0 || closeTheseContent.includes(modalContent)) (closeTheseContent.length === 0 ||
closeTheseContent.includes(modalContent))
) { ) {
setIsModalOpen(false); setIsModalOpen(false);
setModalContent(null); setModalContent(null);
@@ -549,7 +590,8 @@ function App() {
const queryParams = new URLSearchParams(location.search); const queryParams = new URLSearchParams(location.search);
// Clear all query parameters // Clear all query parameters
queryParams.keys() && [...queryParams.keys()].forEach(key => { queryParams.keys() &&
[...queryParams.keys()].forEach((key) => {
queryParams.delete(key); queryParams.delete(key);
}); });
@@ -558,7 +600,6 @@ function App() {
} }
}; };
// useEffect(() => { // useEffect(() => {
// const askNotificationPermission = async () => { // const askNotificationPermission = async () => {
// let permission = Notification.permission; // let permission = Notification.permission;
@@ -617,11 +658,15 @@ function App() {
useEffect(() => { useEffect(() => {
if ("serviceWorker" in navigator) { if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/service-worker.js") navigator.serviceWorker
.then(registration => { .register("/service-worker.js")
console.log("Service Worker registered with scope:", registration.scope); .then((registration) => {
console.log(
"Service Worker registered with scope:",
registration.scope
);
}) })
.catch(error => { .catch((error) => {
console.error("Service Worker registration failed:", error); console.error("Service Worker registration failed:", error);
}); });
} }
@@ -631,6 +676,12 @@ function App() {
<div className="App"> <div className="App">
<header className="App-header" id="header"> <header className="App-header" id="header">
<Routes> <Routes>
<Route
path="/:shopIdentifier/print"
element={
<PrintPage />
}
/>
<Route <Route
path="/" path="/"
element={ element={
@@ -682,37 +733,10 @@ function App() {
cartItemsLength={totalItemsCount} cartItemsLength={totalItemsCount}
totalPrice={totalPrice} totalPrice={totalPrice}
lastTransaction={lastTransaction} lastTransaction={lastTransaction}
shop={shop}
totalItemsCount={totalItemsCount}
deviceType={deviceType}
/> />
{/* <Footer
showTable={true}
shopId={shopIdentifier}
table={table}
cartItemsLength={totalItemsCount}
selectedPage={0}
/> */}
</>
}
/>
<Route
path="/:shopIdentifier/:tableCode?/search"
element={
<>
<SearchResult
cafeId={shopId}
sendParam={handleSetParam}
user={user}
shopItems={shopItems}
guestSides={guestSides}
guestSideOfClerk={guestSideOfClerk}
removeConnectedGuestSides={rmConnectedGuestSides}
setModal={setModal} // Pass the function to open modal
/>
{/* <Footer
shopId={shopIdentifier}
table={table}
cartItemsLength={totalItemsCount}
selectedPage={1}
/> */}
</> </>
} }
/> />
@@ -732,12 +756,6 @@ function App() {
shopItems={shopItems} shopItems={shopItems}
setShopItems={setShopItems} setShopItems={setShopItems}
/> />
{/* <Footer
shopId={shopIdentifier}
table={table}
cartItemsLength={totalItemsCount}
selectedPage={2}
/> */}
</> </>
} }
/> />
@@ -771,6 +789,8 @@ function App() {
sendParam={handleSetParam} sendParam={handleSetParam}
deviceType={deviceType} deviceType={deviceType}
paymentUrl={shop.qrPayment} paymentUrl={shop.qrPayment}
setModal={setModal}
newTransaction={newTransaction}
/> />
{/* <Footer {/* <Footer
shopId={shopIdentifier} shopId={shopIdentifier}
@@ -799,8 +819,10 @@ function App() {
welcomePageConfig={shop.welcomePageConfig} welcomePageConfig={shop.welcomePageConfig}
onClose={closeModal} onClose={closeModal}
setModal={setModal} setModal={setModal}
setIsModalOpen={setIsModalOpen}
onModalCloseFunction={onModalCloseFunction} onModalCloseFunction={onModalCloseFunction}
onModalYesFunction={onModalYesFunction} onModalYesFunction={onModalYesFunction}
shopIdentifier={shopIdentifier}
/> />
</div> </div>
); );

View File

@@ -44,10 +44,10 @@ const App = () => {
useEffect(() => { useEffect(() => {
const shopId = window.location.pathname.split('/')[1]; // Get shopId from the URL const shopId = window.location.pathname.split('/')[1]; // Get shopId from the URL
const userId = localStorage.getItem('userId'); const user_id = localStorage.getItem('user_id');
// Connect to Socket.IO if userId is present // Connect to Socket.IO if user_id is present
// if (userId) { // if (user_id) {
// connectSocket(shopId, 1); // connectSocket(shopId, 1);
// } // }

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import dayjs from "dayjs";
import Chart from "react-apexcharts"; import Chart from "react-apexcharts";
import styles from "./BarChart.module.css"; // Import the CSS module import styles from "./BarChart.module.css"; // Import the CSS module
@@ -7,7 +8,21 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
useEffect(() => { useEffect(() => {
setSelectedIndex(-1); setSelectedIndex(-1);
}, [transactionGraph]); }, [transactionGraph, graphFilter]);
// Helper function to format numbers to Indonesian Rupiah
const formatRupiah = (number) => {
if (number === null || number === undefined) {
return "Rp 0";
}
const formatter = new Intl.NumberFormat("id-ID", {
style: "currency",
currency: "IDR",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
return formatter.format(number);
};
const processData = (graphData) => { const processData = (graphData) => {
if (!graphData) return null; if (!graphData) return null;
@@ -22,61 +37,73 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
"18-21", "18-21",
"21-24", "21-24",
]; ];
console.log(dayData) 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 sumOutcome = (transactions) =>
Array.isArray(transactions) ? transactions.reduce((acc, t) => acc + (t.materialOutcome || t.price * t.stockDifference), 0) : transactions?.outcome || 0;
let seriesData = [] let seriesData = []
if(graphFilter == 'transactions'){ if (graphFilter === 'transactions') {
seriesData = [ seriesData = [
dayData.hour0To3Transactions.reduce((acc, t) => acc + t.sold, 0), sumSold(dayData?.hour0To3Transactions),
dayData.hour3To6Transactions.reduce((acc, t) => acc + t.sold, 0), sumSold(dayData?.hour3To6Transactions),
dayData.hour6To9Transactions.reduce((acc, t) => acc + t.sold, 0), sumSold(dayData?.hour6To9Transactions),
dayData.hour9To12Transactions.reduce((acc, t) => acc + t.sold, 0), sumSold(dayData?.hour9To12Transactions),
dayData.hour12To15Transactions.reduce((acc, t) => acc + t.sold, 0), sumSold(dayData?.hour12To15Transactions),
dayData.hour15To18Transactions.reduce((acc, t) => acc + t.sold, 0), sumSold(dayData?.hour15To18Transactions),
dayData.hour18To21Transactions.reduce((acc, t) => acc + t.sold, 0), sumSold(dayData?.hour18To21Transactions),
dayData.hour21To24Transactions.reduce((acc, t) => acc + t.sold, 0), sumSold(dayData?.hour21To24Transactions),
]; ];
} }
else if(graphFilter == 'income'){ else if (graphFilter === 'income') {
seriesData = [ seriesData = [
dayData.hour0To3Transactions.reduce((acc, t) => acc + t.totalPrice, 0), sumTotal(dayData?.hour0To3Transactions),
dayData.hour3To6Transactions.reduce((acc, t) => acc + t.totalPrice, 0), sumTotal(dayData?.hour3To6Transactions),
dayData.hour6To9Transactions.reduce((acc, t) => acc + t.totalPrice, 0), sumTotal(dayData?.hour6To9Transactions),
dayData.hour9To12Transactions.reduce((acc, t) => acc + t.totalPrice, 0), sumTotal(dayData?.hour9To12Transactions),
dayData.hour12To15Transactions.reduce((acc, t) => acc + t.totalPrice, 0), sumTotal(dayData?.hour12To15Transactions),
dayData.hour15To18Transactions.reduce((acc, t) => acc + t.totalPrice, 0), sumTotal(dayData?.hour15To18Transactions),
dayData.hour18To21Transactions.reduce((acc, t) => acc + t.totalPrice, 0), sumTotal(dayData?.hour18To21Transactions),
dayData.hour21To24Transactions.reduce((acc, t) => acc + t.totalPrice, 0), sumTotal(dayData?.hour21To24Transactions),
]; ];
} }
else if(graphFilter == 'outcome'){ else if (graphFilter === 'outcome') {
seriesData = [ seriesData = [
dayData.hour3To6MaterialIds.reduce((acc, t) => acc + t.materialOutcome, 0), sumOutcome(dayData?.hour0To3MaterialIds),
dayData.hour6To9MaterialIds.reduce((acc, t) => acc + t.materialOutcome, 0), sumOutcome(dayData?.hour3To6MaterialIds),
dayData.hour0To3MaterialIds.reduce((acc, t) => acc + t.materialOutcome, 0), sumOutcome(dayData?.hour6To9MaterialIds),
dayData.hour9To12MaterialIds.reduce((acc, t) => acc + t.materialOutcome, 0), sumOutcome(dayData?.hour9To12MaterialIds),
dayData.hour12To15MaterialIds.reduce((acc, t) => acc + t.materialOutcome, 0), sumOutcome(dayData?.hour12To15MaterialIds),
dayData.hour15To18MaterialIds.reduce((acc, t) => acc + t.materialOutcome, 0), sumOutcome(dayData?.hour15To18MaterialIds),
dayData.hour18To21MaterialIds.reduce((acc, t) => acc + t.materialOutcome, 0), sumOutcome(dayData?.hour18To21MaterialIds),
dayData.hour21To24MaterialIds.reduce((acc, t) => acc + t.materialOutcome, 0), sumOutcome(dayData?.hour21To24MaterialIds),
]; ];
} }
let totalValue = seriesData.reduce((acc, val) => acc + val, 0);
return { return {
date: new Date(dayData.date).toLocaleDateString(), date: dayData.date,
categories, categories,
series: [ series: [
{ {
name: `Transactions on ${new Date(dayData.date).toLocaleDateString()}`, name: graphFilter === 'transactions' ? 'Transaksi' : (graphFilter === 'income' ? 'Pemasukan' : 'Pengeluaran'),
data: seriesData, data: seriesData,
}, },
], ],
totalValue, // ⬅️ Tambahkan ini
}; };
}); });
}; };
const chartData = processData(graphFilter != 'outcome' ? transactionGraph : materialGraph); const chartData = processData(graphFilter !== 'outcome' ? transactionGraph : materialGraph);
let globalMax = null; let globalMax = null;
if (chartData) if (chartData) {
globalMax = chartData.reduce( globalMax = chartData.reduce(
(max, data) => { (max, data) => {
const localMax = Math.max(...data.series[0].data); const localMax = Math.max(...data.series[0].data);
@@ -84,24 +111,17 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
}, },
0 0
); );
}
const formatDate = (dateString) => { const formatDate = (dateString) => {
const date = new Date(dateString); // Parse the date string const d = dayjs(dateString, ["YYYY-MM-DD", "YYYY-MM-DDTHH:mm:ssZ"]);
return { month: d.format("MMM"), day: d.format("D") };
// Create an array of month names (use the same names you had earlier)
const monthNames = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
];
// Get the month and day
const month = monthNames[date.getMonth()]; // Month is 0-indexed (January = 0)
const day = date.getDate(); // Get the day of the month
return { month, day }; // Return the result
}; };
return ( return (
<div className={`${styles.chartItemContainer} ${selectedIndex !== -1 ? styles.expanded : ''}`}> <div className={`${styles.chartItemContainer} ${selectedIndex !== -1 ? styles.expanded : ''}`}>
{chartData && {chartData &&
chartData.map((data, index) => ( chartData.map((data, index) => (
<div <div
@@ -119,33 +139,78 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
key={indexx} key={indexx}
className={`${styles.dateSelector} ${index === indexx ? styles.dateSelectorActive : styles.dateSelectorInactive className={`${styles.dateSelector} ${index === indexx ? styles.dateSelectorActive : styles.dateSelectorInactive
}`} }`}
style={{position: 'relative' }} style={{ position: 'relative', width: 'calc(100% / 7)' }}
onClick={() => onClick={() =>
type == 'yesterday' && selectedIndex == -1 || type != 'yesterday' && selectedIndex !== index ? setSelectedIndex(index) : setSelectedIndex(-1) type == 'yesterday' && selectedIndex == -1 || type != 'yesterday' && selectedIndex !== index ? setSelectedIndex(index) : setSelectedIndex(-1)
} }
// style={{ backgroundColor: index === indexx ? colors[index % colors.length] : 'transparent' }}
> >
<div style={{position: 'absolute', bottom: 0, left: '10%', right: '10%', borderBottom: index == indexx ? `2px solid ${colors[index % colors.length]}` : 'none'}}></div>
<div style={{ position: 'absolute', bottom: '21px', left: '10%', right: '10%', borderBottom: index == indexx ? `2px solid ${colors[index % colors.length]}` : 'none' }}></div>
<div <div
style={{ color: index === indexx ? 'black' : 'transparent' }}> style={{ color: index === indexx ? 'black' : 'transparent' }}>
{indexx !== chartData.length - 1 ? ( {indexx !== chartData.length - 1 ? (
<> <p style={{ fontSize: '13px' }}>{day}{" "}
{day}{" "} {(
{(indexx === 0 || (formatDate(chartData[indexx - 1].date).month !== month && type != 'weekly')) && month} indexx === 0 ||
</> (indexx > 0 &&
dayjs(chartData[indexx - 1].date).month() !== dayjs(item.date).month() &&
type !== "weekly")
) && month}
</p>
) : ( ) : (
'Kemarin' <p style={{ fontSize: '13px' }}>
{type != 'weekly' ? 'Hari ini' : day + ' ' + month}
</p>
)} )}
</div>
{index == indexx && <p style={{
margin: '7px 0 0 0', fontSize: '9px', color: 'black',
position: 'absolute',
width: '100%',
left: 0,
bottom: 6
}}>
{graphFilter === 'transactions'
? chartData[indexx].totalValue
: formatRupiah(chartData[indexx].totalValue)}
</p>}
</div>
</div>
</div>
); );
})} })}
</div> </div>
<div className={styles.chartWrapper}> <div className={styles.chartWrapper}>
<Chart <Chart
options={{ options={{
tooltip: { enabled: false }, tooltip: {
enabled: true,
y: {
formatter: function (val) {
return graphFilter === "transactions"
? val
: formatRupiah(val);
},
},
},
dataLabels: {
enabled: true,
formatter: function (val) {
if (graphFilter === 'transactions') {
return val; // angka biasa
} else {
return formatRupiah(val); // format Rupiah
}
},
style: {
colors: [(index == chartData.length - 1 || selectedIndex != -1) ? "#000" : "transparent"],
fontSize: '7px',
},
offsetY: -10,
background: {
enabled: (index == chartData.length - 1 || selectedIndex != -1) ? true : false
}
},
chart: { chart: {
id: `chart-${index}`, id: `chart-${index}`,
type: "area", type: "area",
@@ -160,7 +225,7 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
categories: data.categories, categories: data.categories,
labels: { labels: {
style: { style: {
colors: index === 0 || index == selectedIndex || selectedIndex == 0 && index == 1 ? "#000" : "transparent", colors: index === 0 || index === selectedIndex || (selectedIndex === 0 && index === 1) ? "#000" : "transparent",
}, },
}, },
}, },
@@ -172,6 +237,9 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
style: { style: {
colors: "transparent", colors: "transparent",
}, },
formatter: function (val) {
return formatRupiah(val);
},
}, },
}, },
grid: { grid: {
@@ -189,7 +257,6 @@ const DailyCharts = ({ incomeGraph, transactionGraph, materialGraph, colors, typ
/> />
</div> </div>
</div> </div>
))} ))}
</div> </div>
); );

View File

@@ -35,11 +35,16 @@ const Title = styled.h2`
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500; font-weight: 500;
font-style: normal; font-style: normal;
font-size:${(props) => (props.HeaderSize)}; font-size: ${(props) => props.HeaderSize};
color: rgba(88, 55, 50, 1); color: rgba(88, 55, 50, 1);
text-transform: uppercase; text-transform: uppercase;
@media (min-width: 768px) {
font-size: 2vw;
}
`; `;
const ProfileName = styled.h2` const ProfileName = styled.h2`
position: absolute; position: absolute;
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
@@ -81,8 +86,8 @@ const gg = keyframes`
height: 60px; height: 60px;
} }
100% { 100% {
top: 45px; top: 5px;
right: 51px; right: 20px;
width: 200px; width: 200px;
height: 40px; height: 40px;
} }
@@ -90,8 +95,8 @@ const gg = keyframes`
const ss = keyframes` const ss = keyframes`
0% { 0% {
top: 45px; top: 5px;
right: 51px; right: 20px;
width: 200px; width: 200px;
height: 40px; height: 40px;
} }
@@ -108,7 +113,7 @@ const ProfileImage = styled.img`
width: 60px; width: 60px;
height: 60px; height: 60px;
border-radius: 50%; border-radius: 50%;
object-fit: contain; object-fit: cover;
cursor: pointer; cursor: pointer;
z-index: 199; z-index: 199;
animation: ${(props) => { animation: ${(props) => {
@@ -127,8 +132,8 @@ const g = keyframes`
height: 60px; height: 60px;
} }
100% { 100% {
top: 28px; top: 34px;
right: 242px; right: 229px;
width: 40px; width: 40px;
height: 40px; height: 40px;
} }
@@ -137,7 +142,7 @@ const g = keyframes`
const s = keyframes` const s = keyframes`
0% { 0% {
top: 28px; top: 28px;
right: 242px; right: 229px;
width: 40px; width: 40px;
height: 40px; height: 40px;
} }
@@ -151,14 +156,15 @@ const s = keyframes`
const grow = keyframes` const grow = keyframes`
0% { 0% {
right: 12px; right: 0px;
top: 0px;
width: 60px; width: 60px;
height: 60px; height: 60px;
border-top-left-radius: 50%; border-top-left-radius: 50%;
border-bottom-left-radius: 50%; border-bottom-left-radius: 50%;
} }
100% { 100% {
right: 28px; right: -17px;
width: 300px; width: 300px;
border-top-left-radius: 15px; border-top-left-radius: 15px;
border-bottom-left-radius: 15px; border-bottom-left-radius: 15px;
@@ -167,13 +173,14 @@ const grow = keyframes`
const shrink = keyframes` const shrink = keyframes`
0% { 0% {
right: 12px; right: -17px;
width: 300px; width: 300px;
height: auto; height: auto;
border-radius: 20px; border-radius: 20px;
} }
100% { 100% {
right: 28px; right: 0px;
top: 0px;
width: 60px; width: 60px;
height: 60px; height: 60px;
border-radius: 50%; border-radius: 50%;
@@ -182,7 +189,7 @@ const shrink = keyframes`
const Rectangle = styled.div` const Rectangle = styled.div`
overflow-y: hidden; overflow-y: hidden;
position: absolute; position: absolute;
top: 39px; top: 9px;
right: 12px; right: 12px;
width: 200px; width: 200px;
max-height: 87vh; /* or another appropriate value */ max-height: 87vh; /* or another appropriate value */
@@ -331,7 +338,7 @@ const Header = ({
: HeaderText : HeaderText
: shopName} : shopName}
</Title> </Title>
<div style={{ visibility: showProfile ? "visible" : "hidden" }}> <div style={{ visibility: showProfile ? "visible" : "hidden", position: 'relative' }}>
<ProfileImage <ProfileImage
src={shopImage && !shopImage.includes('undefined') ? shopImage || 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS-DjX_bGBax4NL14ULvkAdU4FP3FKoWXWu5w&s' : "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS-DjX_bGBax4NL14ULvkAdU4FP3FKoWXWu5w&s"} src={shopImage && !shopImage.includes('undefined') ? shopImage || 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS-DjX_bGBax4NL14ULvkAdU4FP3FKoWXWu5w&s' : "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS-DjX_bGBax4NL14ULvkAdU4FP3FKoWXWu5w&s"}
alt="Profile" alt="Profile"
@@ -355,11 +362,11 @@ const Header = ({
</Child> </Child>
)} )}
{user.roleId == 0 && ( {user.roleId == 0 && (
<Child onClick={()=>setModal('create_coupon', {})}>Buat Voucher</Child>)} <Child onClick={() => setModal('create_coupon', {})}>Buat Voucher</Child>)}
{shopId && user.roleId == 1 && ( {shopId && user.roleId == 1 && (
<Child onClick={goToAdminCafes}>Dashboard</Child>)} <Child onClick={goToAdminCafes}>Dashboard</Child>)}
{shopId && {shopId &&
user.userId == shopOwnerId && user.user_id == shopOwnerId &&
user.username !== undefined && user.username !== undefined &&
user.roleId === 1 && ( user.roleId === 1 && (
<> <>
@@ -367,17 +374,9 @@ const Header = ({
<Child> <Child>
{shopName} {shopName}
</Child> </Child>
<Child>
Mode pengembangan &nbsp;
<Switch
borderRadius={0}
checked={isEditMode}
onChange={() => setIsEditMode(!isEditMode)}
/>
</Child>
<Child onClick={() => setModal("reports")}>Lihat laporan</Child> <Child onClick={() => setModal("reports")}>Lihat laporan</Child>
<Child onClick={() => setModal("add_material")}> <Child onClick={() => setModal("add_material")}>
Kelola bahan baku Kelola stok
</Child> </Child>
<Child hasChildren> <Child hasChildren>
@@ -423,7 +422,7 @@ const Header = ({
<Child>{shopName}</Child> <Child>{shopName}</Child>
<Child> <Child>
Mode pengembangan&nbsp; Mode Edit&nbsp;
<Switch <Switch
borderRadius={0} borderRadius={0}
checked={isEditMode} checked={isEditMode}
@@ -431,7 +430,7 @@ const Header = ({
/> />
</Child> </Child>
<Child onClick={() => setModal("add_material")}> <Child onClick={() => setModal("add_material")}>
Kelola bahan baku Kelola stok
</Child> </Child>
<Child hasChildren> <Child hasChildren>

View File

@@ -29,6 +29,51 @@ const Item = ({
const [itemDescription, setItemDescription] = useState(initialDescription); const [itemDescription, setItemDescription] = useState(initialDescription);
const fileInputRef = useRef(null); const fileInputRef = useRef(null);
const formatToRupiah = (value) => {
if (typeof value !== "number") return value;
return value.toLocaleString("id-ID");
// const stringValue = (value || '').toString();
// // Ambil hanya angka
// const cleaned = stringValue.replace(/[^0-9]/g, '');
// // Pastikan cleaned bukan kosong
// if (!cleaned) return 'Rp0';
// // Ubah ke number dan kalikan 1000
// const number = Number(cleaned) * 1000;
// // Kalau gagal parsing, fallback
// if (isNaN(number)) return 'Rp0';
// // Format rupiah tanpa desimal
// return number.toLocaleString('id-ID', {
// style: 'currency',
// currency: 'IDR',
// minimumFractionDigits: 0,
// maximumFractionDigits: 0
// });
};
const rupiahFormat = (angka, prefix = "Rp. ") => {
let number_string = angka.toString().replace(/[^,\d]/g, '');
let split = number_string.split(',');
let sisa = split[0].length % 3;
let rupiah = split[0].substr(0, sisa);
let ribuan = split[0].substr(sisa).match(/\d{3}/gi);
// tambahkan titik jika yang di input sudah menjadi angka ribuan
if(ribuan){
let separator = sisa ? '.' : '';
rupiah += separator + ribuan.join('.');
}
rupiah = split[1] != undefined ? rupiah + ',' + split[1] : rupiah;
return rupiah ? prefix + rupiah : "";
}
useEffect(() => { useEffect(() => {
console.log(imageUrl); console.log(imageUrl);
console.log(selectedImage); console.log(selectedImage);
@@ -156,7 +201,6 @@ const Item = ({
</h3> </h3>
{forInvoice && ( {forInvoice && (
<> <>
<p className={styles.multiplySymbol}>x</p>
<p className={styles.qtyInvoice}>{itemQty}</p> <p className={styles.qtyInvoice}>{itemQty}</p>
</> </>
)} )}
@@ -200,7 +244,7 @@ const Item = ({
lineHeight: '1rem', lineHeight: '1rem',
justifyContent: 'center' justifyContent: 'center'
}}> }}>
Promo {(((initialPrice - promoPrice) / initialPrice) * 100).toFixed(0)}% Promo
</div> </div>
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
@@ -208,12 +252,12 @@ const Item = ({
marginLeft: '1rem', marginLeft: '1rem',
marginRight: '0.5rem', marginRight: '0.5rem',
marginTop: '0.125rem' marginTop: '0.125rem'
}}>{promoPrice}</span> }}>{formatToRupiah(promoPrice)}</span>
<span style={{ <span style={{
marginTop: '0.125rem', marginTop: '0.125rem',
color: 'rgb(114, 114, 114)', color: 'rgb(114, 114, 114)',
textDecoration: 'line-through' textDecoration: 'line-through'
}}>{initialPrice}</span> }}>{formatToRupiah(initialPrice)}</span>
</div> </div>
</> </>
: :
@@ -223,7 +267,7 @@ const Item = ({
<span style={{ <span style={{
marginRight: '0.5rem', marginRight: '0.5rem',
marginTop: '0.125rem' marginTop: '0.125rem'
}}>{initialPrice}</span> }}>{formatToRupiah(initialPrice)}</span>
</div> </div>
</> </>
} }
@@ -302,7 +346,9 @@ const Item = ({
))} ))}
{forInvoice && ( {forInvoice && (
<p className={styles.itemPriceInvoice}>Rp {itemQty * (promoPrice > 0? promoPrice : itemPrice)}</p> <p className={styles.itemPriceInvoice}>Rp {formatToRupiah(itemQty * (promoPrice > 0? promoPrice : itemPrice))}</p>
// <p className={styles.itemPriceInvoice}>{itemQty * (promoPrice > 0? rupiahFormat(promoPrice) : rupiahFormat(itemPrice))}</p>
)} )}
</div> </div>
{forCart && ( {forCart && (

View File

@@ -13,9 +13,12 @@
margin-bottom: 5px; margin-bottom: 5px;
color: rgba(88, 55, 50, 1); color: rgba(88, 55, 50, 1);
font-size: 32px; font-size: 32px;
box-sizing: border-box; /* Include padding and border in the element's total width */ box-sizing: border-box;
width: 100%; /* Ensure the item does not exceed the parent's width */ /* Include padding and border in the element's total width */
overflow: hidden; /* Prevent internal overflow */ width: 100%;
/* Ensure the item does not exceed the parent's width */
overflow: hidden;
/* Prevent internal overflow */
padding-top: 10px; padding-top: 10px;
margin-bottom: 5px; margin-bottom: 5px;
} }
@@ -24,7 +27,7 @@
/* border-top: 2px solid #00000017; */ /* border-top: 2px solid #00000017; */
} }
.notLast{ .notLast {
padding-bottom: 10px; padding-bottom: 10px;
border-bottom: 2px solid #00000017; border-bottom: 2px solid #00000017;
} }
@@ -40,7 +43,8 @@
} }
.itemInvoice:last-child { .itemInvoice:last-child {
margin-bottom: 0; /* Remove margin-bottom for the last child */ margin-bottom: 0;
/* Remove margin-bottom for the last child */
} }
.itemImage { .itemImage {
@@ -102,7 +106,8 @@
.itemName { .itemName {
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-style: normal; font-style: normal;
width: calc(100% - 15px); /* Adjust the width to prevent overflow */ width: calc(100% - 15px);
/* Adjust the width to prevent overflow */
font-size: 5vw; font-size: 5vw;
font-weight: 500; font-weight: 500;
margin-top: 0; margin-top: 0;
@@ -131,7 +136,8 @@
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
width: calc(100% - 15px); /* Adjust the width to prevent overflow */ width: calc(100% - 15px);
/* Adjust the width to prevent overflow */
font-size: 3.3vw; font-size: 3.3vw;
/* margin-bottom: 35px; */ /* margin-bottom: 35px; */
margin-left: 5px; margin-left: 5px;
@@ -175,7 +181,8 @@
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
width: 30px; /* Adjust the width to prevent overflow */ width: 30px;
/* Adjust the width to prevent overflow */
font-size: 0.9rem; font-size: 0.9rem;
margin-bottom: 10px; margin-bottom: 10px;
text-align: center; text-align: center;
@@ -196,6 +203,7 @@
margin-top: 5px; margin-top: 5px;
border-radius: 20px; border-radius: 20px;
} }
.grayscale { .grayscale {
filter: grayscale(100%); filter: grayscale(100%);
} }
@@ -203,6 +211,7 @@
.disabled { .disabled {
color: gray; color: gray;
} }
.plusNegative { .plusNegative {
width: 35px; width: 35px;
height: 35px; height: 35px;
@@ -217,6 +226,7 @@
left: -33px; left: -33px;
top: 21px; top: 21px;
} }
.remove { .remove {
width: 25px; width: 25px;
height: 25px; height: 25px;
@@ -249,3 +259,267 @@
left: 15px; left: 15px;
right: 15px; right: 15px;
} }
@media (min-width: 768px) {
.itemContainer {
display: flex;
flex-direction: column;
/* gap: 10px; */
}
.item {
display: flex;
align-items: stretch;
justify-content: space-between;
padding-left: 5px;
margin-top: 5px;
margin-bottom: 5px;
color: rgba(88, 55, 50, 1);
font-size: 32px;
box-sizing: border-box;
/* Include padding and border in the element's total width */
width: 100%;
/* Ensure the item does not exceed the parent's width */
overflow: hidden;
/* Prevent internal overflow */
padding-top: 10px;
margin-bottom: 5px;
}
.item:not(.itemInvoice) {
/* border-top: 2px solid #00000017; */
}
.notLast {
padding-bottom: 10px;
border-bottom: 2px solid #00000017;
}
.itemInvoice {
flex-direction: row;
align-items: center;
justify-content: space-around;
font-size: 18px;
margin-top: 0px;
margin-bottom: 0px;
padding-top: 0px;
}
.itemInvoice:last-child {
margin-bottom: 0;
/* Remove margin-bottom for the last child */
}
.itemImage {
width: 100%;
height: 100%;
}
.imageContainer {
position: relative;
width: 20%;
height: 20%;
border-radius: 12px;
object-fit: cover;
}
.overlay {
position: absolute;
top: 15px;
left: 8px;
right: 8px;
bottom: 15px;
background-color: rgba(0, 0, 0, 0.5);
color: white;
display: flex;
justify-content: center;
align-items: center;
border-radius: 20px;
cursor: pointer;
transition: background-color 0.3s ease;
font-size: 3.3vw;
}
.overlay:hover {
background-color: rgba(0, 0, 0, 0.7);
}
.fileInput {
display: none;
}
.itemDetails {
display: flex;
flex-direction: column;
justify-content: space-between;
margin-left: 10px;
margin-right: 10px;
flex-grow: 1;
}
.itemInvoiceDetails {
display: flex;
flex-direction: column;
justify-content: space-between;
margin-left: 10px;
margin-top: -15px;
flex-grow: 1;
}
.itemName {
font-family: "Plus Jakarta Sans", sans-serif;
font-style: normal;
width: calc(100% - 15px);
/* Adjust the width to prevent overflow */
font-size: 5vw;
font-weight: 500;
margin-top: 0;
margin: 0 5px;
color: rgba(88, 55, 50, 1);
background-color: transparent;
text-transform: capitalize;
}
.itemInvoiceName {
width: calc(260% - 15px);
background-color: transparent;
font-size: 1.3rem;
font-weight: 500;
}
.multiplySymbol {
font-weight: 600;
}
.qtyInvoice {
font-weight: 500;
}
.itemPrice {
font-family: "Plus Jakarta Sans", sans-serif;
font-style: normal;
font-weight: 600;
width: calc(100% - 15px);
/* Adjust the width to prevent overflow */
font-size: 3.3vw;
/* margin-bottom: 35px; */
margin-left: 5px;
color: #3a3a3a;
background-color: transparent;
}
.itemPriceInvoice {
font-family: "Plus Jakarta Sans", sans-serif;
font-style: normal;
font-weight: 600;
font-size: 0.9rem;
margin-left: 5px;
color: #d9c61c;
text-align: right;
margin-top: 22px;
}
.itemQty {
display: flex;
align-items: center;
font-size: 0.9rem;
margin-left: 5px;
color: #a8c7a9;
fill: #a8c7a9;
height: 40px;
}
.itemQtyValue {
font-family: "Plus Jakarta Sans", sans-serif;
font-style: normal;
font-weight: 600;
margin-top: 19px;
margin-left: 1px;
margin-right: 1px;
width: 25px;
text-align: center;
}
.itemQtyInput {
font-family: "Plus Jakarta Sans", sans-serif;
font-style: normal;
font-weight: 600;
width: 30px;
/* Adjust the width to prevent overflow */
font-size: 0.9rem;
margin-bottom: 10px;
text-align: center;
background-color: transparent;
}
.addButton {
background-color: #ffffff;
border: 2px solid #a8c7a9;
/* border: none; */
display: inline-block;
font-size: 14px;
font-weight: 600;
cursor: pointer;
width: 87px;
height: 32px;
margin-left: 5px;
margin-top: 5px;
border-radius: 20px;
}
.grayscale {
filter: grayscale(100%);
}
.disabled {
color: gray;
}
.plusNegative {
width: 35px;
height: 35px;
margin: 2.5px 0 -0.5px 0px;
}
.plusNegative2 {
width: 84px;
height: 21px;
position: absolute;
transform: rotate(45deg);
left: -33px;
top: 21px;
}
.remove {
width: 25px;
height: 25px;
margin-top: -10px;
margin-right: 10px;
}
.itemInvoice .itemDetails {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.itemInvoice .itemName,
.itemInvoice .itemPrice,
.itemInvoice .itemQty .qtyInvoice .multiplySymbol {
font-size: 0.9rem;
}
.blank {
border: 1px solid #000000;
}
.notblank {
border: 1px solid #ffffff00;
}
.createItem {
position: absolute;
left: 15px;
right: 15px;
}
}

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import styles from "./Modal.module.css"; import styles from "./Modal.module.css";
import { getImageUrl } from "../helpers/itemHelper.js"; import { ThreeDots } from "react-loader-spinner";
const ItemConfig = ({ const ItemConfig = ({
name: initialName, name: initialName,
@@ -24,11 +25,14 @@ const ItemConfig = ({
const fileInputRef = useRef(null); const fileInputRef = useRef(null);
const textareaRef = useRef(null); const textareaRef = useRef(null);
const [isSaving, setIsSaving] = useState(false);
useEffect(() => { useEffect(() => {
// Prevent scrolling when modal is open // Prevent scrolling when modal is open
document.body.style.overflow = "hidden"; document.body.style.overflow = "hidden";
if(selectedImage){ if (selectedImage) {
const reader = new FileReader(); const reader = new FileReader();
reader.onloadend = () => { reader.onloadend = () => {
setPreviewUrl(reader.result); setPreviewUrl(reader.result);
@@ -79,18 +83,27 @@ const ItemConfig = ({
return () => textarea.removeEventListener("input", handleResize); return () => textarea.removeEventListener("input", handleResize);
} }
}, [textareaRef.current]); }, [textareaRef.current]);
const handleCreate = async () => {
setIsSaving(true);
try {
await handleCreateItem(itemName, itemPrice, selectedImage, previewUrl, itemDescription, itemPromoPrice);
document.body.style.overflow = "auto";
} finally {
setIsSaving(false);
}
};
const handleCreate = () => { const handleUpdate = async () => {
console.log(itemPromoPrice) setIsSaving(true);
handleCreateItem(itemName, itemPrice, selectedImage, previewUrl, itemDescription, itemPromoPrice); try {
document.body.style.overflow = "auto"; await handleUpdateItem(itemName, itemPrice, selectedImage, itemDescription, itemPromoPrice);
};
const handleUpdate = () => {
console.log(itemName, itemPrice, selectedImage, itemDescription, itemPromoPrice)
handleUpdateItem(itemName, itemPrice, selectedImage, itemDescription, itemPromoPrice);
document.body.style.overflow = "auto"; document.body.style.overflow = "auto";
} finally {
setIsSaving(false);
}
}; };
return ( return (
<div onClick={handleOverlayClick} style={{ position: 'fixed', width: '100vw', height: '100vh', left: 0, bottom: 0, display: 'flex', flexDirection: 'column-reverse', zIndex: 301, backgroundColor: '#00000061' }}> <div onClick={handleOverlayClick} style={{ position: 'fixed', width: '100vw', height: '100vh', left: 0, bottom: 0, display: 'flex', flexDirection: 'column-reverse', zIndex: 301, backgroundColor: '#00000061' }}>
<div onClick={handleContentClick} style={{ display: 'flex', flexDirection: 'column', padding: '15px', backgroundColor: 'white', borderRadius: '20px 20px 0 0', overflowY: 'auto' }}> <div onClick={handleContentClick} style={{ display: 'flex', flexDirection: 'column', padding: '15px', backgroundColor: 'white', borderRadius: '20px 20px 0 0', overflowY: 'auto' }}>
@@ -131,7 +144,7 @@ const ItemConfig = ({
transition: 'all 0.3s ease', transition: 'all 0.3s ease',
boxSizing: 'border-box', // Make sure the padding doesn't cause overflow boxSizing: 'border-box', // Make sure the padding doesn't cause overflow
}} }}
onChange={(e)=>setItemName(e.target.value)} onChange={(e) => setItemName(e.target.value)}
onFocus={(e) => e.target.style.borderColor = '#60d37e'} onFocus={(e) => e.target.style.borderColor = '#60d37e'}
onBlur={(e) => e.target.style.borderColor = '#ccc'} onBlur={(e) => e.target.style.borderColor = '#ccc'}
/> />
@@ -153,7 +166,7 @@ const ItemConfig = ({
transition: 'all 0.3s ease', transition: 'all 0.3s ease',
boxSizing: 'border-box', boxSizing: 'border-box',
}} }}
onChange={(e)=>setItemPrice(e.target.value)} onChange={(e) => setItemPrice(e.target.value)}
onFocus={(e) => e.target.style.borderColor = '#60d37e'} onFocus={(e) => e.target.style.borderColor = '#60d37e'}
onBlur={(e) => e.target.style.borderColor = '#ccc'} onBlur={(e) => e.target.style.borderColor = '#ccc'}
/> />
@@ -174,7 +187,7 @@ const ItemConfig = ({
transition: 'all 0.3s ease', transition: 'all 0.3s ease',
boxSizing: 'border-box', boxSizing: 'border-box',
}} }}
onChange={(e)=>setItemPromoPrice(e.target.value)} onChange={(e) => setItemPromoPrice(e.target.value)}
onFocus={(e) => e.target.style.borderColor = '#60d37e'} onFocus={(e) => e.target.style.borderColor = '#60d37e'}
onBlur={(e) => e.target.style.borderColor = '#ccc'} onBlur={(e) => e.target.style.borderColor = '#ccc'}
/> />
@@ -200,17 +213,40 @@ const ItemConfig = ({
}} }}
placeholder="Tambah deskripsi..." placeholder="Tambah deskripsi..."
value={itemDescription} value={itemDescription}
onChange={(e)=>setItemDescription(e.target.value)} onChange={(e) => setItemDescription(e.target.value)}
onFocus={(e) => e.target.style.borderColor = '#60d37e'} onFocus={(e) => e.target.style.borderColor = '#60d37e'}
onBlur={(e) => e.target.style.borderColor = '#ccc'} onBlur={(e) => e.target.style.borderColor = '#ccc'}
/> />
</div> </div>
<div
onClick={() => {
if (!isSaving) {
isBeingEdit ? handleUpdate() : handleCreate();
}
}}
style={{
width: '100%',
height: '40px',
alignContent: 'center',
textAlign: 'center',
borderRadius: '10px',
border: '1px solid #60d37e',
color: isSaving ? '#aaa' : '#60d37e',
backgroundColor: 'white',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
cursor: isSaving ? 'not-allowed' : 'pointer'
}}
>
{isSaving ? (
<div style={{ width: '100%', height: '35px', display: 'flex', justifyContent: 'center', alignItems: 'center' }}> <ThreeDots height={20} width={20} />
<div onClick={() => {isBeingEdit ? handleUpdate() : handleCreate()} } style={{ width: '100%', height: '40px', alignContent: 'center', textAlign: 'center', borderRadius: '10px', border: '1px solid #60d37e', color: '#60d37e', backgroundColor: 'white', display: 'flex', justifyContent: 'center', alignItems: 'center' }}> ) : (
{isBeingEdit? 'Simpan' : 'Buat Item'} isBeingEdit ? 'Simpan' : 'Simpan'
</div> )}
</div> </div>
</div> </div>
</div> </div>
); );

View File

@@ -598,7 +598,7 @@ const ItemLister = ({
return ( return (
<> <>
{(items.length > 0 || {(items.length > 0 ||
(user && (user.cafeId == shopId || user.userId == shopOwnerId))) && ( (user && (user.cafeId == shopId || user.user_id == shopOwnerId))) && (
<div <div
key={itemTypeId} key={itemTypeId}
className={`${styles["item-lister"]} ${isEdit ? styles["fullscreen"] : "" className={`${styles["item-lister"]} ${isEdit ? styles["fullscreen"] : ""
@@ -713,15 +713,6 @@ const ItemLister = ({
} }
imageUrl={getImageUrl("uploads/assets/addnew.png")} imageUrl={getImageUrl("uploads/assets/addnew.png")}
/> />
{/* {typeImage != null && !previewUrl.includes(typeImage) && (
<ItemType
rectangular={true}
onClick={(previewUrl, selectedImage) =>
handleImageChange(previewUrl, selectedImage)
}
imageUrl={getImageUrl(typeImage)}
/>
)} */}
<ItemType <ItemType
rectangular={true} rectangular={true}
@@ -857,7 +848,6 @@ const ItemLister = ({
); );
})} })}
</div> </div>
<button onClick={() => setIsFirstStep(false)} style={{ width: '100%', height: '40px', borderRadius: '20px' }}>selanjutnya</button>
</> </>
)} )}
{(isEdit && !isFirstStep || !isEdit) && {(isEdit && !isFirstStep || !isEdit) &&
@@ -866,7 +856,7 @@ const ItemLister = ({
<h2 className={styles["item-list-title"]}>{items && items.length < 1 ? 'Buat item' : 'Daftar item'}</h2></div>} <h2 className={styles["item-list-title"]}>{items && items.length < 1 ? 'Buat item' : 'Daftar item'}</h2></div>}
<div className={styles["item-list"]}> <div className={styles["item-list"]}>
{user && ( {user && (
user.userId == shopOwnerId || user.cafeId == shopId) && user.user_id == shopOwnerId || user.cafeId == shopId) &&
isEditMode && ( isEditMode && (
<> <>
{!isAddingNewItem && ( {!isAddingNewItem && (
@@ -1113,7 +1103,7 @@ const ItemLister = ({
{user && {user &&
user.roleId == 1 && user.roleId == 1 &&
user.userId == shopOwnerId && user.user_id == shopOwnerId &&
isEdit && ( isEdit && (
<> <>
{/* <button {/* <button

View File

@@ -24,60 +24,8 @@
/* padding: 10px; */ /* padding: 10px; */
/* max-height: calc(3 * (25vw - 20px) + 20px); */ /* max-height: calc(3 * (25vw - 20px) + 20px); */
overflow-y: auto; overflow-y: auto;
height: calc(49vw - 20px);
} }
@media (min-height: 0px) {
.grid-container {
height: 27vh;
}
}
@media (min-height: 630px) {
.grid-container {
height: 27vh;
}
}
@media (min-height: 636px) {
.grid-container {
height: 29vh;
}
}
@media (min-height: 650px) {
.grid-container {
height: 34vh;
}
}
@media (min-height: 705px) {
.grid-container {
height: 37vh;
}
}
@media (min-height: 735px) {
.grid-container {
height: 38vh;
}
}
@media (min-height: 759px) {
.grid-container {
height: 40vh;
}
}
@media (min-height: 819px) {
.grid-container {
height: 44vh;
}
}
@media (min-height: 830px) {
.grid-container {
height: 47vh;
}
}
@media (min-height: 892px) {
.grid-container {
height: 49vh;
}
}
.title-container { .title-container {
display: flex; display: flex;
@@ -171,7 +119,7 @@
.TotalContainer { .TotalContainer {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
width: 80vw; /* width: 80vw; */
margin: 0 auto; margin: 0 auto;
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600; font-weight: 600;

View File

@@ -9,6 +9,7 @@ export default function ItemType({
imageUrl, imageUrl,
selected, selected,
rectangular, rectangular,
typeLength
}) { }) {
const inputRef = useRef(null); const inputRef = useRef(null);
const [namee, setName] = useState(name); const [namee, setName] = useState(name);
@@ -68,7 +69,7 @@ export default function ItemType({
: "item-type-nomargin" : "item-type-nomargin"
] ]
} }
style={{ zIndex: blank ? 301 : "inherit" }} style={{ zIndex: blank ? 301 : "inherit", width: `calc(${100 / (Math.min(typeLength, 3) + 1)}% - 10px)` }}
> >
<div <div
onClick={ onClick={

View File

@@ -94,3 +94,102 @@
.noborder { .noborder {
border: 1px solid #ffffff00; border: 1px solid #ffffff00;
} }
@media (min-width: 768px){
.item-type {
width: calc(25% - 20px);
height: calc(30% - 20px);
margin: 1px 10px 0px;
overflow: visible;
text-align: center;
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
}
.item-type-rectangular {
width: calc(100% - 20px);
height: calc(100% - 20px);
overflow: visible;
text-align: center;
align-items: center;
display: flex;
flex-direction: column;
justify-content: start;
position: relative; /* Ensure absolute positioning inside works */
}
.item-type-nomargin {
width: calc(25% - 20px);
height: calc(39% - 20px);
overflow: visible;
text-align: center;
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
position: relative; /* Ensure absolute positioning inside works */
}
.item-type-rect {
position: relative;
height: 30%;
width: 30%;
object-fit: cover;
border-radius: 15px;
background-color: #fff;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.item-type-name {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-style: normal;
font-size: 14px;
color: #333;
width: calc(25vw - 30px);
text-align: center;
background-color: transparent;
position: relative; /* Needed for positioning the button */
}
.item-type-image {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 15px;
}
.item-type-image-container {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.item-type-image-input {
opacity: 0;
position: absolute;
top: 0;
right: 0;
width: 100%;
height: 100%;
}
.item-type-create {
position: absolute;
top: 76%; /* Position below the input */
left: 50%;
transform: translateX(-50%);
margin-top: 10px; /* Space between input and button */
width: 20vw;
text-align: center; /* Center button text */
}
.border {
border: 1px solid #000000;
}
.noborder {
border: 1px solid #ffffff00;
}
}

View File

@@ -1,5 +1,4 @@
.item-type-lister { .item-type-lister {
width: 100vw;
overflow-x: auto; overflow-x: auto;
white-space: nowrap; white-space: nowrap;
padding: 3px 0px; padding: 3px 0px;
@@ -11,6 +10,7 @@
-ms-overflow-style: none; /* IE and Edge */ -ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */ scrollbar-width: none; /* Firefox */
overflow-y: hidden; overflow-y: hidden;
width: 100%;
} }
.item-type { .item-type {

View File

@@ -103,7 +103,7 @@ const ItemTypeLister = ({
{isEditMode && {isEditMode &&
!isAddingNewItem && !isAddingNewItem &&
user && ( user && (
user.userId == shopOwnerId || user.cafeId == shopId) && ( user.user_id == shopOwnerId || user.cafeId == shopId) && (
<ItemType <ItemType
onClick={toggleAddNewItem} onClick={toggleAddNewItem}
name={"buat baru"} name={"buat baru"}
@@ -111,7 +111,7 @@ const ItemTypeLister = ({
/> />
)} )}
{user &&( {user &&(
user.userId == shopOwnerId || user.cafeId == shopId) && user.user_id == shopOwnerId || user.cafeId == shopId) &&
isAddingNewItem && ( isAddingNewItem && (
<> <>
<ItemLister <ItemLister
@@ -141,13 +141,14 @@ const ItemTypeLister = ({
itemTypes.map( itemTypes.map(
(itemType) => (itemType) =>
( (
itemType.itemList.length > 0 || (user && (user.userId == shopOwnerId || user.cafeId == shopId))) && (
<ItemType <ItemType
key={itemType.itemTypeId} key={itemType.itemTypeId}
name={itemType.name} name={itemType.name}
imageUrl={getImageUrl(itemType.image)} imageUrl={getImageUrl(itemType.image)}
onClick={() => onFilterChange(itemType.itemTypeId)} onClick={() => onFilterChange(itemType.itemTypeId)}
selected={filterId === itemType.itemTypeId} selected={filterId === itemType.itemTypeId}
typeLength={itemTypes.length}
/> />
) )
)} )}

View File

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

View File

@@ -36,3 +36,4 @@
.closeButton:hover { .closeButton:hover {
color: #f44336; /* Change color on hover for better UX */ color: #f44336; /* Change color on hover for better UX */
} }

View File

@@ -367,7 +367,7 @@ export function MusicPlayer({ socket, shopId, user, shopOwnerId, isSpotifyNeedLo
const [text, setText] = useState("Menunggu musik favoritmu"); const [text, setText] = useState("Menunggu musik favoritmu");
const textIndex = useRef(0); const textIndex = useRef(0);
const [messages, setMessages] = useState(["Menunggu musik favoritmu", "Klik untuk putar musik favoritmu"]); const [messages, setMessages] = useState(["Menunggu musik favoritmu", "Upgrade to use"]);
useEffect(() => { useEffect(() => {
@@ -376,7 +376,7 @@ export function MusicPlayer({ socket, shopId, user, shopOwnerId, isSpotifyNeedLo
currentSong != null && currentSong[0]?.trackId != 'kCGs5_oCtBE' && currentSong[0]?.trackId != 'O8eYd7oAZtA' && currentSong[0]?.name != undefined currentSong != null && currentSong[0]?.trackId != 'kCGs5_oCtBE' && currentSong[0]?.trackId != 'O8eYd7oAZtA' && currentSong[0]?.name != undefined
? `${currentSong[0]?.name} - ${currentSong[0]?.artist}` ? `${currentSong[0]?.name} - ${currentSong[0]?.artist}`
: "Menunggu musik favoritmu", : "Menunggu musik favoritmu",
"Klik untuk putar musik favoritmu" "Upgrade to use"
]; ];
setMessages(newMessages); setMessages(newMessages);
@@ -421,7 +421,7 @@ export function MusicPlayer({ socket, shopId, user, shopOwnerId, isSpotifyNeedLo
return ( return (
<div className={`music-player`} style={{ marginBottom: `${viewing ? '-10px' : ''}` }}> <div className={`music-player`} style={{ marginBottom: `${viewing ? '-10px' : ''}` }}>
<div <div
onClick={toggleView} // onClick={toggleView}
className="current-bgr" className="current-bgr"
style={{ backgroundImage: `url(${backgroundImage})` }} style={{ backgroundImage: `url(${backgroundImage})` }}
// style={{ backgroundImage: `url(${videoSrc != "" ? '' : backgroundImage})` }} // style={{ backgroundImage: `url(${videoSrc != "" ? '' : backgroundImage})` }}
@@ -479,7 +479,7 @@ export function MusicPlayer({ socket, shopId, user, shopOwnerId, isSpotifyNeedLo
className={`expandable-container ${expanded ? "expanded" : ""}`} className={`expandable-container ${expanded ? "expanded" : ""}`}
ref={expandableContainerRef} ref={expandableContainerRef}
> >
{user.cafeId == shopId || user.userId == shopOwnerId && ( {user.cafeId == shopId || user.user_id == shopOwnerId && (
<> <>
<div className="auth-box"> <div className="auth-box">
<div <div

View File

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

View File

@@ -106,10 +106,10 @@ export async function getCafeByIdentifier(cafeIdentifyName) {
return -1; return -1;
} }
} }
export async function getOwnedCafes(userId) { export async function getOwnedCafes(user_id) {
try { try {
const response = await fetch( const response = await fetch(
`${API_BASE_URL}/cafe/get-cafe-by-ownerId/` + userId, `${API_BASE_URL}/cafe/get-cafe-by-ownerId/` + user_id,
{ {
method: "POST", method: "POST",
headers: { headers: {

View File

@@ -217,7 +217,8 @@ export const handlePaymentFromClerk = async (
user_email, user_email,
payment_type, payment_type,
serving_type, serving_type,
tableNo tableNo,
notes
) => { ) => {
try { try {
const token = getLocalStorage("auth"); const token = getLocalStorage("auth");
@@ -245,15 +246,15 @@ export const handlePaymentFromClerk = async (
serving_type, serving_type,
tableNo, tableNo,
transactions: structuredItems, transactions: structuredItems,
notes
}), }),
} }
); );
if (response.ok) { if (response.ok) {
// Handle success response const data = await response.json();
console.log("Transaction successful!"); console.log("Transaction successful!", data);
// Optionally return response data or handle further actions upon success return data;
return true;
} else { } else {
// Handle error response // Handle error response
console.error("Transaction failed:", response.statusText); console.error("Transaction failed:", response.statusText);

View File

@@ -10,22 +10,35 @@ import {
import "../App.css"; import "../App.css";
import API_BASE_URL from '../config'; import API_BASE_URL from "../config";
import Watermark from "../components/Watermark"; import Watermark from "../components/Watermark";
import { getImageUrl, createItem, updateItem, moveItemType } from "../helpers/itemHelper.js"; import {
getImageUrl,
createItem,
updateItem,
moveItemType,
} from "../helpers/itemHelper.js";
import SearchInput from "../components/SearchInput"; import SearchInput from "../components/SearchInput";
import ItemTypeLister from "../components/ItemTypeLister"; import ItemTypeLister from "../components/ItemTypeLister";
import { MusicPlayer } from "../components/MusicPlayer"; import { MusicPlayer } from "../components/MusicPlayer";
import ItemLister from "../components/ItemLister"; import ItemLister from "../components/ItemLister";
import Header from "../components/Header"; import Header from "../components/Header";
import Switch from "react-switch";
import { ThreeDots } from "react-loader-spinner"; import { ThreeDots } from "react-loader-spinner";
import { getLocalStorage, updateLocalStorage, removeLocalStorage } from "../helpers/localStorageHelpers"; import {
getLocalStorage,
updateLocalStorage,
removeLocalStorage,
} from "../helpers/localStorageHelpers";
import { unsubscribeUser } from "../helpers/subscribeHelpers.js"; import { unsubscribeUser } from "../helpers/subscribeHelpers.js";
import WelcomePage from "./WelcomePage.js"; import WelcomePage from "./WelcomePage.js";
import { useNavigationHelpers } from "../helpers/navigationHelpers"; import { useNavigationHelpers } from "../helpers/navigationHelpers";
import Cart from "./Cart";
function CafePage({ function CafePage({
shopId, shopId,
table, table,
@@ -46,21 +59,21 @@ function CafePage({
queue, queue,
cartItemsLength, cartItemsLength,
totalPrice, totalPrice,
lastTransaction lastTransaction,
shop,
totalItemsCount,
deviceType,
}) { }) {
const location = useLocation(); const location = useLocation();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const token = searchParams.get("token"); const token = searchParams.get("token");
const { shopIdentifier, tableCode } = useParams(); const { shopIdentifier, tableCode } = useParams();
sendParam({ shopIdentifier, tableCode }); sendParam({ shopIdentifier, tableCode });
const { const { goToCart, goToTransactions } = useNavigationHelpers(
goToCart, shopIdentifier,
goToTransactions table.tableCode
} = useNavigationHelpers(shopIdentifier, table.tableCode); );
const navigate = useNavigate(); const navigate = useNavigate();
@@ -77,18 +90,50 @@ function CafePage({
const [beingEditedType, setBeingEditedType] = useState(0); const [beingEditedType, setBeingEditedType] = useState(0);
const checkWelcomePageConfig = () => { // const checkWelcomePageConfig = () => {
const parsedConfig = JSON.parse(welcomePageConfig); // const parsedConfig = JSON.parse(welcomePageConfig);
if (parsedConfig.isWelcomePageActive == "true") { // if (parsedConfig.isWelcomePageActive == "true") {
const clicked = sessionStorage.getItem("getStartedClicked"); // const clicked = sessionStorage.getItem("getStartedClicked");
if (!clicked) { // if (!clicked) {
sessionStorage.setItem("getStartedClicked", true); // sessionStorage.setItem("getStartedClicked", true);
document.body.style.overflow = "hidden"; // document.body.style.overflow = "hidden";
setIsStarted(true); // setIsStarted(true);
} // }
// }
// };
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);
}; };
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
useEffect(() => {
if (window.gtag && shopIdentifier) {
window.gtag("event", "page_view", {
page_title: `Cafe - ${shopIdentifier}`,
page_location: window.location.href,
page_path: `/` + shopIdentifier,
shop_id: shopId || null, // opsional jika kamu mau track ID juga
});
}
}, [shopIdentifier]);
useEffect(() => { useEffect(() => {
if (welcomePageConfig) { if (welcomePageConfig) {
const parsedConfig = JSON.parse(welcomePageConfig); const parsedConfig = JSON.parse(welcomePageConfig);
@@ -100,25 +145,29 @@ function CafePage({
isActive: parsedConfig.isWelcomePageActive === "true", isActive: parsedConfig.isWelcomePageActive === "true",
}); });
} }
checkWelcomePageConfig(); // checkWelcomePageConfig();
}, [welcomePageConfig]); }, [welcomePageConfig]);
useEffect(() => { useEffect(() => {
function fetchData() { function fetchData() {
console.log(user.userId == shopOwnerId) console.log(user.user_id == shopOwnerId);
setModal("create_item"); setModal("create_item");
} }
console.log(getLocalStorage('auth'))
console.log(getLocalStorage("auth"));
if (getLocalStorage("auth") != null) { if (getLocalStorage("auth") != null) {
const executeFetch = async () => { const executeFetch = async () => {
while (user.length == 0) { while (user.length == 0) {
await new Promise((resolve) => setTimeout(resolve, 100)); // Wait until the user is not null await new Promise((resolve) => setTimeout(resolve, 100)); // Wait until the user is not null
} }
console.log(user) console.log(user);
console.log('open') console.log("open");
if (user.length != 0 && user.userId == shopOwnerId && shopItems.length == 0) fetchData(); if (
user.length != 0 &&
user.user_id == shopOwnerId &&
shopItems.length == 0
)
fetchData();
}; };
executeFetch(); executeFetch();
} }
@@ -141,13 +190,39 @@ function CafePage({
socket.on("joined-room", (response) => { socket.on("joined-room", (response) => {
const { isSpotifyNeedLogin, isExceededDeadline } = response; const { isSpotifyNeedLogin, isExceededDeadline } = response;
setNeedSpotifyLogin(isSpotifyNeedLogin); setNeedSpotifyLogin(isSpotifyNeedLogin);
if (isExceededDeadline) setModal("message",{captMessage:'Kafe sedang tidak tersedia'}); if (isExceededDeadline)
setModal("message", { captMessage: "Kafe sedang tidak tersedia" });
setIsExceededDeadline(isExceededDeadline); setIsExceededDeadline(isExceededDeadline);
}); });
} }
if (socket) fetchData(); if (socket) fetchData();
}, [socket]); }, [socket]);
useEffect(() => {
const isTablet = window.innerWidth >= 768;
const handleFirstClick = async () => {
if (
isTablet &&
document.fullscreenEnabled &&
!document.fullscreenElement
) {
try {
await document.documentElement.requestFullscreen();
document.removeEventListener("click", handleFirstClick);
} catch (err) {
console.warn("Gagal masuk fullscreen:", err);
}
}
};
// Tambahkan listener satu kali
document.addEventListener("click", handleFirstClick);
return () => {
document.removeEventListener("click", handleFirstClick);
};
}, []);
const handleGetStarted = () => { const handleGetStarted = () => {
setIsStarted(false); setIsStarted(false);
@@ -162,16 +237,19 @@ function CafePage({
const newItems = [...shopItems]; const newItems = [...shopItems];
let targetIndex; let targetIndex;
if (direction === 'up' && index > 0) { if (direction === "up" && index > 0) {
targetIndex = index - 1; targetIndex = index - 1;
} else if (direction === 'down' && index < newItems.length - 1) { } else if (direction === "down" && index < newItems.length - 1) {
targetIndex = index + 1; targetIndex = index + 1;
} }
console.log(index); console.log(index);
console.log(targetIndex); console.log(targetIndex);
if (targetIndex !== undefined) { if (targetIndex !== undefined) {
// Swap items // Swap items
[newItems[index], newItems[targetIndex]] = [newItems[targetIndex], newItems[index]]; [newItems[index], newItems[targetIndex]] = [
newItems[targetIndex],
newItems[index],
];
newItems[index].order = targetIndex; newItems[index].order = targetIndex;
newItems[targetIndex].order = index; newItems[targetIndex].order = index;
@@ -179,14 +257,83 @@ function CafePage({
// Call the API to move the item type // Call the API to move the item type
try { try {
await moveItemType(itemTypeId, previousItems[targetIndex].itemTypeId, index, targetIndex); await moveItemType(
itemTypeId,
previousItems[targetIndex].itemTypeId,
index,
targetIndex
);
} catch (error) { } catch (error) {
console.error('Error moving item type:', error); console.error("Error moving item type:", error);
// Revert the changes if the backend fails // Revert the changes if the backend fails
setShopItems(previousItems); setShopItems(previousItems);
} }
} }
}; };
const FullscreenButton = ({ onClick }) => {
return (
<div
onClick={onClick}
style={{
width: 40,
height: 40,
borderRadius: "50%",
backgroundColor: "#7272729e",
display: "flex",
justifyContent: "center",
alignItems: "center",
cursor: "pointer",
userSelect: "none",
position: 'fixed',
top: 0,
left: 0,
zIndex: 1000
}}
title="Toggle Fullscreen"
>
<span
style={{
display: "inline-block",
transform: "rotate(45deg)",
color: "white",
fontWeight: "bold",
fontSize: 24,
lineHeight: 1,
userSelect: "none",
}}
>
&lt;&gt;
</span>
</div>
);
};
const handleFullscreen = () => {
const elem = document.documentElement; // fullscreen seluruh halaman
if (!document.fullscreenElement) {
// masuk fullscreen
if (elem.requestFullscreen) {
elem.requestFullscreen();
} else if (elem.mozRequestFullScreen) { /* Firefox */
elem.mozRequestFullScreen();
} else if (elem.webkitRequestFullscreen) { /* Chrome, Safari & Opera */
elem.webkitRequestFullscreen();
} else if (elem.msRequestFullscreen) { /* IE/Edge */
elem.msRequestFullscreen();
}
} else {
// keluar fullscreen
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) { /* Firefox */
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) { /* Chrome, Safari and Opera */
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) { /* IE/Edge */
document.msExitFullscreen();
}
}
};
if (loading) if (loading)
return ( return (
@@ -211,12 +358,21 @@ function CafePage({
isFullscreen={true} isFullscreen={true}
/> />
) : ( */} ) : ( */}
<div className={`Cafe ${isExceededDeadline ? 'grayscale' : ''}`}> <div
className={`Cafe ${isExceededDeadline ? "grayscale" : ""}`}
{API_BASE_URL != 'https://dev.api.kedaimaster.com' && API_BASE_URL != 'https://api.kedaimaster.com' && style={{
display: isTablet ? "flex" : "block",
flexDirection: "row",
width: "100%",
}}
>
{API_BASE_URL != "https://dev.api.kedaimaster.com" &&
API_BASE_URL != "https://api.kedaimaster.com" && (
<div className="Watermark"></div> <div className="Watermark"></div>
} )}
<div style={{ width: isTablet ? "60%" : "100%" }}>
<div className="App-header"> <div className="App-header">
{isTablet && !isFullscreen && <FullscreenButton onClick={handleFullscreen} />}
<Header <Header
HeaderText={"Menu"} HeaderText={"Menu"}
showProfile={true} showProfile={true}
@@ -244,7 +400,31 @@ function CafePage({
queue={queue} queue={queue}
setModal={setModal} setModal={setModal}
/> />
<div></div> {user.username !== undefined &&
(user.cafeId === shopId || user.user_id === shopOwnerId) &&
(user.roleId === 1 || user.roleId === 2) && (
<div
style={{
backgroundColor: "#5c7c5c",
padding: "7px 28px",
margin: "0 10px",
borderRadius: "15px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
textShadow: "2px 2px 4px rgba(0, 0, 0, 0.7)",
fontSize: "16px",
}}
>
Mode Edit&nbsp;
<Switch
borderRadius={0}
checked={isEditMode}
onChange={() => setIsEditMode(!isEditMode)}
/>
</div>
)}
<ItemTypeLister <ItemTypeLister
user={user} user={user}
shopOwnerId={shopOwnerId} shopOwnerId={shopOwnerId}
@@ -278,8 +458,12 @@ function CafePage({
setShopItems={setShopItems} setShopItems={setShopItems}
itemList={itemType.itemList} itemList={itemType.itemList}
typeVisibility={itemType.visibility} typeVisibility={itemType.visibility}
moveItemTypeUp={(e) => moveItemTypeHandler(e, 'up', index)} moveItemTypeUp={(e) =>
moveItemTypeDown={(e) => moveItemTypeHandler(e, 'down', index)} moveItemTypeHandler(e, "up", index)
}
moveItemTypeDown={(e) =>
moveItemTypeHandler(e, "down", index)
}
isEditMode={isEditMode} isEditMode={isEditMode}
beingEditedType={beingEditedType} beingEditedType={beingEditedType}
setBeingEditedType={setBeingEditedType} setBeingEditedType={setBeingEditedType}
@@ -290,7 +474,7 @@ function CafePage({
price, price,
selectedImage, selectedImage,
description, description,
promoPrice, promoPrice
) => ) =>
createItem( createItem(
shopId, shopId,
@@ -299,41 +483,137 @@ function CafePage({
selectedImage, selectedImage,
itemTypeID, itemTypeID,
description, description,
promoPrice, promoPrice
) )
} }
handleUpdateItem={(itemId, name, price, selectedImage, description, promoPrice) => handleUpdateItem={(
updateItem(itemId, name, price, selectedImage, description, promoPrice) itemId,
name,
price,
selectedImage,
description,
promoPrice
) =>
updateItem(
itemId,
name,
price,
selectedImage,
description,
promoPrice
)
} }
/> />
))} ))}
{!isEditMode && (user.username || cartItemsLength > 0) && {!isEditMode && (user.username || cartItemsLength > 0) && (
<div style={{ marginTop: '10px', height: '40px', position: 'sticky', bottom: '40px', display: 'flex', justifyContent: 'center', alignItems: 'center', textAlign: 'center' }}> <div
{(lastTransaction != null || cartItemsLength > 0) && style={{
<div onClick={goToCart} style={{ backgroundColor: '#73a585', width: user.username ? '55vw' : '70vw', height: '40px', borderRadius: '30px', display: 'flex', justifyContent: 'space-between', padding: '0 20px' }}> marginTop: "10px",
<div style={{ display: 'flex', flexWrap: 'wrap', alignContent: 'center' }}>{lastTransaction != null && '+'}{cartItemsLength} item</div> height: "40px",
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', width: '130px' }}> position: "sticky",
{((lastTransaction == null || lastTransaction?.payment_type != 'paylater')) ? bottom: "40px",
<span style={{ whiteSpace: 'nowrap' }}>Rp{totalPrice}</span> display: "flex",
: justifyContent: "center",
<span style={{ whiteSpace: 'nowrap' }}>Open bill</span> alignItems: "center",
} textAlign: "center",
<div style={{ display: 'flex', alignItems: 'center', marginLeft: '5px', width: '20px' }}> }}
<svg viewBox="0 0 34 34" style={{ fill: 'white', marginTop: '4px' }}> >
{(!isTablet &&(lastTransaction != null || cartItemsLength > 0)) && (
<div
onClick={goToCart}
style={{
backgroundColor: "#73a585",
width: user.username ? "55vw" : "70vw",
height: "40px",
borderRadius: "30px",
display: "flex",
justifyContent: "space-between",
padding: "0 20px",
}}
>
<div
style={{
display: "flex",
flexWrap: "wrap",
alignContent: "center",
}}
>
{lastTransaction != null && "+"}
{cartItemsLength} item
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
width: "130px",
}}
>
{lastTransaction == null ||
lastTransaction?.payment_type != "paylater" ? (
<span style={{ whiteSpace: "nowrap" }}>
Rp{totalPrice}
</span>
) : (
<span style={{ whiteSpace: "nowrap" }}>
Open bill
</span>
)}
<div
style={{
display: "flex",
alignItems: "center",
marginLeft: "5px",
width: "20px",
}}
>
<svg
viewBox="0 0 34 34"
style={{ fill: "white", marginTop: "4px" }}
>
<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"></path> <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"></path>
</svg> </svg>
</div> </div>
</div> </div>
</div> </div>
} )}
{user.username && {user.username && (
<div onClick={goToTransactions} style={{ backgroundColor: '#73a585', width: '15vw', height: '40px', borderRadius: '30px', display: 'flex', justifyContent: 'center', marginLeft: lastTransaction != null || cartItemsLength > 0 ? '6px' : '0px' }}> <div
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '38px', marginRight: '5px' }}> onClick={goToTransactions}
<div style={{ display: 'flex', alignItems: 'center', marginLeft: '5px', width: '20px' }}> style={{
backgroundColor: "#73a585",
width: "15vw",
height: "40px",
borderRadius: "30px",
display: "flex",
justifyContent: "center",
marginLeft:
lastTransaction != null || cartItemsLength > 0
? "6px"
: "0px",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "38px",
marginRight: "5px",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
marginLeft: "5px",
width: "20px",
}}
>
<svg viewBox="0 0 512 512"> <svg viewBox="0 0 512 512">
<g <g
transform="translate(0 460) scale(0.09 -0.09)" transform="translate(0 460) scale(0.09 -0.09)"
style={{ fill: 'white', marginTop: '4px' }} style={{ fill: "white", marginTop: "4px" }}
stroke="none" stroke="none"
> >
<path <path
@@ -364,13 +644,39 @@ function CafePage({
</div> </div>
</div> </div>
</div> </div>
} )}
</div>
)}
</div>
</div>
{/* <Watermark /> */}
</div>
{isTablet &&
<div
style={{
width: "40%",
position: "fixed",
right: 0,
top: 0,
zIndex: 199
}}
>
<Cart
shopId={shopId}
shop={shop}
table={table}
setModal={setModal}
sendParam={sendParam}
socket={socket}
totalItemsCount={totalItemsCount}
deviceType={deviceType}
shopItems={shopItems}
setShopItems={setShopItems}
isTablet={true}
/>
</div> </div>
} }
</div> </div>
</div>
<Watermark/>
</div>
{/* )} */} {/* )} */}
</> </>
); );

View File

@@ -14,7 +14,7 @@ import {
handlePaymentFromGuestSide, handlePaymentFromGuestSide,
handlePaymentFromGuestDevice, handlePaymentFromGuestDevice,
handleExtendFromGuestDevice, handleExtendFromGuestDevice,
handleCloseBillFromGuestDevice handleCloseBillFromGuestDevice,
} from "../helpers/transactionHelpers"; } from "../helpers/transactionHelpers";
import { getItemsByCafeId } from "../helpers/cartHelpers.js"; import { getItemsByCafeId } from "../helpers/cartHelpers.js";
@@ -22,15 +22,27 @@ import { getItemsByCafeId } from "../helpers/cartHelpers.js";
import Dropdown from "./Dropdown.js"; import Dropdown from "./Dropdown.js";
import { useNavigationHelpers } from "../helpers/navigationHelpers"; import { useNavigationHelpers } from "../helpers/navigationHelpers";
const formatToRupiah = (value) => {
if (typeof value !== "number") return value;
return value.toLocaleString("id-ID");
};
export default function Invoice({ shopId, setModal, table, sendParam, deviceType, socket, shopItems, setShopItems }) { export default function Invoice({
shopId,
setModal,
table,
sendParam,
deviceType,
socket,
shopItems,
setShopItems,
isTablet
}) {
const { shopIdentifier, tableCode } = useParams(); const { shopIdentifier, tableCode } = useParams();
sendParam({ shopIdentifier, tableCode }); sendParam({ shopIdentifier, tableCode });
const { const { goToShop } = useNavigationHelpers(shopIdentifier, table.tableCode);
goToShop
} = useNavigationHelpers(shopIdentifier, table.tableCode);
const [cartItems, setCartItems] = useState([]); const [cartItems, setCartItems] = useState([]);
const [totalPrice, setTotalPrice] = useState(0); const [totalPrice, setTotalPrice] = useState(0);
@@ -51,14 +63,14 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
const fetchPaymentMethods = async () => { const fetchPaymentMethods = async () => {
try { try {
const methods = await getPaymentMethods(shopId); const methods = await getPaymentMethods(shopId);
console.log(methods) console.log(methods);
const lastTransaction = JSON.parse(localStorage.getItem('lastTransaction')); const lastTransaction = JSON.parse(
if (lastTransaction?.payment_type == 'paylater') methods.isOpenBillAvailable = false; localStorage.getItem("lastTransaction")
setPaymentMethods(methods) );
if (lastTransaction?.payment_type == "paylater")
} catch (err) { methods.isOpenBillAvailable = false;
setPaymentMethods(methods);
} } catch (err) { }
}; };
if (shopId) { if (shopId) {
@@ -70,29 +82,27 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
const fetchCartItems = async () => { const fetchCartItems = async () => {
try { try {
const cart = getItemsByCafeId(shopId); const cart = getItemsByCafeId(shopId);
const itemMap = new Map(cart.map(item => [item.itemId, item.qty])); const itemMap = new Map(cart.map((item) => [item.itemId, item.qty]));
// Step 2: Filter and transform shopItems // Step 2: Filter and transform shopItems
const filteredItems = shopItems const filteredItems = shopItems
.map(itemType => ({ .map((itemType) => ({
itemTypeId: itemType.itemTypeId, itemTypeId: itemType.itemTypeId,
cafeId: itemType.cafeId, cafeId: itemType.cafeId,
typeName: itemType.name, typeName: itemType.name,
itemList: itemType.itemList itemList: itemType.itemList
.filter(item => itemMap.has(item.itemId)) // Keep only items in getItemsByCafeId .filter((item) => itemMap.has(item.itemId)) // Keep only items in getItemsByCafeId
.map(item => ({ .map((item) => ({
itemId: item.itemId, itemId: item.itemId,
price: (item.promoPrice ? item.promoPrice : item.price), price: item.promoPrice ? item.promoPrice : item.price,
name: item.name, name: item.name,
image: item.image, image: item.image,
qty: itemMap.get(item.itemId), // Add qty from getItemsByCafeId qty: itemMap.get(item.itemId), // Add qty from getItemsByCafeId
availability: item.availability availability: item.availability,
})),
})) }))
})) .filter((itemType) => itemType.itemList.length > 0); // Remove empty itemTypes
.filter(itemType => itemType.itemList.length > 0); // Remove empty itemTypes console.log(filteredItems);
console.log(filteredItems)
// Update local storage by removing unavailable items // Update local storage by removing unavailable items
const updatedLocalStorage = const updatedLocalStorage =
@@ -120,7 +130,10 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
return ( return (
total + total +
itemType.itemList.reduce((subtotal, item) => { itemType.itemList.reduce((subtotal, item) => {
return subtotal + item.qty * (item.promoPrice ? item.promoPrice : item.price); return (
subtotal +
item.qty * (item.promoPrice ? item.promoPrice : item.price)
);
}, 0) }, 0)
); );
}, 0); }, 0);
@@ -128,7 +141,7 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
setTimeout(function () { setTimeout(function () {
setCartItems(filteredItems); setCartItems(filteredItems);
setTotalPrice(totalPrice); setTotalPrice(totalPrice);
setIsLoading(false) setIsLoading(false);
}, 100); //delay is in milliseconds }, 100); //delay is in milliseconds
} catch (error) { } catch (error) {
console.error("Error fetching cart items:", error); console.error("Error fetching cart items:", error);
@@ -138,18 +151,17 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
fetchCartItems(); fetchCartItems();
const getNewestCartItems = async () => { const getNewestCartItems = async () => {
try { try {
// Fetch items from the cart details (latest state) // Fetch items from the cart details (latest state)
const items = await getCartDetails(shopId); const items = await getCartDetails(shopId);
// Loop through each item type in the items from the cart details // Loop through each item type in the items from the cart details
items.forEach(itemType => { items.forEach((itemType) => {
itemType.itemList.forEach(item => { itemType.itemList.forEach((item) => {
// Loop through the shopItems and find the corresponding itemId // Loop through the shopItems and find the corresponding itemId
shopItems.forEach(shopItemType => { shopItems.forEach((shopItemType) => {
shopItemType.itemList.forEach(shopItem => { shopItemType.itemList.forEach((shopItem) => {
if (shopItem.itemId === item.itemId) { if (shopItem.itemId === item.itemId) {
// Update shopItems with the new data from items (e.g., availability, price) // Update shopItems with the new data from items (e.g., availability, price)
shopItem.availability = item.availability; shopItem.availability = item.availability;
@@ -179,7 +191,8 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
}, 100); // delay is in milliseconds }, 100); // delay is in milliseconds
// Update local storage by removing unavailable items and updating prices // Update local storage by removing unavailable items and updating prices
const updatedLocalStorage = JSON.parse(localStorage.getItem("cart")) || []; const updatedLocalStorage =
JSON.parse(localStorage.getItem("cart")) || [];
const newLocalStorage = updatedLocalStorage.map((cafe) => { const newLocalStorage = updatedLocalStorage.map((cafe) => {
if (cafe.cafeId === shopId) { if (cafe.cafeId === shopId) {
return { return {
@@ -194,8 +207,10 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
// Update the price in the local storage item // Update the price in the local storage item
return { return {
...item, ...item,
price: updatedItem.promoPrice ? updatedItem.promoPrice : updatedItem.price, price: updatedItem.promoPrice
availability: updatedItem.availability ? updatedItem.promoPrice
: updatedItem.price,
availability: updatedItem.availability,
}; };
} }
@@ -231,7 +246,10 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
return ( return (
total + total +
itemType.itemList.reduce((subtotal, item) => { itemType.itemList.reduce((subtotal, item) => {
return subtotal + item.qty * (item.promoPrice ? item.promoPrice : item.price); return (
subtotal +
item.qty * (item.promoPrice ? item.promoPrice : item.price)
);
}, 0) }, 0)
); );
}, 0); }, 0);
@@ -240,13 +258,12 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
console.error("Error fetching cart items:", error); console.error("Error fetching cart items:", error);
// Handle error if needed // Handle error if needed
} }
}; };
getNewestCartItems(); getNewestCartItems();
}, [shopId]); }, [shopId, localStorage.getItem('cart')]);
const handlePayCloseBill = async (orderMethod) =>{ const handlePayCloseBill = async (orderMethod) => {
setIsPaymentLoading(true); setIsPaymentLoading(true);
console.log("tipe" + deviceType); console.log("tipe" + deviceType);
if (transactionData) { if (transactionData) {
@@ -257,7 +274,76 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
socketId socketId
); );
} }
} };
const handlePrint = (transaction) => {
console.log(transaction)
const formatWaktu = (() => {
const date = transaction?.createdAt
? new Date(transaction.createdAt)
: new Date(); // UTC now
const tanggal = date.toLocaleDateString("id-ID");
const jam = date.toLocaleTimeString("id-ID", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
return `${tanggal} ${jam}`;
})();
const itemsStr = transaction.DetailedTransactions.map((dt) => {
const name =
dt.Item.name.length > 11
? dt.Item.name.slice(0, 11)
: dt.Item.name.padEnd(11);
const qty = dt.qty.toString().padStart(3);
const total = formatRupiah(dt.qty * (dt.promoPrice || dt.price)).padStart(15);
return `${name} ${qty} ${total}`;
}).join("\n");
const totalHarga = transaction.DetailedTransactions.reduce((acc, dt) => {
return acc + dt.qty * (dt.promoPrice || dt.price);
}, 0);
const totalStr = `Total: ${formatRupiah(totalHarga)}`;
const receiptText = ` CAFE HOREE
Jl. Ahmad Yani No. 12, Kediri
Telp: 0812-1617-6963
==============================
Tanggal : ${formatWaktu}
Bayar : ${transaction.payment_type}
------------------------------
Item Qty Total
------------------------------
${itemsStr}
${totalStr}
==============================
Terima kasih atas kunjungannya!
~~
supported by kedaimaster.com
\n\n\n\n\n`;
const params = new URLSearchParams();
params.append("content", receiptText);
params.append("encode_format", "UTF-8");
const printUrl = `btprinter://print?${params.toString()}`;
window.location.href = printUrl;
};
const formatRupiah = (value) => {
if (typeof value !== "number") return value;
return value.toLocaleString("id-ID", {
style: "currency",
currency: "IDR",
minimumFractionDigits: 0,
});
};
const handlePay = async (orderMethod) => { const handlePay = async (orderMethod) => {
setIsPaymentLoading(true); setIsPaymentLoading(true);
@@ -271,21 +357,28 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
orderMethod, orderMethod,
socketId socketId
); );
localStorage.removeItem('lastTransaction') localStorage.removeItem("lastTransaction");
// Dispatch the custom event // Dispatch the custom event
window.dispatchEvent(new Event("localStorageUpdated")); window.dispatchEvent(new Event("localStorageUpdated"));
} } else if (deviceType == "clerk") {
else
if (deviceType == "clerk") {
const pay = await handlePaymentFromClerk( const pay = await handlePaymentFromClerk(
shopId, shopId,
email, email,
orderMethod, orderMethod,
orderType, orderType,
tableNumber tableNumber,
textareaRef.current.value
); );
} 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( const pay = await handlePaymentFromGuestSide(
shopId, shopId,
email, email,
@@ -293,6 +386,8 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
orderType, orderType,
tableNumber tableNumber
); );
if (pay) window.location.reload();
} else if (deviceType == "guestDevice") { } else if (deviceType == "guestDevice") {
const socketId = socket.id; const socketId = socket.id;
const pay = await handlePaymentFromGuestDevice( const pay = await handlePaymentFromGuestDevice(
@@ -303,6 +398,7 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
textareaRef.current.value, textareaRef.current.value,
socketId socketId
); );
if (pay) window.location.reload();
} }
console.log("transaction from " + deviceType + "success"); console.log("transaction from " + deviceType + "success");
@@ -331,26 +427,34 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
}, [table]); }, [table]);
useEffect(() => { useEffect(() => {
console.log(localStorage.getItem('cart')) console.log(localStorage.getItem("cart"));
console.log(cartItems) console.log(cartItems);
if (localStorage.getItem('cart') == null || localStorage.getItem('cart') == '' || localStorage.getItem('cart') == '[]') return; if (
localStorage.getItem("cart") == null ||
localStorage.getItem("cart") == "" ||
localStorage.getItem("cart") == "[]"
)
return;
// Parse the local storage cart // Parse the local storage cart
const localStorageCart = JSON.parse(localStorage.getItem('cart')); const localStorageCart = JSON.parse(localStorage.getItem("cart"));
console.log(localStorageCart) console.log(localStorageCart);
// Create a set of itemIds from the local storage cart for quick lookup // Create a set of itemIds from the local storage cart for quick lookup
const localStorageItemIds = new Set(localStorageCart[0].items.map(item => item.itemId)); const localStorageItemIds = new Set(
localStorageCart[0].items.map((item) => item.itemId)
);
// Filter out items from cartItems that do not exist in the local storage cart // Filter out items from cartItems that do not exist in the local storage cart
const updatedCartItems = cartItems.map(itemType => ({ const updatedCartItems = cartItems.map((itemType) => ({
...itemType, ...itemType,
itemList: itemType.itemList.filter(item => localStorageItemIds.has(item.itemId)) itemList: itemType.itemList.filter((item) =>
localStorageItemIds.has(item.itemId)
),
})); }));
setCartItems(updatedCartItems); setCartItems(updatedCartItems);
const totalPrice = updatedCartItems.reduce((total, itemType) => { const totalPrice = updatedCartItems.reduce((total, itemType) => {
return ( return (
total + total +
@@ -360,8 +464,7 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
); );
}, 0); }, 0);
setTotalPrice(totalPrice); setTotalPrice(totalPrice);
}, [localStorage.getItem('cart')]); }, [localStorage.getItem("cart")]);
const handleOrderTypeChange = (event) => { const handleOrderTypeChange = (event) => {
setOrderType(event.target.value); setOrderType(event.target.value);
@@ -379,29 +482,66 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
setEmail(event.target.value); setEmail(event.target.value);
}; };
const transactionData = JSON.parse(localStorage.getItem('lastTransaction')); const transactionData = JSON.parse(localStorage.getItem("lastTransaction"));
return ( return (
<div className={styles.Invoice} style={{ height: (getItemsByCafeId(shopId).length > 0 ? '' : '100vh'), minHeight: (getItemsByCafeId(shopId).length > 0 ? '100vh' : '') }}> <div
className={styles.Invoice}
<div onClick={goToShop} style={{ marginLeft: '22px', marginTop: '49px', marginRight: '10px', display: 'flex', flexWrap: 'nowrap', alignItems: 'center', fontSize: '25px' }} ><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>Keranjang</div> style={{
height: getItemsByCafeId(shopId).length > 0 ? "" : "100vh",
{(transactionData == null && getItemsByCafeId(shopId).length < 1) ? minHeight: getItemsByCafeId(shopId).length > 0 ? "100vh" : "",
<div style={{ height: '75vh', display: 'flex', justifyContent: 'center', flexDirection: 'column', alignContent: 'center', alignItems: 'center' }}> }}
<div style={{ width: '50%' }}>
<svg
viewBox="0 0 32 32"
style={{ fill: "#8F8787" }}
> >
<div
onClick={goToShop}
style={{
marginLeft: "22px",
marginTop: "49px",
marginRight: "10px",
display: "flex",
flexWrap: "nowrap",
alignItems: "center",
fontSize: "25px",
}}
>
{!isTablet &&
<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>
}
Keranjang
</div>
{transactionData == null && getItemsByCafeId(shopId).length < 1 ? (
<div
style={{
height: "75vh",
display: "flex",
justifyContent: "center",
flexDirection: "column",
alignContent: "center",
alignItems: "center",
}}
>
<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" /> <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> </svg>
</div> </div>
<h1 style={{ fontSize: '120%', color: '#8F8787' }}>Tidak ada item di keranjang</h1> <h1 style={{ fontSize: "120%", color: "#8F8787" }}>
Tidak ada item di keranjang
</h1>
</div> </div>
: ) : isLoading ? (
(isLoading ? <></> : <></>
) : (
<> <>
{getItemsByCafeId(shopId).length > 0 && {getItemsByCafeId(shopId).length > 0 && (
<div className={styles.RoundedRectangle}> <div className={styles.RoundedRectangle}>
{cartItems.map((itemType) => ( {cartItems.map((itemType) => (
<ItemLister <ItemLister
@@ -444,6 +584,18 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
)} )}
<div className={styles.NoteContainer}> <div className={styles.NoteContainer}>
<span>Atas Nama :</span>
<span></span>
</div>
<div className={styles.NoteContainer}>
<input
className={styles.NoteInput}
placeholder="Tambahkan catatan..."
/>
</div>
<div className={styles.NoteContainer} style={{ height: "18px" }}>
<span>Catatan :</span> <span>Catatan :</span>
<span></span> <span></span>
</div> </div>
@@ -456,19 +608,40 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
/> />
</div> </div>
</div> </div>
} )}
{transactionData && {transactionData && (
<div className={styles.RoundedRectangle} style={{ backgroundColor: '#c3c3c3', fontSize: '15px', display: 'flex', justifyContent: 'space-between' }}> <div
{transactionData.payment_type != 'paylater' ? className={styles.RoundedRectangle}
style={{
backgroundColor: "#c3c3c3",
fontSize: "15px",
display: "flex",
justifyContent: "space-between",
}}
>
{transactionData.payment_type != "paylater" ? (
<> <>
<div onClick={() => setModal('transaction_item', { transactionId: transactionData.transactionId })} className={styles.AddedLastTransaction}> <div
onClick={() =>
setModal("transaction_item", {
transactionId: transactionData.transactionId,
})
}
className={styles.AddedLastTransaction}
>
Pembayaran akan ditambahkan ke transaksi sebelumnya Pembayaran akan ditambahkan ke transaksi sebelumnya
</div> </div>
<div className={styles.CancelAddedLastTransaction} onClick={() => { window.location.reload(); localStorage.removeItem('lastTransaction') }}> <div
className={styles.CancelAddedLastTransaction}
onClick={() => {
window.location.reload();
localStorage.removeItem("lastTransaction");
}}
>
<svg <svg
style={{ width: '40px', height: '40px' }} style={{ width: "40px", height: "40px" }}
className={styles['plusNegative2']} className={styles["plusNegative2"]}
clipRule="evenodd" clipRule="evenodd"
fillRule="evenodd" fillRule="evenodd"
strokeLinejoin="round" strokeLinejoin="round"
@@ -483,98 +656,143 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
</svg> </svg>
</div> </div>
</> </>
: ) : (
<div className={styles.AddedLastTransaction}> <div className={styles.AddedLastTransaction}>
<div> <div>
Open bill Open bill
<div onClick={() => setModal('transaction_item', { transactionId: transactionData.transactionId })}> <div
onClick={() =>
setModal("transaction_item", {
transactionId: transactionData.transactionId,
})
}
>
Lihat tagihan Lihat tagihan
</div> </div>
</div> </div>
{getItemsByCafeId(shopId).length > 0 ? {getItemsByCafeId(shopId).length > 0 ? (
<button
<button className={styles.PayButton3} onClick={() => handlePay(orderMethod)}> className={styles.PayButton3}
onClick={() => handlePay(orderMethod)}
>
{isPaymentLoading ? ( {isPaymentLoading ? (
<ColorRing height="50" width="50" color="white" /> <ColorRing height="40" width="40" color="white" />
) : ( ) : (
<div> <div>
{transactionData ? {transactionData ? (
<span>Tambahkan</span> <span>Tambahkan</span>
: ) : (
<span>Pesan</span> <span>Pesan</span>
} )}
<span>Rp{totalPrice}</span> <span>Rp{formatToRupiah(totalPrice)}</span>
</div> </div>
)} )}
</button> </button>
: ) : (
<button
<button className={styles.PayButton3} style={{ backgroundColor: 'rgb(42 145 24)', letterSpacing: '1px' }} onClick={goToShop}> className={styles.PayButton3}
style={{
backgroundColor: "rgb(42 145 24)",
letterSpacing: "1px",
}}
onClick={goToShop}
>
<div> <div>
<span>Tambahkan item lain</span> <span>Tambahkan item lain</span>
</div> </div>
</button>} </button>
)}
</div> </div>
} )}
</div> </div>
} )}
<div className={styles.PaymentOption}> <div className={styles.PaymentOption}>
<div className={styles.TotalContainer}> <div className={styles.TotalContainer}>
<span>Pembayaran</span> <span>Pembayaran</span>
<span> <span>
{paymentMethods != null && <Dropdown setDropdownKey={() => setDropdownKey(dropdownKey + 1)} paymentMethods={paymentMethods} onChange={handleOrderMethodChange} />} {paymentMethods != null && (
<Dropdown
setDropdownKey={() => setDropdownKey(dropdownKey + 1)}
paymentMethods={paymentMethods}
onChange={handleOrderMethodChange}
/>
)}
</span> </span>
</div> </div>
{transactionData && transactionData.payment_type === 'paylater' ? {transactionData && transactionData.payment_type === "paylater" ? (
<div style={{ display: 'flex', paddingLeft: '25px', paddingRight: '25px', marginTop: '17px' }}> <div
<button className={styles.PayButton} onClick={() => handlePayCloseBill(orderMethod)}> style={{
display: "flex",
paddingLeft: "25px",
paddingRight: "25px",
marginTop: "17px",
}}
>
<button
className={styles.PayButton}
onClick={() => handlePayCloseBill(orderMethod)}
>
{isPaymentLoading ? ( {isPaymentLoading ? (
<ColorRing height="50" width="50" color="white" /> <ColorRing height="50" width="50" color="white" />
) : ( ) : (
<div> <div>
<span>Tutup bill</span> <span>Tutup bill</span>
<span>Rp{ <span>
transactionData.DetailedTransactions.reduce((total, transaction) => { Rp
return total + (transaction.promoPrice == 0 || transaction.promoPrice == null {formatToRupiah(
transactionData.DetailedTransactions.reduce(
(total, transaction) => {
return (
total +
(transaction.promoPrice == 0 ||
transaction.promoPrice == null
? transaction.price * transaction.qty ? transaction.price * transaction.qty
: transaction.promoPrice * transaction.qty); : transaction.promoPrice * transaction.qty)
}, 0) );
}</span> },
0
)
)}
</span>
</div> </div>
)} )}
</button> </button>
</div> </div>
: ) : (
<div
<div style={{ display: 'flex', paddingLeft: '25px', paddingRight: '25px', marginTop: '17px' }}> style={{
<button className={styles.PayButton} onClick={() => handlePay(orderMethod)}> display: "flex",
paddingLeft: "25px",
paddingRight: "25px",
marginTop: "17px",
}}
>
<button
className={styles.PayButton}
onClick={() => handlePay(orderMethod)}
>
{isPaymentLoading ? ( {isPaymentLoading ? (
<ColorRing height="50" width="50" color="white" /> <ColorRing height="50" width="50" color="white" />
) : ( ) : (
<div> <div>
{transactionData ? {transactionData ? (
<span>Tambahkan</span> <span>Tambahkan</span>
: ) : (
<span>Pesan</span> <span>Pesan</span>
} )}
<span>Rp{totalPrice}</span> <span>Rp{formatToRupiah(totalPrice)}</span>
</div> </div>
)} )}
</button> </button>
</div> </div>
} )}
</div> </div>
<div className={styles.PaymentOptionMargin}></div> <div className={styles.PaymentOptionMargin}></div>
</> </>
) )}
}
</div> </div>
); );
} }

View File

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

View File

@@ -28,7 +28,7 @@ const CreateClerk = ({ shopId }) => {
try { try {
const create = await createClerks(shopId || cafeIdParam, username, password); const create = await createClerks(shopId || cafeIdParam, username, password);
if (create) setMessage('Clerk created successfully'); if (create) {setMessage('Clerk created successfully');}
else setMessage('Failed to create clerk'); else setMessage('Failed to create clerk');
} catch (error) { } catch (error) {
setMessage('Error creating clerk'); setMessage('Error creating clerk');

View File

@@ -49,7 +49,7 @@ const Dashboard = ({ user, setModal }) => {
// Create admin functionality // Create admin functionality
createCafeOwner(newItem.email, newItem.username, newItem.password) createCafeOwner(newItem.email, newItem.username, newItem.password)
.then((newitem) => { .then((newitem) => {
setItems([...items, { userId: newitem.userId, name: newitem.username }]); setItems([...items, { user_id: newitem.user_id, name: newitem.username }]);
setIsCreating(false); setIsCreating(false);
setNewItem({ name: "", type: "" }); setNewItem({ name: "", type: "" });
}) })

View File

@@ -71,9 +71,9 @@ const LinktreePage = ({ user, setModal }) => {
// Handle manual coupon code check // Handle manual coupon code check
const handleGetkCoupons = async () => { const handleGetkCoupons = async () => {
const result = await getUserCoupons(); // const result = await getUserCoupons();
setCoupons(result.coupons); // setCoupons(result.coupons);
console.log(result) // console.log(result)
}; };
// Handle user transactions // Handle user transactions
@@ -95,24 +95,20 @@ const LinktreePage = ({ user, setModal }) => {
} }
}; };
// Handle login const handleLogin = () => {
const handleLogin = async () => { const baseUrl = "https://kediritechnopark.com/";
try { const modal = "product";
setError(false); const productId = 1;
setLoading(true);
const response = await loginUser(username, password); const authorizedUri = "https://kedaimaster.com?token=";
if (response.success) { const unauthorizedUri = `${baseUrl}?modal=${modal}&product_id=${productId}`;
localStorage.setItem('auth', response.token);
console.log(response) const url =
window.location.href = response.cafeIdentifyName ? `/${response.cafeIdentifyName}` : '/'; `${baseUrl}?modal=${modal}&product_id=${productId}` +
} else { `&authorized_uri=${encodeURIComponent(authorizedUri)}` +
setError(true); `&unauthorized_uri=${encodeURIComponent(unauthorizedUri)}`;
}
} catch (error) { window.location.href = url;
setError(true);
} finally {
setLoading(false);
}
}; };
// Handle logout // Handle logout
@@ -152,7 +148,7 @@ const LinktreePage = ({ user, setModal }) => {
try { try {
if (user.roleId < 1) { if (user.roleId < 1) {
const newOwner = await createCafeOwner(newItem.email, newItem.username, newItem.password); const newOwner = await createCafeOwner(newItem.email, newItem.username, newItem.password);
setItems([...items, { userId: newOwner.userId, name: newOwner.username }]); setItems([...items, { user_id: newOwner.user_id, name: newOwner.username }]);
} else { } else {
const newCafe = await createCafe(newItem.name); const newCafe = await createCafe(newItem.name);
setItems([...items, { cafeId: newCafe.cafeId, name: newCafe.name }]); setItems([...items, { cafeId: newCafe.cafeId, name: newCafe.name }]);
@@ -202,7 +198,7 @@ const LinktreePage = ({ user, setModal }) => {
]; ];
console.log(items) console.log(items)
const selectedItems = items?.items?.find(item => (item.userId || item.cafeId) === selectedItemId); const selectedItems = items?.items?.find(item => (item.user_id || item.cafeId) === selectedItemId);
// If the selected tenant is found, extract the cafes // If the selected tenant is found, extract the cafes
const selectedSubItems = selectedItems?.subItems || []; const selectedSubItems = selectedItems?.subItems || [];
@@ -238,7 +234,7 @@ const LinktreePage = ({ user, setModal }) => {
return ( return (
<> <>
{user && user.roleId < 2 ? ( {getLocalStorage("auth") ? (
<div> <div>
<div className={styles.header}> <div className={styles.header}>
@@ -278,7 +274,7 @@ const LinktreePage = ({ user, setModal }) => {
))} ))}
</div> </div>
</div> </div>
gratis 3 bulan pertama gratis 1 bulan pertama
</div> </div>
: :
<div className={styles.mainHeading}> <div className={styles.mainHeading}>
@@ -290,57 +286,20 @@ const LinktreePage = ({ user, setModal }) => {
))} ))}
</div> </div>
</div> </div>
Gratis 3 bulan pertama Gratis 1 bulan pertama
</div> </div>
} }
<div className={styles.subHeading}> <div className={styles.subHeading}>
Solusi berbasis web untuk memudahkan pengelolaan kedai, dengan fitur yang mempermudah pemilik, kasir, dan tamu berinteraksi. Solusi berbasis web untuk memudahkan pengelolaan kedai, dengan fitur yang mempermudah pemilik, kasir, dan tamu berinteraksi.
</div> </div>
{getLocalStorage('auth') == null && (
<div className={styles.LoginForm}> <div className={styles.LoginForm}>
<div className={`${styles.FormUsername} ${inputtingPassword ? styles.animateForm : wasInputtingPassword ? styles.reverseForm : ''}`}> <div className={`${styles.FormUsername} ${inputtingPassword ? styles.animateForm : wasInputtingPassword ? styles.reverseForm : ''}`}>
<label htmlFor="username" className={styles.usernameLabel}>---- Masuk -----------------------------</label> <button onClick={() => handleLogin()} className={styles.claimButton}>
<input <span>Masuk</span>
id="username"
placeholder="username"
maxLength="30"
className={!error ? styles.usernameInput : styles.usernameInputError}
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<button onClick={() => { setInputtingPassword(true); setWasInputtingPassword(true) }} className={styles.claimButton}>
<span></span>
</button>
</div>
<div className={`${styles.FormPassword} ${inputtingPassword ? styles.animateForm : wasInputtingPassword ? styles.reverseForm : styles.idleForm}`}>
<span>
<label onClick={() => setInputtingPassword(false)} htmlFor="password" className={styles.usernameLabel}> &lt;--- &lt;-- Kembali </label>
<label htmlFor="password" className={styles.usernameLabel}> &nbsp; ----- &nbsp; </label>
<label onClick={() => setModal('reset-password', { username: username })} className={styles.usernameLabel}>
lupa password?
</label>
</span>
<input
id="password"
placeholder="password"
type="password"
maxLength="30"
className={!error ? styles.usernameInput : styles.usernameInputError}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button
onClick={handleLogin}
className={`${styles.claimButton} ${loading ? styles.loading : ''}`}
disabled={loading}
>
<span>{loading ? 'Loading...' : 'Masuk'}</span>
</button> </button>
</div> </div>
</div> </div>
)}
<div className={styles.footer}> <div className={styles.footer}>
<div className={styles.footerLinks}> <div className={styles.footerLinks}>

View File

@@ -1,3 +1,5 @@
@media (max-width: 768px) {
.Invoice { .Invoice {
overflow-x: hidden; overflow-x: hidden;
background-color: white; background-color: white;
@@ -40,11 +42,10 @@
font-size: calc(10px + 2vmin); font-size: calc(10px + 2vmin);
color: rgba(88, 55, 50, 1); color: rgba(88, 55, 50, 1);
border-radius: 15px 15px 0 0; border-radius: 15px 15px 0 0;
position: fixed; position: fixed;
bottom: 0; bottom: 0;
right: 0; right: 0;
left: 0; width: 100%;
} }
.PaymentOptionMargin { .PaymentOptionMargin {
@@ -59,12 +60,16 @@
position: relative; position: relative;
height: 220px; height: 220px;
bottom: 0;
right: 0;
width: 40%;
} }
.TotalContainer { .TotalContainer {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
width: 80vw; /* width: 80vw; */
margin: 0 auto; margin: 0 auto;
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600; font-weight: 600;
@@ -92,13 +97,13 @@
margin-bottom: 23px; margin-bottom: 23px;
} }
.PayButton div{ .PayButton div {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding-left: 20px; padding-left: 20px;
padding-right: 20px; padding-right: 20px;
} }
.PayButton3{ .PayButton3 {
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500; font-weight: 500;
font-style: normal; font-style: normal;
@@ -115,8 +120,7 @@
margin-top: 10px; margin-top: 10px;
} }
.PayButton3 div {
.PayButton3 div{
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding-left: 20px; padding-left: 20px;
@@ -191,7 +195,7 @@
.NoteInput { .NoteInput {
width: 78vw; width: 78vw;
height: 12vw; height: 18px;
border-radius: 20px; border-radius: 20px;
margin: 0 auto; margin: 0 auto;
padding: 10px; padding: 10px;
@@ -201,22 +205,275 @@
overflow-wrap: break-word; /* Ensure text wraps */ overflow-wrap: break-word; /* Ensure text wraps */
} }
.AddedLastTransaction {
.AddedLastTransaction{
width: 100%; width: 100%;
font-size: 1em; font-size: 1em;
padding: 10px 20px; padding: 10px 20px;
margin-bottom: 7px; margin-bottom: 7px;
font-weight: 600; font-weight: 600;
} }
.AddedLastTransaction div{ .AddedLastTransaction div {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.CancelAddedLastTransaction{ .CancelAddedLastTransaction {
width: 40px; width: 40px;
height: 40px; height: 40px;
margin-right: 30px; margin-right: 30px;
margin-top: 10px; margin-top: 10px;
transform: rotate(45deg); transform: rotate(45deg);
} }
}
@media (min-width: 768px) {
.Invoice {
overflow-x: hidden;
background-color: white;
display: flex;
flex-direction: column;
justify-content: flex-start;
font-size: calc(10px + 2vmin);
color: rgba(88, 55, 50, 1);
background-color: #e9e9e9;
}
.TotalContainer {
display: flex;
justify-content: space-between;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600;
font-style: normal;
font-size: 1em;
padding: 10px 30px;
padding-top: 20px;
/* margin-bottom: 17px; */
}
.Invoice-title {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-style: normal;
font-size: 6vw;
color: rgba(88, 55, 50, 1);
text-align: left;
margin-left: 20px;
margin-top: 7px;
}
.Invoice-detail {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-style: normal;
font-size: 20px;
color: rgba(88, 55, 50, 1);
text-align: left;
margin-left: 20px;
margin-top: 17px;
}
.PaymentOption {
overflow: visible;
background-color: white;
display: flex;
flex-direction: column;
justify-content: center;
font-size: calc(10px + 2vmin);
color: rgba(88, 55, 50, 1);
border-radius: 15px 15px 0 0;
position: fixed;
bottom: 0;
right: 0;
width: 40%;
left: 60%;
}
.PaymentOptionMargin {
z-index: -1;
overflow-x: hidden;
background-color: white;
display: flex;
flex-direction: column;
justify-content: center;
font-size: calc(10px + 2vmin);
color: rgba(88, 55, 50, 1);
position: relative;
height: 220px;
bottom: 0;
right: 0;
width: 40%;
}
.PayButton {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-style: normal;
font-size: 20px;
width: 80vw;
height: 40px;
border-radius: 50px;
background-color: rgba(88, 55, 50, 1);
color: white;
border: none;
margin: 0px auto;
cursor: pointer;
margin-bottom: 23px;
}
.PayButton div {
display: flex;
justify-content: space-between;
padding-left: 20px;
padding-right: 20px;
}
.PayButton3 {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-style: normal;
font-size: 20px;
width: 100%;
height: 40px;
border-radius: 50px;
background-color: rgba(88, 55, 50, 1);
color: white;
border: none;
margin: 0px auto;
cursor: pointer;
margin-top: 10px;
}
.PayButton3 div {
display: flex;
justify-content: space-between;
padding-left: 20px;
padding-right: 20px;
}
.Pay2Button {
text-align: center;
color: rgba(88, 55, 50, 1);
font-size: 1em;
margin-bottom: 25px;
cursor: pointer;
}
.Confirm {
display: flex;
justify-content: space-between;
width: 80vw;
margin: 0 auto;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600;
font-style: normal;
font-size: 1.5em;
padding: 10px 0;
margin-bottom: 17px;
}
.RoundedRectangle {
border-radius: 20px;
padding-top: 5px;
margin: 12px;
background-color: #f9f9f9;
}
.EmailContainer {
display: flex;
justify-content: space-between;
width: 80vw;
margin: 0 auto;
font-size: 1em;
padding: 10px 0;
margin-bottom: 7px;
}
.OrderTypeContainer {
display: flex;
justify-content: space-between;
width: 80vw;
margin: 0 auto;
font-size: 1em;
padding: 10px 0;
margin-bottom: 7px;
}
.Note {
text-align: left;
color: rgba(88, 55, 50, 1);
font-size: 1em;
margin-bottom: 13px;
margin-left: 50px;
cursor: pointer;
}
.NoteContainer {
display: flex;
justify-content: space-between;
width: 80vw;
margin: 0 auto;
font-size: 1em;
padding: 10px 0;
margin-bottom: 7px;
font-weight: 600;
}
.NoteInput {
width: 78vw;
height: 18px;
border-radius: 20px;
margin: 0 auto;
padding: 10px;
font-size: 15px;
border: 1px solid rgba(88, 55, 50, 0.5);
resize: none; /* Prevent resizing */
overflow-wrap: break-word; /* Ensure text wraps */
}
.AddedLastTransaction {
width: 100%;
font-size: 1em;
padding: 10px 20px;
margin-bottom: 7px;
font-weight: 600;
}
.AddedLastTransaction div {
display: flex;
justify-content: space-between;
}
.CancelAddedLastTransaction {
width: 40px;
height: 40px;
margin-right: 30px;
margin-top: 10px;
transform: rotate(45deg);
}
.PaymentOptionMargin {
z-index: -1;
overflow-x: hidden;
background-color: white;
display: flex;
flex-direction: column;
justify-content: center;
font-size: calc(10px + 2vmin);
color: rgba(88, 55, 50, 1);
position: relative;
height: 220px;
}
.TotalContainer {
display: flex;
justify-content: space-between;
/* width: 80vw; */
margin: 0 auto;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600;
font-style: normal;
font-size: 1.3em;
padding: 10px 0;
padding-top: 20px;
/* margin-bottom: 17px; */
}
}

View File

@@ -110,7 +110,7 @@ const LinktreePage = ({ data, setModal }) => {
</div> </div>
<div className={styles.linktreeForm}> <div className={styles.linktreeForm}>
<button onClick={()=>window.open("https://api.whatsapp.com/send?phone=6281318894994&text=Saya%20ingin%20coba%20gratis%203%20bulan")} className={styles.claimButton}> <button onClick={()=>window.open("https://api.whatsapp.com/send?phone=6281318894994&text=Saya%20ingin%20coba%20gratis%203%20bulan")} className={styles.claimButton}>
<span>Dapatkan voucher gratis 3 bulan</span> <span>Dapatkan voucher gratis 1 bulan</span>
</button> </button>
</div> </div>
<div className={styles.footer}> <div className={styles.footer}>

View File

@@ -42,8 +42,12 @@
line-height: 2.25rem; line-height: 2.25rem;
letter-spacing: -1px; letter-spacing: -1px;
margin-bottom: 6px; margin-bottom: 6px;
margin: 10px
} }
.buttonWrapper {
display: flex;
}
.descHeading { .descHeading {
width: 99%; width: 99%;
font-weight: 700; font-weight: 700;

View File

@@ -338,7 +338,6 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
margin-top: 2rem;
} }
.footerLinks { .footerLinks {

View File

@@ -180,7 +180,7 @@ const SetPaymentQr = ({ cafeId }) => {
setLatestMutation(latestMutation); setLatestMutation(latestMutation);
setCurrentQuantity(latestMutation.newStock); setCurrentQuantity(latestMutation.newStock);
setCurrentPrice(formatCurrency(latestMutation.priceAtp)); setCurrentPrice(formatCurrency(latestMutation.priceAtp));
setNewPrice(formatCurrency(latestMutation.priceAtp)); setNewPrice(formatCurrency(latestMutation.priceAtp) || 0);
} else { } else {
setCurrentQuantity(0); // Default value if no mutations exist setCurrentQuantity(0); // Default value if no mutations exist
setLatestMutation({ newStock: 0 }); setLatestMutation({ newStock: 0 });
@@ -195,12 +195,12 @@ const SetPaymentQr = ({ cafeId }) => {
const handleUpdateStock = async () => { const handleUpdateStock = async () => {
setLoading(true); setLoading(true);
console.log('aaa')
try { try {
const newprice = convertToInteger(newPrice)
const newStock = currentQuantity + quantityChange; const newStock = currentQuantity + quantityChange;
const formData = new FormData(); const formData = new FormData();
formData.append("newStock", newStock); formData.append("newStock", newStock);
formData.append("priceAtp", newprice); formData.append("priceAtp", newPrice);
formData.append("reason", "Stock update"); formData.append("reason", "Stock update");
await createMaterialMutation(materials[selectedMaterialIndex].materialId, formData); await createMaterialMutation(materials[selectedMaterialIndex].materialId, formData);
@@ -231,7 +231,7 @@ const SetPaymentQr = ({ cafeId }) => {
<></> <></>
) : ( ) : (
<> <>
<h3 className={styles.title}>Bahan baku</h3> <h3 className={styles.title}>Stok</h3>
<Carousel items={materials} onSelect={(e) => setSelectedMaterialIndex(e)} selectedIndex={selectedMaterialIndex} /> <Carousel items={materials} onSelect={(e) => setSelectedMaterialIndex(e)} selectedIndex={selectedMaterialIndex} />
{selectedMaterialIndex !== -1 ? ( {selectedMaterialIndex !== -1 ? (
<> <>

View File

@@ -27,10 +27,12 @@ const LinktreePage = ({ handleYes, handleNo }) => {
<div className={styles.dashboardContainer} > <div className={styles.dashboardContainer} >
<div className={styles.mainHeading}>{captMessage}</div> <div className={styles.mainHeading}>{captMessage}</div>
{descMessage && <div className={styles.descHeading}>{descMessage}</div>} {descMessage && <div className={styles.descHeading}>{descMessage}</div>}
{handleYes && <div onClick={handleYes} className={styles.button}>{yesText}</div>} <div className={styles.buttonWrapper}>
<div onClick={handleYes} className={styles.button}>{yesText}</div>
{noText && <div onClick={handleNo} className={styles.button}>{noText}</div>} {noText && <div onClick={handleNo} className={styles.button}>{noText}</div>}
</div> </div>
</div> </div>
</div>
); );
}; };

View File

@@ -164,15 +164,15 @@ export default function Transactions({ propsShopId, sendParam, deviceType }) {
{isPaymentLoading ? ( {isPaymentLoading ? (
<ColorRing height="50" width="50" color="white" /> <ColorRing height="50" width="50" color="white" />
) : transaction.confirmed === 1 ? ( ) : transaction.confirmed === 1 ? (
"Confirm has paid" // Display "Confirm has paid" if the transaction is confirmed (1) "Konfirmasi telah bayar" // Display "Confirm has paid" if the transaction is confirmed (1)
) : transaction.confirmed === -1 ? ( ) : transaction.confirmed === -1 ? (
"Declined" // Display "Declined" if the transaction is declined (-1) "Ditolah" // Display "Declined" if the transaction is declined (-1)
) : transaction.confirmed === 2 ? ( ) : transaction.confirmed === 2 ? (
"Confirm item has ready" // Display "Item ready" if the transaction is ready (2) "Konfirmasi item telah siap" // Display "Item ready" if the transaction is ready (2)
) : transaction.confirmed === 3 ? ( ) : transaction.confirmed === 3 ? (
"Transaction success" // Display "Item ready" if the transaction is ready (2) "Transaksi selesai" // Display "Item ready" if the transaction is ready (2)
) : ( ) : (
"Confirm availability" // Display "Confirm availability" if the transaction is not confirmed (0) "Konfirmasi ketersediaan" // Display "Confirm availability" if the transaction is not confirmed (0)
)} )}
</button> </button>
</div> </div>
@@ -181,7 +181,7 @@ export default function Transactions({ propsShopId, sendParam, deviceType }) {
className={styles.DeclineButton} className={styles.DeclineButton}
onClick={() => handleDecline(transaction.transactionId)} onClick={() => handleDecline(transaction.transactionId)}
> >
decline Tolak
</h5> </h5>
)} )}
</div> </div>

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

@@ -13,10 +13,6 @@ import MultiSwitch from "react-multi-switch-toggle";
import DailyCharts from '../components/DailyCharts.js'; import DailyCharts from '../components/DailyCharts.js';
import PeriodCharts from '../components/PeriodCharts.js'; import PeriodCharts from '../components/PeriodCharts.js';
import Coupon from "../components/Coupon.js";
import CreateCouponPage from "./CreateCoupon.js";
const RoundedRectangle = ({ const RoundedRectangle = ({
onClick, onClick,
title, title,
@@ -59,6 +55,9 @@ const RoundedRectangle = ({
? "rgb(85 85 85)" ? "rgb(85 85 85)"
: !isChildren && !children && backgroundColor, : !isChildren && !children && backgroundColor,
color: loading ? "transparent" : color, color: loading ? "transparent" : color,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}; };
const valueAndPercentageContainerStyle = { const valueAndPercentageContainerStyle = {
@@ -82,7 +81,7 @@ const RoundedRectangle = ({
}; };
const percentageStyle = { const percentageStyle = {
fontSize: "16px", fontSize: "14px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
textAlign: "right", textAlign: "right",
@@ -95,20 +94,18 @@ const RoundedRectangle = ({
return ( return (
<div style={containerStyle} onClick={onClick}> <div style={containerStyle} onClick={onClick}>
<div style={titleStyle}>{title}</div> <div style={titleStyle}>
{!children && ( {title}
<div style={valueAndPercentageContainerStyle}>
<div style={valueStyle}>{loading ? "Loading..." : value}</div>
<div style={percentageStyle}> <div style={percentageStyle}>
{loading ? "" : percentage} {loading ? "" : percentage}
{percentage !== undefined && !loading && "%"} {percentage !== undefined && !loading && "%"}
{percentage !== undefined && !loading && (
<span style={arrowStyle}>
{percentage > 0 ? "↗" : percentage === 0 ? "-" : "↘"}
</span>
)}
</div> </div>
</div> </div>
{!children && (
<div style={valueAndPercentageContainerStyle}>
<div style={valueStyle}>{loading ? "Loading..." : value}</div>
</div>
)} )}
{children && <div>{children}</div>} {/* Properly render children */} {children && <div>{children}</div>} {/* Properly render children */}
</div> </div>
@@ -122,7 +119,7 @@ const App = ({ forCafe = true, cafeId = -1,
const [selectedCafeId, setSelectedCafeId] = useState(cafeId); const [selectedCafeId, setSelectedCafeId] = useState(cafeId);
const [analytics, setAnalytics] = useState({}); const [analytics, setAnalytics] = useState({});
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState("monthly"); const [filter, setFilter] = useState("yesterday");
const [circularFilter, setCircularFilter] = useState("item"); const [circularFilter, setCircularFilter] = useState("item");
const [graphFilter, setGraphFilter] = useState("income"); const [graphFilter, setGraphFilter] = useState("income");
@@ -199,102 +196,84 @@ const App = ({ forCafe = true, cafeId = -1,
// Define a color palette or generate colors dynamically // Define a color palette or generate colors dynamically
const colorPalette = colors; const colorPalette = colors;
// Ensure that each segment gets a unique color
let colorIndex = 0; let colorIndex = 0;
console.log(filteredItems) // Segment penjualan item
let segments = (selectedCafeId == 0 || selectedCafeId == -1) ? filteredItems.flatMap((cafe) => { let segments =
selectedCafeId == 0 || selectedCafeId == -1
? filteredItems.flatMap((cafe) => {
const cafeItems = cafe.report?.itemSales || []; const cafeItems = cafe.report?.itemSales || [];
console.log(cafeItems); // Log all items for the cafe return cafeItems.map((item) => {
const color = colorPalette[colorIndex % colorPalette.length];
return cafeItems.map((item, index) => { colorIndex++;
const percentage = totalSoldAcrossAllCafes > 0
? ((item.sold / totalSoldAcrossAllCafes) * 100).toFixed(2)
: 0;
console.log(`${item.itemName}: ${(percentage)}%`); // Log item name and percentage
// Assign a unique color from the color palette
const color = colorPalette[colorIndex % colorPalette.length]; // Use modulo to cycle through colors
colorIndex++; // Increment to ensure a new color for the next item
return { return {
itemName: item.itemName, name: item.itemName,
sold: item.sold, value: item.sold,
percentage: percentage, color,
color: color,
}; };
}); });
}) : filteredItems.map((item, index) => { })
const color = colorPalette[colorIndex % colorPalette.length]; // Use modulo to cycle through colors : filteredItems.map((item) => {
colorIndex++; // Increment to ensure a new color for the next item const color = colorPalette[colorIndex % colorPalette.length];
colorIndex++;
return { return {
itemName: item.itemName, name: item.itemName,
percentage: item.percentage, value: item.sold,
color: color, color,
}; };
}); });
segments.sort((a, b) => b.sold - a.sold); // Urutkan descending berdasarkan value
segments.sort((a, b) => b.value - a.value);
// Reset color index untuk material
colorIndex = 0;
let materialSegments = (selectedCafeId == 0 || selectedCafeId == -1) ? filteredItems.flatMap((cafe) => { // Segment pengeluaran material
let materialSegments =
selectedCafeId == 0 || selectedCafeId == -1
? filteredItems.flatMap((cafe) => {
const cafeItems = cafe.report?.materialSpend || []; const cafeItems = cafe.report?.materialSpend || [];
console.log(cafeItems); // Log all items for the cafe return cafeItems.map((item) => {
const color = colorPalette[colorIndex % colorPalette.length];
return cafeItems.map((item, index) => { colorIndex++;
const percentage = totalSpendAcrossAllCafes > 0
? ((item.spend / totalSpendAcrossAllCafes) * 100).toFixed(2)
: 0;
console.log(`${item.materialName}: ${(percentage)}%`); // Log item name and percentage
// Assign a unique color from the color palette
const color = colorPalette[colorIndex % colorPalette.length]; // Use modulo to cycle through colors
colorIndex++; // Increment to ensure a new color for the next item
return { return {
itemName: item.materialName, name: item.materialName,
sold: item.spend, value: item.spend,
percentage: percentage, unit: item.unit,
color: color, color,
}; };
}); });
}) : filteredItems.map((item, index) => { })
const color = colorPalette[colorIndex % colorPalette.length]; // Use modulo to cycle through colors : filteredItems.map((item) => {
colorIndex++; // Increment to ensure a new color for the next item const color = colorPalette[colorIndex % colorPalette.length];
colorIndex++;
return { return {
itemName: item.materialName, name: item.materialName,
percentage: item.percentage, value: item.spend,
color: color, unit: item.unit,
color,
}; };
}); });
materialSegments.sort((a, b) => b.spend - a.spend); // Urutkan descending berdasarkan value
materialSegments.sort((a, b) => b.value - a.value);
console.log(selectedCafeId) console.log(selectedCafeId)
console.log(segments) console.log(segments)
const formatIncome = (amount) => { const formatIncome = (amount) => {
if (amount >= 1_000_000_000) { if (amount == null) return "0";
// Format for billions
const billions = amount / 1_000_000_000; const formatter = new Intl.NumberFormat("id-ID", {
return billions.toFixed(0) + "b"; // No decimal places for billions style: "currency",
} else if (amount >= 1_000_000) { currency: "IDR",
// Format for millions minimumFractionDigits: 0,
const millions = amount / 1_000_000; });
return millions.toFixed(2).replace(/\.00$/, "") + "m"; // Two decimal places, remove trailing '.00'
} else if (amount >= 1_000) { return formatter.format(amount);
// Format for thousands
const thousands = amount / 1_000;
return thousands.toFixed(1).replace(/\.0$/, "") + "k"; // One decimal place, remove trailing '.0'
} else {
// Less than a thousand
if (amount != null) return amount.toString();
}
}; };
function roundToInteger(num) { function roundToInteger(num) {
@@ -312,7 +291,7 @@ const App = ({ forCafe = true, cafeId = -1,
filterTexts[["yesterday", "weekly", "monthly", "yearly"].indexOf(filter)]; filterTexts[["yesterday", "weekly", "monthly", "yearly"].indexOf(filter)];
const [resetKey, setResetKey] = useState(0); // A key to force re-render const [resetKey, setResetKey] = useState(0); // A key to force re-render
const [texts, setTexts] = useState(['Buat bisnis']); // initially show only first 3 texts const [texts, setTexts] = useState([]); // initially show only first 3 texts
const [fullTexts, setFullTexts] = useState(null); // initially show only first 3 texts const [fullTexts, setFullTexts] = useState(null); // initially show only first 3 texts
const [fullTextsVisible, setFullTextsVisible] = useState(null); // initially show only first 3 texts const [fullTextsVisible, setFullTextsVisible] = useState(null); // initially show only first 3 texts
@@ -322,22 +301,20 @@ const App = ({ forCafe = true, cafeId = -1,
let updatedFullTexts; let updatedFullTexts;
if (otherCafes.length === 0) { if (otherCafes.length === 0) {
// Only include the role-specific option if user.roleId is 1 // Only include the role-specific option if user.roleId is 1
updatedFullTexts = user.roleId == 1 ? [["Buat Bisnis", 0]] : []; updatedFullTexts = user.roleId == 1 ? [[0]] : [];
setSelectedCafeId(-1); setSelectedCafeId(-1);
} else if (otherCafes.length === 1) { } else if (otherCafes.length === 1) {
updatedFullTexts = [ updatedFullTexts = [
[otherCafes[0].cafeIdentifyName || otherCafes[0].username, otherCafes[0].cafeId || otherCafes[0].userId], [otherCafes[0].cafeIdentifyName || otherCafes[0].username, otherCafes[0].cafeId || otherCafes[0].user_id],
// Only add the "Buat Bisnis" option for user.roleId == 1
...(user.roleId == 1 ? [["Buat Bisnis", -1]] : [])
]; ];
setSelectedCafeId(otherCafes[0].cafeId); // Get the cafeId (second part of the pair) setSelectedCafeId(otherCafes[0].cafeId); // Get the cafeId (second part of the pair)
} else { } else {
updatedFullTexts = [ updatedFullTexts = [
["semua", 0], // First entry is "semua" ["semua", 0], // First entry is "semua"
...otherCafes.map(item => [item.cafeIdentifyName || item.username, item.cafeId || item.userId]), // Map over cafes to get name and cafeId pairs ...otherCafes.map(item => [item.cafeIdentifyName || item.username, item.cafeId || item.user_id]), // Map over cafes to get name and cafeId pairs
// Only add "Buat Bisnis +" option for user.roleId == 1
...(user.roleId == 1 ? [["Buat Bisnis +", -1]] : []) ...(user.roleId == 1 ? [] : [])
]; ];
setSelectedCafeId(0); setSelectedCafeId(0);
@@ -411,10 +388,10 @@ const App = ({ forCafe = true, cafeId = -1,
console.log(analytics) console.log(analytics)
if (user && user.roleId === 0 && analytics) { if (user && user.roleId === 0 && analytics) {
// Filter the analytics items based on userId // Filter the analytics items based on user_id
if(selectedItem[1] != 0 && selectedItem[1] != -1){ if (selectedItem[1] != 0 && selectedItem[1] != -1) {
const filteredData = analytics.items.filter( const filteredData = analytics.items.filter(
(data) => data.userId === nextSelectedId (data) => data.user_id === nextSelectedId
); );
// Extract coupons from the filtered data // Extract coupons from the filtered data
@@ -484,7 +461,7 @@ const App = ({ forCafe = true, cafeId = -1,
{forCafe && <div style={{ marginTop: '49px', marginRight: '10px', display: 'flex', flexWrap: 'nowrap', alignItems: 'center', fontSize: '25px' }} onClick={handleClose}><svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 512 512"><path d="M48,256c0,114.87,93.13,208,208,208s208-93.13,208-208S370.87,48,256,48,48,141.13,48,256Zm212.65-91.36a16,16,0,0,1,.09,22.63L208.42,240H342a16,16,0,0,1,0,32H208.42l52.32,52.73A16,16,0,1,1,238,347.27l-79.39-80a16,16,0,0,1,0-22.54l79.39-80A16,16,0,0,1,260.65,164.64Z" /></svg>Laporan</div>} {forCafe && <div style={{ marginTop: '49px', marginRight: '10px', display: 'flex', flexWrap: 'nowrap', alignItems: 'center', fontSize: '25px' }} onClick={handleClose}><svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 512 512"><path d="M48,256c0,114.87,93.13,208,208,208s208-93.13,208-208S370.87,48,256,48,48,141.13,48,256Zm212.65-91.36a16,16,0,0,1,.09,22.63L208.42,240H342a16,16,0,0,1,0,32H208.42l52.32,52.73A16,16,0,1,1,238,347.27l-79.39-80a16,16,0,0,1,0-22.54l79.39-80A16,16,0,0,1,260.65,164.64Z" /></svg>Laporan</div>}
<div style={{ marginTop: '10px' }}> <div style={{ marginTop: '10px' }}>
{!forCafe && {!forCafe &&
<div className={styles.dateSelectorWrapper} style={{ fontSize: '12px' }}> <div className={styles.dateSelectorWrapper} style={{ fontSize: '16px', textTransform: 'uppercase' }}>
{texts.map((item, indexx) => { {texts.map((item, indexx) => {
return ( return (
<div <div
@@ -511,7 +488,7 @@ const App = ({ forCafe = true, cafeId = -1,
marginTop: '30px' marginTop: '30px'
}}> }}>
<MultiSwitch <MultiSwitch
texts={["Kemarin", "Minggu ini", "Bulan ini", "Tahun ini"]} texts={["Hari ini", "Minggu ini", "Bulan ini", "Tahun ini"]}
selectedSwitch={["yesterday", "weekly", "monthly", "yearly"].indexOf( selectedSwitch={["yesterday", "weekly", "monthly", "yearly"].indexOf(
filter filter
)} )}
@@ -537,7 +514,7 @@ const App = ({ forCafe = true, cafeId = -1,
<RoundedRectangle <RoundedRectangle
title="Pendapatan" title="Pendapatan"
fontSize="12px" fontSize="12px"
value={!loading && "Rp" + formatIncome(analytics?.currentTotals?.income)} value={!loading && formatIncome(analytics?.currentTotals?.income)}
percentage={roundToInteger(analytics?.growth?.incomeGrowth)} percentage={roundToInteger(analytics?.growth?.incomeGrowth)}
invert={false} invert={false}
loading={loading} loading={loading}
@@ -549,7 +526,7 @@ const App = ({ forCafe = true, cafeId = -1,
<RoundedRectangle <RoundedRectangle
title="Pengeluaran" title="Pengeluaran"
fontSize="12px" fontSize="12px"
value={!loading && "Rp" + formatIncome(analytics?.currentTotals?.outcome)} value={!loading && formatIncome(analytics?.currentTotals?.outcome)}
percentage={roundToInteger(analytics?.growth?.outcomeGrowth)} percentage={roundToInteger(analytics?.growth?.outcomeGrowth)}
invert={true} invert={true}
loading={loading} loading={loading}
@@ -602,7 +579,7 @@ const App = ({ forCafe = true, cafeId = -1,
{!forCafe && selectedCafeId != -1 && selectedCafeId != 0 && ( {!forCafe && selectedCafeId != -1 && selectedCafeId != 0 && (
<RoundedRectangle <RoundedRectangle
title={"Kunjungi bisnis"} title={"Kunjungi bisnis"}
width= {`calc(${'100%'} - 10px)`} width={`calc(${'100%'} - 10px)`}
height='10px' height='10px'
onClick={() => window.location.href = window.location.origin + '/' + otherCafes.find(item => item.cafeId === selectedCafeId).cafeIdentifyName} onClick={() => window.location.href = window.location.origin + '/' + otherCafes.find(item => item.cafeId === selectedCafeId).cafeIdentifyName}
marginBottom={'0px'} marginBottom={'0px'}
@@ -615,9 +592,11 @@ const App = ({ forCafe = true, cafeId = -1,
> >
<div style={{ marginRight: "5px", fontSize: "1.2em" }}></div> <div style={{ marginRight: "5px", fontSize: "1.2em" }}></div>
<h6 style={{ margin: 0, textAlign: "left", fontSize: '10px', fontWeight: 500 }}> <h6 style={{ margin: 0, textAlign: "left", fontSize: '10px', fontWeight: 500 }}>
{(filter == 'yesterday' || filter == 'weekly') ? {(filter == 'weekly') ?
`Data dihitung dengan membandingkan `Data dihitung dengan membandingkan
${comparisonText} hari terakhir dengan ${comparisonText} hari sebelumnya, dengan penghitungan dimulai dari data kemarin.` 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> </h6>
@@ -632,7 +611,7 @@ const App = ({ forCafe = true, cafeId = -1,
} }
style={{ color: 'black', position: 'relative' }} style={{ color: 'black', position: 'relative' }}
> >
<div>Item laku</div> <div>Penjualan</div>
</div> </div>
<div <div
className={`${styles.filterSelector} ${circularFilter == 'material' ? '' : styles.filterSelectorInactive} className={`${styles.filterSelector} ${circularFilter == 'material' ? '' : styles.filterSelectorInactive}
@@ -657,29 +636,6 @@ const App = ({ forCafe = true, cafeId = -1,
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<CircularDiagram segments={circularFilter == 'item' ? segments : materialSegments} /> <CircularDiagram segments={circularFilter == 'item' ? segments : materialSegments} />
</div> </div>
<div style={{ flex: 1, marginLeft: "20px" }}>
{(circularFilter === 'item' ? segments : materialSegments).map((item, index) => (
<div
key={index}
style={{
display: "flex",
alignItems: "center",
margin: "10px",
}}
>
<div
style={{
marginRight: "5px",
fontSize: "1.2em",
color: colors[index],
}}
>
</div>
<h5 style={{ margin: 0, textAlign: "left" }}>{item.itemName}</h5>
</div>
))}
</div>
</div> </div>
<div className={styles.filterSelectorWrapper}> <div className={styles.filterSelectorWrapper}>
@@ -719,77 +675,6 @@ const App = ({ forCafe = true, cafeId = -1,
<PeriodCharts type={filter} graphFilter={graphFilter} aggregatedCurrentReports={analytics?.aggregatedCurrentReports} aggregatedPreviousReports={analytics?.aggregatedPreviousReports} colors={colors} /> <PeriodCharts type={filter} graphFilter={graphFilter} aggregatedCurrentReports={analytics?.aggregatedCurrentReports} aggregatedPreviousReports={analytics?.aggregatedPreviousReports} colors={colors} />
} }
</div> </div>
}
{!forCafe && selectedCafeId == -1 && user.roleId == 1 &&
<div style={{
textAlign: "center",
}}>
<div
style={{
display: "flex",
flexWrap: "wrap",
justifyContent: "center",
padding: "20px",
}}
>
<RoundedRectangle
title={"Masukkan nama bisnis"}
width="calc(100% - 10px)"
>
<input
value={itemName}
onChange={(e) => setItemName(e.target.value)}
style={{
width: '70%',
fontSize: '25px',
borderRadius: '7px',
border: '1px solid black'
}}
/>
</RoundedRectangle>
<RoundedRectangle
title={"Buat Bisnis"}
width="calc(100% - 10px)"
onClick={handleClick}
/>
</div>
</div>
}
{!forCafe &&
<>
<div className={`${styles.couponContainer}`}>
<div>
{!forCafe &&
<div className={styles.dateSelectorWrapper} style={{ fontSize: '13px' }}>
<div
className={`${styles.dateSelector} ${styles.dateSelectorActive}`} style={{ position: 'relative', width: 'calc(32vw - 30px)' }}
>
<div style={{ position: 'absolute', bottom: 0, left: '10%', right: '10%', borderBottom: `1px solid green` }}></div>
<div
style={{ color: 'black' }}>
Voucher
</div>
</div>
</div>
}
</div>
</div>
<div style={{ padding: '25px', paddingTop: '0', paddingBottom: '0' }}>
{/* <h1>{couponList.length}</h1> */}
{couponList && couponList.map((coupon) => {
return <Coupon
code={coupon?.code || null}
value={coupon?.discountValue}
period={coupon?.discountPeriods}
expiration={coupon?.discountEndDate}
/>
})}
<button className={`${styles.addCoupon}`} onClick={() => setModal('claim-coupon')}>Tambahkan Voucher</button>
</div>
</>
} }
</div> </div>
); );

View File

@@ -1,6 +1,8 @@
import React, { useRef, useEffect, useState } from "react"; import React, { useRef, useEffect, useState } from "react";
import qs from 'qs';
import styles from "./Transactions.module.css"; import styles from "./Transactions.module.css";
import { useParams } from "react-router-dom"; import { useParams, useNavigate } from "react-router-dom";
import { ColorRing } from "react-loader-spinner"; import { ColorRing } from "react-loader-spinner";
import { import {
getTransaction, getTransaction,
@@ -11,7 +13,7 @@ import { getTables } from "../helpers/tableHelper";
import TableCanvas from "../components/TableCanvas"; import TableCanvas from "../components/TableCanvas";
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
export default function Transactions({ propsShopId, sendParam, deviceType, handleMoveToTransaction, depth, shopImg }) { export default function Transactions({ propsShopId,setIsModalOpen,cafeIdentityName, sendParam, deviceType, handleMoveToTransaction, depth, shopImg, setModal }) {
const { shopId, tableId } = useParams(); const { shopId, tableId } = useParams();
if (sendParam) sendParam({ shopId, tableId }); if (sendParam) sendParam({ shopId, tableId });
@@ -25,6 +27,9 @@ export default function Transactions({ propsShopId, sendParam, deviceType, handl
const [transactionRefreshKey, setTransactionRefreshKey] = useState(0); const [transactionRefreshKey, setTransactionRefreshKey] = useState(0);
const navigate = useNavigate();
useEffect(() => { useEffect(() => {
const transactionId = searchParams.get("transactionId") || ""; const transactionId = searchParams.get("transactionId") || "";
@@ -67,7 +72,7 @@ export default function Transactions({ propsShopId, sendParam, deviceType, handl
if (c) { if (c) {
console.log(c) console.log(c)
setTransaction(c); setTransaction(c);
setTransactionRefreshKey(transactionRefreshKey+1); setTransactionRefreshKey(transactionRefreshKey + 1);
} }
} catch (error) { } catch (error) {
console.error("Error processing payment:", error); console.error("Error processing payment:", error);
@@ -84,7 +89,7 @@ export default function Transactions({ propsShopId, sendParam, deviceType, handl
if (c) { if (c) {
console.log(c) console.log(c)
setTransaction({ ...transaction, confirmed: c.confirmed }); setTransaction({ ...transaction, confirmed: c.confirmed });
setTransactionRefreshKey(transactionRefreshKey+1); setTransactionRefreshKey(transactionRefreshKey + 1);
} }
// if (c) { // if (c) {
// // Update the confirmed status locally // // Update the confirmed status locally
@@ -116,6 +121,29 @@ export default function Transactions({ propsShopId, sendParam, deviceType, handl
} }
}, [transaction?.notes]); }, [transaction?.notes]);
const handlePrint = (transaction) => {
// Pilih data yang ingin dikirim
const printableData = {
transactionId: transaction.transactionId,
items: transaction.DetailedTransactions.map(dt => ({
name: dt.Item.name,
qty: dt.qty,
price: dt.promoPrice || dt.price,
})),
total: calculateTotalPrice(transaction.DetailedTransactions),
date: transaction.createdAt,
payment_type: transaction.payment_type,
table: transaction.Table?.tableNo || "N/A",
};
// Serialize to query string
const queryString = qs.stringify({ data: JSON.stringify(printableData) });
// Navigate to /print with query string
setIsModalOpen(false);
navigate(`/${cafeIdentityName}/print?${queryString}`);
};
return ( return (
<div key={transactionRefreshKey} className={styles.Transaction}> <div key={transactionRefreshKey} className={styles.Transaction}>
@@ -128,10 +156,9 @@ export default function Transactions({ propsShopId, sendParam, deviceType, handl
setSelectedTable(transaction.Table || { tableId: 0 }) setSelectedTable(transaction.Table || { tableId: 0 })
} }
> >
<div className={styles['receipt-header']}> <div className={styles['receipt-header']}>
<div className={styles['receipt-info']}> <div className={styles['receipt-info']}>
<h3>{transaction.payment_type == 'cash' ? 'Tunai' : transaction.payment_type == 'cashless' ? 'Non tunai' : transaction.payment_type == 'paylater' ? 'Open bill' :'Close bill' }</h3> <h3>{transaction.payment_type == 'cash' ? 'Tunai' : transaction.payment_type == 'cashless' ? 'Non tunai' : transaction.payment_type == 'paylater' ? 'Open bill' : 'Close bill'}</h3>
<p>Transaction ID: {transaction.transactionId}</p> <p>Transaction ID: {transaction.transactionId}</p>
{ {
transaction.payment_type == 'paylater/cash' ? transaction.payment_type == 'paylater/cash' ?
@@ -162,7 +189,7 @@ export default function Transactions({ propsShopId, sendParam, deviceType, handl
</div> </div>
{depth > 0 && {depth > 0 &&
<div className={styles.circle}>{depth}</div> <div className={styles.circle}>{depth}</div>
} }
</div> </div>
<div className={styles['line']} ></div> <div className={styles['line']} ></div>
<div className={styles['circle-right']} onClick={() => handleMoveToTransaction('next', transaction.transactionId)}> <div className={styles['circle-right']} onClick={() => handleMoveToTransaction('next', transaction.transactionId)}>
@@ -231,13 +258,29 @@ export default function Transactions({ propsShopId, sendParam, deviceType, handl
</div> </div>
))} ))}
</div> </div>
{!transaction.is_paid && transaction.confirmed > -1 &&
<div
onClick={() => {
localStorage.setItem('lastTransaction', JSON.stringify(transaction));
setModal("message", { captMessage: 'Silahkan tambahkan pesanan', descMessage: 'Pembayaran akan ditambahkan ke transaksi sebelumnya.' }, null, null);
// Dispatch the custom event
window.dispatchEvent(new Event("localStorageUpdated"));
}}
className={styles["addNewItem"]}
>
Tambah pesanan
</div>
}
<h2 className={styles["Transactions-detail"]}> <h2 className={styles["Transactions-detail"]}>
{transaction.serving_type === "pickup" {transaction.serving_type === "pickup"
? "Ambil sendiri" ? "Ambil sendiri"
: `Diantar ke ${transaction.Table ? transaction.Table.tableNo : "N/A" : `Diantar ke ${transaction.Table ? transaction.Table.tableNo : "N/A"
}`} }`}
</h2> </h2>
{transaction.notes != null && ( {transaction.notes != '' && (
<> <>
<div className={styles.NoteContainer}> <div className={styles.NoteContainer}>
<span>Note :</span> <span>Note :</span>
@@ -295,9 +338,55 @@ export default function Transactions({ propsShopId, sendParam, deviceType, handl
batalkan batalkan
</h5> </h5>
)} )}
{transaction.confirmed > 1 && (
<h5
className={styles.DeclineButton}
onClick={() => handlePrint(transaction)}
>
Cetak struk
</h5>
)}
</div> </div>
)} )}
</div> </div>
{transaction &&
<div id="print-section" className={styles.printContainer}>
<div className="receipt">
<h2 className="center-text">Struk Pembayaran</h2>
<hr />
<p>ID Transaksi: {transaction.transactionId}</p>
<p>Metode: {transaction.payment_type === 'cash' ? 'Tunai' :
transaction.payment_type === 'cashless' ? 'Non Tunai' :
'Open Bill'}</p>
<p>Meja: {transaction.Table?.tableNo || '-'}</p>
<hr />
{transaction.DetailedTransactions.map((detail) => (
<div key={detail.detailedTransactionId} className="item-line">
<p>{detail.Item.name}</p>
<p>{detail.qty} x Rp{detail.promoPrice || detail.price}</p>
</div>
))}
<hr />
<p className="total">Total: Rp {calculateTotalPrice(transaction.DetailedTransactions)}</p>
{transaction.notes && (
<>
<hr />
<p>Catatan:</p>
<p>{transaction.notes}</p>
</>
)}
<hr />
<p className="center-text">Terima Kasih!</p>
</div>
</div>
}
</div> </div>
); );
} }

View File

@@ -192,7 +192,7 @@ export default function Transactions({
))} ))}
</ul> </ul>
{transaction.payment_type != 'paylater/cash' && transaction.payment_type != 'paylater/cashless' && {(transaction.payment_type != 'paylater/cash' && transaction.payment_type != 'paylater/cashless') &&
<div <div
onClick={() => { onClick={() => {
localStorage.setItem('lastTransaction', JSON.stringify(transaction)); localStorage.setItem('lastTransaction', JSON.stringify(transaction));

View File

@@ -16,12 +16,8 @@ export default function Transaction_pending() {
<div className={styles.Transaction}> <div className={styles.Transaction}>
<div className={containerStyle}> <div className={containerStyle}>
<div style={{ marginTop: "30px", textAlign: "center" }}> <div style={{ marginTop: "30px", textAlign: "center" }}>
<h2>transaction failed</h2> <h2>Transaksi dibatalkan</h2>
<img
className={styles.expression}
src="https://i.imgur.com/B6k9exa.png"
alt="Failed"
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react"; import qs from 'qs';
import styles from "./Transactions.module.css"; import styles from "./Transactions.module.css";
import { useParams } from "react-router-dom"; import { useParams, useNavigate } from "react-router-dom";
import { ColorRing } from "react-loader-spinner"; import { ColorRing } from "react-loader-spinner";
import { import {
getMyTransactions, getMyTransactions,
@@ -18,7 +19,7 @@ import ButtonWithReplica from "../components/ButtonWithReplica";
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(timezone); dayjs.extend(timezone);
export default function Transactions({ shop, shopId, propsShopId, sendParam, deviceType, paymentUrl }) { export default function Transactions({ shop, shopId, propsShopId, sendParam, deviceType, paymentUrl, setModal, newTransaction }) {
const { shopIdentifier, tableId } = useParams(); const { shopIdentifier, tableId } = useParams();
if (sendParam) sendParam({ shopIdentifier, tableId }); if (sendParam) sendParam({ shopIdentifier, tableId });
@@ -26,20 +27,25 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
const [isPaymentLoading, setIsPaymentLoading] = useState(false); const [isPaymentLoading, setIsPaymentLoading] = useState(false);
const [isPaymentOpen, setIsPaymentOpen] = useState(false); const [isPaymentOpen, setIsPaymentOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [matchedItems, setMatchedItems] = useState([]); const [matchedItems, setMatchedItems] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
useEffect(() => { const navigate = useNavigate();
setMatchedItems(searchAndAggregateItems(transactions, searchTerm));
}, [searchTerm, transactions]);
useEffect(() => {
setMatchedItems(searchAndAggregateItems(transactions, searchTerm));
}, [searchTerm, transactions]);
useEffect(() => { useEffect(() => {
const fetchTransactions = async () => { const fetchTransactions = async () => {
try { try {
// response = await getMyTransactions(shopId || propsShopId, 5);
// setMyTransactions(response);
setLoading(true); setLoading(true);
let response = await getTransactionsFromCafe(shopId || propsShopId, 5, false); let response = await getTransactionsFromCafe(shopId || propsShopId, -1, false);
setLoading(false); setLoading(false);
if (response) setTransactions(response); if (response) setTransactions(response);
@@ -47,9 +53,9 @@ useEffect(() => {
console.error("Error fetching transactions:", error); console.error("Error fetching transactions:", error);
} }
}; };
console.log(deviceType)
fetchTransactions(); fetchTransactions();
}, [deviceType]); }, [deviceType, newTransaction]);
const calculateTotalPrice = (detailedTransactions) => { const calculateTotalPrice = (detailedTransactions) => {
return detailedTransactions.reduce((total, dt) => { return detailedTransactions.reduce((total, dt) => {
@@ -57,24 +63,36 @@ useEffect(() => {
}, 0); }, 0);
}; };
const formatRupiah = (number) => {
return 'Rp' + number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.');
};
const calculateAllTransactionsTotal = (transactions) => { const calculateAllTransactionsTotal = (transactions) => {
return transactions.reduce((grandTotal, transaction) => { return transactions
.filter(transaction => transaction.confirmed > 1) // Filter transactions where confirmed > 1
.reduce((grandTotal, transaction) => {
return grandTotal + calculateTotalPrice(transaction.DetailedTransactions); return grandTotal + calculateTotalPrice(transaction.DetailedTransactions);
}, 0); }, 0);
}; };
const searchAndAggregateItems = (transactions, searchTerm) => { const searchAndAggregateItems = (transactions, searchTerm) => {
if (!searchTerm.trim()) return []; if (!searchTerm.trim()) return [];
const normalizedTerm = searchTerm.trim().toLowerCase(); const normalizedTerm = searchTerm.trim().toLowerCase();
// Map with key = `${itemId}-${confirmedGroup}` to keep confirmed groups separate
const aggregatedItems = new Map(); const aggregatedItems = new Map();
transactions.forEach(transaction => { 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 => { transaction.DetailedTransactions.forEach(detail => {
const itemName = detail.Item.name; const itemName = detail.Item.name;
const itemNameLower = itemName.toLowerCase(); const itemNameLower = itemName.toLowerCase();
if (itemNameLower.includes(normalizedTerm)) { if (itemNameLower.includes(normalizedTerm)) {
const key = detail.itemId; // Combine itemId and confirmedGroup to keep them separated
const key = `${detail.itemId}-${confirmedGroup}`;
if (!aggregatedItems.has(key)) { if (!aggregatedItems.has(key)) {
aggregatedItems.set(key, { aggregatedItems.set(key, {
@@ -82,6 +100,7 @@ const searchAndAggregateItems = (transactions, searchTerm) => {
name: itemName, name: itemName,
totalQty: 0, totalQty: 0,
totalPrice: 0, totalPrice: 0,
confirmedGroup, // Keep track of which group this belongs to
}); });
} }
@@ -91,9 +110,10 @@ const searchAndAggregateItems = (transactions, searchTerm) => {
} }
}); });
}); });
console.log(aggregatedItems.values())
return Array.from(aggregatedItems.values()); return Array.from(aggregatedItems.values());
}; };
const handleConfirm = async (transactionId) => { const handleConfirm = async (transactionId) => {
@@ -103,7 +123,7 @@ const searchAndAggregateItems = (transactions, searchTerm) => {
if (result) { if (result) {
setTransactions(prev => setTransactions(prev =>
prev.map(t => prev.map(t =>
t.transactionId === transactionId ? { ...t, confirmed: 1 } : t t.transactionId === transactionId ? result : t
) )
); );
} }
@@ -132,6 +152,64 @@ const searchAndAggregateItems = (transactions, searchTerm) => {
} }
}; };
const handlePrint = (transaction) => {
const formatWaktu = (() => {
const date = new Date(transaction.createdAt);
const tanggal = date.toLocaleDateString('id-ID');
const jam = date.toLocaleTimeString('id-ID', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
});
return `${tanggal} ${jam}`;
})();
const itemsStr = transaction.DetailedTransactions.map((dt) => {
const name = dt.Item.name.length > 11
? dt.Item.name.slice(0, 11)
: dt.Item.name.padEnd(11);
const qty = dt.qty.toString().padStart(3);
const total = formatRupiah(dt.qty * (dt.promoPrice || dt.price)).padStart(15);
return `${name} ${qty} ${total}`;
}).join('\n');
const totalHarga = calculateTotalPrice(transaction.DetailedTransactions);
const totalStr = `Total: ${formatRupiah(totalHarga)}`;
const receiptText = (
` CAFE HOREE
Jl. Ahmad Yani No. 12, Kediri
Telp: 0812-1617-6963
==============================
Tanggal : ${formatWaktu}
Bayar : ${transaction.payment_type}
------------------------------
Item Qty Total
------------------------------
${itemsStr}
${totalStr}
==============================
Terima kasih atas kunjungannya!
~~
supported by kedaimaster.com
\n\n\n\n\n`
);
const params = new URLSearchParams();
params.append("content", receiptText);
params.append("encode_format", "UTF-8");
const printUrl = `btprinter://print?${params.toString()}`;
// Trigger aplikasi printer via URL scheme
window.location.href = printUrl;
};
if (loading) if (loading)
return ( return (
<div className="Loader"> <div className="Loader">
@@ -144,7 +222,7 @@ const searchAndAggregateItems = (transactions, searchTerm) => {
return ( return (
<div className={styles.Transactions}> <div className={styles.Transactions}>
<h2 className={styles["Transactions-title"]}> <h2 className={styles["Transactions-title"]}>
Daftar transaksi Rp {calculateAllTransactionsTotal(transactions)} Transaksi selesai {formatRupiah(calculateAllTransactionsTotal(transactions))}
</h2> </h2>
<input <input
@@ -152,17 +230,17 @@ const searchAndAggregateItems = (transactions, searchTerm) => {
placeholder="Cari nama item..." placeholder="Cari nama item..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
style={{ border:'0px',height: '42px',borderRadius: '15px', margin: '7px auto 10px', width: '88%', paddingLeft: '8px' }} style={{ border: '0px', height: '42px', borderRadius: '15px', margin: '7px auto 10px', width: '88%', paddingLeft: '8px' }}
/> />
{/* Existing Transactions List (keep all your JSX below unchanged) */} {/* Existing Transactions List (keep all your JSX below unchanged) */}
<div className={styles.TransactionListContainer} style={{ padding: '0 8px' }}> <div className={styles.TransactionListContainer} style={{ padding: '0 8px' }}>
{matchedItems.length > 0 && matchedItems.map(item => ( {matchedItems.length > 0 && matchedItems.map(item => (
<div <div
key={item.itemId} key={`${item.itemId}-${item.confirmedGroup}`}
className={styles.RoundedRectangle} className={styles.RoundedRectangle}
style={{ overflow: "hidden" }} style={{ overflow: "hidden" }}
> >
@@ -173,10 +251,10 @@ const searchAndAggregateItems = (transactions, searchTerm) => {
</ul> </ul>
<div className={styles.TotalContainer}> <div className={styles.TotalContainer}>
<span>Total:</span> <span>Total:</span>
<span>Rp {item.totalPrice}</span> <span>{formatRupiah(item.totalPrice)}</span>
</div> </div>
</div> </div>
))} ))}
{transactions && {transactions &&
transactions.map((transaction) => ( transactions.map((transaction) => (
<div <div
@@ -188,7 +266,7 @@ const searchAndAggregateItems = (transactions, searchTerm) => {
<div className={styles['receipt-header']}> <div className={styles['receipt-header']}>
{transaction.confirmed === 1 ? ( {transaction.confirmed === 1 ? (
<ColorRing className={styles['receipt-logo']} /> <ColorRing className={styles['receipt-logo']} />
) : transaction.confirmed === -1 || transaction.confirmed === -2 ? ( ) : (transaction.confirmed == -1 && !transaction.is_paid) || (transaction.confirmed === -2 && !transaction.is_paid) ? (
<div style={{ display: 'flex', justifyContent: 'center', margin: '16px 0px' }}> <div style={{ display: 'flex', justifyContent: 'center', margin: '16px 0px' }}>
<svg <svg
style={{ width: '60px', transform: 'Rotate(45deg)' }} style={{ width: '60px', transform: 'Rotate(45deg)' }}
@@ -205,9 +283,9 @@ const searchAndAggregateItems = (transactions, searchTerm) => {
/> />
</svg> </svg>
</div> </div>
) : transaction.confirmed === 2 ? ( ) : transaction.confirmed === 2 && !transaction.is_paid ? (
<ColorRing className={styles['receipt-logo']} /> <ColorRing className={styles['receipt-logo']} />
) : transaction.confirmed === 3 ? ( ) : transaction.confirmed === 3 || transaction.is_paid ? (
<div> <div>
<svg <svg
height="60px" height="60px"
@@ -242,15 +320,15 @@ const searchAndAggregateItems = (transactions, searchTerm) => {
<div className={styles['receipt-info']}> <div className={styles['receipt-info']}>
{deviceType == 'clerk' ? {deviceType == 'clerk' ?
<h3>{transaction.confirmed === 1 ? ( <h3>{transaction.confirmed === 1 && !transaction.is_paid ? (
"Silahkan Cek Pembayaran" "Silahkan Cek Pembayaran"
) : transaction.confirmed === -1 ? ( ) : transaction.confirmed === -1 && !transaction.is_paid ? (
"Dibatalkan Oleh Kasir" "Dibatalkan Oleh Kasir"
) : transaction.confirmed === -2 ? ( ) : transaction.confirmed === -2 && !transaction.is_paid ? (
"Dibatalkan Oleh Pelanggan" "Dibatalkan Oleh Pelanggan"
) : transaction.confirmed === 2 ? ( ) : transaction.confirmed === 2 && !transaction.is_paid ? (
"Sedang Diproses" "Sedang Diproses"
) : transaction.confirmed === 3 ? ( ) : transaction.confirmed === 3 || transaction.is_paid ? (
"Transaksi Sukses" "Transaksi Sukses"
) : ( ) : (
"Silahkan Cek Ketersediaan" "Silahkan Cek Ketersediaan"
@@ -294,11 +372,28 @@ const searchAndAggregateItems = (transactions, searchTerm) => {
<ul> <ul>
{transaction.DetailedTransactions.map((detail) => ( {transaction.DetailedTransactions.map((detail) => (
<li key={detail.detailedTransactionId}> <li key={detail.detailedTransactionId}>
<span>{detail.Item.name}</span> - {detail.qty < 1 ? 'tidak tersedia' : `${detail.qty} x Rp <span>{detail.Item.name}</span> - {detail.qty < 1
${detail.promoPrice ? detail.promoPrice : detail.price}`} ? 'tidak tersedia'
: `${detail.qty} x ${formatRupiah(detail.promoPrice || detail.price)}`
}
</li> </li>
))} ))}
</ul> </ul>
{!transaction.is_paid && transaction.confirmed > -1 &&
<div
onClick={() => {
localStorage.setItem('lastTransaction', JSON.stringify(transaction));
setModal("message", { captMessage: 'Silahkan tambahkan pesanan', descMessage: 'Pembayaran akan ditambahkan ke transaksi sebelumnya.' }, null, null);
// Dispatch the custom event
window.dispatchEvent(new Event("localStorageUpdated"));
}}
className={styles["addNewItem"]}
>
Tambah pesanan
</div>
}
<h2 className={styles["Transactions-detail"]}> <h2 className={styles["Transactions-detail"]}>
{transaction.serving_type === "pickup" {transaction.serving_type === "pickup"
? "Self pickup" ? "Self pickup"
@@ -326,12 +421,13 @@ const searchAndAggregateItems = (transactions, searchTerm) => {
<div className={styles.TotalContainer}> <div className={styles.TotalContainer}>
<span>Total:</span> <span>Total:</span>
<span> <span>
Rp {calculateTotalPrice(transaction.DetailedTransactions)} {formatRupiah(calculateTotalPrice(transaction.DetailedTransactions))}
</span> </span>
</div> </div>
<div className={styles.TotalContainer}> <div className={styles.TotalContainer}>
{(deviceType == 'clerk' && (transaction.confirmed == 0 || transaction.confirmed == 1 || transaction.confirmed == 2)) && {(deviceType == 'clerk' && !transaction.is_paid && (transaction.confirmed == 0 || transaction.confirmed == 1 || transaction.confirmed == 2)) &&
<>
<button <button
className={styles.PayButton} className={styles.PayButton}
onClick={() => handleConfirm(transaction.transactionId)} onClick={() => handleConfirm(transaction.transactionId)}
@@ -350,14 +446,22 @@ const searchAndAggregateItems = (transactions, searchTerm) => {
} }
</button> </button>
</>
} }
{deviceType == 'clerk' && transaction.confirmed > 1 && (
<h5
className={styles.DeclineButton}
onClick={() => handlePrint(transaction)}
>
Cetak struk
</h5>
)}
{deviceType == 'guestDevice' && transaction.confirmed < 2 && transaction.payment_type != 'cash' && transaction.payment_type != 'paylater/cash' && {deviceType == 'guestDevice' && transaction.confirmed < 2 && transaction.payment_type != 'cash' && transaction.payment_type != 'paylater/cash' &&
<ButtonWithReplica <ButtonWithReplica
paymentUrl={paymentUrl} paymentUrl={paymentUrl}
price={ price={formatRupiah(calculateTotalPrice(transaction.DetailedTransactions))}
"Rp" + calculateTotalPrice(transaction.DetailedTransactions)
}
disabled={isPaymentLoading} disabled={isPaymentLoading}
isPaymentLoading={isPaymentLoading} isPaymentLoading={isPaymentLoading}
handleClick={() => handleConfirm(transaction.transactionId)} handleClick={() => handleConfirm(transaction.transactionId)}

View File

@@ -396,3 +396,65 @@
color: white; color: white;
line-height: 27px; line-height: 27px;
} }
/* Transactions.module.css */
.printContainer {
display: none; /* Hidden in normal view */
}
@media print {
@page {
size: portrait;
margin: 15mm;
}
body * {
visibility: hidden;
}
#print-section,
#print-section * {
visibility: visible;
}
#print-section {
position: absolute;
left: 0;
top: 0;
width: 100%;
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;
}
}