This commit is contained in:
client perkafean
2024-08-26 06:34:56 +00:00
parent 696ac38e33
commit 3a2e388a33
16 changed files with 1162 additions and 43 deletions

View File

@@ -114,7 +114,7 @@ export default function Footer({
</span>
{table.length == 0 && (
<img
src="https://static-00.iconduck.com/assets.00/qr-scan-icon-2048x2048-aeh36n7y.png"
src="https://i.ibb.co.com/Pt2s8N8/qr-scan-icon-2048x2048-aeh36n7y.png"
alt="QR Code"
className={styles.qrIcon}
/>

View File

@@ -1,3 +1,6 @@
.footerContainer {
margin-top: 40px;
}
.footer-rect {
height: 75px;
background-color: #fff;

View File

@@ -284,7 +284,7 @@ const Header = ({
<Title>{HeaderText}</Title>
<div style={{ visibility: showProfile ? "visible" : "hidden" }}>
<ProfileImage
src="https://static-00.iconduck.com/assets.00/profile-major-icon-1024x1024-9rtgyx30.png"
src="https://i.ibb.co.com/fpg1v8J/profile-major-icon-1024x1024-9rtgyx30.png"
alt="Profile"
onClick={handleImageClick}
animate={showRectangle && animate}
@@ -349,6 +349,8 @@ const Header = ({
<Child onClick={() => setModal("payment_option")}>
payment options
</Child>
<Child onClick={() => setModal("reports")}>reports</Child>
</Child>
</>
)}
@@ -385,6 +387,8 @@ const Header = ({
))}
</Child>
)}
<Child onClick={() => setModal("reports")}>reports</Child>
</Child>
)}
{user.username !== undefined && (

View File

@@ -10,6 +10,7 @@ import Transaction_success from "../pages/Transaction_success";
import Transaction_failed from "../pages/Transaction_failed";
import MaterialList from "../pages/MaterialList.js";
import MaterialMutationsPage from "../pages/MaterialMutationsPage.js";
import Reports from "../pages/Reports.js";
const Modal = ({ shop, isOpen, onClose, modalContent }) => {
if (!isOpen) return null;
@@ -51,6 +52,9 @@ const Modal = ({ shop, isOpen, onClose, modalContent }) => {
{modalContent === "update_stock" && (
<MaterialMutationsPage cafeId={shop.cafeId} />
)}
{modalContent === "reports" && (
<Reports cafeId={shop.cafeId} />
)}
</div>
</div>
);

View File

@@ -90,8 +90,8 @@
}
.expandable-container.expanded {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
position: relative;
max-height: 400px;
/* Adjust the max-height as needed */
@@ -101,12 +101,11 @@
.expand-button {
font-size: 20px;
padding-bottom: 10px;
position: relative;
left: 0;
right: 0;
height: 25px;
background-color: rgb(108, 255, 128);
/* background-color: rgb(218 163 99); */
border-radius: 0 0 15px 15px;
/* Rounded corners at the bottom */
cursor: pointer;
@@ -116,7 +115,9 @@
}
.expand-button h5 {
font-weight: 500;
margin-top: 0px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.36);
}
.expand-button:hover {

View File

@@ -1,5 +1,5 @@
// src/config.js
const API_BASE_URL = "https://2jtmkn-5000.csb.app"; // Replace with your actual backend URL
const API_BASE_URL = "https://8h8rx8-5000.csb.app"; // Replace with your actual backend URL
export default API_BASE_URL;

View File

@@ -28,7 +28,7 @@ export async function getOwnedCafes(userId) {
}
}
export async function createCafe(userId) {
export async function createCafe(cafeName) {
try {
const response = await fetch(`${API_BASE_URL}/cafe/create-cafe`, {
method: "POST",
@@ -36,7 +36,8 @@ export async function createCafe(userId) {
"Content-Type": "application/json",
Authorization: `Bearer ${getAuthToken()}`,
},
body: JSON.stringify({ ownerId: userId }),
body: JSON.stringify({
name: cafeName }),
});
if (!response.ok) {

View File

@@ -2,21 +2,16 @@ import API_BASE_URL from "../config.js";
// Function to retrieve the authentication token from localStorage
function getAuthToken() {
if (localStorage.getItem("auth")) return localStorage.getItem("auth");
return null;
return localStorage.getItem("auth") || null;
}
// Helper function to get headers with authentication token
const getHeaders = (method = "GET", body = null) => {
const headers = {
Authorization: `Bearer ${getAuthToken()}`,
"Content-Type": "application/json",
};
const getHeaders = () => {
return {
method,
headers,
body: body ? JSON.stringify(body) : null,
headers: {
Authorization: `Bearer ${getAuthToken()}`,
"Content-Type": "application/json",
},
};
};
@@ -25,7 +20,14 @@ export const createMaterialMutation = async (materialId, data) => {
try {
const response = await fetch(
`${API_BASE_URL}/mutation/create/${materialId}`,
getHeaders("POST", data)
{
...getHeaders(),
method: "POST",
body: JSON.stringify({
newStock: data.get("newStock"),
reason: data.get("reason"),
}),
}
);
if (!response.ok) throw new Error("Failed to create material mutation.");
return await response.json();
@@ -40,7 +42,10 @@ export const getMaterialMutations = async (cafeId) => {
try {
const response = await fetch(
`${API_BASE_URL}/mutation/get-material-mutations/${cafeId}`,
getHeaders()
{
...getHeaders(),
method: "GET",
}
);
if (!response.ok) throw new Error("Failed to retrieve material mutations.");
return await response.json();
@@ -55,7 +60,10 @@ export const getMaterialMutationById = async (mutationId) => {
try {
const response = await fetch(
`${API_BASE_URL}/mutation/get-material-mutation/${mutationId}`,
getHeaders()
{
...getHeaders(),
method: "GET",
}
);
if (!response.ok) throw new Error("Failed to retrieve material mutation.");
return await response.json();
@@ -70,7 +78,10 @@ export const getMaterialMutationsByMaterialId = async (materialId) => {
try {
const response = await fetch(
`${API_BASE_URL}/mutation/get-material-mutations-by-material/${materialId}`,
getHeaders()
{
...getHeaders(),
method: "GET",
}
);
if (!response.ok)
throw new Error("Failed to retrieve material mutations by material ID.");

View File

@@ -288,3 +288,39 @@ export const handlePaymentFromGuestDevice = async (
return false;
}
};
// Function to retrieve the authentication token from localStorage
function getAuthToken() {
return localStorage.getItem("auth");
}
// Helper function to get headers with authentication token
const getHeaders = (method = "GET") => {
const headers = {
Authorization: `Bearer ${getAuthToken()}`,
};
// No content-type header needed for FormData; fetch will handle it automatically
if (method !== "POST" && method !== "PUT") {
return { method, headers };
}
return {
method,
headers,
};
};
export const getFavourite = async (cafeId) => {
const response = await fetch(
`${API_BASE_URL}/transaction/get-favourite/${cafeId}`,
getHeaders()
);
return response.json();
};
export const getAnalytics = async (cafeId) => {
const response = await fetch(
`${API_BASE_URL}/transaction/get-analytics/${cafeId}`,
getHeaders()
);
return response.json();
};

View File

@@ -0,0 +1,62 @@
import React from "react";
const CircularDiagram = ({ segments }) => {
const radius = 100; // Radius of the circle
const strokeWidth = 30; // Width of each portion
const circumference = 2 * Math.PI * (radius - strokeWidth / 2);
// Calculate total value to be displayed
const totalSold = segments.reduce((sum, segment) => sum + segment.value, 0);
let startOffset = 0; // Initial offset for each segment
const svgStyles = {
display: "block",
margin: "0 auto",
};
return (
<svg
width={radius * 2}
height={radius * 2}
viewBox={`0 0 ${radius * 2} ${radius * 2}`}
style={svgStyles}
>
<circle
cx={radius}
cy={radius}
r={radius - strokeWidth / 2}
stroke="#eee"
strokeWidth={strokeWidth}
fill="none"
/>
{segments.map((segment, index) => {
const { value, color } = segment;
const segmentLength = (circumference * value) / totalSold;
const strokeDashoffset = circumference - startOffset;
startOffset += segmentLength;
return (
<circle
key={index}
cx={radius}
cy={radius}
r={radius - strokeWidth / 2}
stroke={color}
strokeWidth={strokeWidth}
fill="none"
strokeDasharray={`${segmentLength} ${
circumference - segmentLength
}`}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round" // Rounds the edges of each segment
transform={`rotate(-90 ${radius} ${radius})`}
/>
);
})}
</svg>
);
};
export default CircularDiagram;

View File

@@ -113,7 +113,7 @@ const Dashboard = ({ user, setModal }) => {
className={styles.rectangle}
onClick={() => setIsCreating(true)}
>
Create Cafe
+
</div>
)}
</div>

View File

@@ -23,6 +23,7 @@ const MaterialList = ({ cafeId }) => {
const [selectedMaterialId, setSelectedMaterialId] = useState(null);
const [currentQuantity, setCurrentQuantity] = useState(0);
const [quantityChange, setQuantityChange] = useState(0);
const [sortOrder, setSortOrder] = useState("desc");
useEffect(() => {
const fetchMaterials = async () => {
@@ -63,7 +64,7 @@ const MaterialList = ({ cafeId }) => {
if (materialMutations.length > 0) {
const latestMutation = materialMutations.reduce(
(latest, current) =>
new Date(current.timestamp) > new Date(latest.timestamp)
new Date(current.createdAt) > new Date(latest.createdAt)
? current
: latest,
materialMutations[0]
@@ -75,10 +76,24 @@ const MaterialList = ({ cafeId }) => {
}
}, [selectedMaterialId, mutations]);
const handleSortChange = (e) => {
setSortOrder(e.target.value);
};
const filteredMutations = selectedMaterialId
? mutations.filter((mutation) => mutation.materialId === selectedMaterialId)
: [];
const sortedMutations = filteredMutations
.filter((mutation) => mutation.materialId === selectedMaterialId)
.sort((a, b) => {
if (sortOrder === "asc") {
return new Date(a.createdAt) - new Date(b.createdAt);
} else {
return new Date(b.createdAt) - new Date(a.createdAt);
}
});
const handleCreateMaterial = async (e) => {
e.preventDefault();
setLoading(true);
@@ -166,11 +181,10 @@ const MaterialList = ({ cafeId }) => {
try {
const newStock = currentQuantity + quantityChange;
const formData = new FormData();
formData.append("materialId", selectedMaterialId);
formData.append("newStock", newStock);
formData.append("reason", "Stock update");
await createMaterialMutation(cafeId, formData);
await createMaterialMutation(selectedMaterialId, formData);
setQuantityChange(0);
const updatedMutations = await getMaterialMutations(cafeId);
setMutations(updatedMutations);
@@ -188,7 +202,11 @@ const MaterialList = ({ cafeId }) => {
const currentMaterial = materials.find(
(material) => material.materialId === selectedMaterialId
);
const formatDate = (timestamp) => {
const date = new Date(timestamp);
return date.toLocaleString();
};
return (
<div style={styles.container}>
<h1 style={styles.heading}>Materials List</h1>
@@ -299,7 +317,7 @@ const MaterialList = ({ cafeId }) => {
-
</button>
<button style={styles.quantityDisplay}>
{currentQuantity + quantityChange}
{currentQuantity + quantityChange}
</button>
<button
onClick={() => handleQuantityChange(1)}
@@ -346,15 +364,27 @@ const MaterialList = ({ cafeId }) => {
</div>
)}
<div style={styles.sortContainer}>
<label htmlFor="sortOrder">Sort by : </label>
<select
id="sortOrder"
value={sortOrder}
onChange={handleSortChange}
style={styles.sortSelect}
>
<option value="desc">latest</option>
<option value="asc">oldest</option>
</select>
</div>
{selectedMaterialId && !loading && (
<div style={styles.mutationContainer}>
{filteredMutations.length > 0 ? (
filteredMutations.map((mutation) => (
{sortedMutations.length > 0 ? (
sortedMutations.map((mutation) => (
<div key={mutation.id} style={styles.mutationCard}>
<h4 style={styles.mutationTitle}>Mutation ID: {mutation.id}</h4>
<h4 style={styles.mutationTitle}>{formatDate(mutation.createdAt)}</h4>
<p>Details: {mutation.reason}</p>
<p>dari {mutation.oldStock}</p>
<p>ke {mutation.newStock}</p>
<p>stok {mutation.newStock}</p>
</div>
))
) : (

259
src/pages/Reports.js Normal file
View File

@@ -0,0 +1,259 @@
import React, { useEffect, useState } from "react";
import { ThreeDots } from "react-loader-spinner";
import { getFavourite, getAnalytics } from "../helpers/transactionHelpers.js";
import CircularDiagram from "./CircularDiagram";
import styles from "./Transactions.module.css";
const RoundedRectangle = ({ bgColor, title, value, percentage }) => {
const containerStyle = {
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
justifyContent: "center",
width: "calc(100% / 2 - 10px)",
maxWidth: "250px",
height: "auto",
borderRadius: "15px",
padding: "20px",
margin: "5px",
textAlign: "left",
fontFamily: "Arial, sans-serif",
boxSizing: "border-box",
};
const titleStyle = {
fontWeight: "bold",
marginBottom: "10px",
width: "100%",
};
const valueAndPercentageContainerStyle = {
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
width: "100%",
};
const valueStyle = {
fontSize: "20px",
fontWeight: "bold",
flex: "1",
textAlign: "left",
};
const percentageStyle = {
fontSize: "16px",
display: "flex",
alignItems: "center",
textAlign: "right",
};
const arrowStyle = {
marginLeft: "5px",
};
return (
<div
style={{
...containerStyle,
backgroundColor: bgColor,
}}
>
<div style={titleStyle}>{title}</div>
<div style={valueAndPercentageContainerStyle}>
<div style={valueStyle}>{value}</div>
<div
style={{
...percentageStyle,
color: percentage > 0 ? "#007bff" : "red",
}}
>
{percentage}%
<span style={arrowStyle}>{percentage > 0 ? "↗" : "↘"}</span>
</div>
</div>
</div>
);
};
const App = ({ cafeId }) => {
const [favouriteItems, setFavouriteItems] = useState([]);
const [analytics, setAnalytics] = useState({});
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState("monthly"); // Default filter is 'monthly'
const [colors, setColors] = useState([]);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const items = await getFavourite(cafeId);
const analyticsData = await getAnalytics(cafeId);
if (items) setFavouriteItems(items);
if (analyticsData) setAnalytics(analyticsData);
} catch (error) {
console.error("Error fetching data:", error);
} finally {
setLoading(false);
}
};
fetchData();
}, [cafeId]);
useEffect(() => {
const getRandomColor = () => {
const letters = "0123456789ABCDEF";
let color = "#";
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
const r = parseInt(color.substring(1, 3), 16);
const g = parseInt(color.substring(3, 5), 16);
const b = parseInt(color.substring(5, 7), 16);
return `rgba(${r}, ${g}, ${b}, 1)`;
};
const colors = favouriteItems[filter]?.map(() => getRandomColor()) || [];
setColors(colors);
}, [favouriteItems, filter]);
if (loading)
return (
<div className="Loader">
<div className="LoaderChild">
<ThreeDots />
</div>
</div>
);
const [sold, percentage] = analytics[filter] || [0, 0];
const filteredItems = favouriteItems[filter] || [];
const totalSold = filteredItems.reduce((sum, item) => sum + item.sold, 0);
const segments = filteredItems.map((item, index) => ({
value: item.sold,
color: colors[index] || "#cccccc",
}));
return (
<div className={styles.Transactions} style={{ backgroundColor: "#cfcfcf" }}>
<h2 className={styles["Transactions-title"]}>Reports</h2>
<div style={{ marginTop: "30px", textAlign: "center" }}>
<div>
<label>
<input
type="radio"
name="filter"
value="daily"
checked={filter === "daily"}
onChange={() => setFilter("daily")}
/>
Daily
</label>
<label style={{ marginLeft: "10px" }}>
<input
type="radio"
name="filter"
value="weekly"
checked={filter === "weekly"}
onChange={() => setFilter("weekly")}
/>
Weekly
</label>
<label style={{ marginLeft: "10px" }}>
<input
type="radio"
name="filter"
value="monthly"
checked={filter === "monthly"}
onChange={() => setFilter("monthly")}
/>
Monthly
</label>
<label style={{ marginLeft: "10px" }}>
<input
type="radio"
name="filter"
value="yearly"
checked={filter === "yearly"}
onChange={() => setFilter("yearly")}
/>
Yearly
</label>
</div>
<div
style={{
display: "flex",
flexWrap: "wrap",
justifyContent: "center",
marginTop: "20px",
padding: "20px",
}}
>
<RoundedRectangle
bgColor="#E3F5FF"
title="Transactions"
value={sold}
percentage={percentage}
/>
{filteredItems[0]?.Item != undefined && (
<RoundedRectangle
bgColor="#E5ECF6"
title={filteredItems[0]?.Item.name}
value={filteredItems[0]?.sold}
percentage={filteredItems[0]?.percentageByPreviousPeriod}
/>
)}
{filteredItems[0]?.Item == undefined && (
<RoundedRectangle bgColor="#E5ECF6" value={"No item"} />
)}
<RoundedRectangle
bgColor="#E5ECF6"
title="Transactions"
value={sold}
percentage={percentage}
/>
<RoundedRectangle
bgColor="#E3F5FF"
title="Transactions"
value={sold}
percentage={percentage}
/>
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "20px",
}}
>
<div style={{ flex: 1 }}>
<CircularDiagram segments={segments} />
</div>
<div style={{ flex: 1, marginLeft: "20px" }}>
{filteredItems.map((item, index) => (
<h6
key={item.itemId}
style={{
textAlign: "left",
color: colors[index],
margin: "5px 0",
}}
>
{item.Item.name}: {item.sold}
</h6>
))}
</div>
</div>
</div>
</div>
);
};
export default App;

View File

@@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom";
import { QrReader } from "react-qr-reader"; // Import QrReader as named import
import styles from "./GuestSideLogin.module.css"; // Import module CSS file for styles
import { getLocalStorage } from "../helpers/localStorageHelpers";
import { getTableByCode } from "../helpers/tableHelper";
const GuestSideLogin = ({ socket }) => {
const navigate = useNavigate();
@@ -15,9 +15,8 @@ const GuestSideLogin = ({ socket }) => {
navigate("/" + shopId);
});
const setLoginGuestSide = () => {
const token = getLocalStorage("auth");
socket.emit("read_qrCode", { qrCode, token });
const setLoginGuestSide = async () => {
window.location.href = qrCode;
};
// Function to handle QR code scan
@@ -39,7 +38,7 @@ const GuestSideLogin = ({ socket }) => {
};
useEffect(() => {
if (qrCode.length === 11) {
if (qrCode.length === 40) {
const timer = setTimeout(() => {
setLoginGuestSide();
}, 1000); // Delay of 1 second (1000 milliseconds)