This commit is contained in:
frontend perkafean
2024-09-10 09:21:16 +00:00
parent 762bee40bb
commit f46639e05c
10 changed files with 768 additions and 337 deletions

View File

@@ -343,6 +343,7 @@ function App() {
<Cart <Cart
table={table} table={table}
sendParam={handleSetParam} sendParam={handleSetParam}
socket={socket}
totalItemsCount={totalItemsCount} totalItemsCount={totalItemsCount}
deviceType={deviceType} deviceType={deviceType}
/> />

View File

@@ -9,10 +9,17 @@ const TableList = ({ shop, tables, onSelectTable, selectedTable }) => {
const [bgImageUrl, setBgImageUrl] = useState(shop.qrBackground); const [bgImageUrl, setBgImageUrl] = useState(shop.qrBackground);
const shopUrl = window.location.hostname + "/" + shop.cafeId; const shopUrl = window.location.hostname + "/" + shop.cafeId;
const generateQRCodeUrl = (tableCode) => const generateQRCodeUrl = (tableCode) => {
`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent( if (tableCode != null) {
shopUrl + "/" + tableCode return `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(
)}`; shopUrl + "/" + tableCode
)}`;
} else {
return `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(
shopUrl
)}`;
}
};
const handleBackgroundUrlChange = (newUrl) => { const handleBackgroundUrlChange = (newUrl) => {
setBgImageUrl(newUrl); setBgImageUrl(newUrl);
@@ -57,7 +64,7 @@ const TableList = ({ shop, tables, onSelectTable, selectedTable }) => {
handleQrSave={handleQrSave} handleQrSave={handleQrSave}
setInitialPos={setInitialPos} setInitialPos={setInitialPos}
setInitialSize={setInitialSize} setInitialSize={setInitialSize}
qrCodeUrl={generateQRCodeUrl("sample")} qrCodeUrl={generateQRCodeUrl("")}
backgroundUrl={bgImageUrl} backgroundUrl={bgImageUrl}
initialQrPosition={initialPos} initialQrPosition={initialPos}
initialQrSize={initialSize} initialQrSize={initialSize}

View File

@@ -50,6 +50,30 @@ export async function declineTransaction(transactionId) {
} }
} }
export async function cancelTransaction(transactionId) {
try {
console.log(transactionId);
const token = getLocalStorage("auth");
const response = await fetch(
`${API_BASE_URL}/transaction/claim-transaction/${transactionId}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
}
);
if (!response.ok) {
return false;
}
return true;
} catch (error) {
console.error("Error:", error);
}
}
export async function handleClaimHasPaid(transactionId) { export async function handleClaimHasPaid(transactionId) {
try { try {
console.log(transactionId); console.log(transactionId);
@@ -264,6 +288,7 @@ export const handlePaymentFromGuestDevice = async (
payment_type, payment_type,
serving_type, serving_type,
tableNo, tableNo,
notes,
socketId socketId
) => { ) => {
try { try {
@@ -291,6 +316,7 @@ export const handlePaymentFromGuestDevice = async (
serving_type, serving_type,
tableNo, tableNo,
transactions: structuredItems, transactions: structuredItems,
notes: notes,
socketId, socketId,
}), }),
} }

266
src/pages/Cart copy.js Normal file
View File

@@ -0,0 +1,266 @@
import React, { useRef, useEffect, useState } from "react";
import styles from "./Cart.module.css";
import ItemLister from "../components/ItemLister";
import { ThreeDots, ColorRing } from "react-loader-spinner";
import { useParams } from "react-router-dom";
import { useNavigationHelpers } from "../helpers/navigationHelpers";
import { getTable } from "../helpers/tableHelper.js";
import { getCartDetails } from "../helpers/itemHelper.js";
import { getItemsByCafeId } from "../helpers/cartHelpers"; // Import getItemsByCafeId
import Modal from "../components/Modal"; // Import the reusable Modal component
export default function Cart({
table,
sendParam,
totalItemsCount,
deviceType,
}) {
const { shopId, tableCode } = useParams();
sendParam({ shopId, tableCode });
const { goToShop, goToInvoice } = useNavigationHelpers(shopId, tableCode);
const [cartItems, setCartItems] = useState([]);
const [totalPrice, setTotalPrice] = useState(0);
const [orderType, setOrderType] = useState("serve");
const [tableNumber, setTableNumber] = useState("");
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalContent, setModalContent] = useState(null);
const [isCheckoutLoading, setIsCheckoutLoading] = useState(false); // State for checkout button loading animation
const [email, setEmail] = useState("");
const textareaRef = useRef(null);
useEffect(() => {
const fetchCartItems = async () => {
try {
setLoading(true);
const items = await getCartDetails(shopId);
setLoading(false);
if (items) setCartItems(items);
const initialTotalPrice = items.reduce((total, itemType) => {
return (
total +
itemType.itemList.reduce((subtotal, item) => {
return subtotal + item.qty * item.price;
}, 0)
);
}, 0);
setTotalPrice(initialTotalPrice);
} catch (error) {
console.error("Error fetching cart items:", error);
}
};
fetchCartItems();
const textarea = textareaRef.current;
if (textarea) {
const handleResize = () => {
textarea.style.height = "auto";
textarea.style.height = `${textarea.scrollHeight}px`;
};
textarea.addEventListener("input", handleResize);
handleResize();
return () => textarea.removeEventListener("input", handleResize);
}
}, [shopId]);
useEffect(() => {
const textarea = textareaRef.current;
if (textarea) {
const handleResize = () => {
textarea.style.height = "auto";
textarea.style.height = `${textarea.scrollHeight}px`;
};
handleResize(); // Initial resize
textarea.addEventListener("input", handleResize);
return () => textarea.removeEventListener("input", handleResize);
}
}, [textareaRef.current]);
const refreshTotal = async () => {
try {
const items = await getItemsByCafeId(shopId);
const updatedTotalPrice = items.reduce((total, localItem) => {
const cartItem = cartItems.find((itemType) =>
itemType.itemList.some((item) => item.itemId === localItem.itemId)
);
if (cartItem) {
const itemDetails = cartItem.itemList.find(
(item) => item.itemId === localItem.itemId
);
return total + localItem.qty * itemDetails.price;
}
return total;
}, 0);
setTotalPrice(updatedTotalPrice);
} catch (error) {
console.error("Error refreshing total price:", error);
}
};
const handleOrderTypeChange = (event) => {
setOrderType(event.target.value);
};
const handleTableNumberChange = (event) => {
setTableNumber(event.target.value);
};
const handleEmailChange = (event) => {
setEmail(event.target.value);
};
const handlCloseModal = () => {
setIsModalOpen(false);
setIsCheckoutLoading(false);
};
const isValidEmail = (email) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const handleCheckout = async () => {
setIsCheckoutLoading(true); // Start loading animation
if (email != "" && !isValidEmail(email)) {
setModalContent(<div>Please enter a valid email address.</div>);
setIsModalOpen(true);
setIsCheckoutLoading(false); // Stop loading animation
return;
}
if (orderType === "serve") {
console.log("serve");
if (tableNumber !== "" && table.tableNo == undefined) {
console.log("getting with tableNumber");
const table = await getTable(shopId, tableNumber);
if (!table) {
setModalContent(
<div>Table not found. Please enter a valid table number.</div>
);
setIsModalOpen(true);
} else {
goToInvoice(orderType, table.tableNo, email);
}
} else if (table.tableNo != undefined) {
console.log("getting with table code" + table.tableNo);
goToInvoice(orderType, null, email);
} else {
setModalContent(<div>Please enter a table number.</div>);
setIsModalOpen(true);
}
} else {
console.log("getting with pickup");
goToInvoice(orderType, tableNumber, email);
}
setIsCheckoutLoading(false); // Stop loading animation
};
if (loading)
return (
<div className="Loader">
<div className="LoaderChild">
<ThreeDots />
</div>
</div>
);
else
return (
<div className={styles.Cart}>
<div style={{ marginTop: "30px" }}></div>
<h2 className={styles["Cart-title"]}>
{totalItemsCount} {totalItemsCount !== 1 ? "items" : "item"} in Cart
</h2>
<div style={{ marginTop: "-45px" }}></div>
{cartItems.map((itemType) => (
<ItemLister
key={itemType.itemTypeId}
refreshTotal={refreshTotal}
shopId={shopId}
forCart={true}
typeName={itemType.typeName}
itemList={itemType.itemList}
/>
))}
{deviceType != "guestDevice" && (
<div className={styles.EmailContainer}>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
placeholder="log this transaction (optional)"
value={email}
onChange={handleEmailChange}
className={styles.EmailInput}
/>
</div>
)}
<div className={styles.OrderTypeContainer}>
<span htmlFor="orderType">Order Type:</span>
<select
id="orderType"
value={orderType}
onChange={handleOrderTypeChange}
>
{table != null && (
<option value="serve">Serve to table {table.tableNo}</option>
)}
<option value="pickup">Pickup</option>
{table == null && <option value="serve">Serve</option>}
{/* tableId harus di check terlebih dahulu untuk mendapatkan tableNo */}
</select>
{orderType === "serve" && table.length < 1 && (
<input
type="text"
placeholder="Table Number"
value={tableNumber}
onChange={handleTableNumberChange}
className={styles.TableNumberInput}
/>
)}
</div>
<div className={styles.NoteContainer}>
<span>Note</span>
<span></span>
</div>
<textarea
ref={textareaRef}
className={styles.NoteInput}
placeholder="Add a note..."
/>
<div className={styles.TotalContainer}>
<span>Total:</span>
<span>Rp {totalPrice}</span>
</div>
<button onClick={handleCheckout} className={styles.CheckoutButton}>
{isCheckoutLoading ? (
<ColorRing height="50" width="50" color="white" />
) : (
"Checkout"
)}
</button>
<div onClick={goToShop} className={styles.BackToMenu}>
Back to menu
</div>
<Modal isOpen={isModalOpen} onClose={() => handlCloseModal()}>
{modalContent}
</Modal>
</div>
);
}

View File

@@ -1,46 +1,44 @@
import React, { useRef, useEffect, useState } from "react"; import React, { useRef, useEffect, useState } from "react";
import styles from "./Cart.module.css"; import styles from "./Invoice.module.css";
import ItemLister from "../components/ItemLister"; import { useParams, useLocation } from "react-router-dom"; // Changed from useSearchParams to useLocation
import { ThreeDots, ColorRing } from "react-loader-spinner"; import { ThreeDots, ColorRing } from "react-loader-spinner";
import { useParams } from "react-router-dom";
import { useNavigationHelpers } from "../helpers/navigationHelpers";
import { getTable } from "../helpers/tableHelper.js";
import { getCartDetails } from "../helpers/itemHelper.js";
import { getItemsByCafeId } from "../helpers/cartHelpers"; // Import getItemsByCafeId
import Modal from "../components/Modal"; // Import the reusable Modal component
export default function Cart({ import ItemLister from "../components/ItemLister";
table, import { getCartDetails } from "../helpers/itemHelper";
sendParam, import {
totalItemsCount, handlePaymentFromClerk,
deviceType, handlePaymentFromGuestSide,
}) { handlePaymentFromGuestDevice,
} from "../helpers/transactionHelpers";
export default function Invoice({ table, sendParam, deviceType, socket }) {
const { shopId, tableCode } = useParams(); const { shopId, tableCode } = useParams();
sendParam({ shopId, tableCode }); sendParam({ shopId, tableCode });
const { goToShop, goToInvoice } = useNavigationHelpers(shopId, tableCode); const location = useLocation(); // Use useLocation hook instead of useSearchParams
const searchParams = new URLSearchParams(location.search); // Pass location.search directly
// const email = searchParams.get("email");
// const orderType = searchParams.get("orderType");
// const tableNumber = searchParams.get("tableNumber");
const [cartItems, setCartItems] = useState([]); const [cartItems, setCartItems] = useState([]);
const [totalPrice, setTotalPrice] = useState(0); const [totalPrice, setTotalPrice] = useState(0);
const [orderType, setOrderType] = useState("serve"); const [isPaymentLoading, setIsPaymentLoading] = useState(false); // State for payment button loading animation
const [tableNumber, setTableNumber] = useState("");
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalContent, setModalContent] = useState(null);
const [isCheckoutLoading, setIsCheckoutLoading] = useState(false); // State for checkout button loading animation
const [email, setEmail] = useState("");
const textareaRef = useRef(null); const textareaRef = useRef(null);
const [orderType, setOrderType] = useState("serve");
const [tableNumber, setTableNumber] = useState("");
const [email, setEmail] = useState("");
useEffect(() => { useEffect(() => {
const fetchCartItems = async () => { const fetchCartItems = async () => {
try { try {
setLoading(true);
const items = await getCartDetails(shopId); const items = await getCartDetails(shopId);
setLoading(false); setCartItems(items);
if (items) setCartItems(items); // Calculate total price based on fetched cart items
const totalPrice = items.reduce((total, itemType) => {
const initialTotalPrice = items.reduce((total, itemType) => {
return ( return (
total + total +
itemType.itemList.reduce((subtotal, item) => { itemType.itemList.reduce((subtotal, item) => {
@@ -48,26 +46,51 @@ export default function Cart({
}, 0) }, 0)
); );
}, 0); }, 0);
setTotalPrice(initialTotalPrice); setTotalPrice(totalPrice);
} catch (error) { } catch (error) {
console.error("Error fetching cart items:", error); console.error("Error fetching cart items:", error);
// Handle error if needed
} }
}; };
fetchCartItems(); fetchCartItems();
const textarea = textareaRef.current;
if (textarea) {
const handleResize = () => {
textarea.style.height = "auto";
textarea.style.height = `${textarea.scrollHeight}px`;
};
textarea.addEventListener("input", handleResize);
handleResize();
return () => textarea.removeEventListener("input", handleResize);
}
}, [shopId]); }, [shopId]);
const handlePay = async (isCash) => {
setIsPaymentLoading(true);
console.log("tipe" + deviceType);
if (deviceType == "clerk") {
const pay = await handlePaymentFromClerk(
shopId,
email,
isCash ? "cash" : "cashless",
orderType,
tableNumber
);
} else if (deviceType == "guestSide") {
const pay = await handlePaymentFromGuestSide(
shopId,
email,
isCash ? "cash" : "cashless",
orderType,
tableNumber
);
} else if (deviceType == "guestDevice") {
const socketId = socket.id;
const pay = await handlePaymentFromGuestDevice(
shopId,
isCash ? "cash" : "cashless",
orderType,
table.tableNo || tableNumber,
textareaRef.current.value,
socketId
);
}
console.log("transaction from " + deviceType + "success");
setIsPaymentLoading(false);
};
useEffect(() => { useEffect(() => {
const textarea = textareaRef.current; const textarea = textareaRef.current;
@@ -84,29 +107,6 @@ export default function Cart({
} }
}, [textareaRef.current]); }, [textareaRef.current]);
const refreshTotal = async () => {
try {
const items = await getItemsByCafeId(shopId);
const updatedTotalPrice = items.reduce((total, localItem) => {
const cartItem = cartItems.find((itemType) =>
itemType.itemList.some((item) => item.itemId === localItem.itemId)
);
if (cartItem) {
const itemDetails = cartItem.itemList.find(
(item) => item.itemId === localItem.itemId
);
return total + localItem.qty * itemDetails.price;
}
return total;
}, 0);
setTotalPrice(updatedTotalPrice);
} catch (error) {
console.error("Error refreshing total price:", error);
}
};
const handleOrderTypeChange = (event) => { const handleOrderTypeChange = (event) => {
setOrderType(event.target.value); setOrderType(event.target.value);
}; };
@@ -118,94 +118,22 @@ export default function Cart({
const handleEmailChange = (event) => { const handleEmailChange = (event) => {
setEmail(event.target.value); setEmail(event.target.value);
}; };
return (
const handlCloseModal = () => { <div className={styles.Invoice}>
setIsModalOpen(false); <div style={{ marginTop: "30px" }}></div>
setIsCheckoutLoading(false); <h2 className={styles["Invoice-title"]}>Cart</h2>
}; <div style={{ marginTop: "30px" }}></div>
<div className={styles.RoundedRectangle}>
const isValidEmail = (email) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const handleCheckout = async () => {
setIsCheckoutLoading(true); // Start loading animation
if (email != "" && !isValidEmail(email)) {
setModalContent(<div>Please enter a valid email address.</div>);
setIsModalOpen(true);
setIsCheckoutLoading(false); // Stop loading animation
return;
}
if (orderType === "serve") {
console.log("serve");
if (tableNumber !== "" && table.tableNo == undefined) {
console.log("getting with tableNumber");
const table = await getTable(shopId, tableNumber);
if (!table) {
setModalContent(
<div>Table not found. Please enter a valid table number.</div>
);
setIsModalOpen(true);
} else {
goToInvoice(orderType, table.tableNo, email);
}
} else if (table.tableNo != undefined) {
console.log("getting with table code" + table.tableNo);
goToInvoice(orderType, null, email);
} else {
setModalContent(<div>Please enter a table number.</div>);
setIsModalOpen(true);
}
} else {
console.log("getting with pickup");
goToInvoice(orderType, tableNumber, email);
}
setIsCheckoutLoading(false); // Stop loading animation
};
if (loading)
return (
<div className="Loader">
<div className="LoaderChild">
<ThreeDots />
</div>
</div>
);
else
return (
<div className={styles.Cart}>
<div style={{ marginTop: "30px" }}></div>
<h2 className={styles["Cart-title"]}>
{totalItemsCount} {totalItemsCount !== 1 ? "items" : "item"} in Cart
</h2>
<div style={{ marginTop: "-45px" }}></div>
{cartItems.map((itemType) => ( {cartItems.map((itemType) => (
<ItemLister <ItemLister
key={itemType.itemTypeId}
refreshTotal={refreshTotal}
shopId={shopId} shopId={shopId}
forCart={true} forInvoice={true}
key={itemType.id}
typeName={itemType.typeName} typeName={itemType.typeName}
itemList={itemType.itemList} itemList={itemType.itemList}
/> />
))} ))}
{deviceType != "guestDevice" && (
<div className={styles.EmailContainer}>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
placeholder="log this transaction (optional)"
value={email}
onChange={handleEmailChange}
className={styles.EmailInput}
/>
</div>
)}
<div className={styles.OrderTypeContainer}> <div className={styles.OrderTypeContainer}>
<span htmlFor="orderType">Order Type:</span> <span htmlFor="orderType">Order Type:</span>
<select <select
@@ -221,7 +149,10 @@ export default function Cart({
{/* tableId harus di check terlebih dahulu untuk mendapatkan tableNo */} {/* tableId harus di check terlebih dahulu untuk mendapatkan tableNo */}
</select> </select>
{orderType === "serve" && table.length < 1 && ( </div>
{orderType === "serve" && table.length < 1 && (
<div className={styles.OrderTypeContainer}>
<span htmlFor="orderType">Serve to:</span>
<input <input
type="text" type="text"
placeholder="Table Number" placeholder="Table Number"
@@ -229,38 +160,47 @@ export default function Cart({
onChange={handleTableNumberChange} onChange={handleTableNumberChange}
className={styles.TableNumberInput} className={styles.TableNumberInput}
/> />
)} </div>
</div> )}
<div className={styles.NoteContainer}> <div className={styles.NoteContainer}>
<span>Note</span> <span>Note :</span>
<span></span> <span></span>
</div> </div>
<textarea <div className={styles.NoteContainer}>
ref={textareaRef} <textarea
className={styles.NoteInput} ref={textareaRef}
placeholder="Add a note..." className={styles.NoteInput}
/> placeholder="Add a note..."
/>
</div>
<div className={styles.TotalContainer}> <div className={styles.TotalContainer}>
<span>Total:</span> <span>Total:</span>
<span>Rp {totalPrice}</span> <span>Rp {totalPrice}</span>
</div> </div>
<button onClick={handleCheckout} className={styles.CheckoutButton}> </div>
{isCheckoutLoading ? ( <div className={styles.PaymentOption}>
<div className={styles.TotalContainer}>
<span>Payment Option</span>
<span></span>
</div>
<button className={styles.PayButton} onClick={() => handlePay(false)}>
{isPaymentLoading ? (
<ColorRing height="50" width="50" color="white" /> <ColorRing height="50" width="50" color="white" />
) : ( ) : (
"Checkout" "Cashless"
)} )}
</button> </button>
<div onClick={goToShop} className={styles.BackToMenu}> <div className={styles.Pay2Button} onClick={() => handlePay(true)}>
Back to menu {isPaymentLoading ? (
<ColorRing height="12" width="12" color="white" />
) : (
"Cash"
)}
</div> </div>
<Modal isOpen={isModalOpen} onClose={() => handlCloseModal()}>
{modalContent}
</Modal>
</div> </div>
); <div className={styles.PaymentOptionMargin}></div>
</div>
);
} }

View File

@@ -118,3 +118,54 @@
margin: 26px; margin: 26px;
background-color: #f9f9f9; background-color: #f9f9f9;
} }
.EmailContainer {
display: flex;
justify-content: space-between;
width: 80vw;
margin: 0 auto;
font-size: 1em;
padding: 10px 0;
margin-bottom: 7px;
}
.OrderTypeContainer {
display: flex;
justify-content: space-between;
width: 80vw;
margin: 0 auto;
font-size: 1em;
padding: 10px 0;
margin-bottom: 7px;
}
.Note {
text-align: left;
color: rgba(88, 55, 50, 1);
font-size: 1em;
margin-bottom: 13px;
margin-left: 50px;
cursor: pointer;
}
.NoteContainer {
display: flex;
justify-content: space-between;
width: 80vw;
margin: 0 auto;
font-size: 1em;
padding: 10px 0;
margin-bottom: 7px;
}
.NoteInput {
width: 78vw;
height: 12vw;
border-radius: 20px;
margin: 0 auto;
padding: 10px;
font-size: 1.2em;
border: 1px solid rgba(88, 55, 50, 0.5);
margin-bottom: 27px;
resize: none; /* Prevent resizing */
overflow-wrap: break-word; /* Ensure text wraps */
}

View File

@@ -1,16 +1,23 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useEffect, useState } from "react";
import { ColorRing } from "react-loader-spinner";
import jsQR from "jsqr";
import QRCode from "qrcode.react";
import styles from "./Transactions.module.css"; import styles from "./Transactions.module.css";
import { getImageUrl } from "../helpers/itemHelper"; import { useParams } from "react-router-dom";
import { useSearchParams } from "react-router-dom"; import { ColorRing } from "react-loader-spinner";
import { import {
getTransaction, getTransaction,
handleConfirmHasPaid, confirmTransaction,
declineTransaction,
} from "../helpers/transactionHelpers"; } from "../helpers/transactionHelpers";
import { getTables } from "../helpers/tableHelper";
import TableCanvas from "../components/TableCanvas";
import { useSearchParams } from "react-router-dom";
export default function Transaction_pending() { export default function Transactions({ propsShopId, sendParam, deviceType }) {
const { shopId, tableId } = useParams();
if (sendParam) sendParam({ shopId, tableId });
const [tables, setTables] = useState([]);
const [selectedTable, setSelectedTable] = useState(null);
const [isPaymentLoading, setIsPaymentLoading] = useState(false);
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [transaction, setTransaction] = useState(null); const [transaction, setTransaction] = useState(null);
@@ -29,31 +36,75 @@ export default function Transaction_pending() {
fetchData(); fetchData();
}, [searchParams]); }, [searchParams]);
const calculateTotalPrice = (detailedTransactions) => { useEffect(() => {
if (!Array.isArray(detailedTransactions)) return 0; const fetchData = async () => {
try {
return detailedTransactions.reduce((total, dt) => { const fetchedTables = await getTables(shopId || propsShopId);
if ( setTables(fetchedTables);
dt.Item && } catch (error) {
typeof dt.Item.price === "number" && console.error("Error fetching tables:", error);
typeof dt.qty === "number"
) {
return total + dt.Item.price * dt.qty;
} }
return total; };
fetchData();
}, [shopId || propsShopId]);
const calculateTotalPrice = (detailedTransactions) => {
return detailedTransactions.reduce((total, dt) => {
return total + dt.qty * dt.Item.price;
}, 0); }, 0);
}; };
const handleConfirm = async (transactionId) => {
if (isPaymentLoading) return;
setIsPaymentLoading(true);
try {
const c = await confirmTransaction(transactionId);
if (c) {
setTransaction({ ...transaction, confirmed: c.confirmed });
}
} catch (error) {
console.error("Error processing payment:", error);
} finally {
setIsPaymentLoading(false);
}
};
const handleDecline = async (transactionId) => {
if (isPaymentLoading) return;
setIsPaymentLoading(true);
try {
const c = await declineTransaction(transactionId);
// if (c) {
// // Update the confirmed status locally
// setTransactions((prevTransactions) =>
// prevTransactions.map((transaction) =>
// transaction.transactionId === transactionId
// ? { ...transaction, confirmed: -1 } // Set to confirmed
// : transaction
// )
// );
// }
} catch (error) {
console.error("Error processing payment:", error);
} finally {
setIsPaymentLoading(false);
}
};
return ( return (
<div className={styles.Transactions}> <div className={styles.Transactions}>
<div style={{ marginTop: "30px" }}></div> <div style={{ marginTop: "30px" }}></div>
<h2 className={styles["Transactions-title"]}>Payment Claimed</h2> <h2 className={styles["Transactions-title"]}>Transactions</h2>
<div style={{ marginTop: "30px" }}></div> {/* <TableCanvas tables={tables} selectedTable={selectedTable} /> */}
<div className={styles.TransactionListContainer}> <div className={styles.TransactionListContainer}>
{transaction && ( {transaction && (
<div <div
key={transaction.transactionId} key={transaction.transactionId}
className={styles.RoundedRectangle} className={styles.RoundedRectangle}
onClick={() =>
setSelectedTable(transaction.Table || { tableId: 0 })
}
> >
<h2 className={styles["Transactions-detail"]}> <h2 className={styles["Transactions-detail"]}>
Transaction ID: {transaction.transactionId} Transaction ID: {transaction.transactionId}
@@ -76,6 +127,22 @@ export default function Transaction_pending() {
transaction.Table ? transaction.Table.tableNo : "N/A" transaction.Table ? transaction.Table.tableNo : "N/A"
}`} }`}
</h2> </h2>
{transaction.notes != null && (
<>
<div className={styles.NoteContainer}>
<span>Note :</span>
<span></span>
</div>
<div className={styles.NoteContainer}>
<textarea
className={styles.NoteInput}
value={transaction.notes}
disabled
/>
</div>
</>
)}
<div className={styles.TotalContainer}> <div className={styles.TotalContainer}>
<span>Total:</span> <span>Total:</span>
<span> <span>
@@ -85,11 +152,31 @@ export default function Transaction_pending() {
<div className={styles.TotalContainer}> <div className={styles.TotalContainer}>
<button <button
className={styles.PayButton} className={styles.PayButton}
onClick={() => handleConfirmHasPaid(transaction.transactionId)} onClick={() => handleConfirm(transaction.transactionId)}
></button> disabled={isPaymentLoading} // Disable button if confirmed (1) or declined (-1) or loading
>
{isPaymentLoading ? (
<ColorRing height="50" width="50" color="white" />
) : transaction.confirmed === 1 ? (
"Confirm has paid" // Display "Confirm has paid" if the transaction is confirmed (1)
) : transaction.confirmed === -1 ? (
"Declined" // Display "Declined" if the transaction is declined (-1)
) : transaction.confirmed === 2 ? (
"Confirm item has ready" // Display "Item ready" if the transaction is ready (2)
) : transaction.confirmed === 3 ? (
"Transaction success" // Display "Item ready" if the transaction is ready (2)
) : (
"Confirm availability" // Display "Confirm availability" if the transaction is not confirmed (0)
)}
</button>
</div> </div>
{transaction.confirmed == 0 && ( {transaction.confirmed == 0 && (
<h5 className={styles.DeclineButton}>decline</h5> <h5
className={styles.DeclineButton}
onClick={() => handleDecline(transaction.transactionId)}
>
decline
</h5>
)} )}
</div> </div>
)} )}

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import React, { useRef, useEffect, useState } from "react";
import styles from "./Transactions.module.css"; import styles from "./Transactions.module.css";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { ColorRing } from "react-loader-spinner"; import { ColorRing } from "react-loader-spinner";
@@ -20,6 +20,7 @@ export default function Transactions({ propsShopId, sendParam, deviceType }) {
const [isPaymentLoading, setIsPaymentLoading] = useState(false); const [isPaymentLoading, setIsPaymentLoading] = useState(false);
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [transaction, setTransaction] = useState(null); const [transaction, setTransaction] = useState(null);
const noteRef = useRef(null);
useEffect(() => { useEffect(() => {
const transactionId = searchParams.get("transactionId") || ""; const transactionId = searchParams.get("transactionId") || "";
@@ -92,11 +93,23 @@ export default function Transactions({ propsShopId, sendParam, deviceType }) {
} }
}; };
const autoResizeTextArea = (textarea) => {
if (textarea) {
textarea.style.height = "auto"; // Reset height
textarea.style.height = `${textarea.scrollHeight}px`; // Set new height
}
};
useEffect(() => {
if (noteRef.current) {
autoResizeTextArea(noteRef.current);
}
}, [transaction?.notes]);
return ( return (
<div className={styles.Transactions}> <div className={styles.Transactions}>
<div style={{ marginTop: "30px" }}></div> <div style={{ marginTop: "30px" }}></div>
<h2 className={styles["Transactions-title"]}>Transactions</h2> <h2 className={styles["Transactions-title"]}>Transactions</h2>
<div style={{ marginTop: "30px" }}></div>
{/* <TableCanvas tables={tables} selectedTable={selectedTable} /> */} {/* <TableCanvas tables={tables} selectedTable={selectedTable} /> */}
<div className={styles.TransactionListContainer}> <div className={styles.TransactionListContainer}>
{transaction && ( {transaction && (
@@ -128,6 +141,23 @@ export default function Transactions({ propsShopId, sendParam, deviceType }) {
transaction.Table ? transaction.Table.tableNo : "N/A" transaction.Table ? transaction.Table.tableNo : "N/A"
}`} }`}
</h2> </h2>
{transaction.notes != "" && (
<>
<div className={styles.NoteContainer}>
<span>Note :</span>
<span></span>
</div>
<div className={styles.NoteContainer}>
<textarea
className={styles.NoteInput}
value={transaction.notes}
ref={noteRef}
disabled
/>
</div>
</>
)}
<div className={styles.TotalContainer}> <div className={styles.TotalContainer}>
<span>Total:</span> <span>Total:</span>
<span> <span>

View File

@@ -1,21 +1,27 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useRef, useEffect, useState } from "react";
import { ColorRing } from "react-loader-spinner";
import jsQR from "jsqr";
import QRCode from "qrcode.react";
import styles from "./Transactions.module.css"; import styles from "./Transactions.module.css";
import { getImageUrl } from "../helpers/itemHelper"; import { useParams } from "react-router-dom";
import { useSearchParams } from "react-router-dom"; import { ColorRing } from "react-loader-spinner";
import { import {
handleClaimHasPaid,
getTransaction, getTransaction,
confirmTransaction,
declineTransaction,
cancelTransaction,
} from "../helpers/transactionHelpers"; } from "../helpers/transactionHelpers";
import html2canvas from "html2canvas"; import { getTables } from "../helpers/tableHelper";
import TableCanvas from "../components/TableCanvas";
import { useSearchParams } from "react-router-dom";
export default function Transaction_pending({ paymentUrl }) { export default function Transactions({ propsShopId, sendParam, deviceType }) {
const { shopId, tableId } = useParams();
if (sendParam) sendParam({ shopId, tableId });
const [tables, setTables] = useState([]);
const [selectedTable, setSelectedTable] = useState(null);
const [isPaymentLoading, setIsPaymentLoading] = useState(false);
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [qrData, setQrData] = useState(null);
const [transaction, setTransaction] = useState(null); const [transaction, setTransaction] = useState(null);
const qrCodeRef = useRef(null); const noteRef = useRef(null);
useEffect(() => { useEffect(() => {
const transactionId = searchParams.get("transactionId") || ""; const transactionId = searchParams.get("transactionId") || "";
@@ -24,7 +30,7 @@ export default function Transaction_pending({ paymentUrl }) {
try { try {
const fetchedTransaction = await getTransaction(transactionId); const fetchedTransaction = await getTransaction(transactionId);
setTransaction(fetchedTransaction); setTransaction(fetchedTransaction);
console.log(fetchedTransaction); console.log(transaction);
} catch (error) { } catch (error) {
console.error("Error fetching transaction:", error); console.error("Error fetching transaction:", error);
} }
@@ -33,160 +39,151 @@ export default function Transaction_pending({ paymentUrl }) {
}, [searchParams]); }, [searchParams]);
useEffect(() => { useEffect(() => {
const detectQRCode = async () => { const fetchData = async () => {
if (paymentUrl) { try {
const img = new Image(); const fetchedTables = await getTables(shopId || propsShopId);
img.crossOrigin = "Anonymous"; // Handle CORS if needed setTables(fetchedTables);
img.src = getImageUrl(paymentUrl); } catch (error) {
console.error("Error fetching tables:", error);
img.onload = () => {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
canvas.width = img.width;
canvas.height = img.height;
// Draw image on canvas
context.drawImage(img, 0, 0, img.width, img.height);
// Get image data
const imageData = context.getImageData(
0,
0,
canvas.width,
canvas.height
);
const qrCode = jsQR(imageData.data, canvas.width, canvas.height);
if (qrCode) {
setQrData(qrCode.data); // Set the QR data
console.log(qrCode.data);
} else {
console.log("No QR Code detected");
}
};
} }
}; };
detectQRCode(); fetchData();
}, [paymentUrl]); }, [shopId || propsShopId]);
const calculateTotalPrice = (detailedTransactions) => { const calculateTotalPrice = (detailedTransactions) => {
if (!Array.isArray(detailedTransactions)) return 0;
return detailedTransactions.reduce((total, dt) => { return detailedTransactions.reduce((total, dt) => {
if ( return total + dt.qty * dt.Item.price;
dt.Item &&
typeof dt.Item.price === "number" &&
typeof dt.qty === "number"
) {
return total + dt.Item.price * dt.qty;
}
return total;
}, 0); }, 0);
}; };
const downloadQRCode = async () => { const handleConfirm = async (transactionId) => {
if (qrCodeRef.current) { if (isPaymentLoading) return;
try { setIsPaymentLoading(true);
const canvas = await html2canvas(qrCodeRef.current); try {
const link = document.createElement("a"); const c = await confirmTransaction(transactionId);
link.href = canvas.toDataURL("image/png"); if (c) {
link.download = "qr-code.png"; setTransaction({ ...transaction, confirmed: c.confirmed });
link.click();
} catch (error) {
console.error("Error downloading QR Code:", error);
} }
} else { } catch (error) {
console.log("QR Code element not found."); console.error("Error processing payment:", error);
} finally {
setIsPaymentLoading(false);
} }
}; };
const handleDecline = async (transactionId) => {
if (isPaymentLoading) return;
setIsPaymentLoading(true);
try {
const c = await cancelTransaction(transactionId);
} catch (error) {
console.error("Error processing payment:", error);
} finally {
setIsPaymentLoading(false);
}
};
const autoResizeTextArea = (textarea) => {
if (textarea) {
textarea.style.height = "auto"; // Reset height
textarea.style.height = `${textarea.scrollHeight}px`; // Set new height
}
};
useEffect(() => {
if (noteRef.current) {
autoResizeTextArea(noteRef.current);
}
}, [transaction?.notes]);
return ( return (
<div className={styles.Transactions}> <div className={styles.Transactions}>
<div style={{ marginTop: "30px" }}></div> <div style={{ marginTop: "30px" }}></div>
<h2 className={styles["Transactions-title"]}>Transaction Confirmed</h2> <h2 className={styles["Transactions-title"]}>Transactions</h2>
<div style={{ marginTop: "30px" }}></div> {/* <TableCanvas tables={tables} selectedTable={selectedTable} /> */}
<div className={styles.TransactionListContainer}> <div className={styles.TransactionListContainer}>
<div style={{ marginTop: "30px", textAlign: "center" }}> {transaction && (
{qrData ? ( <div
<div style={{ marginTop: "20px" }}> key={transaction.transactionId}
<div ref={qrCodeRef}> className={styles.RoundedRectangle}
<QRCode value={qrData} size={256} /> {/* Generate QR code */} onClick={() =>
</div> setSelectedTable(transaction.Table || { tableId: 0 })
<button }
onClick={downloadQRCode} >
style={{ <h2 className={styles["Transactions-detail"]}>
marginTop: "20px", Transaction ID: {transaction.transactionId}
padding: "10px 20px", </h2>
fontSize: "16px", <h2 className={styles["Transactions-detail"]}>
backgroundColor: "#007bff", Payment Type: {transaction.payment_type}
color: "#fff", </h2>
border: "none", <ul>
borderRadius: "4px", {transaction.DetailedTransactions.map((detail) => (
cursor: "pointer", <li key={detail.detailedTransactionId}>
transition: "background-color 0.3s", <span>{detail.Item.name}</span> - {detail.qty} x Rp{" "}
}} {detail.Item.price}
onMouseOver={(e) => </li>
(e.currentTarget.style.backgroundColor = "#0056b3") ))}
} </ul>
onMouseOut={(e) => <h2 className={styles["Transactions-detail"]}>
(e.currentTarget.style.backgroundColor = "#007bff") {transaction.serving_type === "pickup"
} ? "Self pickup"
> : `Serve to ${
Download QR Code transaction.Table ? transaction.Table.tableNo : "N/A"
</button> }`}
</div> </h2>
) : ( {transaction.notes != "" && (
<div style={{ marginTop: "20px" }}> <>
<ColorRing <div className={styles.NoteContainer}>
visible={true} <span>Note :</span>
height="80" <span></span>
width="80" </div>
ariaLabel="blocks-loading"
wrapperStyle={{}}
wrapperClass="blocks-wrapper"
colors={["#4fa94d", "#f7c34c", "#ffa53c", "#e34f53", "#d23a8d"]}
/>
<p>Loading QR Code Data...</p>
</div>
)}
{transaction && transaction.DetailedTransactions ? ( <div className={styles.NoteContainer}>
<div <textarea
className={styles.TotalContainer} className={styles.NoteInput}
style={{ marginBottom: "20px" }} value={transaction.notes}
> ref={noteRef}
disabled
/>
</div>
</>
)}
<div className={styles.TotalContainer}>
<span>Total:</span> <span>Total:</span>
<span> <span>
Rp{" "} Rp {calculateTotalPrice(transaction.DetailedTransactions)}
{calculateTotalPrice(
transaction.DetailedTransactions
).toLocaleString()}
</span> </span>
</div> </div>
) : ( <div className={styles.TotalContainer}>
<div style={{ marginTop: "20px" }}> <button
<ColorRing className={styles.PayButton}
visible={true} onClick={() => handleConfirm(transaction.transactionId)}
height="80" disabled={isPaymentLoading} // Disable button if confirmed (1) or declined (-1) or loading
width="80" >
ariaLabel="blocks-loading" {isPaymentLoading ? (
wrapperStyle={{}} <ColorRing height="50" width="50" color="white" />
wrapperClass="blocks-wrapper" ) : transaction.confirmed === 1 ? (
colors={["#4fa94d", "#f7c34c", "#ffa53c", "#e34f53", "#d23a8d"]} "Show payment" // Display "Confirm has paid" if the transaction is confirmed (1)
/> ) : transaction.confirmed === -1 ? (
<p>Loading Transaction Data...</p> "Declined" // Display "Declined" if the transaction is declined (-1)
) : transaction.confirmed === 2 ? (
"Confirm item has ready" // Display "Item ready" if the transaction is ready (2)
) : transaction.confirmed === 3 ? (
"Transaction success" // Display "Item ready" if the transaction is ready (2)
) : (
"Confirm availability" // Display "Confirm availability" if the transaction is not confirmed (0)
)}
</button>
</div> </div>
)} <h5
className={styles.DeclineButton}
<button onClick={() => handleDecline(transaction.transactionId)}
onClick={() => handleClaimHasPaid(transaction.transactionId)} >
className={styles.PayButton} cancel
> </h5>
I've already paid </div>
</button> )}
<div style={{ marginBottom: "20px" }}></div>
</div>
</div> </div>
</div> </div>
); );

View File

@@ -90,3 +90,29 @@
.expression { .expression {
width: 100%; width: 100%;
} }
.Note {
text-align: left;
color: rgba(88, 55, 50, 1);
font-size: 1em;
cursor: pointer;
}
.NoteContainer {
display: flex;
justify-content: space-between;
margin-left: 20px;
font-size: 1em;
margin-bottom: 15px;
}
.NoteInput {
height: 12vw;
border-radius: 20px;
margin: 0 auto;
padding: 10px;
font-size: 1.2em;
border: 1px solid rgba(88, 55, 50, 0.5);
resize: none; /* Prevent resizing */
overflow-wrap: break-word; /* Ensure text wraps */
}