ok
This commit is contained in:
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
266
src/pages/Cart copy.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user