Compare commits

..

10 Commits

Author SHA1 Message Date
Vassshhh
67cf759b31 ok 2025-08-25 23:41:35 +07:00
Vassshhh
53e091d3a4 ok 2025-07-28 01:15:07 +07:00
everythingonblack
3a431b1b14 ok 2025-05-23 10:50:39 +07:00
everythingonblack
69a07be3cd ok 2025-05-22 16:43:50 +07:00
everythingonblack
b012517568 ok 2025-05-22 02:15:12 +07:00
everythingonblack
3e35468f2c ok 2025-05-21 16:52:38 +07:00
everythingonblack
df7c4f737c ok 2025-05-20 17:47:43 +07:00
everythingonblack
b726ae6919 ok 2025-05-16 19:54:09 +07:00
everythingonblack
da317f83c9 ok 2025-05-15 17:52:24 +07:00
everythingonblack
f6482d24d2 ok 2025-05-15 15:49:40 +07:00
23 changed files with 7069 additions and 4611 deletions

10994
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,7 @@
"html-to-image": "^1.11.11",
"html2canvas": "^1.4.1",
"jsqr": "^1.4.0",
"qr-scanner": "^1.4.2",
"qrcode.react": "^3.1.0",
"react": "^18.3.1",
"react-apexcharts": "^1.7.0",

View File

