diff --git a/src/components/Header.js b/src/components/Header.js index c48a83e..25018aa 100644 --- a/src/components/Header.js +++ b/src/components/Header.js @@ -5,7 +5,7 @@ import { useNavigationHelpers } from "../helpers/navigationHelpers"; import Switch from "react-switch"; const HeaderBar = styled.div` - margin-top: 25px; + margin-top:${(props) => (props.HeaderMargin)}; display: flex; justify-content: space-between; align-items: center; @@ -230,6 +230,7 @@ const Header = ({ removeConnectedGuestSides, setIsEditMode, isEditMode, + HeaderMargin='25px' }) => { const { goToLogin, goToGuestSideLogin, goToAdminCafes } = useNavigationHelpers(shopId, tableCode); @@ -300,7 +301,7 @@ const Header = ({ return `${cafeName}'s menu`; }; return ( - + {shopName == null ? HeaderText == null diff --git a/src/helpers/transactionHelpers.js b/src/helpers/transactionHelpers.js index 802399e..4e68aff 100644 --- a/src/helpers/transactionHelpers.js +++ b/src/helpers/transactionHelpers.js @@ -390,9 +390,9 @@ export const getFavourite = async (cafeId) => { return response.json(); }; -export const getAnalytics = async (cafeId, filter) => { +export const getReports = async (cafeId, filter) => { const response = await fetch( - API_BASE_URL + "/transaction/get-analytics/" + cafeId + "?type=" + filter, + API_BASE_URL + "/transaction/reports/" + cafeId + "?type=" + filter, { method: "POST", headers: { @@ -402,6 +402,13 @@ export const getAnalytics = async (cafeId, filter) => { ); return response.json(); }; +export const getAnalytics = async (filter) => { + const response = await fetch( + API_BASE_URL + "/transaction/get-analytics" + "?type=" + filter, + getHeaders('POST') + ); + return response.json(); +}; export const getIncome = async (cafeId) => { const response = await fetch( diff --git a/src/helpers/userHelpers.js b/src/helpers/userHelpers.js index 941e100..62ccfe4 100644 --- a/src/helpers/userHelpers.js +++ b/src/helpers/userHelpers.js @@ -175,7 +175,7 @@ export const getAllCafeOwner = async (formData) => { const token = getLocalStorage("auth"); if (token) { try { - const response = await fetch(API_BASE_URL + "/user/get-admin", { + const response = await fetch(API_BASE_URL + "/transaction/get-analytics", { method: "POST", headers: { "Content-Type": "application/json", @@ -186,6 +186,7 @@ export const getAllCafeOwner = async (formData) => { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); + console.log(data) return data; } catch (error) { console.error("Error updating user:", error); diff --git a/src/pages/Dashboard.js b/src/pages/Dashboard.js index bd74237..6fe87d4 100644 --- a/src/pages/Dashboard.js +++ b/src/pages/Dashboard.js @@ -1,16 +1,14 @@ import React, { useState, useEffect } from 'react'; -import styles from './LinktreePage.module.css'; // Import the module.css file -import { loginUser } from "../helpers/userHelpers"; -import { ThreeDots } from "react-loader-spinner"; import { useNavigate } from "react-router-dom"; - -import { getLocalStorage, removeLocalStorage } from "../helpers/localStorageHelpers"; -import { getAllCafeOwner, createCafeOwner } from "../helpers/userHelpers"; +import styles from './LinktreePage.module.css'; +import { loginUser, getAllCafeOwner, createCafeOwner } from "../helpers/userHelpers"; import { getOwnedCafes, createCafe, updateCafe } from "../helpers/cafeHelpers"; import { getMyTransactions } from "../helpers/transactionHelpers"; import { unsubscribeUser } from "../helpers/subscribeHelpers.js"; - +import { getLocalStorage, removeLocalStorage } from "../helpers/localStorageHelpers"; +import { ThreeDots } from "react-loader-spinner"; import Header from '../components/Header'; +import CircularDiagram from "./CircularDiagram"; const LinktreePage = ({ user, setModal }) => { const navigate = useNavigate(); @@ -19,203 +17,238 @@ const LinktreePage = ({ user, setModal }) => { const [password, setPassword] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(false); - const [items, setItems] = useState([]); const [isCreating, setIsCreating] = useState(false); const [newItem, setNewItem] = useState({ name: "", type: "" }); const [isModalOpen, setIsModalOpen] = useState(false); const [expanded, setIsExpand] = useState(false); - const [expandedCafeId, setExpandedCafeId] = useState(null); // Track which cafe is expanded + const [expandedCafeId, setExpandedCafeId] = useState(null); + const [selectedItemId, setSelectedItemId] = useState(0); + // Handle expand/collapse of cafe details const handleToggleExpand = (cafeId) => { - setExpandedCafeId(expandedCafeId === cafeId ? null : cafeId); // Toggle expand for a specific cafe + setExpandedCafeId(expandedCafeId === cafeId ? null : cafeId); }; + // Handle user transactions const handleMyTransactions = async () => { try { setError(false); setLoading(true); const response = await getMyTransactions(); - if (response) { - console.log(response) - return response; + setItems(response); } else { - setError(true); // Trigger error state in the button - console.error('Login failed'); + setError(true); } } catch (error) { setError(true); - console.error('Error occurred while logging in:', error.message); } finally { - setLoading(false); // Ensure loading state is cleared + setLoading(false); + console.log(items); } }; + // Handle login const handleLogin = async () => { try { setError(false); setLoading(true); const response = await loginUser(username, password); - if (response.success) { localStorage.setItem('auth', response.token); - - if (response.cafeId !== null) { - window.location.href = response.cafeId; - } else { - let destination = '/'; - window.location.href = destination; - } + window.location.href = response.cafeId ? `/${response.cafeId}` : '/'; } else { - setError(true); // Trigger error state in the button - console.error('Login failed'); + setError(true); } } catch (error) { setError(true); - console.error('Error occurred while logging in:', error.message); } finally { - setLoading(false); // Ensure loading state is cleared + setLoading(false); } }; - const handleModalClose = () => { - setIsModalOpen(false); - }; - + // Handle logout const handleLogout = () => { removeLocalStorage("auth"); unsubscribeUser(); navigate(0); }; + // Fetch data when the user changes useEffect(() => { - if (user && user.roleId === 0) { + if (user) { setLoading(true); - getAllCafeOwner() - .then((data) => { - setItems(data); + switch (user.roleId) { + case 0: + getAllCafeOwner().then(setItems).catch(console.error).finally(() => setLoading(false)); + break; + case 1: + getOwnedCafes(user.userId).then(setItems).catch(console.error).finally(() => setLoading(false)); + break; + case 3: + handleMyTransactions(); + break; + default: setLoading(false); - }) - .catch((error) => { - console.error("Error fetching cafe owners:", error); - setLoading(false); - }); + break; + } } - if (user && user.roleId === 1) { - setLoading(true); - getOwnedCafes(user.userId) - .then((data) => { - setItems(data); - setLoading(false); - }) - .catch((error) => { - console.error("Error fetching owned cafes:", error); - setLoading(false); - }); - } - if (user && user.roleId == 3) { - handleMyTransactions() - .then((data) => { - setItems(data); - setLoading(false); - }) - .catch((error) => { - console.error("Error fetching owned cafes:", error); - setLoading(false); - }); - } + console.log(items); }, [user]); - const handleCreateItem = () => { - if (user.roleId < 1) { - // Create admin functionality - createCafeOwner(newItem.email, newItem.username, newItem.password) - .then((newitem) => { - setItems([...items, { userId: newitem.userId, name: newitem.username }]); - setIsCreating(false); - setNewItem({ name: "", type: "" }); - }) - .catch((error) => { - console.error("Error creating admin:", error); - }); + + // Handle create item (admin or cafe owner) + const handleCreateItem = async () => { + try { + if (user.roleId < 1) { + const newOwner = await createCafeOwner(newItem.email, newItem.username, newItem.password); + setItems([...items, { userId: newOwner.userId, name: newOwner.username }]); + } else { + const newCafe = await createCafe(newItem.name); + setItems([...items, { cafeId: newCafe.cafeId, name: newCafe.name }]); + } + setIsCreating(false); + setNewItem({ name: "", type: "" }); + } catch (error) { + console.error("Error creating item:", error); + } + }; + + 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 { - // Create cafe functionality - createCafe(newItem.name) - .then((newitem) => { - setItems([...items, { cafeId: newitem.cafeId, name: newitem.name }]); - setIsCreating(false); - setNewItem({ name: "", type: "" }); - }) - .catch((error) => { - console.error("Error creating cafe:", error); - }); + // Less than a thousand + if (amount != null) return amount.toString(); } }; return ( - <div className={styles.linktreePage}> - {/* SVG Icon */} + <> + {user && user.roleId < 2 ? ( + <div className={styles.nonCenteredLinktreePage}> - {user && user.roleId < 2 && ( - <div className={styles.dashboard}> - {loading && <ThreeDots />} - {items.map((item, index) => ( - <div - key={index} - onClick={() => navigate("/" + item.cafeId)} - className={styles.rectangle} - > - <h1>{item.name || item.username}</h1> - <div><h1>{item.report?.totalIncome}</h1></div> + <div className={styles.dashboard}> + <div className={styles.header}> + + <Header + HeaderText={"selamat siang " + user.username} + isEdit={() => setIsModalOpen(true)} + isLogout={handleLogout} + user={user} + showProfile={true} + setModal={setModal} + HeaderMargin='0px' + /> </div> - ))} - {user && user.roleId < 1 ? ( - <div - className={styles.rectangle} - onClick={() => setIsCreating(true)} - > - Create Client + <div className={styles.headerCardWrapper}> + <div className={styles.headerCard}> + <h1>Total pemasukan</h1> + </div> </div> - ) : ( - <div - className={styles.rectangle} - onClick={() => setIsCreating(true)} - > - + + + </div> + + <div style={{ flex: 1 }}> + <CircularDiagram segments={[]} /> + </div> + + <div style={{ flex: 1 }}> + <CircularDiagram segments={[]} /> + </div> + <div className={styles.cafeListWrapper}> + <div className={styles.cafeListHeader}> + Semua {user.roleId < 1 ? 'penyewa' : 'kedai yang dikau miliki'} </div> - )} + <div className={styles.cafeList}> + </div> + + {user.roleId < 1 && + <div className={styles.rectangle} + style={{ backgroundColor: selectedItemId == -1 ? 'rgb(69, 69, 69)' : 'rgb(114, 114, 114)' }} + onClick={() => setSelectedItemId(selectedItemId == 0 ? -1 : 0)} + > + Tambah penyewa + </div> + } + { + items && Array.isArray(items.tenants) && items.tenants.length > 0 ? ( + items.tenants.map((tenant, tenantIndex) => ( + <> + <div key={tenant.userId} onClick={() => setSelectedItemId(selectedItemId == tenant.userId ? 0 : tenant.userId)} style={{ backgroundColor: selectedItemId == tenant.userId ? 'rgb(69, 69, 69)' : 'rgb(114, 114, 114)' }} className={styles.rectangle}> + <h3>{tenant.username}</h3> + <div> + Total Pendapatan: {formatIncome(tenant.totalIncome)} {/* Total Income for the tenant */} + </div> + + </div> + + {selectedItemId == tenant.userId && tenant.cafes && tenant.cafes.length > 0 && tenant.cafes.map((cafe, cafeIndex) => ( + <div> + <h5 >↳</h5> + <div key={cafe.cafeId} className={styles.subRectangle} + onClick={() => setSelectedItemId(selectedItemId === cafe.cafeId ? -1 : cafe.cafeId)} + style={{ + backgroundColor: selectedItemId === cafe.cafeId ? 'rgb(69, 69, 69)' : 'rgb(114, 114, 114)' + }} + > + {cafe.name} +  pendapatan {formatIncome(cafe.report?.totalIncome || 0)} + </div> + </div> + ))} + </> + )) + ) : ( + <div>No tenants available</div> + ) + } + + {user.roleId > 0 && + <div className={styles.rectangle} + + style={{ backgroundColor: selectedItemId == 0 ? 'rgb(69, 69, 69)' : 'rgb(114, 114, 114)' }} + onClick={() => { setSelectedItemId(selectedItemId == 0 ? -1 : 0); setModal('create-cafe'); }} + > + Tambah kedai + </div> + } + </div> </div> - )} - {(user.length == 0 || user.roleId > 1) && - <> + ) : ( + <div className={styles.centeredLinktreePage}> <div className={styles.dashboardLine}></div> - <div className={styles.dashboardContainer}> - {/* Main Heading */} <div className={styles.mainHeading}> COBA KEDAIMASTER <div className={styles.swipeContainer}> <div className={styles.swipeContent}> - <div className={styles.swipeItem}>pemesanan langsung dari meja</div> - <div className={styles.swipeItem}>pengelolaan pesanan dan keuangan</div> - <div className={styles.swipeItem}>tentukan suasana musik</div> - <div className={styles.swipeItem}>pengelolaan stok dan manajemen</div> - <div className={styles.swipeItem}>jangan pernah ragukan pelanggan</div> + {['pemesanan langsung dari meja', 'pengelolaan pesanan dan keuangan', 'tentukan suasana musik', 'pengelolaan stok dan manajemen', 'jangan pernah ragukan pelanggan'].map((item, index) => ( + <div key={index} className={styles.swipeItem}>{item}</div> + ))} </div> </div> diskon 0% </div> - {/* Sub Heading */} <div className={styles.subHeading}> Solusi berbasis web untuk memudahkan pengelolaan kedai, dengan fitur yang mempermudah pemilik, kasir, dan tamu berinteraksi. </div> - {getLocalStorage('auth') == null && + + {getLocalStorage('auth') == null && ( <div className={styles.LoginForm}> <div className={`${styles.FormUsername} ${inputtingPassword ? styles.animateForm : styles.reverseForm}`}> - <label htmlFor="username" className={styles.usernameLabel}> - ---- masuk ------------------------------- - </label> + <label htmlFor="username" className={styles.usernameLabel}>---- masuk -------------------------------</label> <input id="username" placeholder="username" @@ -231,13 +264,12 @@ const LinktreePage = ({ user, setModal }) => { <div className={`${styles.FormPassword} ${inputtingPassword ? styles.animateForm : styles.reverseForm}`}> <span> - <label onClick={() => setInputtingPassword(false)} htmlFor="password" className={styles.usernameLabel}> - <--- <-- kembali - </label><label htmlFor="password" className={styles.usernameLabel}> -   ------   - </label><label onClick={() => setModal('reset-password', { username: username })} htmlFor="password" className={styles.usernameLabel}> + <label onClick={() => setInputtingPassword(false)} htmlFor="password" className={styles.usernameLabel}> <--- <-- kembali </label> + <label htmlFor="password" className={styles.usernameLabel}>   -----   </label> + <label onClick={() => setModal('reset-password', { username: username })} className={styles.usernameLabel}> lupa password? - - </label></span> + </label> + </span> <input id="password" placeholder="password" @@ -256,133 +288,34 @@ const LinktreePage = ({ user, setModal }) => { </button> </div> </div> + )} - } - {/* Footer Links */} <div className={styles.footer}> <div className={styles.footerLinks}> - <a - href="https://linktr.ee/discover/trending" - target="_blank" - rel="noreferrer" - className={styles.footerLink} - > - Pelajari lebih lanjut - </a> - <a - href="https://linktr.ee" - target="_blank" - rel="noreferrer" - className={styles.footerLink} - > - Tentang kedaimaster.com - </a> + <a href="https://linktr.ee/discover/trending" target="_blank" rel="noreferrer" className={styles.footerLink}>Pelajari lebih lanjut</a> + <a href="https://linktr.ee" target="_blank" rel="noreferrer" className={styles.footerLink}>Tentang kedaimaster.com</a> <a target="_blank" rel="noreferrer" className={styles.signupButton} - style={{ textDecoration: 'none' }} onClick={() => setModal('join')} > Daftarkan kedaimu </a> </div> <div className={styles.footerImage}> - <img - style={{ height: '226px', width: '150px', objectFit: 'cover' }} - src="/kedai.png" - alt="Linktree visual" - onError={(e) => e.target.src = '/fallback-image.png'} - /> - </div> - </div> - {user.length != 0 && - <div className={` ${expanded ? styles.userInfoExpanded : styles.userInfo}`}> - <div onClick={() => setIsExpand(!expanded)} className={styles.userInfoExpandButton}> - {!expanded ? - <svg - viewBox="0 0 16 16" - xmlns="http://www.w3.org/2000/svg" - fill="#000000" - style={{ width: '100%', height: '100%' }} // Ensure SVG fits the div - > - <g id="SVGRepo_bgCarrier" strokeWidth="0"></g> - <g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g> - <g id="SVGRepo_iconCarrier"> - <path d="m 1 11 c 0 -0.265625 0.105469 -0.519531 0.292969 -0.707031 l 6 -6 c 0.390625 -0.390625 1.023437 -0.390625 1.414062 0 l 6 6 c 0.1875 0.1875 0.292969 0.441406 0.292969 0.707031 s -0.105469 0.519531 -0.292969 0.707031 c -0.390625 0.390625 -1.023437 0.390625 -1.414062 0 l -5.292969 -5.292969 l -5.292969 5.292969 c -0.390625 0.390625 -1.023437 0.390625 -1.414062 0 c -0.1875 -0.1875 -0.292969 -0.441406 -0.292969 -0.707031 z m 0 0" fill={"#2e3436"}></path> - </g> - </svg> - : - - <svg - viewBox="0 0 16 16" - xmlns="http://www.w3.org/2000/svg" - fill="#000000" - style={{ width: '100%', height: '100%' }} // Ensure SVG fits the div - > - <g id="SVGRepo_bgCarrier" strokeWidth="0"></g> - <g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g> - <g id="SVGRepo_iconCarrier"> - <path d="m 1 5 c 0 -0.265625 0.105469 -0.519531 0.292969 -0.707031 c 0.390625 -0.390625 1.023437 -0.390625 1.414062 0 l 5.292969 5.292969 l 5.292969 -5.292969 c 0.390625 -0.390625 1.023437 -0.390625 1.414062 0 c 0.1875 0.1875 0.292969 0.441406 0.292969 0.707031 s -0.105469 0.519531 -0.292969 0.707031 l -6 6 c -0.390625 0.390625 -1.023437 0.390625 -1.414062 0 l -6 -6 c -0.1875 -0.1875 -0.292969 -0.441406 -0.292969 -0.707031 z m 0 0" fill={"#2e3436"}></path> - </g> - </svg> - } - </div> - <Header - HeaderText={"Kedai yang pernah kamu kunjungi"} - HeaderSize={'4vw'} - showProfile={true} - setModal={setModal} - isLogout={handleLogout} - user={user} + <img + style={{ height: '226px', width: '150px', objectFit: 'cover' }} + src="/kedai.png" + alt="Linktree visual" + onError={(e) => e.target.src = '/fallback-image.png'} /> - <div className={styles.ItemContainer} style={{height: expanded?'85%':'43.5%'}}> - - {items.map((item, index) => ( - <div className={styles.Item}onClick={() => handleToggleExpand(item.cafeId)} key={index}> - {/* Render cafes */} - <div - // Toggle expansion on cafe click - className={styles.rectangle} - > - <h1>{item.name || item.username}</h1> </div> - - {/* Render transactions if the cafe is expanded */} - {expandedCafeId === item.cafeId && item.transactions && ( - <div className={styles.Item}> - {item.transactions.map((transaction, transactionIndex) => ( - <div - key={transactionIndex} - className={styles.transaction} - style={{ backgroundColor: 'orange' }} - > - {transaction.detailedTransactions && transaction.detailedTransactions.map((detailedTransaction, detailedTransactionIndex) => ( - <div key={detailedTransactionIndex}> - <p>Quantity: {detailedTransaction.qty}</p> - {detailedTransaction.Item && ( - <div> - <h4>Item Name: {detailedTransaction.Item.name}</h4> - <p>Price: {detailedTransaction.Item.price}</p> - </div> - )} - </div> - ))} - </div> - ))} - </div> - )} </div> - ))} - - - </div> - </div> - } </div> - </> - } - </div> + </div> + )} + </> ); }; diff --git a/src/pages/LinktreePage.module.css b/src/pages/LinktreePage.module.css index a1c5d24..6ed83a4 100644 --- a/src/pages/LinktreePage.module.css +++ b/src/pages/LinktreePage.module.css @@ -1,5 +1,5 @@ /* General container */ -.linktreePage { +.centeredLinktreePage { height: 100vh; display: flex; flex-direction: column; @@ -7,6 +7,14 @@ background-color: rgb(210, 232, 35); } +.nonCenteredLinktreePage { + height: 100vh; + display: flex; + flex-direction: column; + justify-content: start; + background-color: rgb(193 201 134); +} + .dashboardLine { position: fixed; left: 0px; @@ -319,11 +327,90 @@ .rectangle { - height: 150px; /* Height of each rectangle */ + height: 50px; /* Height of each rectangle */ display: flex; justify-content: center; align-items: center; border-radius: 10px; /* Rounded corners */ - font-size: 24px; + font-size: 20px; background-color: rgb(114, 114, 114); + margin: 5%; + position: relative; + font-weight: 500; +} +.subRectangle { + height: 50px; /* Height of each rectangle */ + display: flex; + justify-content: center; + align-items: center; + border-radius: 10px; /* Rounded corners */ + font-size: 20px; + background-color: rgb(114, 114, 114); + margin: 5%; + position: relative; + font-weight: 500; + margin-left: 60px; +} + + +.header { + background-color: white; + top: 0; + width: 100%; + height: calc(25vh - 25px); + margin-bottom: 15vh; + padding-top: 25px; +} + +.headerCardWrapper { + top: 0; + position: absolute; + width: 100%; + bottom: 50vh; + justify-content: center; + display: flex; + align-items: center; +} +.headerCard { + background-color: #947257; + position: relative; + width: 90%; + border-radius: 20px; + height: 40%; +} +.cafeListWrapper{ + background-color: white; + border-radius: 20px 20px 0 0; + bottom: 0; + position: absolute; + width: 100%; + max-height: 164.5px; +} + +.cafeListHeader{ + background-color: white; + border-radius: 20px 20px 0 0; + bottom: 0; + position: absolute; + width: 100%; + height: 100%; + top: -45px; + background-color: #b09c72; + text-align: center; + padding-top: 10px; + font-weight: 500; +} +.cafeList{ + background-color: white; + border-radius: 20px 20px 0 0; + bottom: 0; + position: absolute; + width: 100%; + height: 100%; +} + +.itemInput{ + width: 50%; + height: 60%; + border-radius: 14px; } \ No newline at end of file diff --git a/src/pages/Reports.js b/src/pages/Reports.js index bbdd279..eb84117 100644 --- a/src/pages/Reports.js +++ b/src/pages/Reports.js @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { ThreeDots } from "react-loader-spinner"; import { getFavourite, - getAnalytics, + getReports, getIncome, } from "../helpers/transactionHelpers.js"; import CircularDiagram from "./CircularDiagram"; @@ -115,7 +115,7 @@ const App = ({ cafeId, try { setLoading(true); // Fetch the analytics data with the selected filter - const analyticsData = await getAnalytics(cafeId, filter); + const analyticsData = await getReports(cafeId, filter); console.log(analyticsData); if (analyticsData) setAnalytics(analyticsData); } catch (error) {