import React, { useEffect, useState } from "react"; import { getReports, getAnalytics } from "../helpers/transactionHelpers.js"; import { createCafe } from "../helpers/cafeHelpers.js" import { createCoupon } from "../helpers/couponHelpers.js" import CircularDiagram from "./CircularDiagram"; import styles from "./Transactions.module.css"; import "./Switch.css"; import MultiSwitch from "react-multi-switch-toggle"; import DailyCharts from '../components/DailyCharts.js'; import PeriodCharts from '../components/PeriodCharts.js'; import Coupon from "../components/Coupon.js"; import CreateCouponPage from "./CreateCoupon.js"; const RoundedRectangle = ({ onClick, title, value, percentage, invert, fontSize = "15px", loading = false, children, // Assuming this is a React component or JSX isChildren, width = 'calc(100% / 2 - 10px)' }) => { const containerStyle = { display: "flex", flexDirection: "column", alignItems: "flex-start", justifyContent: "center", width: width, height: "auto", borderRadius: "15px", padding: "20px", margin: "5px", textAlign: "left", fontFamily: "Arial, sans-serif", boxSizing: "border-box", backgroundColor: loading ? "rgb(127 127 127)" : 'white', border: '1px solid #b3b1b1' }; const titleStyle = { fontWeight: "bold", marginBottom: "10px", width: "100%", backgroundColor: loading ? "rgb(85 85 85)" : !isChildren && !children && "inherit", color: loading ? "transparent" : "inherit", }; const valueAndPercentageContainerStyle = { display: "flex", flexDirection: "row", justifyContent: "space-between", alignItems: "center", width: "100%", }; const valueStyle = { fontSize: loading ? "15px" : fontSize, fontWeight: "bold", flex: "1", textAlign: "left", color: loading ? "transparent" : "inherit", backgroundColor: loading ? "rgb(85 85 85)" : "transparent", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", }; const percentageStyle = { fontSize: "16px", display: "flex", alignItems: "center", textAlign: "right", color: loading ? "black" : percentage >= 0 ? (invert ? "red" : "#2fd45e") : (invert ? "#2fd45e" : "red"), }; const arrowStyle = { marginLeft: "5px", }; return (
{title}
{!children && (
{loading ? "Loading..." : value}
{loading ? "" : percentage} {percentage !== undefined && !loading && "%"} {percentage !== undefined && !loading && ( {percentage > 0 ? "↗" : percentage === 0 ? "-" : "↘"} )}
)} {children &&
{children}
} {/* Properly render children */}
); }; const App = ({ forCafe = true, cafeId = -1, handleClose, otherCafes, coupons, setModal, user }) => { const [modalStatus, setModalStatus] = useState(null); const [selectedCafeId, setSelectedCafeId] = useState(cafeId); const [analytics, setAnalytics] = useState({}); const [loading, setLoading] = useState(true); const [filter, setFilter] = useState("monthly"); const [circularFilter, setCircularFilter] = useState("item"); const [graphFilter, setGraphFilter] = useState("income"); const [itemName, setItemName] = useState(''); const fetchData = async (filter) => { if (selectedCafeId == '-1') return; try { setLoading(true); // Fetch the analytics data with the selected filter let analyticsData = null; if(user.roleId == 1) analyticsData = (selectedCafeId !== '' && selectedCafeId !== 0) ? await getReports(selectedCafeId, filter) : await getAnalytics(filter); else analyticsData = await getAnalytics(filter, selectedCafeId); console.log(analyticsData); if (analyticsData) setAnalytics(analyticsData); } catch (error) { console.error("Error fetching data:", error); } finally { setLoading(false); } }; useEffect(() => { setSelectedCafeId(cafeId) }, [cafeId]); useEffect(() => { fetchData(filter); // Fetch data when filter changes }, [filter, selectedCafeId]); const filteredItems = (selectedCafeId != 0 && selectedCafeId != -1) ? (circularFilter == 'item' ? analytics?.itemSales || [] : analytics.materialSpend || []) : analytics?.items || []; const colors = [ "#af9463", // Bright Red "#F4A261", // Soft Orange "#FFD166", // Sunshine Yellow "#06D6A0", // Mint Green "#118AB2", // Rich Teal "#9D4EDD", // Vivid Violet "#FF66B3", // Bubblegum Pink "#FF7F50", // Coral "#FFA07A", // Light Salmon "#F7CAD0", // Blush Pink "#A8DADC", // Aqua Blue "#457B9D", // Steel Blue "#2A9D8F", // Jungle Green "#FFB703", // Mustard Yellow "#E9C46A", // Honey Gold "#264653", // Charcoal Green "#D4A5A5", // Rose Beige "#A6D6F0", // Sky Blue "#BC4749", // Cranberry Red ]; const totalSoldAcrossAllCafes = filteredItems.reduce((total, cafe) => { const cafeTotal = (cafe.report?.itemSales || []).reduce((sum, item) => sum + item.sold, 0); return total + cafeTotal; }, 0); console.log(totalSoldAcrossAllCafes) const totalSpendAcrossAllCafes = filteredItems.reduce((total, cafe) => { const cafeTotal = (cafe.report?.materialSpend || []).reduce((sum, item) => sum + item.spend, 0); return total + cafeTotal; }, 0); // Define a color palette or generate colors dynamically const colorPalette = colors; // Ensure that each segment gets a unique color let colorIndex = 0; console.log(filteredItems) let segments = (selectedCafeId == 0 || selectedCafeId == -1) ? filteredItems.flatMap((cafe) => { const cafeItems = cafe.report?.itemSales || []; console.log(cafeItems); // Log all items for the cafe return cafeItems.map((item, index) => { const percentage = totalSoldAcrossAllCafes > 0 ? ((item.sold / totalSoldAcrossAllCafes) * 100).toFixed(2) : 0; console.log(`${item.itemName}: ${(percentage)}%`); // Log item name and percentage // Assign a unique color from the color palette const color = colorPalette[colorIndex % colorPalette.length]; // Use modulo to cycle through colors colorIndex++; // Increment to ensure a new color for the next item return { itemName: item.itemName, sold: item.sold, percentage: percentage, color: color, }; }); }) : filteredItems.map((item, index) => { const color = colorPalette[colorIndex % colorPalette.length]; // Use modulo to cycle through colors colorIndex++; // Increment to ensure a new color for the next item return { itemName: item.itemName, percentage: item.percentage, color: color, }; }); segments.sort((a, b) => b.sold - a.sold); let materialSegments = (selectedCafeId == 0 || selectedCafeId == -1) ? filteredItems.flatMap((cafe) => { const cafeItems = cafe.report?.materialSpend || []; console.log(cafeItems); // Log all items for the cafe return cafeItems.map((item, index) => { const percentage = totalSpendAcrossAllCafes > 0 ? ((item.spend / totalSpendAcrossAllCafes) * 100).toFixed(2) : 0; console.log(`${item.materialName}: ${(percentage)}%`); // Log item name and percentage // Assign a unique color from the color palette const color = colorPalette[colorIndex % colorPalette.length]; // Use modulo to cycle through colors colorIndex++; // Increment to ensure a new color for the next item return { itemName: item.materialName, sold: item.spend, percentage: percentage, color: color, }; }); }) : filteredItems.map((item, index) => { const color = colorPalette[colorIndex % colorPalette.length]; // Use modulo to cycle through colors colorIndex++; // Increment to ensure a new color for the next item return { itemName: item.materialName, percentage: item.percentage, color: color, }; }); materialSegments.sort((a, b) => b.spend - a.spend); console.log(selectedCafeId) console.log(segments) const formatIncome = (amount) => { if (amount >= 1_000_000_000) { // Format for billions const billions = amount / 1_000_000_000; return billions.toFixed(0) + "b"; // No decimal places for billions } else if (amount >= 1_000_000) { // Format for millions const millions = amount / 1_000_000; return millions.toFixed(2).replace(/\.00$/, "") + "m"; // Two decimal places, remove trailing '.00' } else if (amount >= 1_000) { // Format for thousands const thousands = amount / 1_000; return thousands.toFixed(1).replace(/\.0$/, "") + "k"; // One decimal place, remove trailing '.0' } else { // Less than a thousand if (amount != null) return amount.toString(); } }; function roundToInteger(num) { return Math.round(num); } function onToggle(selectedItem) { setAnalytics({}); const filterMap = ["yesterday", "weekly", "monthly", "yearly"]; setFilter(filterMap[selectedItem]); } const filterTexts = ["1", "7", "30", "365"]; const comparisonText = filterTexts[["yesterday", "weekly", "monthly", "yearly"].indexOf(filter)]; const [resetKey, setResetKey] = useState(0); // A key to force re-render const [texts, setTexts] = useState(['Buat bisnis']); // 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 useEffect(() => { if (otherCafes != null) { console.log(otherCafes) let updatedFullTexts; if (otherCafes.length === 0) { updatedFullTexts = [[user.roleId == 0 ? "Buat Voucher":"Buat bisnis", 0]]; setSelectedCafeId(-1); } else if (otherCafes.length === 1) { updatedFullTexts = [ [otherCafes[0].name || otherCafes[0].username, otherCafes[0].cafeId || otherCafes[0].userId], [user.roleId == 0 ? "Buat Voucher":"Buat bisnis", -1] ]; setSelectedCafeId(otherCafes[0].cafeId); // Get the cafeId (second part of the pair) } else { updatedFullTexts = [ ["semua", 0], // First entry is "semua" ...otherCafes.map(item => [item.name || item.username, item.cafeId || item.userId]), // Map over cafes to get name and cafeId pairs [user.roleId == 0 ? "Buat Voucher":"Tambah Bisnis +", -1] // Add the "+" entry ]; setSelectedCafeId(0); } setFullTexts(updatedFullTexts); // Set fullTexts with the original structure // Set fullTextsVisible to an array of names only setFullTextsVisible(updatedFullTexts.map(item => item[0])); // Extract just the names (first part of each pair) // Set the first 3 items in texts setTexts(updatedFullTexts.map(item => item[0]).slice(0, 3)); // Increment resetKey to trigger re-render setResetKey(prevKey => prevKey + 1); } console.log(otherCafes); }, [otherCafes]); console.log(fullTexts) const [selectedSwitch, setSelectedSwitch] = useState(0); const onItemToggle = (index) => { // When user clicks the last visible option (index === 2 in the current view) if (index === 2) { console.log(fullTexts); if (fullTexts.findIndex(item => item[0] === texts[2]) < fullTexts.length - 1) { setTexts((prevTexts) => { const newTexts = [...prevTexts]; console.log(prevTexts.length) const nextText = fullTexts[fullTexts.findIndex(item => item[0] === texts[2]) + 1][0]; // Get the next item in the full list newTexts.shift(); // Remove the first element newTexts.push(nextText); // Add the next item to the end setSelectedSwitch(1); // Change the selected index return newTexts; }); } } // When user clicks the first visible option (index === 0 in the current view) if (index === 0) { // Check if there is a previous text in the full list (before the first visible text) if (fullTexts.findIndex(item => item[0] === texts[0]) > 0) { setTexts((prevTexts) => { const newTexts = [...prevTexts]; const prevText = fullTexts[fullTexts.findIndex(item => item[0] === newTexts[0]) - 1]; // Get the previous item newTexts.pop(); // Remove the last element newTexts.unshift(prevText[0]); // Add the previous item to the start setSelectedSwitch(1); // Change the selected index return newTexts; }); } } // Dynamically update selectedSwitch state to trigger re-render setSelectedSwitch(index); // Get the cafeId from the selected text based on the index const selectedText = texts[index]; // Get the selected name from the texts array const selectedItem = fullTexts.find(item => item[0] === selectedText); // Find the corresponding full item if (selectedItem) { setSelectedCafeId(selectedItem[1]); // Get the cafeId (second part of the pair) } if(selectedItem[1] == -1 && user.roleId == 0) setModal('create_coupon', {}, () => {setSelectedSwitch(0);setSelectedCafeId(0)}); setResetKey((prevKey) => prevKey + 1); // Increase the key to force re-render }; // useEffect(() => { // // This effect will run whenever the selectedSwitch changes // // We could add logic here if needed for side effects // }, [selectedSwitch]); const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); const handleClick = async () => { if (user?.roleId === 0) { setModal('loading'); const create = await createCoupon(itemName); if (!create) { setModalStatus('failed'); } // Add a 2-second delay before proceeding await delay(2000); handleClose(); setModalStatus(null); // Reset status window.location.reload(); // Reload the page return; } else { setModal('loading'); const create = await createCafe(itemName); if (!create) { setModalStatus('failed'); } // Add a 2-second delay before proceeding await delay(2000); handleClose(); setModalStatus(null); // Reset status window.location.reload(); // Reload the page } }; return (
{forCafe &&
}
{!forCafe &&
{texts.map((item, indexx) => { return (
onItemToggle(indexx)} >
{item}
); })}
}
{!forCafe && selectedCafeId != -1 &&
{((analytics?.itemSales && analytics?.itemSales.length > 0) || (!analytics?.itemSales && segments.length > 0)) ? ( 0 && analytics?.itemSales[0] != undefined ? analytics?.itemSales[0]?.itemName : segments[0].itemName} loading={loading} /> ) : ( ) } {!forCafe && selectedCafeId != -1 && selectedCafeId != 0 && ( window.location.href = window.location.origin + '/' + otherCafes.find(item => item.cafeId === selectedCafeId).cafeIdentifyName} /> )}
{(filter == 'yesterday' || filter == 'weekly') ? `Data dihitung dengan membandingkan ${comparisonText} hari terakhir dengan ${comparisonText} hari sebelumnya, dengan penghitungan dimulai dari 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.`}
setCircularFilter('item') } style={{ color: 'black', position: 'relative' }} >
Item laku
setCircularFilter('material') } >
Bahan baku
{(circularFilter === 'item' ? segments : materialSegments).map((item, index) => (
{item.itemName}
))}
setGraphFilter('income') } style={{ color: 'black', position: 'relative' }} >
Pemasukan
setGraphFilter('outcome') } >
Pengeluaran
setGraphFilter('transactions') } >
Transaksi
{filter == 'yesterday' || filter == 'weekly' ? : }
} {!forCafe && selectedCafeId == -1 && user.roleId == 1 &&
setItemName(e.target.value)} style={{ width: '70%', fontSize: '25px', borderRadius: '7px', border: '1px solid black' }} />
} {user?.roleId == 1 && <>
{forCafe &&
}
{!forCafe &&
Voucher
}
{coupons && coupons.map((coupon) => { return })}
}
); }; export default App;