@@ -28,6 +28,15 @@
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>KedaiMaster</title>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-2SKSCVFB2N"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){ dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', 'G-2SKSCVFB2N');
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -3,12 +3,10 @@
@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@200;300;400;500;600;700;800&ital,wght@0,200..800;1,200..800&display=swap");
html,
body {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.App {
/* overflow-x: hidden; */
}
.Cafe {

View File

@@ -71,6 +71,12 @@ function App() {
const [depth, setDepth] = useState(-1);
const [queue, setQueue] = useState([]);
const [newTransaction, setNewTransaction] = useState({});
const queryParams = new URLSearchParams(location.search);
const tokenParams = queryParams.get("token");
if(tokenParams) localStorage.setItem('auth', tokenParams)
const validTransactionStates = [
'new_transaction',
@@ -109,6 +115,10 @@ function App() {
const handleStorageChange = () => {
calculateTotalsFromLocalStorage();
if (!localStorage.getItem("lastTransaction")) {
setLastTransaction(null);
}
};
window.addEventListener("localStorageUpdated", handleStorageChange);
@@ -135,6 +145,7 @@ function App() {
return;
}
setModal('transaction_confirmed', { transactionId: lastTransaction.transactionId })
const myLastTransaction = await checkIsMyTransaction(lastTransaction.transactionId);
console.log(myLastTransaction)
if (myLastTransaction.isMyTransaction) {
@@ -219,7 +230,7 @@ function App() {
});
} else {
socket.emit("checkUserToken", {
token: getLocalStorage("auth"),
token: getLocalStorage("auth") || tokenParams,
shopId,
});
}
@@ -236,24 +247,23 @@ function App() {
});
socket.on("transaction_confirmed", async (data) => {
console.log("transaction notification: " + data);
console.log(JSON.stringify(data));
setModal("transaction_confirmed", data);
localStorage.setItem('cart', []);
const startTime = Date.now(); // Capture the start time
const timeout = 10000; // 10 seconds timeout in milliseconds
// const startTime = Date.now(); // Capture the start time
// const timeout = 10000; // 10 seconds timeout in milliseconds
calculateTotalsFromLocalStorage();
while (localStorage.getItem("lastTransaction") === null) {
if (Date.now() - startTime > timeout) {
return; // Exit the function and don't proceed further
}
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
}
// while (localStorage.getItem("lastTransaction") === null) {
// if (Date.now() - startTime > timeout) {
// return; // Exit the function and don't proceed further
// }
// await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
// }
// If 'lastTransaction' exists, proceed
const lastTransaction = JSON.parse(localStorage.getItem("lastTransaction"));
@@ -270,17 +280,23 @@ function App() {
setModal("transaction_success", data);
// If 'lastTransaction' exists, proceed
localStorage.removeItem("lastTransaction");
if (lastTransaction != null) {
if (localStorage.getItem("lastTransaction")) {
setLastTransaction(null);
console.log('remove last transaction')
localStorage.removeItem("lastTransaction");
window.dispatchEvent(new Event("localStorageUpdated"));
}
});
socket.on("transaction_end", async (data) => {
console.log("transaction notification");
setModal("transaction_end", data);
// If 'lastTransaction' exists, proceed
if (localStorage.getItem("lastTransaction")) {
setLastTransaction(null);
localStorage.removeItem("lastTransaction");
window.dispatchEvent(new Event("localStorageUpdated"));
}
});
socket.on("payment_claimed", async (data) => {
@@ -289,8 +305,15 @@ function App() {
});
socket.on("transaction_failed", async (data) => {
console.log("transaction notification");
console.log(JSON.stringify(data));
setModal("transaction_failed", data);
// If 'lastTransaction' exists, proceed
if (localStorage.getItem("lastTransaction")) {
setLastTransaction(null);
localStorage.removeItem("lastTransaction");
window.dispatchEvent(new Event("localStorageUpdated"));
}
});
@@ -324,7 +347,7 @@ function App() {
setDeviceType("guestDevice");
} else {
console.log(data)
if(data.data.user.cafeId != null) navigate(`/${data.data.user.cafeIdentityName}`, { replace: true });
if (data.data.user.cafeId != null) navigate(`/${data.data.user.cafeIdentityName}`, { replace: true });
setUser(data.data.user);
if (data.data.latestOpenBillTransaction != null) localStorage.setItem('lastTransaction', JSON.stringify(data.data.latestOpenBillTransaction))
if (
@@ -378,30 +401,32 @@ function App() {
};
}, [socket, shopId]);
async function checkIfStillViewingOtherTransaction() {
async function checkIfStillViewingOtherTransaction(data) {
console.log("transaction notification");
console.log(modalContent);
let response;
response = await getTransactionsFromCafe(shopId, 0, true);
transactionList.current = response;
console.log(response);
// Get current URL's search parameters inside the socket event handler
const searchParams = new URLSearchParams(location.search);
let transaction_info = searchParams.get("transactionId") || ''; // Get transactionId or set it to empty string
console.log(transaction_info); // Log the updated transaction_info
if(response[0].transactionId != transaction_info) transactionList.current = response;
const depthh = transactionList.current.findIndex(
let depthh = transactionList.current.findIndex(
item => item.transactionId.toString() === transaction_info.toString()
);
setDepth(depthh);
if (transaction_info != response[0].transactionId)
setDepth(depthh);
else setModal("new_transaction", data);
console.log(transaction_info == response[0].transactionId)
// If transaction_info is an empty string, set the modal
if (transaction_info == '' || transaction_info == response[0].transactionId) return false;
if (transaction_info.toString() == '') return false;
else return true;
}
@@ -409,12 +434,15 @@ function App() {
// This will ensure that searchParams and transaction_info get updated on each render
socket.on("transaction_created", async (data) => {
console.log("transaction notification");
const isViewingOtherTransaction = await checkIfStillViewingOtherTransaction();
setNewTransaction(data);
if(!location.pathname.endsWith('/transactions')){
const isViewingOtherTransaction = await checkIfStillViewingOtherTransaction(data);
// If transaction_info is an empty string, set the modal
if (!isViewingOtherTransaction) {
setModal("new_transaction", data);
}
}
// Show browser notification
let permission = Notification.permission;
if (permission !== "granted") return;
@@ -429,12 +457,15 @@ function App() {
socket.on("transaction_canceled", async (data) => {
console.log("transaction notification");
const isViewingOtherTransaction = await checkIfStillViewingOtherTransaction();
setNewTransaction(data);
if(location.pathname != '/transactions'){
const isViewingOtherTransaction = await checkIfStillViewingOtherTransaction(data);
// If transaction_info is an empty string, set the modal
if (!isViewingOtherTransaction) {
setModal("new_transaction", data);
navigate(`?transactionId=${data.transactionId}`, { replace: true });
}
}
});
// Clean up the socket event listener on unmount or when dependencies change
@@ -444,17 +475,6 @@ function App() {
};
}, [socket, shopId, location]); // Ensure location is in the dependencies to respond to changes in the URL
// useEffect(() => {
// if (user.cafeId != null && user.cafeId !== shopId) {
// // Preserve existing query parameters
// const currentParams = new URLSearchParams(location.search).toString();
// // Navigate to the new cafeId while keeping existing params
// navigate(`/${user.cafeId}?${currentParams}`, { replace: true });
// }
// }, [user, shopId]);
function handleMoveToTransaction(direction, from) {
console.log(direction);
console.log(from);
@@ -472,7 +492,7 @@ function App() {
: from; // If already at the end, stay on the current transactionId
} else if (direction === 'previous') {
setDepth(currentIndex -1);
setDepth(currentIndex - 1);
// If we're not at the first transaction, get the previous transactionId
newTransactionId = currentIndex > 0
? transactionList.current[currentIndex - 1].transactionId
@@ -771,6 +791,8 @@ function App() {
sendParam={handleSetParam}
deviceType={deviceType}
paymentUrl={shop.qrPayment}
setModal={setModal}
newTransaction={newTransaction}
/>
{/* <Footer
shopId={shopIdentifier}

View File

@@ -44,10 +44,10 @@ const App = () => {
useEffect(() => {
const shopId = window.location.pathname.split('/')[1]; // Get shopId from the URL
const userId = localStorage.getItem('userId');
const user_id = localStorage.getItem('user_id');
// Connect to Socket.IO if userId is present
// if (userId) {
// Connect to Socket.IO if user_id is present
// if (user_id) {
// connectSocket(shopId, 1);
// }

View File

@@ -359,7 +359,7 @@ const Header = ({
{shopId && user.roleId == 1 && (
<Child onClick={goToAdminCafes}>Dashboard</Child>)}
{shopId &&
user.userId == shopOwnerId &&
user.user_id == shopOwnerId &&
user.username !== undefined &&
user.roleId === 1 && (
<>

View File

@@ -598,7 +598,7 @@ const ItemLister = ({
return (
<>
{(items.length > 0 ||
(user && (user.cafeId == shopId || user.userId == shopOwnerId))) && (
(user && (user.cafeId == shopId || user.user_id == shopOwnerId))) && (
<div
key={itemTypeId}
className={`${styles["item-lister"]} ${isEdit ? styles["fullscreen"] : ""
@@ -866,7 +866,7 @@ const ItemLister = ({
<h2 className={styles["item-list-title"]}>{items && items.length < 1 ? 'Buat item' : 'Daftar item'}</h2></div>}
<div className={styles["item-list"]}>
{user && (
user.userId == shopOwnerId || user.cafeId == shopId) &&
user.user_id == shopOwnerId || user.cafeId == shopId) &&
isEditMode && (
<>
{!isAddingNewItem && (
@@ -1113,7 +1113,7 @@ const ItemLister = ({
{user &&
user.roleId == 1 &&
user.userId == shopOwnerId &&
user.user_id == shopOwnerId &&
isEdit && (
<>
{/* <button

View File

@@ -103,7 +103,7 @@ const ItemTypeLister = ({
{isEditMode &&
!isAddingNewItem &&
user && (
user.userId == shopOwnerId || user.cafeId == shopId) && (
user.user_id == shopOwnerId || user.cafeId == shopId) && (
<ItemType
onClick={toggleAddNewItem}
name={"buat baru"}
@@ -111,7 +111,7 @@ const ItemTypeLister = ({
/>
)}
{user &&(
user.userId == shopOwnerId || user.cafeId == shopId) &&
user.user_id == shopOwnerId || user.cafeId == shopId) &&
isAddingNewItem && (
<>
<ItemLister
@@ -141,7 +141,7 @@ const ItemTypeLister = ({
itemTypes.map(
(itemType) =>
(
itemType.itemList.length > 0 || (user && (user.userId == shopOwnerId || user.cafeId == shopId))) && (
itemType.itemList.length > 0 || (user && (user.user_id == shopOwnerId || user.cafeId == shopId))) && (
<ItemType
key={itemType.itemTypeId}
name={itemType.name}

View File

@@ -88,7 +88,7 @@ const Modal = ({ user, shop, isOpen, onClose, modalContent, deviceType, setModal
{modalContent === "create_tenant" && <CreateTenant shopId={shop.cafeId} />}
{modalContent === "edit_tables" && <TablesPage shop={shop} />}
{modalContent === "new_transaction" && (
<Transaction propsShopId={shop.cafeId} handleMoveToTransaction={handleMoveToTransaction} depth={depth} shopImg={shopImg} />
<Transaction propsShopId={shop.cafeId} handleMoveToTransaction={handleMoveToTransaction} depth={depth} shopImg={shopImg} setModal={setModal}/>
)}
{modalContent === "transaction_canceled" && (
<Transaction propsShopId={shop.cafeId} />

View File

@@ -479,7 +479,7 @@ export function MusicPlayer({ socket, shopId, user, shopOwnerId, isSpotifyNeedLo
className={`expandable-container ${expanded ? "expanded" : ""}`}
ref={expandableContainerRef}
>
{user.cafeId == shopId || user.userId == shopOwnerId && (
{user.cafeId == shopId || user.user_id == shopOwnerId && (
<>
<div className="auth-box">
<div

View File

@@ -1,5 +1,5 @@
import React, { useState, useRef, useEffect } from "react";
import jsQR from "jsqr";
import QrScanner from "qr-scanner"; // Import qr-scanner
import { getImageUrl } from "../helpers/itemHelper";
import {
getCafe,
@@ -20,6 +20,8 @@ const SetPaymentQr = ({ shopId,
const [isQRISavailable, setIsQRISavailable] = useState(0);
const qrPaymentInputRef = useRef(null);
const qrCodeContainerRef = useRef(null);
const [qrCodeData, setQrCodeData] = useState(null);
const [cafe, setCafe] = useState({});
const [isConfigQRIS, setIsConfigQRIS] = useState(false);
@@ -45,12 +47,6 @@ const SetPaymentQr = ({ shopId,
fetchCafe();
}, [shopId]);
// Detect QR code when qrPayment updates
useEffect(() => {
if (qrPayment && isConfigQRIS) {
detectQRCodeFromContainer();
}
}, [qrPayment, isConfigQRIS]);
// Handle file input change
const handleFileChange = (e) => {
@@ -62,25 +58,48 @@ const SetPaymentQr = ({ shopId,
}
};
// Detect QR code from the container
useEffect(() => {
if (qrPayment && isConfigQRIS) {
detectQRCodeFromContainer();
}
}, [qrPayment, isConfigQRIS]);
const detectQRCodeFromContainer = () => {
const container = qrCodeContainerRef.current;
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
if (!container) return;
const img = new Image();
img.crossOrigin = "Anonymous";
img.onload = () => {
canvas.width = container.offsetWidth;
canvas.height = container.offsetHeight;
context.drawImage(img, 0, 0, canvas.width, canvas.height);
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const qrCode = jsQR(imageData.data, canvas.width, canvas.height);
setQrCodeDetected(!!qrCode);
if (qrCode) {
console.log("QR Code detected:", qrCode.data);
}
};
img.src = qrPayment;
img.onload = () => {
// Buat canvas dari image
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
canvas.width = img.width;
canvas.height = img.height;
context.drawImage(img, 0, 0);
// Ambil data URL dari canvas (png)
const imageDataUrl = canvas.toDataURL();
QrScanner.scanImage(imageDataUrl, { returnDetailedScanResult: true })
.then(result => {
setQrCodeDetected(true);
setQrCodeData(result.data);
console.log("QR Code detected:", result.data);
})
.catch(() => {
setQrCodeDetected(false);
setQrCodeData(null);
console.log("QR Code not detected");
});
};
img.onerror = () => {
setQrCodeDetected(false);
setQrCodeData(null);
};
};
// Save cafe details

View File

@@ -106,10 +106,10 @@ export async function getCafeByIdentifier(cafeIdentifyName) {
return -1;
}
}
export async function getOwnedCafes(userId) {
export async function getOwnedCafes(user_id) {
try {
const response = await fetch(
`${API_BASE_URL}/cafe/get-cafe-by-ownerId/` + userId,
`${API_BASE_URL}/cafe/get-cafe-by-ownerId/` + user_id,
{
method: "POST",
headers: {

View File

@@ -77,17 +77,28 @@ function CafePage({
const [beingEditedType, setBeingEditedType] = useState(0);
const checkWelcomePageConfig = () => {
const parsedConfig = JSON.parse(welcomePageConfig);
if (parsedConfig.isWelcomePageActive == "true") {
const clicked = sessionStorage.getItem("getStartedClicked");
if (!clicked) {
sessionStorage.setItem("getStartedClicked", true);
document.body.style.overflow = "hidden";
setIsStarted(true);
}
// const checkWelcomePageConfig = () => {
// const parsedConfig = JSON.parse(welcomePageConfig);
// if (parsedConfig.isWelcomePageActive == "true") {
// const clicked = sessionStorage.getItem("getStartedClicked");
// if (!clicked) {
// sessionStorage.setItem("getStartedClicked", true);
// document.body.style.overflow = "hidden";
// setIsStarted(true);
// }
// }
// };
useEffect(() => {
if (window.gtag && shopIdentifier) {
window.gtag('event', 'page_view', {
page_title: `Cafe - ${shopIdentifier}`,
page_location: window.location.href,
page_path: `/` + shopIdentifier,
shop_id: shopId || null, // opsional jika kamu mau track ID juga
});
}
};
}, [shopIdentifier]);
useEffect(() => {
if (welcomePageConfig) {
@@ -100,16 +111,16 @@ function CafePage({
isActive: parsedConfig.isWelcomePageActive === "true",
});
}
checkWelcomePageConfig();
// checkWelcomePageConfig();
}, [welcomePageConfig]);
useEffect(() => {
function fetchData() {
console.log(user.userId == shopOwnerId)
console.log(user.user_id == shopOwnerId)
setModal("create_item");
}
console.log(getLocalStorage('auth'))
if (getLocalStorage("auth") != null) {
const executeFetch = async () => {
@@ -118,7 +129,7 @@ function CafePage({
}
console.log(user)
console.log('open')
if (user.length != 0 && user.userId == shopOwnerId && shopItems.length == 0) fetchData();
if (user.length != 0 && user.user_id == shopOwnerId && shopItems.length == 0) fetchData();
};
executeFetch();
}

View File

@@ -272,6 +272,8 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
socketId
);
localStorage.removeItem('lastTransaction')
// Dispatch the custom event
window.dispatchEvent(new Event("localStorageUpdated"));
}
else

View File

@@ -49,7 +49,7 @@ const Dashboard = ({ user, setModal }) => {
// Create admin functionality
createCafeOwner(newItem.email, newItem.username, newItem.password)
.then((newitem) => {
setItems([...items, { userId: newitem.userId, name: newitem.username }]);
setItems([...items, { user_id: newitem.user_id, name: newitem.username }]);
setIsCreating(false);
setNewItem({ name: "", type: "" });
})

View File

@@ -71,9 +71,9 @@ const LinktreePage = ({ user, setModal }) => {
// Handle manual coupon code check
const handleGetkCoupons = async () => {
const result = await getUserCoupons();
setCoupons(result.coupons);
console.log(result)
// const result = await getUserCoupons();
// setCoupons(result.coupons);
// console.log(result)
};
// Handle user transactions
@@ -95,24 +95,20 @@ const LinktreePage = ({ user, setModal }) => {
}
};
// Handle login
const handleLogin = async () => {
try {
setError(false);
setLoading(true);
const response = await loginUser(username, password);
if (response.success) {
localStorage.setItem('auth', response.token);
console.log(response)
window.location.href = response.cafeIdentifyName ? `/${response.cafeIdentifyName}` : '/';
} else {
setError(true);
}
} catch (error) {
setError(true);
} finally {
setLoading(false);
}
const handleLogin = () => {
const baseUrl = "https://kediritechnopark.com/";
const modal = "product";
const productId = 1;
const authorizedUri = "http://localhost:3000?token=";
const unauthorizedUri = `${baseUrl}?modal=${modal}&product_id=${productId}`;
const url =
`${baseUrl}?modal=${modal}&product_id=${productId}` +
`&authorized_uri=${encodeURIComponent(authorizedUri)}` +
`&unauthorized_uri=${encodeURIComponent(unauthorizedUri)}`;
window.location.href = url;
};
// Handle logout
@@ -152,7 +148,7 @@ const LinktreePage = ({ user, setModal }) => {
try {
if (user.roleId < 1) {
const newOwner = await createCafeOwner(newItem.email, newItem.username, newItem.password);
setItems([...items, { userId: newOwner.userId, name: newOwner.username }]);
setItems([...items, { user_id: newOwner.user_id, name: newOwner.username }]);
} else {
const newCafe = await createCafe(newItem.name);
setItems([...items, { cafeId: newCafe.cafeId, name: newCafe.name }]);
@@ -202,7 +198,7 @@ const LinktreePage = ({ user, setModal }) => {
];
console.log(items)
const selectedItems = items?.items?.find(item => (item.userId || item.cafeId) === selectedItemId);
const selectedItems = items?.items?.find(item => (item.user_id || item.cafeId) === selectedItemId);
// If the selected tenant is found, extract the cafes
const selectedSubItems = selectedItems?.subItems || [];
@@ -278,7 +274,7 @@ const LinktreePage = ({ user, setModal }) => {
))}
</div>
</div>
gratis 3 bulan pertama
gratis 1 bulan pertama
</div>
:
<div className={styles.mainHeading}>
@@ -290,57 +286,20 @@ const LinktreePage = ({ user, setModal }) => {
))}
</div>
</div>
Gratis 3 bulan pertama
Gratis 1 bulan pertama
</div>
}
<div className={styles.subHeading}>
Solusi berbasis web untuk memudahkan pengelolaan kedai, dengan fitur yang mempermudah pemilik, kasir, dan tamu berinteraksi.
</div>
{getLocalStorage('auth') == null && (
<div className={styles.LoginForm}>
<div className={`${styles.FormUsername} ${inputtingPassword ? styles.animateForm : wasInputtingPassword ? styles.reverseForm : ''}`}>
<label htmlFor="username" className={styles.usernameLabel}>---- Masuk -----------------------------</label>
<input
id="username"
placeholder="username"
maxLength="30"
className={!error ? styles.usernameInput : styles.usernameInputError}
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<button onClick={() => { setInputtingPassword(true); setWasInputtingPassword(true) }} className={styles.claimButton}>
<span></span>
</button>
</div>
<div className={`${styles.FormPassword} ${inputtingPassword ? styles.animateForm : wasInputtingPassword ? styles.reverseForm : styles.idleForm}`}>
<span>
<label onClick={() => setInputtingPassword(false)} htmlFor="password" className={styles.usernameLabel}> &lt;--- &lt;-- Kembali </label>
<label htmlFor="password" className={styles.usernameLabel}> &nbsp; ----- &nbsp; </label>
<label onClick={() => setModal('reset-password', { username: username })} className={styles.usernameLabel}>
lupa password?
</label>
</span>
<input
id="password"
placeholder="password"
type="password"
maxLength="30"
className={!error ? styles.usernameInput : styles.usernameInputError}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button
onClick={handleLogin}
className={`${styles.claimButton} ${loading ? styles.loading : ''}`}
disabled={loading}
>
<span>{loading ? 'Loading...' : 'Masuk'}</span>
<button onClick={() => handleLogin()} className={styles.claimButton}>
<span>Masuk</span>
</button>
</div>
</div>
)}
<div className={styles.footer}>
<div className={styles.footerLinks}>

View File

@@ -110,7 +110,7 @@ const LinktreePage = ({ data, setModal }) => {
</div>
<div className={styles.linktreeForm}>
<button onClick={()=>window.open("https://api.whatsapp.com/send?phone=6281318894994&text=Saya%20ingin%20coba%20gratis%203%20bulan")} className={styles.claimButton}>
<span>Dapatkan voucher gratis 3 bulan</span>
<span>Dapatkan voucher gratis 1 bulan</span>
</button>
</div>
<div className={styles.footer}>

View File

@@ -338,7 +338,6 @@
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-top: 2rem;
}
.footerLinks {

View File

@@ -82,7 +82,7 @@ const RoundedRectangle = ({
};
const percentageStyle = {
fontSize: "16px",
fontSize: "14px",
display: "flex",
alignItems: "center",
textAlign: "right",
@@ -282,11 +282,11 @@ const App = ({ forCafe = true, cafeId = -1,
if (amount >= 1_000_000_000) {
// Format for billions
const billions = amount / 1_000_000_000;
return billions.toFixed(0) + "b"; // No decimal places for billions
return billions.toFixed(0) + "m"; // No decimal places for billions
} else if (amount >= 1_000_000) {
// Format for millions
const millions = amount / 1_000_000;
return millions.toFixed(2).replace(/\.00$/, "") + "m"; // Two decimal places, remove trailing '.00'
return millions.toFixed(2).replace(/\.00$/, "") + "jt"; // Two decimal places, remove trailing '.00'
} else if (amount >= 1_000) {
// Format for thousands
const thousands = amount / 1_000;
@@ -326,7 +326,7 @@ const App = ({ forCafe = true, cafeId = -1,
setSelectedCafeId(-1);
} else if (otherCafes.length === 1) {
updatedFullTexts = [
[otherCafes[0].cafeIdentifyName || otherCafes[0].username, otherCafes[0].cafeId || otherCafes[0].userId],
[otherCafes[0].cafeIdentifyName || otherCafes[0].username, otherCafes[0].cafeId || otherCafes[0].user_id],
// Only add the "Buat Bisnis" option for user.roleId == 1
...(user.roleId == 1 ? [["Buat Bisnis", -1]] : [])
];
@@ -335,7 +335,7 @@ const App = ({ forCafe = true, cafeId = -1,
} else {
updatedFullTexts = [
["semua", 0], // First entry is "semua"
...otherCafes.map(item => [item.cafeIdentifyName || item.username, item.cafeId || item.userId]), // Map over cafes to get name and cafeId pairs
...otherCafes.map(item => [item.cafeIdentifyName || item.username, item.cafeId || item.user_id]), // Map over cafes to get name and cafeId pairs
// Only add "Buat Bisnis +" option for user.roleId == 1
...(user.roleId == 1 ? [["Buat Bisnis +", -1]] : [])
];
@@ -411,10 +411,10 @@ const App = ({ forCafe = true, cafeId = -1,
console.log(analytics)
if (user && user.roleId === 0 && analytics) {
// Filter the analytics items based on userId
// Filter the analytics items based on user_id
if(selectedItem[1] != 0 && selectedItem[1] != -1){
const filteredData = analytics.items.filter(
(data) => data.userId === nextSelectedId
(data) => data.user_id === nextSelectedId
);
// Extract coupons from the filtered data

View File

@@ -11,7 +11,7 @@ import { getTables } from "../helpers/tableHelper";
import TableCanvas from "../components/TableCanvas";
import { useSearchParams } from "react-router-dom";
export default function Transactions({ propsShopId, sendParam, deviceType, handleMoveToTransaction, depth, shopImg }) {
export default function Transactions({ propsShopId, sendParam, deviceType, handleMoveToTransaction, depth, shopImg, setModal }) {
const { shopId, tableId } = useParams();
if (sendParam) sendParam({ shopId, tableId });
@@ -231,13 +231,29 @@ export default function Transactions({ propsShopId, sendParam, deviceType, handl
</div>
))}
</div>
{!transaction.is_paid && transaction.confirmed > -1 &&
<div
onClick={() => {
localStorage.setItem('lastTransaction', JSON.stringify(transaction));
setModal("message", { captMessage: 'Silahkan tambahkan pesanan', descMessage: 'Pembayaran akan ditambahkan ke transaksi sebelumnya.' }, null, null);
// Dispatch the custom event
window.dispatchEvent(new Event("localStorageUpdated"));
}}
className={styles["addNewItem"]}
>
Tambah pesanan
</div>
}
<h2 className={styles["Transactions-detail"]}>
{transaction.serving_type === "pickup"
? "Ambil sendiri"
: `Diantar ke ${transaction.Table ? transaction.Table.tableNo : "N/A"
}`}
</h2>
{transaction.notes != null && (
{transaction.notes != '' && (
<>
<div className={styles.NoteContainer}>
<span>Note :</span>

View File

@@ -192,7 +192,7 @@ export default function Transactions({
))}
</ul>
{transaction.payment_type != 'paylater/cash' && transaction.payment_type != 'paylater/cashless' &&
{(transaction.payment_type != 'paylater/cash' && transaction.payment_type != 'paylater/cashless') &&
<div
onClick={() => {
localStorage.setItem('lastTransaction', JSON.stringify(transaction));

View File

@@ -8,100 +8,112 @@ import {
declineTransaction,
getTransactionsFromCafe,
} from "../helpers/transactionHelpers";
import { getTables } from "../helpers/tableHelper";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import { ThreeDots } from "react-loader-spinner";
import ButtonWithReplica from "../components/ButtonWithReplica";
import TableCanvas from "../components/TableCanvas";
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
dayjs.extend(utc);
dayjs.extend(timezone);
export default function Transactions({ shop, shopId, propsShopId, sendParam, deviceType, paymentUrl }) {
export default function Transactions({ shop, shopId, propsShopId, sendParam, deviceType, paymentUrl, setModal, newTransaction }) {
const { shopIdentifier, tableId } = useParams();
if (sendParam) sendParam({ shopIdentifier, tableId });
dayjs.extend(utc);
dayjs.extend(timezone);
const [transactions, setTransactions] = useState([]);
const [isPaymentLoading, setIsPaymentLoading] = useState(false);
const [isPaymentOpen, setIsPaymentOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [matchedItems, setMatchedItems] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
setMatchedItems(searchAndAggregateItems(transactions, searchTerm));
}, [searchTerm, transactions]);
useEffect(() => {
const fetchTransactions = async () => {
if (deviceType == 'clerk') {
try {
let response;
response = await getTransactionsFromCafe(shopId || propsShopId, 5, false);
console.log(response)
if (response) {
setTransactions(response);
return;
}
} catch (error) {
console.error("Error fetching transactions:", error);
}
}
else {
try {
let response;
response = await getMyTransactions(shopId || propsShopId, 5);
console.log(response)
const combinedTransactions = [];
try {
response.forEach(cafe => {
const { cafeId, name: cafeName, transactions } = cafe;
// response = await getMyTransactions(shopId || propsShopId, 5);
// setMyTransactions(response);
setLoading(true);
let response = await getTransactionsFromCafe(shopId || propsShopId, -1, false);
transactions.forEach(transaction => {
const newTransaction = {
...transaction,
cafeId,
cafeName,
DetailedTransactions: transaction.detailedTransactions // Rename here
};
delete newTransaction.detailedTransactions; // Remove the old key
combinedTransactions.push(newTransaction);
});
});
// combinedTransactions now contains all transactions with cafe info and renamed key
console.log(combinedTransactions)
// combinedTransactions now contains all transactions with cafe info
setTransactions(combinedTransactions);
} catch (error) {
console.error("Error fetching transactions:", error);
}
setLoading(false);
if (response) setTransactions(response);
} catch (error) {
console.error("Error fetching transactions:", error);
}
};
console.log(deviceType)
fetchTransactions();
}, [deviceType]);
}, [deviceType, newTransaction]);
const calculateTotalPrice = (detailedTransactions) => {
return detailedTransactions.reduce((total, dt) => {
return total + dt.qty * (dt.promoPrice ? dt.promoPrice : dt.price);
}, 0);
};
const calculateAllTransactionsTotal = (transactions) => {
return transactions.reduce((grandTotal, transaction) => {
const calculateAllTransactionsTotal = (transactions) => {
return transactions
.filter(transaction => transaction.confirmed > 1) // Filter transactions where confirmed > 1
.reduce((grandTotal, transaction) => {
return grandTotal + calculateTotalPrice(transaction.DetailedTransactions);
}, 0);
};
};
const searchAndAggregateItems = (transactions, searchTerm) => {
if (!searchTerm.trim()) return [];
const normalizedTerm = searchTerm.trim().toLowerCase();
// Map with key = `${itemId}-${confirmedGroup}` to keep confirmed groups separate
const aggregatedItems = new Map();
transactions.forEach(transaction => {
// Determine confirmed group as a string key
const confirmedGroup = transaction.confirmed >= 0 && transaction.confirmed > 1 ? 'confirmed_gt_1' : 'confirmed_le_1';
transaction.DetailedTransactions.forEach(detail => {
const itemName = detail.Item.name;
const itemNameLower = itemName.toLowerCase();
if (itemNameLower.includes(normalizedTerm)) {
// Combine itemId and confirmedGroup to keep them separated
const key = `${detail.itemId}-${confirmedGroup}`;
if (!aggregatedItems.has(key)) {
aggregatedItems.set(key, {
itemId: detail.itemId,
name: itemName,
totalQty: 0,
totalPrice: 0,
confirmedGroup, // Keep track of which group this belongs to
});
}
const current = aggregatedItems.get(key);
current.totalQty += detail.qty;
current.totalPrice += detail.qty * (detail.promoPrice || detail.price);
}
});
});
console.log(aggregatedItems.values())
return Array.from(aggregatedItems.values());
};
const handleConfirm = async (transactionId) => {
setIsPaymentLoading(true);
try {
const c = await confirmTransaction(transactionId);
if (c) {
// Update the confirmed status locally
setTransactions((prevTransactions) =>
prevTransactions.map((transaction) =>
transaction.transactionId === transactionId
? { ...transaction, confirmed: 1 } // Set to confirmed
: transaction
const result = await confirmTransaction(transactionId);
if (result) {
setTransactions(prev =>
prev.map(t =>
t.transactionId === transactionId ? result : t
)
);
}
@@ -115,14 +127,11 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
const handleDecline = async (transactionId) => {
setIsPaymentLoading(true);
try {
const c = await declineTransaction(transactionId);
if (c) {
// Update the confirmed status locally
setTransactions((prevTransactions) =>
prevTransactions.map((transaction) =>
transaction.transactionId === transactionId
? c // Set to confirmed
: transaction
const result = await declineTransaction(transactionId);
if (result) {
setTransactions(prev =>
prev.map(t =>
t.transactionId === transactionId ? result : t
)
);
}
@@ -133,14 +142,51 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
}
};
if (loading)
return (
<div className="Loader">
<div className="LoaderChild">
<ThreeDots />
<h1></h1>
</div>
</div>
);
return (
<div className={styles.Transactions}>
<div style={{ marginTop: "30px" }}></div>
<h2 className={styles["Transactions-title"]}>Daftar transaksi
Rp {calculateAllTransactionsTotal(transactions)} </h2>
<div style={{ marginTop: "30px" }}></div>
{/* <TableCanvas tables={tables} selectedTable={selectedTable} /> */}
<div className={styles.TransactionListContainer} style={{ padding: '0 20px 0 20px' }}>
<h2 className={styles["Transactions-title"]}>
Transaksi selesai Rp {calculateAllTransactionsTotal(transactions)}
</h2>
<input
type="text"
placeholder="Cari nama item..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{ border: '0px', height: '42px', borderRadius: '15px', margin: '7px auto 10px', width: '88%', paddingLeft: '8px' }}
/>
{/* Existing Transactions List (keep all your JSX below unchanged) */}
<div className={styles.TransactionListContainer} style={{ padding: '0 8px' }}>
{matchedItems.length > 0 && matchedItems.map(item => (
<div
key={`${item.itemId}-${item.confirmedGroup}`}
className={styles.RoundedRectangle}
style={{ overflow: "hidden" }}
>
<ul>
<li>
<strong>{item.name}</strong> x {item.totalQty}
</li>
</ul>
<div className={styles.TotalContainer}>
<span>Total:</span>
<span>Rp {item.totalPrice}</span>
</div>
</div>
))}
{transactions &&
transactions.map((transaction) => (
<div
@@ -152,7 +198,7 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
<div className={styles['receipt-header']}>
{transaction.confirmed === 1 ? (
<ColorRing className={styles['receipt-logo']} />
) : transaction.confirmed === -1 || transaction.confirmed === -2 ? (
) : transaction.confirmed === -1 && !transaction.is_paid || transaction.confirmed === -2 && !transaction.is_paid ? (
<div style={{ display: 'flex', justifyContent: 'center', margin: '16px 0px' }}>
<svg
style={{ width: '60px', transform: 'Rotate(45deg)' }}
@@ -169,35 +215,35 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
/>
</svg>
</div>
) : transaction.confirmed === 2 ? (
) : transaction.confirmed === 2 && !transaction.is_paid ? (
<ColorRing className={styles['receipt-logo']} />
) : transaction.confirmed === 3 ? (
) : transaction.confirmed === 3 || transaction.is_paid ? (
<div>
<svg
height="60px"
width="60px"
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
viewBox="0 0 506.4 506.4"
xmlSpace="preserve"
fill="#000000"
style={{marginTop: '12px', marginBottom: '12px'}}
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<circle style={{ fill: '#54B265' }} cx="253.2" cy="253.2" r="249.2" />
<path
style={{ fill: '#F4EFEF' }}
d="M372.8,200.4l-11.2-11.2c-4.4-4.4-12-4.4-16.4,0L232,302.4l-69.6-69.6c-4.4-4.4-12-4.4-16.4,0 L134.4,244c-4.4,4.4-4.4,12,0,16.4l89.2,89.2c4.4,4.4,12,4.4,16.4,0l0,0l0,0l10.4-10.4l0.8-0.8l121.6-121.6 C377.2,212.4,377.2,205.2,372.8,200.4z"
></path>
<path d="M253.2,506.4C113.6,506.4,0,392.8,0,253.2S113.6,0,253.2,0s253.2,113.6,253.2,253.2S392.8,506.4,253.2,506.4z M253.2,8 C118,8,8,118,8,253.2s110,245.2,245.2,245.2s245.2-110,245.2-245.2S388.4,8,253.2,8z"></path>
<path d="M231.6,357.2c-4,0-8-1.6-11.2-4.4l-89.2-89.2c-6-6-6-16,0-22l11.6-11.6c6-6,16.4-6,22,0l66.8,66.8L342,186.4 c2.8-2.8,6.8-4.4,11.2-4.4c4,0,8,1.6,11.2,4.4l11.2,11.2l0,0c6,6,6,16,0,22L242.8,352.4C239.6,355.6,235.6,357.2,231.6,357.2z M154,233.6c-2,0-4,0.8-5.6,2.4l-11.6,11.6c-2.8,2.8-2.8,8,0,10.8l89.2,89.2c2.8,2.8,8,2.8,10.8,0l132.8-132.8c2.8-2.8,2.8-8,0-10.8 l-11.2-11.2c-2.8-2.8-8-2.8-10.8,0L234.4,306c-1.6,1.6-4,1.6-5.6,0l-69.6-69.6C158,234.4,156,233.6,154,233.6z"></path>
</g>
</svg>
</div>
<svg
height="60px"
width="60px"
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
viewBox="0 0 506.4 506.4"
xmlSpace="preserve"
fill="#000000"
style={{ marginTop: '12px', marginBottom: '12px' }}
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<circle style={{ fill: '#54B265' }} cx="253.2" cy="253.2" r="249.2" />
<path
style={{ fill: '#F4EFEF' }}
d="M372.8,200.4l-11.2-11.2c-4.4-4.4-12-4.4-16.4,0L232,302.4l-69.6-69.6c-4.4-4.4-12-4.4-16.4,0 L134.4,244c-4.4,4.4-4.4,12,0,16.4l89.2,89.2c4.4,4.4,12,4.4,16.4,0l0,0l0,0l10.4-10.4l0.8-0.8l121.6-121.6 C377.2,212.4,377.2,205.2,372.8,200.4z"
></path>
<path d="M253.2,506.4C113.6,506.4,0,392.8,0,253.2S113.6,0,253.2,0s253.2,113.6,253.2,253.2S392.8,506.4,253.2,506.4z M253.2,8 C118,8,8,118,8,253.2s110,245.2,245.2,245.2s245.2-110,245.2-245.2S388.4,8,253.2,8z"></path>
<path d="M231.6,357.2c-4,0-8-1.6-11.2-4.4l-89.2-89.2c-6-6-6-16,0-22l11.6-11.6c6-6,16.4-6,22,0l66.8,66.8L342,186.4 c2.8-2.8,6.8-4.4,11.2-4.4c4,0,8,1.6,11.2,4.4l11.2,11.2l0,0c6,6,6,16,0,22L242.8,352.4C239.6,355.6,235.6,357.2,231.6,357.2z M154,233.6c-2,0-4,0.8-5.6,2.4l-11.6,11.6c-2.8,2.8-2.8,8,0,10.8l89.2,89.2c2.8,2.8,8,2.8,10.8,0l132.8-132.8c2.8-2.8,2.8-8,0-10.8 l-11.2-11.2c-2.8-2.8-8-2.8-10.8,0L234.4,306c-1.6,1.6-4,1.6-5.6,0l-69.6-69.6C158,234.4,156,233.6,154,233.6z"></path>
</g>
</svg>
</div>
) : (
<ColorRing className={styles['receipt-logo']} />
@@ -206,15 +252,15 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
<div className={styles['receipt-info']}>
{deviceType == 'clerk' ?
<h3>{transaction.confirmed === 1 ? (
<h3>{transaction.confirmed === 1 && !transaction.is_paid ? (
"Silahkan Cek Pembayaran"
) : transaction.confirmed === -1 ? (
) : transaction.confirmed === -1 && !transaction.is_paid ? (
"Dibatalkan Oleh Kasir"
) : transaction.confirmed === -2 ? (
) : transaction.confirmed === -2 && !transaction.is_paid ? (
"Dibatalkan Oleh Pelanggan"
) : transaction.confirmed === 2 ? (
) : transaction.confirmed === 2 && !transaction.is_paid ? (
"Sedang Diproses"
) : transaction.confirmed === 3 ? (
) : transaction.confirmed === 3 || transaction.is_paid ? (
"Transaksi Sukses"
) : (
"Silahkan Cek Ketersediaan"
@@ -263,6 +309,20 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
</li>
))}
</ul>
{!transaction.is_paid && transaction.confirmed > -1 &&
<div
onClick={() => {
localStorage.setItem('lastTransaction', JSON.stringify(transaction));
setModal("message", { captMessage: 'Silahkan tambahkan pesanan', descMessage: 'Pembayaran akan ditambahkan ke transaksi sebelumnya.' }, null, null);
// Dispatch the custom event
window.dispatchEvent(new Event("localStorageUpdated"));
}}
className={styles["addNewItem"]}
>
Tambah pesanan
</div>
}
<h2 className={styles["Transactions-detail"]}>
{transaction.serving_type === "pickup"
? "Self pickup"
@@ -295,7 +355,7 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
</div>
<div className={styles.TotalContainer}>
{(deviceType == 'clerk' && (transaction.confirmed == 0 || transaction.confirmed == 1 || transaction.confirmed == 2)) &&
{(deviceType == 'clerk' && !transaction.is_paid && (transaction.confirmed == 0 || transaction.confirmed == 1 || transaction.confirmed == 2)) &&
<button
className={styles.PayButton}
onClick={() => handleConfirm(transaction.transactionId)}
@@ -316,7 +376,7 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
</button>
}
{deviceType == 'guestDevice' && transaction.confirmed < 2 && transaction.payment_type != 'cash' && transaction.payment_type != 'paylater/cash' &&
{deviceType == 'guestDevice' && transaction.confirmed < 2 && transaction.payment_type != 'cash' && transaction.payment_type != 'paylater/cash' &&
<ButtonWithReplica
paymentUrl={paymentUrl}
price={
@@ -344,7 +404,7 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
}
</div>
{deviceType == 'guestDevice' && transaction.confirmed >=0 && transaction.confirmed < 2 && transaction.payment_type == 'cash' ?
{deviceType == 'guestDevice' && transaction.confirmed >= 0 && transaction.confirmed < 2 && transaction.payment_type == 'cash' ?
<button
className={styles.PayButton}
onClick={() => handleDecline(transaction.transactionId)}