import React, { useState, useEffect, useRef } from "react"; import "./App.css"; import "./components/Loading.css"; import { BrowserRouter as Router, Route, Routes, useNavigate, useLocation, } from "react-router-dom"; import socket from "./services/socketService"; import Dashboard from "./pages/Dashboard"; import ScanMeja from "./pages/ScanMeja"; import CafePage from "./pages/CafePage"; import SearchResult from "./pages/SearchResult"; import Cart from "./pages/Cart"; import Invoice from "./pages/Invoice"; import Transactions from "./pages/Transactions"; import Footer from "./components/Footer"; import GuestSideLogin from "./pages/GuestSideLogin"; import GuestSide from "./pages/GuestSide"; import { getItemTypesWithItems } from "./helpers/itemHelper.js"; import { getTableByCode } from "./helpers/tableHelper.js"; import { getTransactionsFromCafe, } from "./helpers/transactionHelpers"; import { getConnectedGuestSides, getClerks, removeConnectedGuestSides, } from "./helpers/userHelpers.js"; import { getLocalStorage, removeLocalStorage, } from "./helpers/localStorageHelpers"; import { calculateTotals } from "./helpers/cartHelpers"; import { subscribeUser, resetNotificationSubscription, } from "./helpers/subscribeHelpers.js"; import Modal from "./components/Modal"; // Import your modal component import { requestNotificationPermission } from './services/notificationService'; // Import the notification service function App() { const location = useLocation(); const navigate = useNavigate(); const [user, setUser] = useState([]); const [shopClerks, setShopClerks] = useState([]); const [guestSideOfClerk, setGuestSideOfClerk] = useState(null); const [guestSides, setGuestSides] = useState([]); const [shopId, setShopId] = useState(""); const [shopIdentifier, setShopIdentifier] = useState(""); const [table, setTable] = useState([]); const [totalItemsCount, setTotalItemsCount] = useState(0); const [totalPrice, setTotalPrice] = useState(0); const [lastTransaction, setLastTransaction] = useState(null); const [deviceType, setDeviceType] = useState(""); const [shop, setShop] = useState([]); const [shopItems, setShopItems] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); const [modalContent, setModalContent] = useState(null); const [onModalCloseFunction, setOnModalCloseFunction] = useState(null); const [onModalYesFunction, setOnModalYesFunction] = useState(null); const transactionList = useRef(null); const [queue, setQueue] = useState([]); const validTransactionStates = [ 'new_transaction', 'transaction_canceled', 'transaction_pending', 'transaction_confirmed', 'payment_claimed', 'transaction_success', 'transaction_end', 'transaction_failed', ]; const calculateTotalsFromLocalStorage = () => { const { totalCount, totalPrice } = calculateTotals(shopId); setTotalItemsCount(totalCount); setTotalPrice(totalPrice); // If 'lastTransaction' exists, proceed const lastTransaction = JSON.parse(localStorage.getItem("lastTransaction")); console.log(lastTransaction); if (lastTransaction != null) { setLastTransaction(lastTransaction); } }; useEffect(() => { calculateTotalsFromLocalStorage(); const handleStorageChange = () => { calculateTotalsFromLocalStorage(); }; window.addEventListener("localStorageUpdated", handleStorageChange); return () => { window.removeEventListener("localStorageUpdated", handleStorageChange); }; }, [shopId]); const handleSetParam = async ({ shopIdentifier, tableCode }) => { setShopIdentifier(shopIdentifier); if (tableCode) if (table.length == 0) { const gettable = await getTableByCode(shopIdentifier, tableCode); if (gettable) setTable(gettable); } }; useEffect(() => { const fetchData = async () => { console.log("gettingItems"); try { const { response, cafe, data } = await getItemTypesWithItems(shopIdentifier); if (response.status === 200) { setShopId(cafe.cafeId) setShop(cafe); setShopItems(data); console.log(data) // Filter out unavailable items const filteredData = data .map((itemType) => ({ ...itemType, itemList: itemType.itemList.filter((item) => item.availability), })) .filter((itemType) => itemType.itemList.length > 0); // Remove empty itemTypes // Update local storage by removing unavailable items const updatedLocalStorage = JSON.parse(localStorage.getItem("cart")) || []; const newLocalStorage = updatedLocalStorage.map((cafee) => { if (cafee.cafeId === cafe.cafeId) { return { ...cafee, items: cafee.items.filter((item) => filteredData.some((filtered) => filtered.itemList.some( (i) => i.itemId === item.itemId && i.availability ) ) ), }; } return cafee; }); localStorage.setItem("cart", JSON.stringify(newLocalStorage)); socket.on("transaction_created", () => { console.log("transaction created"); }); } } catch (error) { console.error("Error fetching shop items:", error); } }; if (shopIdentifier !== "") fetchData(); }, [shopIdentifier]); const rmConnectedGuestSides = async (gueseSideSessionId) => { const sessionLeft = await removeConnectedGuestSides(gueseSideSessionId); setGuestSides(sessionLeft.guestSideList); }; useEffect(() => { if (socket == null) return; if (getLocalStorage("authGuestSide")) { socket.emit("checkGuestSideToken", { token: getLocalStorage("authGuestSide"), }); } else { socket.emit("checkUserToken", { token: getLocalStorage("auth"), shopId, }); } //for guest socket.on("transaction_pending", async (data) => { console.log("transaction notification"); // Call `setModal` with content and parameters setModal("transaction_pending", data); localStorage.setItem('cart', []); calculateTotalsFromLocalStorage(); }); socket.on("transaction_confirmed", async (data) => { console.log("transaction notification: " + data); setModal("transaction_confirmed", data); localStorage.setItem('cart', []); const startTime = Date.now(); // Capture the start time const timeout = 10000; // 10 seconds timeout in milliseconds calculateTotalsFromLocalStorage(); while (localStorage.getItem("lastTransaction") === null) { if (Date.now() - startTime > timeout) { return; // Exit the function and don't proceed further } await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second } // If 'lastTransaction' exists, proceed const lastTransaction = JSON.parse(localStorage.getItem("lastTransaction")); console.log(lastTransaction); if (lastTransaction != null) { setLastTransaction(lastTransaction); } }); socket.on("transaction_success", async (data) => { console.log("transaction notification"); setModal("transaction_success", data); }); socket.on("transaction_end", async (data) => { console.log("transaction notification"); setModal("transaction_end", data); }); socket.on("payment_claimed", async (data) => { console.log(data); setModal("payment_claimed", data); }); socket.on("transaction_failed", async (data) => { console.log("transaction notification"); setModal("transaction_failed", data); }); const handleNotificationClick = async () => { const permission = await requestNotificationPermission(); if (permission === "granted") { console.log("Notification permission granted."); // Set up notifications or show a success modal } else { console.error("Notification permission denied."); setModal('blocked_notification'); // Show modal for blocked notifications } }; const checkNotifications = () => { let permission = Notification.permission; // Check current permission const searchParams = new URLSearchParams(location.search); let searchModal = searchParams.get("modal") || ''; // Get transactionId or set it to empty string if (permission !== "granted" && searchModal == '') { setModal("message", { captMessage: 'Notifikasi tidak aktif', descMessage: 'Aktifkan notifikasi supaya kamu tetap dapat info pesanan, meski sedang buka aplikasi lain.' }, null, handleNotificationClick); } }; socket.on("checkUserTokenRes", async (data) => { if (data.status !== 200) { removeLocalStorage("auth"); setDeviceType("guestDevice"); } else { console.log(data) if(data.data.user.cafeId != null) navigate(`/${data.data.user.cafeIdentityName}`, { replace: true }); setUser(data.data.user); if (data.data.latestOpenBillTransaction != null) localStorage.setItem('lastTransaction', JSON.stringify(data.data.latestOpenBillTransaction)) if ( data.data.user.password == "unsetunsetunset" && localStorage.getItem("settings") ) setModal("complete_account"); if (data.data.user.cafeId == shopId || data.data.isTheOwner) { const connectedGuestSides = await getConnectedGuestSides(); setGuestSides(connectedGuestSides.sessionDatas); console.log("getting guest side"); setDeviceType("clerk"); checkNotifications(); } else { setDeviceType("guestDevice"); } if (data.data.user.roleId == 1 && user.userId == shop.ownerId) { // shopClerks is can only be obtained by the shop owner // so every user that is admin will try to getting shopClerks, even not yet proven that this is their shop const shopClerks = await getClerks(shopId); if (shopClerks != null) setShopClerks(shopClerks); } } }); socket.on("checkGuestSideTokenRes", (data) => { if (data.status !== 200) { removeLocalStorage("authGuestSide"); navigate("/guest-side"); } else { setGuestSideOfClerk({ clerkId: data.sessionData.clerkId, clerkUsername: data.sessionData.clerkUsername, }); setDeviceType("guestSide"); } }); socket.on("signout-guest-session", () => { navigate("/guest-side"); }); socket.on("updateQueue", ({ queue }) => { setQueue(queue); // Only set the queue if it's a valid non-empty array console.log("Updated Queue:", queue); // Log the valid queue }); return () => { socket.off("signout-guest-session"); }; }, [socket, shopId]); async function checkIfStillViewingOtherTransaction() { console.log("transaction notification"); console.log(modalContent); let response; response = await getTransactionsFromCafe(shopId, 0, true); transactionList.current = response; console.log(response); // Get current URL's search parameters inside the socket event handler const searchParams = new URLSearchParams(location.search); let transaction_info = searchParams.get("transactionId") || ''; // Get transactionId or set it to empty string console.log(transaction_info); // Log the updated transaction_info // If transaction_info is an empty string, set the modal if (transaction_info == '') return false; else return true; } useEffect(() => { // This will ensure that searchParams and transaction_info get updated on each render socket.on("transaction_created", async (data) => { console.log("transaction notification"); const isViewingOtherTransaction = await checkIfStillViewingOtherTransaction(); // If transaction_info is an empty string, set the modal if (!isViewingOtherTransaction) { setModal("new_transaction", data); } // Show browser notification let permission = Notification.permission; if (permission !== "granted") return; navigator.serviceWorker.ready.then((registration) => { registration.showNotification("New Transaction", { body: `A new transaction was created: ${data.transactionDetails}`, icon: "icon.png", // Optional icon }); }); }); socket.on("transaction_canceled", async (data) => { console.log("transaction notification"); const isViewingOtherTransaction = await checkIfStillViewingOtherTransaction(); // If transaction_info is an empty string, set the modal if (!isViewingOtherTransaction) { setModal("new_transaction", data); } }); // Clean up the socket event listener on unmount or when dependencies change return () => { socket.off("transaction_created"); socket.off("transaction_canceled"); }; }, [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) { console.log(direction); console.log(from); // Find the current index based on the 'from' transactionId const currentIndex = transactionList.current.findIndex(item => item.transactionId == from); // Determine the new transactionId based on the direction let newTransactionId; if (direction === 'next') { // If we're not at the last transaction, get the next transactionId newTransactionId = currentIndex < transactionList.current.length - 1 ? transactionList.current[currentIndex + 1].transactionId : from; // If already at the end, stay on the current transactionId } else if (direction === 'previous') { // If we're not at the first transaction, get the previous transactionId newTransactionId = currentIndex > 0 ? transactionList.current[currentIndex - 1].transactionId : from; // If already at the beginning, stay on the current transactionId } // Log the new transactionId console.log('New Transaction ID:', newTransactionId); // Update the URL with the new transactionId using navigate navigate(`?transactionId=${newTransactionId}`, { replace: true }); // Optionally, update state or perform further actions based on the new transactionId // Example: // setModalContent({ cafeId: shopId, transactionId: newTransactionId }); } const handleModalFromURL = () => { const queryParams = new URLSearchParams(location.search); const modal = queryParams.get("modal"); if (modal) setModal(modal); }; useEffect(() => { handleModalFromURL(); }, [shopId]); useEffect(() => { console.log(shopId + table?.tableCode); }, [navigate]); // Function to open the modal const setModal = (content, params = {}, onCloseFunction, onYesFunction) => { const queryParams = new URLSearchParams(location.search); // Update the modal and any additional params queryParams.set("modal", content); Object.entries(params).forEach(([key, value]) => { queryParams.set(key, value); }); // Update URL with new parameters navigate(`?${queryParams.toString()}`, { replace: true }); // Prevent scrolling when modal is open document.body.style.overflow = "hidden"; setIsModalOpen(true); setModalContent(content) console.log(onCloseFunction) if (onCloseFunction) { setOnModalCloseFunction(() => onCloseFunction); // Store the close function } else { setOnModalCloseFunction(null); } if (onYesFunction) { setOnModalYesFunction(() => onYesFunction); // Store the close function } else { setOnModalYesFunction(null); } }; const closeModal = (closeTheseContent = []) => { if ( Array.isArray(closeTheseContent) && (closeTheseContent.length === 0 || closeTheseContent.includes(modalContent)) ) { setIsModalOpen(false); setModalContent(null); document.body.style.overflow = "auto"; const queryParams = new URLSearchParams(location.search); // Clear all query parameters queryParams.keys() && [...queryParams.keys()].forEach(key => { queryParams.delete(key); }); // Update the URL without any query parameters navigate({ search: queryParams.toString() }, { replace: true }); } }; // useEffect(() => { // const askNotificationPermission = async () => { // let permission = Notification.permission; // // Check current permission // if (permission === "default") { // setModal("req_notification"); // // Request permission and wait for the result // permission = await Notification.requestPermission(); // } // // Handle permission results // if (permission === "granted") { // await resetNotificationSubscription(); // closeModal(["req_notification", "denied_notification"]); // } else if (permission === "denied") { // setModal("blocked_notification"); // console.error("Notification permission denied."); // } // }; // const handleTransactionConfirmed = async (data) => { // console.log("transaction notification", data); // await askNotificationPermission(); // setModal("transaction_success", data); // }; // // Add socket listener for transaction confirmations // socket.on("transaction_success", handleTransactionConfirmed); // // Cleanup the socket listener on component unmount // return () => { // socket.off("transaction_success", handleTransactionConfirmed); // }; // }, [user]); useEffect(() => { const startPermissionPolling = async () => { const checkInterval = 5000; // Check every 5 seconds const intervalId = setInterval(async () => { const permission = Notification.permission; if (permission === "granted") { await resetNotificationSubscription(); clearInterval(intervalId); // Stop checking once subscribed } else if (permission === "denied") { console.error("Notification permission denied."); } }, checkInterval); }; startPermissionPolling(); }, []); useEffect(() => { if ("serviceWorker" in navigator) { navigator.serviceWorker.register("/service-worker.js") .then(registration => { console.log("Service Worker registered with scope:", registration.scope); }) .catch(error => { console.error("Service Worker registration failed:", error); }); } }, []); return (