Files
groovee/src/pages/Reports.js
insvrgent ebf9f89089 ok
2025-02-07 06:46:24 +07:00

759 lines
27 KiB
JavaScript

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 (
<div style={containerStyle} onClick={onClick}>
<div style={titleStyle}>{title}</div>
{!children && (
<div style={valueAndPercentageContainerStyle}>
<div style={valueStyle}>{loading ? "Loading..." : value}</div>
<div style={percentageStyle}>
{loading ? "" : percentage}
{percentage !== undefined && !loading && "%"}
{percentage !== undefined && !loading && (
<span style={arrowStyle}>
{percentage > 0 ? "↗" : percentage === 0 ? "-" : "↘"}
</span>
)}
</div>
</div>
)}
{children && <div>{children}</div>} {/* Properly render children */}
</div>
);
};
const App = ({ forCafe = true, cafeId = -1,
handleClose, otherCafes, coupons, setModal, user }) => {
const [couponList, setCouponList] = useState(coupons);
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
let unSelectedText;
if (texts[index - 1] != undefined) unSelectedText = texts[index - 1]; // Get the selected name from the texts array
const selectedItem = fullTexts.find(item => item[0] === selectedText); // Find the corresponding full item
const unSelectedItem = fullTexts.find(item => item[0] === unSelectedText); // Find the corresponding full item
if (selectedItem) {
setSelectedCafeId(selectedItem[1]); // Get the cafeId (second part of the pair)
}
let nextSelectedId = selectedItem[1]
if (selectedItem[1] == -1 && user.roleId == 0) setModal('create_coupon', {}, () => { setSelectedSwitch(1); setSelectedCafeId(unSelectedItem[1]); nextSelectedId=unSelectedItem[1] });
console.log(analytics)
if (user && user.roleId === 0 && analytics) {
// Filter the analytics items based on userId
if(selectedItem[1] != 0 && selectedItem[1] != -1){
const filteredData = analytics.items.filter(
(data) => data.userId === nextSelectedId
);
// Extract coupons from the filtered data
const couponsToAdd = filteredData.flatMap((data) => data.coupons);
// Log the coupons to be added
console.log(couponsToAdd);
// Assuming setCouponList is a function that updates the coupon list
setCouponList(couponsToAdd || []);
}
else setCouponList([])
}
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 (
<div style={{
position: forCafe ? 'fixed' : 'relative',
height: '100vh',
width: '100vw',
top: 0,
right: 0,
backgroundColor: 'white',
overflowY: forCafe ? 'auto' : 'none',
color: 'black',
marginTop: forCafe ? 0 : '-22vh'
}}
>
<div style={{ display: 'flex', alignItems: 'center', padding: forCafe ? '0px 20px' : '0px 15px', justifyContent: forCafe ? 'flex-start' : 'space-between' }}>
{forCafe && <div style={{ marginTop: '49px', marginRight: '10px' }} 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></div>}
<div style={{ marginTop: '10px' }}>
{!forCafe &&
<div className={styles.dateSelectorWrapper} style={{ fontSize: '12px' }}>
{texts.map((item, indexx) => {
return (
<div
key={indexx}
className={`${styles.dateSelector} ${styles.dateSelectorActive}`} style={{ position: 'relative', width: 'calc(32vw - 30px)' }}
onClick={() => onItemToggle(indexx)}
>
<div style={{ position: 'absolute', bottom: 0, left: '10%', right: '10%', borderBottom: selectedSwitch == indexx ? `1px solid green` : 'none' }}></div>
<div
style={{ color: 'black' }}>
{item}
</div>
</div>
);
})}
</div>
}
</div>
</div>
{!forCafe && selectedCafeId != -1 &&
<div style={{
textAlign: "center",
marginTop: '30px'
}}>
<MultiSwitch
texts={["Kemarin", "Minggu ini", "Bulan ini", "Tahun ini"]}
selectedSwitch={["yesterday", "weekly", "monthly", "yearly"].indexOf(
filter
)}
bgColor={'#f4efe6'}
borderColor={'transparent'}
borderWidth={0.1}
onToggleCallback={onToggle}
fontColor={"#af9463"}
selectedFontColor={"black"}
selectedSwitchColor={"white"}
eachSwitchWidth={70}
height={"25px"}
fontSize={"12px"}
/>
<div
style={{
display: "flex",
flexWrap: "wrap",
justifyContent: "center",
padding: "20px",
}}
>
<RoundedRectangle
title="Pendapatan"
fontSize="12px"
value={!loading && "Rp" + formatIncome(analytics?.currentTotals?.income)}
percentage={roundToInteger(analytics?.growth?.incomeGrowth)}
invert={false}
loading={loading}
/>
<RoundedRectangle
title="Pengeluaran"
fontSize="12px"
value={!loading && "Rp" + formatIncome(analytics?.currentTotals?.outcome)}
percentage={roundToInteger(analytics?.growth?.outcomeGrowth)}
invert={true}
loading={loading}
/>
<RoundedRectangle
title="Transaksi"
value={analytics?.currentTotals?.transactions}
percentage={roundToInteger(analytics?.growth?.transactionGrowth)}
invert={false}
loading={loading}
/>
{((analytics?.itemSales &&
analytics?.itemSales.length > 0) || (!analytics?.itemSales && segments.length > 0)) ? (
<RoundedRectangle
title={"Item favorit"}
value={analytics?.itemSales?.length > 0 && analytics?.itemSales[0] != undefined ? analytics?.itemSales[0]?.itemName
: segments[0].itemName}
loading={loading}
/>
)
:
(
<RoundedRectangle
title={"Item favorit"}
value={"-"}
loading={loading}
/>
)
}
{!forCafe && selectedCafeId != -1 && selectedCafeId != 0 && (
<RoundedRectangle
title={"Kunjungi bisnis"}
loading={loading}
width="calc(100% - 10px)"
onClick={() => window.location.href = window.location.origin + '/' + otherCafes.find(item => item.cafeId === selectedCafeId).cafeIdentifyName}
/>
)}
<div
style={{ display: "flex", alignItems: "center", margin: "10px" }}
>
<div style={{ marginRight: "5px", fontSize: "1.2em" }}></div>
<h6 style={{ margin: 0, textAlign: "left", fontSize: '10px', fontWeight: 500 }}>
{(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.`}
</h6>
</div>
</div>
<div className={styles.filterSelectorWrapper}>
<div className={`${styles.filterSelector} ${circularFilter == 'item' ? '' : styles.filterSelectorInactive}
}`}
onClick={() =>
setCircularFilter('item')
}
style={{ color: 'black', position: 'relative' }}
>
<div>Item laku</div>
</div>
<div
className={`${styles.filterSelector} ${circularFilter == 'material' ? '' : styles.filterSelectorInactive}
}`}
onClick={() =>
setCircularFilter('material')
}
>
<div>Bahan baku</div>
</div>
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "20px",
paddingTop: 0
}}
>
<div style={{ flex: 1 }}>
<CircularDiagram segments={circularFilter == 'item' ? segments : materialSegments} />
</div>
<div style={{ flex: 1, marginLeft: "20px" }}>
{(circularFilter === 'item' ? segments : materialSegments).map((item, index) => (
<div
key={index}
style={{
display: "flex",
alignItems: "center",
margin: "10px",
}}
>
<div
style={{
marginRight: "5px",
fontSize: "1.2em",
color: colors[index],
}}
>
</div>
<h5 style={{ margin: 0, textAlign: "left" }}>{item.itemName}</h5>
</div>
))}
</div>
</div>
<div className={styles.filterSelectorWrapper}>
<div className={`${styles.filterSelector} ${graphFilter == 'income' ? '' : styles.filterSelectorInactive}
}`}
onClick={() =>
setGraphFilter('income')
}
style={{ color: 'black', position: 'relative' }}
>
<div>Pemasukan</div>
</div>
<div
className={`${styles.filterSelector} ${graphFilter == 'outcome' ? '' : styles.filterSelectorInactive}
}`}
onClick={() =>
setGraphFilter('outcome')
}
>
<div>Pengeluaran</div>
</div>
<div
className={`${styles.filterSelector} ${graphFilter == 'transactions' ? '' : styles.filterSelectorInactive}
}`}
onClick={() =>
setGraphFilter('transactions')
}
>
<div>Transaksi</div>
</div>
</div>
{filter == 'yesterday' || filter == 'weekly' ?
<DailyCharts transactionGraph={analytics?.transactionGraph || analytics?.combinedTransactionGraph} materialGraph={analytics?.materialGraph || analytics?.combinedMaterialGraph} type={filter} graphFilter={graphFilter} colors={colors} />
:
<PeriodCharts type={filter} graphFilter={graphFilter} aggregatedCurrentReports={analytics?.aggregatedCurrentReports} aggregatedPreviousReports={analytics?.aggregatedPreviousReports} colors={colors} />
}
</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>
}
<div className={`${styles.couponContainer}`}>
{forCafe && <div style={{ marginTop: '49px', marginRight: '10px' }} 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></div>}
<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>
);
};
export default App;