Compare commits

..

18 Commits

Author SHA1 Message Date
karyamanswasta
2add1c5090 ok 2025-08-27 08:15:48 +07:00
karyamanswasta
3fbe0bb870 ok 2025-08-27 07:41:31 +07:00
karyamanswasta
dae7fb9221 ok 2025-08-27 07:12:39 +07:00
karyamanswasta
6ed982d6ef ok 2025-08-27 07:00:14 +07:00
karyamanswasta
df203447a9 ok 2025-08-27 05:54:02 +07:00
karyamanswasta
f58b40c70d ok 2025-08-27 03:20:49 +07:00
karyamanswasta
6127415e37 ok 2025-08-27 01:55:07 +07:00
karyamanswasta
b28c6ed0fe ok 2025-08-26 13:07:13 +07:00
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
55 changed files with 12746 additions and 8170 deletions

58
note/detailUI.md Normal file
View File

@@ -0,0 +1,58 @@
**Overlay/Status**
- Loader Overlay: layar penuh dengan spinner `ThreeDots` dan pesan `screenMessag
e`; muncul saat `loading` true.
- Grayscale State: seluruh konten `div.Cafe` menjadi abu-abu saat `isExceededDea
dline` true (kelas `grayscale`).
- Modal Pesan: “Kafe sedang tidak tersedia” dapat muncul via `setModal("message"
, ...)` ketika `isExceededDeadline` dari socket; modalnya dikelola global (di lu
ar file ini).
**Header**
- Bar Atas “Menu”: judul “Menu”, info toko (nama, gambar), profil user, opsi log
out, akses pegawai, table code.
- Edit Mode Toggle: kontrol untuk mengaktifkan/menonaktifkan mode edit kategori/
item (prop `isEditMode` + `setIsEditMode`).
**Branding/Watermark**
- Dev Watermark (atas): `div.Watermark` kecil hanya tampil jika `API_BASE_URL` b
ukan domain resmi prod/dev (indikator environment).
- Footer Watermark: komponen `<Watermark />` di bagian paling bawah halaman.
**Music**
- Music Player: widget pemutar/antrian lagu dengan dukungan Spotify; menampilkan
status login Spotify (`isSpotifyNeedLogin`) dan antrian (`queue`), interaksi vi
a `socket`.
**Kategori (ItemTypeLister)**
- Daftar Kategori: list tipe menu (nama + gambar tipe, visibilitas).
- Filter Kategori: memilih 1 kategori untuk menyaring tampilan item (`filterId`)
.
- Edit Kategori: saat `isEditMode` aktif, bisa memilih tipe yang sedang diedit (
`beingEditedType`) dan mengubah urutan tipe (via kontrol di ItemLister).
**Daftar Item per Kategori (ItemLister x N)**
- Section per Kategori: render berulang untuk setiap tipe yang lolos filter. Men
ampilkan:
- Judul/nama kategori dan gambar (bila ada).
- Daftar item dalam kategori (nama, harga, promo, deskripsi, gambar, visibilit
as).
- Aksi Item:
- Tambah item: tombol/form untuk membuat item (owner/akses yang berwenang).
- Ubah item: edit nama, harga, promo, deskripsi, gambar.
- Reorder Kategori: panah/aksi “naik/turun” pada section untuk memindahkan posis
i kategori (memanggil `moveItemType*`).
- Raw Mode: jika tidak edit dan filter spesifik aktif, `raw` true untuk gaya tam
pilan ringkas.
**Sticky Bar (Keranjang & Transaksi)**
- Tombol Cart (kiri, utama): muncul jika bukan edit mode dan (user login atau ke
ranjang > 0).
- Menampilkan jumlah item (dengan “+” jika ada transaksi terakhir), total harg
a “Rp{totalPrice}” atau teks “Open bill” (jika `lastTransaction.payment_type ==
'paylater'`).
- Posisi: sticky di bawah (offset bottom ~40px), lebar responsif.
- Tombol Transactions (kanan, kecil): hanya muncul jika user login; navigasi ke
riwayat transaksi.
material list, material

11023
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,12 +10,15 @@
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"apexcharts": "^5.3.4",
"caniuse-lite": "^1.0.30001690", "caniuse-lite": "^1.0.30001690",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"jsqr": "^1.4.0", "jsqr": "^1.4.0",
"lucide-react": "^0.541.0",
"qr-scanner": "^1.4.2",
"qrcode.react": "^3.1.0", "qrcode.react": "^3.1.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-apexcharts": "^1.7.0", "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`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>KedaiMaster</title> <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> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -1,14 +1,21 @@
@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Aboreto&family=Rubik+Doodle+Shadow&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Aboreto&family=Rubik+Doodle+Shadow&display=swap");
@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"); @import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@200;300;400;500;600;700;800&display");
:root {
--brand-primary: #73a585; /* general brand (e. g., music player) */
--brand-sage: #6B8F71; /* sage green for active category */
--brand-sage-50: #F0F6F2; /* very light hover bg */
--brand-sage-100: #E9F3ED; /* light hover bg */
--brand-sage-hover: #7FAE7D; /* hover for filled buttons */
--brand-sage-muted: #CFD8D3; /* disabled button */
}
html, html,
body { body {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */ scrollbar-width: none; /* Firefox */
} }
.App { .App {
/* overflow-x: hidden; */
} }
.Cafe { .Cafe {
@@ -29,7 +36,7 @@ body {
height: 100%; height: 100%;
width: 100%; width: 100%;
background-image: url(https://i.ibb.co.com/F4FMw1jz/testuseonly.png); background-image: url(https://i.ibb.co.com/F4FMw1jz/testuseonly.png);
z-index: 1000; z-index: 0; /* align with item lister */
filter: opacity(0.04); filter: opacity(0.04);
pointer-events: none; pointer-events: none;
} }
@@ -56,6 +63,142 @@ body {
color: white; color: white;
} }
/* removed two-column layout; reverted to single column */
/* Ensure sticky cart bar stays above item overlays */
.StickyCartBar {
z-index: 120 !important; /* above content, below modal */
position: fixed;
left: 0;
right: 0;
bottom: 24px;
background: transparent;
box-shadow: none;
pointer-events: none; /* only children capture clicks */
display: flex;
justify-content: center;
align-items: center;
padding: 0 12px;
}
/* Intro slide/fade when bar first appears */
.StickyCartBar.intro { animation: bar-in 450ms ease-out; }
@keyframes bar-in {
0% { opacity: 0; transform: translateY(12px); }
100% { opacity: 1; transform: translateY(0); }
}
/* Floating cart buttons inside sticky bar */
.StickyCartBar .cartBtn {
pointer-events: auto;
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
padding: 0 14px;
border-radius: 999px;
border: none;
background: rgba(115,165,133,0.96);
color: #fff;
backdrop-filter: blur(3px);
box-shadow: 0 12px 28px rgba(115,165,133,0.35);
width: clamp(200px, 60vw, 420px);
transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.2s ease;
position: relative;
}
.StickyCartBar .cartBtn:hover { box-shadow: 0 14px 32px rgba(115,165,133,0.4); transform: translateY(-1px); }
.StickyCartBar .cartBtn:active { transform: translateY(0); }
.StickyCartBar .cartBtn:focus-visible { outline: 3px solid rgba(115,165,133,0.4); outline-offset: 2px; }
/* Bump + ping animation when items are added */
.StickyCartBar .cartBtn.bump { animation: cart-bump 300ms ease; }
.StickyCartBar .cartBtn.bump::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: 0 0 0 0 rgba(115,165,133,0.35);
animation: cart-ping 600ms ease-out;
}
@keyframes cart-bump {
0% { transform: scale(1); }
10% { transform: scale(0.98); }
30% { transform: scale(1.04); }
50% { transform: scale(1.02); }
100% { transform: scale(1); }
}
@keyframes cart-ping {
0% { box-shadow: 0 0 0 0 rgba(115,165,133,0.35); }
100% { box-shadow: 0 0 0 14px rgba(115,165,133,0); }
}
.StickyCartBar .historyBtn {
pointer-events: auto;
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 44px;
margin-left: 8px;
border-radius: 999px;
border: none;
background: rgba(115,165,133,0.96);
color: #fff;
backdrop-filter: blur(3px);
box-shadow: 0 12px 28px rgba(115,165,133,0.35);
transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.2s ease;
}
.StickyCartBar .historyBtn:hover { box-shadow: 0 14px 32px rgba(115,165,133,0.4); transform: translateY(-1px); }
.StickyCartBar .historyBtn:active { transform: translateY(0); }
.StickyCartBar .historyBtn:focus-visible { outline: 3px solid rgba(115,165,133,0.4); outline-offset: 2px; }
/* Subtle pulse to draw attention on new transaction */
.StickyCartBar .historyBtn.pulse { animation: btn-pulse 900ms ease-out; }
@keyframes btn-pulse {
0% { box-shadow: 0 12px 28px rgba(115,165,133,0.35); }
60% { box-shadow: 0 18px 38px rgba(115,165,133,0.5); }
100% { box-shadow: 0 12px 28px rgba(115,165,133,0.35); }
}
.StickyCartBar .summary {
display: flex;
align-items: center;
gap: 8px;
font-weight: 700;
}
.StickyCartBar .value { white-space: nowrap; font-weight: 800; }
.StickyCartBar .icon { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; margin-left: 8px; }
.StickyCartBar .icon { position: relative; }
.StickyCartBar .icon .badge {
position: absolute;
top: -6px;
right: -6px;
width: 18px;
height: 18px;
border-radius: 50%;
background: #ff6b6b;
color: #fff;
font-size: 11px;
font-weight: 800;
line-height: 18px;
text-align: center;
padding: 0;
box-shadow: 0 2px 6px rgba(255,107,107,0.35);
}
.StickyCartBar .icon .badge.pop { animation: badge-pop 280ms ease-out; }
@keyframes badge-pop {
0% { transform: scale(0.8); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
@media (max-width: 420px) {
.StickyCartBar { bottom: 16px; }
.StickyCartBar .cartBtn { height: 46px; }
.StickyCartBar .historyBtn { height: 46px; width: 48px; }
}
.App-link { .App-link {
color: #61dafb; color: #61dafb;
} }

View File

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

View File

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

View File

@@ -1,52 +0,0 @@
import React, { useState } from "react";
import styles from "./Carousel.module.css"; // Import CSS module
const Carousel = ({ selectedIndex, items, onSelect }) => {
const moveToNext = () => {
console.log('aa')
if (selectedIndex < items.length - 1) {
console.log('bb')
onSelect(selectedIndex + 1); // Send the next index to the parent
}
};
const moveToPrev = () => {
if (selectedIndex > -1) {
onSelect(selectedIndex - 1); // Send the previous index to the parent
}
};
return (
<div className={styles.carouselContainer}>
<div className={styles.carousel}>
{/* Previous Item */}
<div
className={`${styles.carouselItem} ${styles.prev}`}
onClick={moveToPrev}
style={{ visibility: selectedIndex === -1 && items.length > 0 ? "hidden" : "visible" , backgroundColor: items.length < 1 ? '#919191':'#007bff'}}
>
{selectedIndex === -1 ? (items.length > 0 ? "+" : "") : items[selectedIndex - 1]?.name || "+"}
</div>
{/* Current Item */}
<div className={`${styles.carouselItem} ${styles.center}`}>
{selectedIndex >= 0 ? items[selectedIndex]?.name : "+"}
</div>
{/* Next Item */}
<div
className={`${styles.carouselItem} ${styles.next}`}
onClick={moveToNext}
style={{
visibility: selectedIndex === items.length -1 && items.length > 0 ? "hidden" : "visible", backgroundColor: items.length < 1 ? '#919191':'#007bff'
}}
>
{selectedIndex < items.length - 1 && items[selectedIndex + 1]?.name}
</div>
</div>
</div>
);
};
export default Carousel;

View File

@@ -1,74 +0,0 @@
/* Carousel.module.css */
.carouselContainer {
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.carousel {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
max-width: 600px;
overflow: hidden;
position: relative;
}
.carouselItem {
display: flex;
justify-content: center;
align-items: center;
transition: transform 0.3s ease-in-out;
height: 200px;
background-color: #007bff;
color: white;
font-size: 24px;
width: 100%;
border-radius: 8px;
}
.carouselItem.prev,
.carouselItem.next {
transform: scale(0.8);
}
.carouselItem.center {
transform: scale(1);
width: 134%;
}
.prevBtn,
.nextBtn {
position: absolute;
top: 50%;
transform: translateY(-50%);
color: white;
border: none;
padding: 10px 20px;
font-size: 16px;
border-radius: 8px;
cursor: pointer;
}
.prevBtn {
left: 10px;
}
.nextBtn {
right: 10px;
}
@media (max-width: 600px) {
.carouselItem {
font-size: 18px;
height: 110px;
}
.prevBtn,
.nextBtn {
padding: 8px 15px;
font-size: 14px;
}
}

View File

@@ -4,6 +4,7 @@ import { useLocation } from "react-router-dom";
import { useNavigationHelpers } from "../helpers/navigationHelpers"; import { useNavigationHelpers } from "../helpers/navigationHelpers";
import Switch from "react-switch"; import Switch from "react-switch";
// Restore original gradient background for header container when shopName exists
const HeaderBarbackground = styled.div` const HeaderBarbackground = styled.div`
${({ shopName }) => ${({ shopName }) =>
shopName && shopName &&
@@ -19,10 +20,10 @@ const HeaderBar = styled.div`
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 20px 15px; padding: 12px 14px;
color: black; color: black;
background-color: #ffffff; background-color: #ffffff;
z-index: 200; z-index: ${(props) => (props.zIndexLevel !== undefined ? props.zIndexLevel : 200)};
border: 1px solid #00000000; border: 1px solid #00000000;
margin: 20px 12px; margin: 20px 12px;
border-radius: 13px; border-radius: 13px;
@@ -33,30 +34,15 @@ const HeaderBar = styled.div`
const Title = styled.h2` const Title = styled.h2`
margin: 0; margin: 0;
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500; font-weight: 600;
font-style: normal; font-style: normal;
font-size:${(props) => (props.HeaderSize)}; font-size:${(props) => (props.HeaderSize)};
color: rgba(88, 55, 50, 1); color: rgba(45, 45, 45, 1);
text-transform: uppercase;
`; `;
const ProfileName = styled.h2` // SubTitle removed per redesign (no subtitle below cafe name)
position: absolute;
font-family: "Plus Jakarta Sans", sans-serif; // Deprecated the animated ProfileName in favor of a cleaner layout
font-weight: 500;
font-style: normal;
font-size: 30px;
z-index: 199;
overflow: hidden;
white-space: nowrap;
animation: ${(props) => {
if (props.animate === "grow") return gg;
if (props.animate === "shrink") return ss;
return nn;
}}
0.5s forwards;
text-align: left;
`;
const nn = keyframes` const nn = keyframes`
0% { 0% {
@@ -103,22 +89,17 @@ const ss = keyframes`
} }
`; `;
const ProfileImage = styled.img` const CafeAvatar = styled.img`
position: relative; width: clamp(32px, 5vw, 56px);
width: 60px; height: clamp(32px, 5vw, 56px);
height: 60px; border-radius: 8px;
border-radius: 50%; object-fit: cover;
object-fit: contain; background: #f2f2f2;
cursor: pointer; margin-left: 8px; /* extra left padding so its not too tight */
z-index: 199;
animation: ${(props) => {
if (props.animate === "grow") return g;
if (props.animate === "shrink") return s;
return "none";
}}
0.5s forwards;
`; `;
// User initial avatar removed; only cafe image is shown on the left
const g = keyframes` const g = keyframes`
0% { 0% {
top: 0px; top: 0px;
@@ -149,62 +130,43 @@ const s = keyframes`
} }
`; `;
/* Replace bubble-like animation with subtle fade/slide */
const grow = keyframes` const grow = keyframes`
0% { 0% { opacity: 0; transform: translateY(-6px) scale(0.98); }
right: 12px; 100% { opacity: 1; transform: translateY(0) scale(1); }
width: 60px;
height: 60px;
border-top-left-radius: 50%;
border-bottom-left-radius: 50%;
}
100% {
right: 28px;
width: 300px;
border-top-left-radius: 15px;
border-bottom-left-radius: 15px;
}
`; `;
const shrink = keyframes` const shrink = keyframes`
0% { 0% { opacity: 1; transform: translateY(0) scale(1); }
right: 12px; 100% { opacity: 0; transform: translateY(-6px) scale(0.98); }
width: 300px;
height: auto;
border-radius: 20px;
}
100% {
right: 28px;
width: 60px;
height: 60px;
border-radius: 50%;
}
`; `;
const Rectangle = styled.div` const Rectangle = styled.div`
overflow-y: hidden; overflow-y: auto;
position: absolute; position: absolute;
top: 39px; top: calc(100% + 8px);
right: 12px; right: 0;
width: 200px; width: 240px;
max-height: 87vh; /* or another appropriate value */ max-height: 75vh;
background-color: white; background-color: white;
z-index: 198; z-index: ${(props) => (props.baseZIndex !== undefined ? props.baseZIndex : 198)};
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
animation: ${(props) => (props.animate === "grow" ? grow : shrink)} 0.5s border: 1px solid #f0f0f0;
forwards; border-radius: 12px;
padding: 10px; animation: ${(props) => (props.animate === "grow" ? grow : shrink)} 0.2s forwards;
padding: 10px 12px;
box-sizing: border-box; box-sizing: border-box;
overflow-x: hidden; overflow-x: hidden;
font-size: 14px; font-size: 14px;
color: #393939; color: #393939;
backdrop-filter: blur(2px);
`; `;
const ChildContainer = styled.div` const ChildContainer = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-end; align-items: stretch;
flex-wrap: wrap; flex-wrap: nowrap;
padding-top: 70px;
`; `;
const ChildWrapper = styled.div` const ChildWrapper = styled.div`
@@ -214,24 +176,87 @@ const ChildWrapper = styled.div`
`; `;
const Child = styled.div` const Child = styled.div`
width: 100%; width: 100%;
height: 36px; min-height: 36px;
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500; font-weight: 500;
font-style: normal; font-style: normal;
padding: ${(props) => (props.hasChildren ? '8px 0 4px' : '8px 4px')};
${(props) => ${(props) =>
props.hasChildren props.hasChildren
? ` ? `
margin-top: 14px; margin-top: 10px;
border-top: 0.5px solid #a5a5a5; border-top: 0.5px solid #e9e9e9;
height: auto; height: auto;
` `
: ` : `
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
cursor: pointer;
`} `}
`; `;
const LeftGroup = styled.div`
display: flex;
align-items: center;
gap: 10px;
`;
const CenterGroup = styled.div`
flex: 1;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none; /* so clicks pass through center when needed */
`;
const CafeInfo = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
`;
const RightGroup = styled.div`
display: flex;
align-items: center;
gap: 10px;
`;
const CategoryLabel = styled.div`
font-family: "Plus Jakarta Sans", sans-serif;
font-size: 12px;
font-weight: 700;
color: #6B8F71;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 6px 2px;
pointer-events: none;
`;
const HamburgerButton = styled.button`
width: clamp(32px, 4.5vw, 52px);
height: clamp(32px, 4.5vw, 52px);
border-radius: 8px;
border: 1px solid #e5e5e5;
background: #ffffff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
& > svg {
width: 60%;
height: 60%;
}
`;
const HamburgerIcon = () => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="6" width="18" height="2" rx="1" fill="#2d2d2d"/>
<rect x="3" y="11" width="18" height="2" rx="1" fill="#2d2d2d"/>
<rect x="3" y="16" width="18" height="2" rx="1" fill="#2d2d2d"/>
</svg>
);
const Header = ({ const Header = ({
HeaderText, HeaderText,
@@ -251,7 +276,9 @@ const Header = ({
removeConnectedGuestSides, removeConnectedGuestSides,
setIsEditMode, setIsEditMode,
isEditMode, isEditMode,
HeaderMargin = '25px' HeaderMargin = '25px',
zIndexLevel,
rectZIndex
}) => { }) => {
const { goToLogin, goToGuestSideLogin, goToAdminCafes } = const { goToLogin, goToGuestSideLogin, goToAdminCafes } =
useNavigationHelpers(shopId, tableCode); useNavigationHelpers(shopId, tableCode);
@@ -261,10 +288,10 @@ const Header = ({
const [guestSideOf, setGuestSideOf] = useState(null); const [guestSideOf, setGuestSideOf] = useState(null);
const location = useLocation(); const location = useLocation();
const handleImageClick = () => { const toggleMenu = () => {
if (showRectangle) { if (showRectangle) {
setAnimate("shrink"); setAnimate("shrink");
setTimeout(() => setShowRectangle(false), 500); setTimeout(() => setShowRectangle(false), 200);
} else { } else {
setAnimate("grow"); setAnimate("grow");
setShowRectangle(true); setShowRectangle(true);
@@ -274,15 +301,14 @@ const Header = ({
const handleClickOutside = (event) => { const handleClickOutside = (event) => {
if (rectangleRef.current && !rectangleRef.current.contains(event.target)) { if (rectangleRef.current && !rectangleRef.current.contains(event.target)) {
setAnimate("shrink"); setAnimate("shrink");
setTimeout(() => setShowRectangle(false), 500); setTimeout(() => setShowRectangle(false), 200);
rectangleRef.current.style.overflow = "hidden";
} }
}; };
const handleScroll = () => { const handleScroll = () => {
if (showRectangle) { if (showRectangle) {
setAnimate("shrink"); setAnimate("shrink");
setTimeout(() => setShowRectangle(false), 500); setTimeout(() => setShowRectangle(false), 200);
} }
}; };
@@ -321,28 +347,40 @@ const Header = ({
// Otherwise, use the possessive function // Otherwise, use the possessive function
return `${cafeName}'s menu`; return `${cafeName}'s menu`;
}; };
const formatCafeName = (name) => {
if (!name) return name;
return name
.toLowerCase()
.replace(/\b\w/g, (c) => c.toUpperCase());
};
return ( return (
<HeaderBarbackground shopName={shopName}> <HeaderBarbackground shopName={shopName}>
<HeaderBar HeaderMargin={HeaderMargin} shopName={shopName}> <HeaderBar HeaderMargin={HeaderMargin} shopName={shopName} zIndexLevel={zIndexLevel}>
<LeftGroup>
<CafeAvatar
src={shopImage && !shopImage.includes('undefined') ? shopImage : "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS-DjX_bGBax4NL14ULvkAdU4FP3FKoWXWu5w&s"}
alt="Cafe"
/>
</LeftGroup>
<CenterGroup>
<Title HeaderSize={HeaderSize}> <Title HeaderSize={HeaderSize}>
{shopName == null {formatCafeName(
shopName == null
? HeaderText == null ? HeaderText == null
? "kedaimaster" ? "kedaimaster"
: HeaderText : HeaderText
: shopName} : shopName
)}
</Title> </Title>
<div style={{ visibility: showProfile ? "visible" : "hidden" }}> </CenterGroup>
<ProfileImage
src={shopImage && !shopImage.includes('undefined') ? shopImage || 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS-DjX_bGBax4NL14ULvkAdU4FP3FKoWXWu5w&s' : "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS-DjX_bGBax4NL14ULvkAdU4FP3FKoWXWu5w&s"} <RightGroup style={{ visibility: showProfile ? "visible" : "hidden", position: 'relative' }}>
alt="Profile" <HamburgerButton onClick={toggleMenu} aria-label="Open menu">
onClick={user.username !== undefined ? handleImageClick : null} <HamburgerIcon />
animate={showRectangle && animate} </HamburgerButton>
/>
<ProfileName animate={showRectangle && animate}>
{showProfile && user.username !== undefined ? user.username : "guest"}
</ProfileName>
{showRectangle && ( {showRectangle && (
<Rectangle ref={rectangleRef} animate={animate}> <Rectangle ref={rectangleRef} animate={animate} baseZIndex={rectZIndex !== undefined ? rectZIndex : zIndexLevel}>
<ChildContainer> <ChildContainer>
{guestSideOfClerk && guestSideOfClerk.clerkUsername && ( {guestSideOfClerk && guestSideOfClerk.clerkUsername && (
<Child hasChildren> <Child hasChildren>
@@ -350,23 +388,21 @@ const Header = ({
</Child> </Child>
)} )}
{user.username !== undefined && ( {user.username !== undefined && (
<Child onClick={() => setModal("edit_account")}> <CategoryLabel>Kelola akun</CategoryLabel>
Kelola akun
</Child>
)} )}
{user.roleId == 0 && ( {user.roleId == 0 && (
<Child onClick={()=>setModal('create_coupon', {})}>Buat Voucher</Child>)} <Child onClick={()=>setModal('create_coupon', {})}>Buat Voucher</Child>)}
{shopId && user.roleId == 1 && ( {shopId && user.roleId == 1 && (
<Child onClick={goToAdminCafes}>Dashboard</Child>)} <Child onClick={goToAdminCafes}>Dashboard</Child>)}
{shopId && {shopId &&
user.userId == shopOwnerId && user.user_id == shopOwnerId &&
user.username !== undefined && user.username !== undefined &&
user.roleId === 1 && ( user.roleId === 1 && (
<> <>
<Child hasChildren> <Child hasChildren>
<Child> <CategoryLabel>
{shopName} {formatCafeName(shopName)}
</Child> </CategoryLabel>
<Child> <Child>
Mode pengembangan &nbsp; Mode pengembangan &nbsp;
<Switch <Switch
@@ -381,7 +417,7 @@ const Header = ({
</Child> </Child>
<Child hasChildren> <Child hasChildren>
<Child>Konfigurasi</Child> <CategoryLabel>Konfigurasi</CategoryLabel>
<Child onClick={() => setModal("welcome_config")}> <Child onClick={() => setModal("welcome_config")}>
Desain kafe Desain kafe
</Child> </Child>
@@ -393,7 +429,7 @@ const Header = ({
</Child> </Child>
</Child> </Child>
<Child hasChildren> <Child hasChildren>
<Child>Kasir</Child> <CategoryLabel>Kasir</CategoryLabel>
<Child onClick={() => setModal("create_clerk")}> <Child onClick={() => setModal("create_clerk")}>
+ Tambah + Tambah
</Child> </Child>
@@ -420,7 +456,7 @@ const Header = ({
user.cafeId == shopId && user.cafeId == shopId &&
user.roleId === 2 && ( user.roleId === 2 && (
<Child hasChildren> <Child hasChildren>
<Child>{shopName}</Child> <CategoryLabel>{formatCafeName(shopName)}</CategoryLabel>
<Child> <Child>
Mode pengembangan&nbsp; Mode pengembangan&nbsp;
@@ -435,7 +471,7 @@ const Header = ({
</Child> </Child>
<Child hasChildren> <Child hasChildren>
<Child>Konfigurasi</Child> <CategoryLabel>Konfigurasi</CategoryLabel>
<Child onClick={() => setModal("welcome_config")}> <Child onClick={() => setModal("welcome_config")}>
Desain kafe Desain kafe
</Child> </Child>
@@ -481,8 +517,9 @@ const Header = ({
</ChildContainer> </ChildContainer>
</Rectangle> </Rectangle>
)} )}
</div> </RightGroup>
</HeaderBar></HeaderBarbackground> </HeaderBar>
</HeaderBarbackground>
); );
}; };

View File

@@ -0,0 +1,348 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import styles from "./IdentifyCafeModal.module.css";
import API_BASE_URL from "../config.js";
import { getTables, createTable } from "../helpers/tableHelper";
import { saveCafeDetails } from "../helpers/cafeHelpers";
import { getImageUrl } from "../helpers/itemHelper";
import { toPng } from "html-to-image";
export default function IdentifyCafeModal({ shop }) {
const [cafeIdentifyName, setCafeIdentifyName] = useState(shop.cafeIdentifyName || "");
const [availability, setAvailability] = useState(null); // 200 ok, 409 taken
const [checking, setChecking] = useState(false);
const [tables, setTables] = useState([]);
const [selectedTable, setSelectedTable] = useState(null);
const [qrSize, setQrSize] = useState(Number(shop.scale) || 1);
const [qrX, setQrX] = useState(Number(shop.xposition) || 50);
const [qrY, setQrY] = useState(Number(shop.yposition) || 50);
const [fontSize, setFontSize] = useState(Number(shop.fontsize) || 16);
const [fontColor, setFontColor] = useState(shop.fontcolor || "#FFFFFF");
const [fontX, setFontX] = useState(Number(shop.fontxposition) || 50);
const [fontY, setFontY] = useState(Number(shop.fontyposition) || 85);
const [bgImageUrl, setBgImageUrl] = useState(getImageUrl(shop.qrBackground));
const bgFileRef = useRef(null);
const [newTableNo, setNewTableNo] = useState("");
const previewRef = useRef(null);
const [copied, setCopied] = useState(false);
const [currentStep, setCurrentStep] = useState(1); // 1=Alamat, 2=Desain QR, 3=Meja
const [saveStatus, setSaveStatus] = useState(null); // 'success' | 'error'
const [saving, setSaving] = useState(false);
const initialDesignRef = useRef({
qrSize: Number(shop.scale) || 1,
qrX: Number(shop.xposition) || 50,
qrY: Number(shop.yposition) || 50,
fontSize: Number(shop.fontsize) || 16,
fontColor: shop.fontcolor || "#FFFFFF",
fontX: Number(shop.fontxposition) || 50,
fontY: Number(shop.fontyposition) || 85,
bgImageUrl: getImageUrl(shop.qrBackground),
});
const shopHost = useMemo(() => window.location.hostname, []);
const fullLink = useMemo(() => `${shopHost}/${cafeIdentifyName}`, [shopHost, cafeIdentifyName]);
// Debounced availability check
const debounceRef = useRef(null);
const handleIdentifyChange = (e) => {
const val = e.target.value
.toLowerCase()
.replace(/\s+/g, "_")
.replace(/[^a-z0-9_]/g, "");
setCafeIdentifyName(val);
setChecking(true);
setAvailability(null);
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(async () => {
try {
const res = await fetch(`${API_BASE_URL}/cafe/check-identifyName/${val}`);
setAvailability(res.ok ? 200 : 409);
} catch (_) {
setAvailability(409);
} finally {
setChecking(false);
}
}, 600);
};
// Load tables
useEffect(() => {
(async () => {
try {
const fetched = await getTables(shop.cafeId);
setTables(fetched || []);
} catch (e) {
// ignore
}
})();
}, [shop.cafeId]);
const handleUploadBg = (e) => {
const file = e.target.files?.[0];
if (file) {
const url = URL.createObjectURL(file);
setBgImageUrl(url);
}
};
const handleCreateTable = async () => {
if (!newTableNo) return;
try {
const created = await createTable(shop.cafeId, { tableNo: newTableNo });
setTables((t) => [...t, created]);
setNewTableNo("");
} catch (e) {
// noop
}
};
const handleSave = async () => {
setSaving(true);
setSaveStatus(null);
const qrBackgroundFile = bgFileRef.current?.files?.[0];
const details = {
qrSize,
qrPosition: { left: qrX, top: qrY },
qrBackgroundFile,
fontsize: fontSize,
fontcolor: fontColor,
fontPosition: { left: fontX, top: fontY },
cafeIdentifyName: shop.cafeIdentifyName !== cafeIdentifyName ? cafeIdentifyName : null,
};
try {
await saveCafeDetails(shop.cafeId, details);
setSaveStatus('success');
} catch (e) {
setSaveStatus('error');
} finally {
setSaving(false);
}
};
const downloadPreview = async () => {
if (!previewRef.current) return;
const node = previewRef.current;
const originalBg = node.style.backgroundColor;
node.style.backgroundColor = "transparent";
try {
const dataUrl = await toPng(node, { pixelRatio: 2 });
const link = document.createElement("a");
link.href = dataUrl;
link.download = selectedTable ? `QR Meja (${selectedTable.tableNo}).png` : `QR ${shop.name}.png`;
link.click();
} catch (e) {
// noop
} finally {
node.style.backgroundColor = originalBg;
}
};
const copyLink = async () => {
try {
await navigator.clipboard.writeText(fullLink);
setCopied(true);
setTimeout(()=>setCopied(false), 1400);
} catch (_) {}
};
const applyPreset = (preset) => {
if (preset === 'center') {
setQrX(50); setQrY(50); setQrSize(1);
setFontX(50); setFontY(85);
} else if (preset === 'topLeft') {
setQrX(25); setQrY(30); setQrSize(1);
setFontX(50); setFontY(85);
} else if (preset === 'bottomRight') {
setQrX(75); setQrY(70); setQrSize(1);
setFontX(50); setFontY(15);
}
};
const resetDesign = () => {
const d = initialDesignRef.current;
setQrSize(d.qrSize);
setQrX(d.qrX);
setQrY(d.qrY);
setFontSize(d.fontSize);
setFontColor(d.fontColor);
setFontX(d.fontX);
setFontY(d.fontY);
setBgImageUrl(d.bgImageUrl);
if (bgFileRef.current) bgFileRef.current.value = '';
};
// Positioning helpers
const qrStyle = {
left: `${qrX}%`,
top: `${qrY}%`,
transform: `translate(-50%, -50%) scale(${qrSize})`,
};
const fontStyle = {
left: `${fontX}%`,
top: `${fontY}%`,
transform: `translate(-50%, -50%)`,
color: fontColor,
fontSize: `${fontSize}px`,
position: "absolute",
fontWeight: 700,
};
const qrData = selectedTable ? `${fullLink}/${selectedTable.tableNo}` : fullLink;
const canProceedFromStep1 = () => {
if (!cafeIdentifyName) return false;
if (cafeIdentifyName === (shop.cafeIdentifyName || '')) return true;
return availability === 200 && !checking;
};
return (
<div className={styles.container}>
<div className={styles.header}>
<div className={styles.title}>Identifikasi kedai</div>
<div className={styles.steps}>
<div className={`${styles.step} ${currentStep === 1 ? styles.stepActive : ''}`}>
<span className={styles.stepNumber}>1</span> Alamat
</div>
<div className={`${styles.step} ${currentStep === 2 ? styles.stepActive : ''}`}>
<span className={styles.stepNumber}>2</span> Desain QR
</div>
<div className={`${styles.step} ${currentStep === 3 ? styles.stepActive : ''}`}>
<span className={styles.stepNumber}>3</span> Meja
</div>
</div>
</div>
<div className={styles.scrollArea}>
{currentStep === 1 && (
<div className={styles.section}>
<div className={styles.sectionTitle}>Alamat kedai</div>
<div className={styles.row}>
<div className={styles.domainPrefix}>{shopHost}/</div>
<input
value={cafeIdentifyName}
onChange={handleIdentifyChange}
className={styles.input}
placeholder="alamat_kedai"
/>
<div className={`${styles.status} ${availability === 200 ? styles.statusOk : availability === 409 ? styles.statusBad : ''}`}>
{checking ? 'Memeriksa…' : availability === 200 ? 'Tersedia' : availability === 409 ? 'Terpakai' : 'Menunggu input'}
</div>
</div>
<div className={styles.helpText}>Gunakan huruf kecil, angka, dan garis bawah (_). Contoh: kopikenangan_malam</div>
<div className={styles.copyRow}>
<input className={styles.linkField} readOnly value={fullLink} />
<button className={styles.button} onClick={copyLink}>{copied ? 'Disalin' : 'Salin'}</button>
</div>
</div>
)}
{currentStep === 2 && (
<div className={styles.section}>
<div className={styles.sectionTitle}>Desain QR</div>
<div className={styles.presets}>
<button className={styles.presetButton} onClick={()=>applyPreset('center')}>Tengah</button>
<button className={styles.presetButton} onClick={()=>applyPreset('topLeft')}>Atas-Kiri</button>
<button className={styles.presetButton} onClick={()=>applyPreset('bottomRight')}>Bawah-Kanan</button>
<button className={styles.presetButton} onClick={resetDesign}>Reset desain</button>
</div>
<div className={styles.grid}>
<div>
<div ref={previewRef} id="qr-code-container" className={styles.previewBox} style={{ backgroundColor: '#fff' }}>
{bgImageUrl && <img src={bgImageUrl} alt="Background" className={styles.bgPreview} />}
<img
className={styles.qrLayer}
style={qrStyle}
alt="QR"
src={`https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=${encodeURIComponent(qrData)}`}
/>
<div style={fontStyle}>{selectedTable ? selectedTable.tableNo : 'Kedai'}</div>
</div>
</div>
<div className={styles.controls}>
<div className={styles.field}>
<label className={styles.label}>Gambar latar</label>
<input ref={bgFileRef} type="file" accept="image/*" onChange={handleUploadBg} />
</div>
<div className={styles.field}>
<label className={styles.label}>Skala QR ({qrSize}x)</label>
<input className={styles.range} type="range" min="0.5" max="3" step="0.1" value={qrSize} onChange={(e)=>setQrSize(parseFloat(e.target.value))} />
</div>
<div className={styles.field}>
<label className={styles.label}>Posisi QR - horizontal ({qrX}%)</label>
<input className={styles.range} type="range" min="0" max="100" step="1" value={qrX} onChange={(e)=>setQrX(parseInt(e.target.value))} />
</div>
<div className={styles.field}>
<label className={styles.label}>Posisi QR - vertikal ({qrY}%)</label>
<input className={styles.range} type="range" min="0" max="100" step="1" value={qrY} onChange={(e)=>setQrY(parseInt(e.target.value))} />
</div>
<div className={styles.field}>
<label className={styles.label}>Ukuran teks ({fontSize}px)</label>
<input className={styles.range} type="range" min="8" max="48" step="1" value={fontSize} onChange={(e)=>setFontSize(parseInt(e.target.value))} />
</div>
<div className={styles.field}>
<label className={styles.label}>Warna teks</label>
<input className={styles.colorInput} type="color" value={fontColor} onChange={(e)=>setFontColor(e.target.value)} />
</div>
<div className={styles.field}>
<label className={styles.label}>Posisi teks - horizontal ({fontX}%)</label>
<input className={styles.range} type="range" min="0" max="100" step="1" value={fontX} onChange={(e)=>setFontX(parseInt(e.target.value))} />
</div>
<div className={styles.field}>
<label className={styles.label}>Posisi teks - vertikal ({fontY}%)</label>
<input className={styles.range} type="range" min="0" max="100" step="1" value={fontY} onChange={(e)=>setFontY(parseInt(e.target.value))} />
</div>
</div>
</div>
<div className={styles.helpText}>Tip: Gunakan latar yang kontras agar QR mudah dipindai.</div>
</div>
)}
{currentStep === 3 && (
<div className={styles.section}>
<div className={styles.sectionTitle}>Daftar meja</div>
<div className={styles.tables}>
<input className={styles.input} placeholder="Meja A1" value={newTableNo} onChange={(e)=>setNewTableNo(e.target.value)} />
<button className={styles.button} onClick={handleCreateTable}>Buat meja</button>
</div>
<div className={styles.tableList}>
{tables && tables
.filter((t)=>t.tableNo !== 0)
.map((t)=>{
const active = selectedTable && selectedTable.tableId === t.tableId;
return (
<div
key={t.tableId}
className={`${styles.tableItem} ${active ? styles.tableItemActive : ''}`}
onClick={()=>setSelectedTable(active ? null : t)}
>
{t.tableNo}
</div>
);
})}
</div>
<div className={styles.helpText}>Pilih meja untuk membuat QR khusus meja (opsional).</div>
</div>
)}
</div>
<div className={styles.footer}>
<div className={styles.actions}>
<button className={`${styles.button} ${styles.secondary}`} disabled={currentStep === 1} onClick={()=>setCurrentStep((s)=>Math.max(1, s-1))}>Kembali</button>
{currentStep < 3 ? (
<button className={`${styles.button} ${styles.primary}`} disabled={currentStep === 1 && !canProceedFromStep1()} onClick={()=>setCurrentStep((s)=>Math.min(3, s+1))}>Lanjut</button>
) : (
<button className={`${styles.button} ${styles.primary}`} onClick={handleSave} disabled={saving}>{saving ? 'Menyimpan…' : 'Simpan'}</button>
)}
</div>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
{saveStatus === 'success' && <div className={`${styles.banner} ${styles.bannerSuccess}`}>Simpan berhasil</div>}
{saveStatus === 'error' && <div className={`${styles.banner} ${styles.bannerError}`}>Gagal menyimpan</div>}
<button className={`${styles.button} ${styles.muted}`} onClick={downloadPreview}>Download QR {selectedTable ? 'meja' : 'kedai'}</button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,299 @@
/* IdentifyCafeModal.module.css */
.container {
display: flex;
flex-direction: column;
gap: 16px;
padding: 20px;
background: #fff;
height: 100%;
}
.steps {
display: flex;
gap: 8px;
align-items: center;
}
.step {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 999px;
border: 1px solid #e6e6e6;
background: #fafafa;
color: #666;
font-weight: 600;
font-size: 13px;
}
.stepNumber {
width: 22px;
height: 22px;
border-radius: 50%;
background: #e9ecef;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 12px;
}
.stepActive {
border-color: #cdebd8;
background: #e9f7ef;
color: #245c3d;
}
.helpText {
font-size: 12px;
color: #666;
margin-top: 6px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
.title {
font-size: 18px;
font-weight: 700;
}
.scrollArea {
overflow-y: auto;
overflow-x: hidden;
}
.section {
background: #fafafa;
border: 1px solid #e6e6e6;
border-radius: 12px;
padding: 16px;
}
.sectionTitle {
margin: 0 0 10px 0;
font-weight: 600;
font-size: 15px;
}
.row {
display: flex;
gap: 12px;
align-items: center;
}
.domainPrefix {
background: #f0f0f0;
border: 1px solid #ddd;
border-radius: 10px 0 0 10px;
padding: 10px 12px;
font-size: 14px;
color: #555;
}
.input {
flex: 1;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 0 10px 10px 0;
outline: none;
font-size: 14px;
}
.status {
font-size: 12px;
font-weight: 700;
padding: 6px 10px;
border-radius: 999px;
}
.statusOk {
color: #155724;
background: #d4edda;
border: 1px solid #c3e6cb;
}
.statusBad {
color: #721c24;
background: #f8d7da;
border: 1px solid #f5c6cb;
}
.copyRow {
display: flex;
gap: 8px;
align-items: center;
margin-top: 10px;
}
.linkField {
flex: 1;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 10px;
font-size: 14px;
color: #333;
background: #fff;
}
.button {
padding: 10px 14px;
border-radius: 10px;
border: 1px solid #ddd;
background: #fff;
cursor: pointer;
}
.button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.grid {
display: grid;
grid-template-columns: 1.3fr 1fr;
gap: 16px;
}
.previewBox {
border: 1px solid #e6e6e6;
border-radius: 12px;
background: #fff;
height: 280px;
position: relative;
overflow: hidden;
}
.bgPreview {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.qrLayer {
position: absolute;
}
.controls {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.presets {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 8px 0 4px 0;
}
.presetButton {
padding: 6px 10px;
border-radius: 8px;
border: 1px solid #e6e6e6;
background: #fff;
font-size: 12px;
cursor: pointer;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
.label {
font-size: 12px;
color: #555;
}
.range {
width: 100%;
}
.colorInput {
height: 38px;
padding: 0 6px;
}
.tables {
display: grid;
grid-template-columns: 1fr auto;
gap: 10px;
align-items: center;
}
.tableList {
max-height: 180px;
overflow: auto;
border: 1px solid #e6e6e6;
border-radius: 8px;
padding: 8px;
}
.tableItem {
padding: 8px 10px;
border-radius: 6px;
cursor: pointer;
}
.tableItemActive {
background: #e9f7ef;
border: 1px solid #cdebd8;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.primary {
background: #28a745;
color: #fff;
border: none;
}
.muted {
background: #f6f6f6;
}
.actions {
display: flex;
gap: 8px;
}
.secondary {
background: #f0f0f0;
}
.banner {
padding: 10px 12px;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
}
.bannerSuccess {
color: #155724;
background: #d4edda;
border: 1px solid #c3e6cb;
}
.bannerError {
color: #721c24;
background: #f8d7da;
border: 1px solid #f5c6cb;
}
@media (max-width: 820px) {
.grid { grid-template-columns: 1fr; }
.controls { grid-template-columns: 1fr; }
}

View File

@@ -4,6 +4,8 @@ const Item = ({
blank, blank,
forCart, forCart,
forInvoice, forInvoice,
portrait,
hideDetails,
name: initialName, name: initialName,
description: initialDescription, description: initialDescription,
price: initialPrice, price: initialPrice,
@@ -53,10 +55,10 @@ const Item = ({
}; };
const handleCreate = () => { const handleCreate = () => {
handleCreateItem(itemName, itemPrice, selectedImage, previewUrl); handleCreateItem(itemName, itemPrice, selectedImage, previewUrl, itemDescription, promoPrice);
}; };
const handleUpdate = () => { const handleUpdate = () => {
handleUpdateItem(itemName, itemPrice, selectedImage, previewUrl); handleUpdateItem(itemName, itemPrice, selectedImage, itemDescription, promoPrice);
}; };
const handleRemoveClick = () => { const handleRemoveClick = () => {
@@ -74,6 +76,11 @@ const Item = ({
} }
}; };
const formatCurrency = (value) => {
const num = Number(value) || 0;
return num.toLocaleString('id-ID');
};
const handlePriceChange = (event) => { const handlePriceChange = (event) => {
setItemPrice(event.target.value); setItemPrice(event.target.value);
}; };
@@ -89,245 +96,73 @@ const Item = ({
setItemName(event.target.value); setItemName(event.target.value);
}; };
const toTitleCase = (str) => {
if (!str) return str;
return String(str)
.toLowerCase()
.replace(/(^|[\s\-/'])([a-zA-Z\u00C0-\u024F])/g, (m, p1, p2) => p1 + p2.toUpperCase());
};
const displayName = toTitleCase(itemName);
return ( return (
<div className={`${!last && !forInvoice ? styles.notLast : ""}`}> <div className={`${(!portrait && !last && !forInvoice) ? styles.notLast : ""}`}>
<div className={`${styles.item} ${forInvoice ? styles.itemInvoice : ""} `}> <div className={`${styles.item} ${forInvoice ? styles.itemInvoice : ""} ${portrait ? styles.itemPortrait : ""} `}>
{!forInvoice && ( {!forInvoice && (
// <div className={styles.imageContainer}> <div className={styles.imageWrap}>
<img <img
src={ src={previewUrl}
previewUrl
}
onError={({ currentTarget }) => { onError={({ currentTarget }) => {
currentTarget.onerror = null; // prevents looping currentTarget.onerror = null;
currentTarget.src = currentTarget.src =
"https://png.pngtree.com/png-vector/20221125/ourmid/pngtree-no-image-available-icon-flatvector-illustration-pic-design-profile-vector-png-image_40966566.jpg"; "https://png.pngtree.com/png-vector/20221125/ourmid/pngtree-no-image-available-icon-flatvector-illustration-pic-design-profile-vector-png-image_40966566.jpg";
}} }}
alt={itemName} alt={itemName}
style={{ style={{ filter: !isAvailable ? "grayscale(100%)" : "none" }}
filter: !isAvailable ? "grayscale(100%)" : "none",
}}
className={styles.imageContainer} className={styles.imageContainer}
/> />
// </div> {promoPrice && promoPrice != 0 && promoPrice != '' && (
<div className={styles.promoPill}>Promo</div>
)} )}
<div className={styles.itemDetails}> {portrait && (
{forInvoice &&
<svg
className={styles.plusNegative2}
onClick={onRemoveClick}
clipRule="evenodd"
fillRule="evenodd"
strokeLinejoin="round"
strokeMiterlimit="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12.002 2c5.518 0 9.998 4.48 9.998 9.998 0 5.517-4.48 9.997-9.998 9.997-5.517 0-9.997-4.48-9.997-9.997 0-5.518 4.48-9.998 9.997-9.998zm0 1.5c-4.69 0-8.497 3.808-8.497 8.498s3.807 8.497 8.497 8.497 8.498-3.807 8.498-8.497-3.808-8.498-8.498-8.498zm-.747 7.75h-3.5c-.414 0-.75.336-.75.75s.336.75.75.75h3.5v3.5c0 .414.336.75.75.75s.75-.336.75-.75v-3.5h3.5c.414 0 .75-.336.75-.75s-.336-.75-.75-.75h-3.5v-3.5c0-.414-.336-.75-.75-.75s-.75.336-.75.75z"
fillRule="nonzero"
/>
</svg>
}
{/* <input
className={`${
forInvoice ? styles.itemInvoiceName : styles.itemName
} ${isBeingEdit || blank ? styles.blank : styles.notblank} ${
!isAvailable ? styles.disabled : ""
}`}
value={itemName}
placeholder="Nama item"
onChange={handleNameChange}
disabled={!blank && !isBeingEdit}
/> */}
<h3 style={{
textTransform: 'capitalize',
margin: `${forInvoice ? '13px 0px 10px 10px' : '5px 0px 10px 10px'}`,
fontSize: '16px',
display: '-webkit-box',
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
WebkitLineClamp: 2,
textOverflow: 'ellipsis',
width: `${forInvoice? '160px' : 'unset'}`
}}>
{itemName}
</h3>
{forInvoice && (
<> <>
<div className={styles.overlayName}>{displayName}</div>
<div className={styles.bottomOverlay}>
<div className={styles.overlayPriceCol}>
{(promoPrice && promoPrice != 0 && promoPrice != '') ? (
<>
<span className={styles.overlayOriginal}>Rp {formatCurrency(initialPrice)}</span>
<span className={styles.overlayPromo}>Rp {formatCurrency(promoPrice)}</span>
</>
) : (
<span className={styles.overlayPromo}>Rp {formatCurrency(initialPrice)}</span>
)}
</div>
<div className={styles.qtyGroup}>
<button className={styles.qtyBtn} onClick={handleNegativeClick} aria-label="Kurangi">-</button>
<span className={styles.qtyVal}>{itemQty}</span>
<button className={styles.qtyBtn} onClick={handlePlusClick} aria-label="Tambah">+</button>
</div>
</div>
</>
)}
</div>
)}
{forInvoice && (
<div className={styles.itemDetails}>
<div className={styles.plusNegative2} onClick={onRemoveClick}></div>
<p className={styles.multiplySymbol}>x</p> <p className={styles.multiplySymbol}>x</p>
<p className={styles.qtyInvoice}>{itemQty}</p> <p className={styles.qtyInvoice}>{itemQty}</p>
</> <p className={styles.itemPriceInvoice}>Rp {formatCurrency(itemQty * (promoPrice > 0 ? promoPrice : itemPrice))}</p>
)}
{!forInvoice && (
// <input
// className={`${styles.itemPrice} ${
// isBeingEdit || blank ? styles.blank : styles.notblank
// } ${!isAvailable ? styles.disabled : ""}`}
// value={itemPrice}
// placeholder="Harga"
// onChange={handlePriceChange}
// disabled={!blank && !isBeingEdit}
// />
<div style={{
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
color: 'rgb(28, 29, 29)',
fontSize: '0.875rem',
fontWeight: 600,
lineHeight: '1rem',
marginLeft: 10
}}>
{promoPrice && promoPrice != 0 && promoPrice != '' ?
<>
<div style={{
position: 'relative',
marginTop: '0.125rem',
display: 'flex',
width: '87px',
alignItems: 'center',
whiteSpace: 'nowrap',
borderRadius: '9999px',
backgroundColor: !isAvailable ? 'gray' : 'unset',
backgroundImage: isAvailable && 'linear-gradient(to right, #e52535, #fe6d78)',
padding: '0.25rem 0rem',
color: 'rgb(255, 255, 255)',
fontSize: '0.75rem',
fontWeight: 600,
lineHeight: '1rem',
justifyContent: 'center'
}}>
Promo {(((initialPrice - promoPrice) / initialPrice) * 100).toFixed(0)}%
</div>
<div style={{ display: 'flex' }}>
<span style={{
marginLeft: '1rem',
marginRight: '0.5rem',
marginTop: '0.125rem'
}}>{promoPrice}</span>
<span style={{
marginTop: '0.125rem',
color: 'rgb(114, 114, 114)',
textDecoration: 'line-through'
}}>{initialPrice}</span>
</div>
</>
:
<>
<div style={{ display: 'flex' }}>
<span style={{
marginRight: '0.5rem',
marginTop: '0.125rem'
}}>{initialPrice}</span>
</div>
</>
}
</div> </div>
)} )}
{!forInvoice &&
(!isBeingEdit && itemQty != 0 ? (
<div className={styles.itemQty}>
<svg
className={styles.plusNegative}
onClick={handleNegativeClick}
clipRule="evenodd"
fillRule="evenodd"
strokeLinejoin="round"
strokeMiterlimit="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12.002 2.005c5.518 0 9.998 4.48 9.998 9.997 0 5.518-4.48 9.998-9.998 9.998-5.517 0-9.997-4.48-9.997-9.998 0-5.517 4.48-9.997 9.997-9.997zm0 1.5c-4.69 0-8.497 3.807-8.497 8.497s3.807 8.498 8.497 8.498 8.498-3.808 8.498-8.498-3.808-8.497-8.498-8.497zm4.253 7.75h-8.5c-.414 0-.75.336-.75.75s.336.75.75.75h8.5c.414 0 .75-.336.75-.75s-.336-.75-.75-.75z"
fillRule="nonzero"
/>
</svg>
{!blank && !isBeingEdit ? (
<p className={styles.itemQtyValue}>{itemQty}</p>
) : (
<input
className={styles.itemQtyInput}
value={itemQty}
onChange={handleQtyChange}
disabled={!blank && !isBeingEdit}
/>
)}
<svg
className={styles.plusNegative}
onClick={handlePlusClick}
clipRule="evenodd"
fillRule="evenodd"
strokeLinejoin="round"
strokeMiterlimit="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12.002 2c5.518 0 9.998 4.48 9.998 9.998 0 5.517-4.48 9.997-9.998 9.997-5.517 0-9.997-4.48-9.997-9.997 0-5.518 4.48-9.998 9.997-9.998zm0 1.5c-4.69 0-8.497 3.808-8.497 8.498s3.807 8.497 8.497 8.497 8.498-3.807 8.498-8.497-3.808-8.498-8.498-8.498zm-.747 7.75h-3.5c-.414 0-.75.336-.75.75s.336.75.75.75h3.5v3.5c0 .414.336.75.75.75s.75-.336.75-.75v-3.5h3.5c.414 0 .75-.336.75-.75s-.336-.75-.75-.75h-3.5v-3.5c0-.414-.336-.75-.75-.75s-.75.336-.75.75z"
fillRule="nonzero"
/>
</svg>
</div>
) : !blank && !isBeingEdit ? (
<div className={styles.itemQty}>
<button
className={styles.addButton}
style={{ backgroundColor: !isAvailable ? "" : "inherit", border: `2px solid ${isAvailable ? 'inherit' : 'gray'}`, color: `${isAvailable ? '#a8c7a9' : 'gray'}` }}
onClick={handlePlusClick}
disabled={!isAvailable} // Optionally disable the button if not available
>
Pesan
</button>
</div>
) : (
<div className={styles.itemQty}>
<button
className={styles.addButton}
style={{
backgroundColor: "white",
width: "150px",
color: '#a8c7a9'
}}
onClick={isBeingEdit ? handleUpdate : handleCreate}
>
{isBeingEdit ? "Simpan" : "Buat"}
</button>
</div>
))}
{forInvoice && (
<p className={styles.itemPriceInvoice}>Rp {itemQty * (promoPrice > 0? promoPrice : itemPrice)}</p>
)}
</div>
{forCart && ( {forCart && (
<div className={styles.remove} onClick={handleRemoveClick}> <div className={styles.remove} onClick={handleRemoveClick}>
</div> </div>
)} )}
{/* {blank && (
<button className={styles.createItem} onClick={handleCreate}>
Create Item
</button>
)} */}
</div> </div>
{itemDescription && itemDescription != 'undefined' && itemDescription?.length && {null}
<div>
<p style={{
maxHeight: '34px',
display: '-webkit-box',
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
WebkitLineClamp: 2,
textOverflow: 'ellipsis', color: '#5f5f5f', fontSize: '14px', padding: '5px', margin: 0
}}>{itemDescription}</p>
</div>
}
</div> </div>
); );
}; };

View File

@@ -1,32 +1,100 @@
.itemContainer { .itemContainer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
/* gap: 10px; */
} }
.item { .item {
display: flex; display: flex;
align-items: stretch; align-items: center;
justify-content: space-between; justify-content: space-between;
padding-left: 5px; width: 100%;
margin-top: 5px; gap: 12px;
margin-bottom: 5px; padding: 16px;
color: rgba(88, 55, 50, 1); margin: 0;
font-size: 32px; border: none;
box-sizing: border-box; /* Include padding and border in the element's total width */ border-radius: 12px;
width: 100%; /* Ensure the item does not exceed the parent's width */ background: #ffffff;
overflow: hidden; /* Prevent internal overflow */ box-shadow: 0 1px 5px rgba(0,0,0,0.08);
padding-top: 10px; box-sizing: border-box;
margin-bottom: 5px; transition: all 0.2s ease;
position: relative;
} }
.item:not(.itemInvoice) { .item:hover { /* remove hover effect on list items */ }
/* border-top: 2px solid #00000017; */
/* Portrait variant for cafe page grid */
.itemPortrait {
flex-direction: column;
align-items: stretch;
padding: 0;
margin: 0;
border: none;
background: transparent;
box-shadow: none;
border-radius: 12px;
overflow: hidden;
} }
.notLast{ .imageWrap {
padding-bottom: 10px; position: relative;
border-bottom: 2px solid #00000017; border-radius: 10px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
width: 80px;
height: 80px;
flex-shrink: 0;
}
.itemPortrait .imageWrap {
width: 100%;
height: auto;
aspect-ratio: 1 / 1;
border-radius: 12px;
}
.imageContainer {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.itemPortrait .imageContainer {
border-radius: 12px;
}
.itemDetails {
display: flex;
flex-direction: column;
justify-content: center;
align-items: stretch;
gap: 8px;
flex: 1;
min-width: 0;
}
.itemPortrait .itemDetails {
align-items: flex-start;
text-align: left;
gap: 8px;
padding: 12px 0;
}
.itemPortrait .itemQty {
justify-content: flex-start;
}
.itemPortrait .title {
font-size: 15px;
}
.itemPortrait .priceNow {
font-size: 14px;
}
.notLast {
border-bottom: none;
padding-bottom: 16px;
} }
.itemInvoice { .itemInvoice {
@@ -37,10 +105,14 @@
margin-top: 0px; margin-top: 0px;
margin-bottom: 0px; margin-bottom: 0px;
padding-top: 0px; padding-top: 0px;
padding: 16px;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 1px 5px rgba(0,0,0,0.08);
} }
.itemInvoice:last-child { .itemInvoice:last-child {
margin-bottom: 0; /* Remove margin-bottom for the last child */ margin-bottom: 0;
} }
.itemImage { .itemImage {
@@ -48,29 +120,30 @@
height: 100%; height: 100%;
} }
.imageContainer { .item:not(.itemPortrait) .imageContainer {
position: relative; position: relative;
width: 26vw; width: 80px;
height: 26vw; height: 80px;
border-radius: 12px; border-radius: 10px;
object-fit: cover; object-fit: cover;
} }
.overlay { .overlay {
position: absolute; position: absolute;
top: 15px; top: 16px;
left: 8px; left: 12px;
right: 8px; right: 12px;
bottom: 15px; bottom: 16px;
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
color: white; color: white;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border-radius: 20px; border-radius: 16px;
cursor: pointer; cursor: pointer;
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
font-size: 3.3vw; font-size: 16px;
font-weight: 500;
} }
.overlay:hover { .overlay:hover {
@@ -81,20 +154,18 @@
display: none; display: none;
} }
.itemDetails { .infoRow {
display: flex; display: flex;
flex-direction: column; align-items: baseline;
justify-content: space-between; justify-content: space-between;
margin-left: 10px; gap: 12px;
margin-right: 10px;
flex-grow: 1;
} }
.itemInvoiceDetails { .itemInvoiceDetails {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
margin-left: 10px; margin-left: 12px;
margin-top: -15px; margin-top: -15px;
flex-grow: 1; flex-grow: 1;
} }
@@ -102,50 +173,47 @@
.itemName { .itemName {
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-style: normal; font-style: normal;
width: calc(100% - 15px); /* Adjust the width to prevent overflow */ width: calc(100% - 20px);
font-size: 5vw; font-size: 16px;
font-weight: 500; font-weight: 600;
margin-top: 0; margin-top: 0;
margin: 0 5px; margin: 0 6px;
color: rgba(88, 55, 50, 1); color: #333;
background-color: transparent; background-color: transparent;
text-transform: capitalize; text-transform: capitalize;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.itemInvoiceName { .itemInvoiceName {
width: calc(260% - 15px); width: calc(260% - 20px);
background-color: transparent; background-color: transparent;
font-size: 1.3rem; font-size: 1.2rem;
font-weight: 500; font-weight: 600;
} }
.multiplySymbol { .multiplySymbol {
font-weight: 600; font-weight: 600;
color: #555;
} }
.qtyInvoice { .qtyInvoice {
font-weight: 500; font-weight: 500;
color: #555;
} }
.itemPrice { .itemPrice {
font-family: "Plus Jakarta Sans", sans-serif; display: none;
font-style: normal;
font-weight: 600;
width: calc(100% - 15px); /* Adjust the width to prevent overflow */
font-size: 3.3vw;
/* margin-bottom: 35px; */
margin-left: 5px;
color: #3a3a3a;
background-color: transparent;
} }
.itemPriceInvoice { .itemPriceInvoice {
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 700;
font-size: 0.9rem; font-size: 1rem;
margin-left: 5px; margin-left: 6px;
color: #d9c61c; color: #6B8F71;
text-align: right; text-align: right;
margin-top: 22px; margin-top: 22px;
} }
@@ -153,75 +221,404 @@
.itemQty { .itemQty {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 0.9rem; justify-content: flex-end;
margin-left: 5px; gap: 12px;
color: #a8c7a9; min-height: 40px;
fill: #a8c7a9;
height: 40px;
} }
.itemQtyValue { .itemQtyValue {
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 700;
margin-top: 19px; margin-top: 22px;
margin-left: 1px; margin-left: 2px;
margin-right: 1px; margin-right: 2px;
width: 25px; width: 32px;
text-align: center; text-align: center;
color: #333;
} }
.itemQtyInput { .itemQtyInput {
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 700;
width: 30px; /* Adjust the width to prevent overflow */ width: 40px;
font-size: 0.9rem; font-size: 1rem;
margin-bottom: 10px; margin-bottom: 12px;
text-align: center; text-align: center;
background-color: transparent; background-color: transparent;
border: 1px solid #ddd;
border-radius: 8px;
padding: 8px;
} }
.addButton { .addButton {
background-color: #ffffff; background-color: var(--brand-sage, #6B8F71);
border: 2px solid #a8c7a9; border: 1px solid var(--brand-sage, #6B8F71);
/* border: none; */ color: #ffffff;
display: inline-block; display: inline-block;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
width: 87px; min-width: 90px;
height: 32px; height: 40px;
margin-left: 5px; padding: 0 16px;
margin-top: 5px; border-radius: 10px;
border-radius: 20px; box-shadow: 0 2px 6px rgba(107, 143, 113, 0.2);
transition: all 0.2s ease;
} }
.addButton:hover {
background-color: var(--brand-sage-hover, #5a7a60);
border-color: var(--brand-sage-hover, #5a7a60);
box-shadow: 0 4px 10px rgba(107, 143, 113, 0.3);
}
.addButton:disabled {
background-color: var(--brand-sage-muted, #CFD8D3);
border-color: var(--brand-sage-muted, #CFD8D3);
cursor: default;
box-shadow: none;
}
.grayscale { .grayscale {
filter: grayscale(100%); filter: grayscale(100%);
} }
.disabled { .disabled {
color: gray; color: #999;
} }
.plusNegative { .plusNegative {
width: 35px; width: 40px;
height: 35px; height: 40px;
margin: 2.5px 0 -0.5px 0px; border-radius: 10px;
border: 1px solid #ddd;
background: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 700;
color: #666;
cursor: pointer;
transition: all 0.2s ease;
}
.plusNegative:hover {
background: #e9ecef;
border-color: #ccc;
} }
.plusNegative2 { .plusNegative2 {
width: 84px; width: 40px;
height: 21px; height: 40px;
position: absolute; position: absolute;
transform: rotate(45deg); right: 16px;
left: -33px; top: 50%;
top: 21px; transform: translateY(-50%);
border-radius: 10px;
border: 1px solid #ddd;
background: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 700;
color: #666;
cursor: pointer;
transition: all 0.2s ease;
} }
.plusNegative2:hover {
background: #e9ecef;
border-color: #ccc;
}
.remove { .remove {
width: 25px; width: 32px;
height: 25px; height: 32px;
margin-top: -10px; position: absolute;
margin-right: 10px; top: 12px;
right: 12px;
border-radius: 50%;
background: #ff4d4d;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 6px rgba(255, 77, 77, 0.3);
}
.remove:hover {
background: #ff1a1a;
transform: scale(1.1);
}
/* New elements for clean cafe item card */
.title {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600;
font-size: 17px;
color: #333;
margin: 0 0 4px 0;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
-webkit-line-clamp: 2;
text-transform: capitalize;
text-align: left;
flex: 1;
min-width: 0;
}
/* Responsive type scale for title and price */
@media (min-width: 600px) {
.title {
font-size: 18px;
}
.priceNow {
font-size: 16px;
}
}
@media (min-width: 992px) {
.title {
font-size: 19px;
}
.priceNow {
font-size: 17px;
}
}
.desc {
color: #666;
font-size: 13px;
line-height: 1.4;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
-webkit-line-clamp: 2;
}
.priceRow {
display: inline-flex;
align-items: center;
gap: 10px;
}
.promoBadge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 4px 10px;
height: 24px;
border-radius: 999px;
background: linear-gradient(to right, #e52535, #fe6d78);
color: #fff;
font-size: 12px;
font-weight: 700;
}
.priceNow {
color: #1c1d1d;
font-weight: 700;
font-size: 15px;
white-space: nowrap;
}
.priceOld {
color: #727272;
font-size: 13px;
text-decoration: line-through;
white-space: nowrap;
}
.qtyGroup {
display: inline-flex;
align-items: center;
border: 1px solid #ddd;
border-radius: 10px;
height: 36px;
overflow: hidden;
background: #ffffff;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.qtyBtn {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: #f8f9fa;
color: #333;
border: none;
font-size: 18px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
}
.qtyBtn:hover {
background: #e9ecef;
}
.qtyVal {
min-width: 30px;
text-align: center;
font-weight: 700;
color: #333;
font-size: 16px;
}
.actionRow {
display: flex;
align-items: center;
gap: 10px;
justify-content: space-between;
width: 100%;
}
/* Promo pill overlay on image */
.promoPill {
position: absolute;
top: 10px;
right: 10px;
z-index: 2;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 6px 12px;
height: auto;
border-radius: 8px;
background: #e53935;
color: #fff;
font-size: 12px;
font-weight: 700;
box-shadow: 0 2px 6px rgba(229, 57, 53, 0.3);
}
/* Info overlay on image (name + price) */
.overlayName {
position: absolute;
top: 12px;
left: 12px;
z-index: 2;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 700;
font-size: 15px;
line-height: 1.3;
color: #fff;
mix-blend-mode: difference;
margin: 0;
max-width: 70%;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
-webkit-line-clamp: 2;
pointer-events: none;
text-transform: capitalize;
}
@media (min-width: 600px) {
.overlayName {
font-size: 16px;
}
}
.overlayPrice {
display: flex;
align-items: center;
gap: 8px;
}
.overlayNow {
color: #fff;
font-weight: 800;
font-size: 13px;
}
.overlayOld {
color: rgba(255,255,255,0.8);
font-size: 12px;
text-decoration: line-through;
}
/* Bottom overlay bar: price left, qty right */
.bottomOverlay {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
background: rgba(0,0,0,0.6);
color: #fff;
border-radius: 0 0 12px 12px;
}
/* New stacked price for clarity */
.overlayPriceCol {
display: flex;
flex-direction: column;
align-items: flex-start;
line-height: 1.2;
}
.overlayOriginal {
color: rgba(255,255,255,0.85);
font-weight: 700;
font-size: 12px;
text-decoration: line-through;
}
@media (min-width: 600px) {
.overlayOriginal {
font-size: 13px;
}
}
.overlayPromo {
color: #fff;
font-weight: 800;
font-size: 17px;
}
@media (min-width: 600px) {
.overlayOriginal {
font-size: 15px;
}
.overlayPromo {
font-size: 19px;
}
}
/* Title text below image for portrait mode */
.title {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 700;
font-size: 16px;
color: #333;
margin: 10px 4px 6px 4px;
text-transform: capitalize;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
-webkit-line-clamp: 2;
}
@media (min-width: 600px) {
.title {
font-size: 17px;
}
} }
.itemInvoice .itemDetails { .itemInvoice .itemDetails {
@@ -233,7 +630,7 @@
.itemInvoice .itemName, .itemInvoice .itemName,
.itemInvoice .itemPrice, .itemInvoice .itemPrice,
.itemInvoice .itemQty .qtyInvoice .multiplySymbol { .itemInvoice .itemQty .qtyInvoice .multiplySymbol {
font-size: 0.9rem; font-size: 1rem;
} }
.blank { .blank {
@@ -246,6 +643,98 @@
.createItem { .createItem {
position: absolute; position: absolute;
left: 15px; left: 20px;
right: 15px; right: 20px;
bottom: 20px;
} }
/* Item list container */
.item-list-container {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px 0;
}
/* Section header */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #e6e6e6;
margin-bottom: 16px;
}
.section-title {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 700;
font-size: 20px;
color: #333;
margin: 0;
}
.section-actions {
display: flex;
gap: 12px;
}
/* Empty state */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
text-align: center;
background: #f8f9fa;
border-radius: 12px;
margin: 20px 0;
}
.empty-state-icon {
font-size: 48px;
color: #ccc;
margin-bottom: 16px;
}
.empty-state-title {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600;
font-size: 18px;
color: #666;
margin-bottom: 8px;
}
.empty-state-description {
font-family: "Plus Jakarta Sans", sans-serif;
font-size: 14px;
color: #999;
margin-bottom: 20px;
max-width: 300px;
}
/* Card variant */
.item-card {
background: #ffffff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
padding: 16px;
transition: all 0.2s ease;
}
.item-card:hover { /* no hover */ }
/* List variant */
.item-list-item {
background: #ffffff;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
padding: 16px;
display: flex;
align-items: center;
gap: 16px;
transition: all 0.2s ease;
}
.item-list-item:hover { /* no hover */ }

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import styles from "./Modal.module.css"; import styles from "./Modal.module.css";
import { getImageUrl } from "../helpers/itemHelper.js"; import { getImageUrl } from "../helpers/itemHelper.js";
@@ -23,9 +24,10 @@ const ItemConfig = ({
const [itemDescription, setItemDescription] = useState(initialDescription); const [itemDescription, setItemDescription] = useState(initialDescription);
const fileInputRef = useRef(null); const fileInputRef = useRef(null);
const textareaRef = useRef(null); const textareaRef = useRef(null);
const [saving, setSaving] = useState(false);
const [saveStatus, setSaveStatus] = useState(null); // 'success' | 'error'
useEffect(() => { useEffect(() => {
// Prevent scrolling when modal is open // Prevent scrolling when modal is open
document.body.style.overflow = "hidden"; document.body.style.overflow = "hidden";
if(selectedImage){ if(selectedImage){
@@ -80,23 +82,54 @@ const ItemConfig = ({
} }
}, [textareaRef.current]); }, [textareaRef.current]);
const handleCreate = () => { const handleCreate = async () => {
console.log(itemPromoPrice) setSaving(true);
handleCreateItem(itemName, itemPrice, selectedImage, previewUrl, itemDescription, itemPromoPrice); setSaveStatus(null);
document.body.style.overflow = "auto"; try {
await Promise.resolve(handleCreateItem(itemName, itemPrice, selectedImage, previewUrl, itemDescription, itemPromoPrice));
setSaveStatus('success');
} catch (e) {
setSaveStatus('error');
} finally {
setSaving(false);
}
}; };
const handleUpdate = () => { const handleUpdate = async () => {
console.log(itemName, itemPrice, selectedImage, itemDescription, itemPromoPrice) setSaving(true);
handleUpdateItem(itemName, itemPrice, selectedImage, itemDescription, itemPromoPrice); setSaveStatus(null);
document.body.style.overflow = "auto"; try {
await Promise.resolve(handleUpdateItem(itemName, itemPrice, selectedImage, itemDescription, itemPromoPrice));
setSaveStatus('success');
} catch (e) {
setSaveStatus('error');
} finally {
setSaving(false);
}
}; };
return ( return createPortal(
<div onClick={handleOverlayClick} style={{ position: 'fixed', width: '100vw', height: '100vh', left: 0, bottom: 0, display: 'flex', flexDirection: 'column-reverse', zIndex: 301, backgroundColor: '#00000061' }}> <div onClick={handleOverlayClick} className={styles.modalOverlay}>
<div onClick={handleContentClick} style={{ display: 'flex', flexDirection: 'column', padding: '15px', backgroundColor: 'white', borderRadius: '20px 20px 0 0', overflowY: 'auto' }}> <div onClick={handleContentClick} className={styles.modalContent}>
<div style={{ display: 'flex' }}> <div className={styles.imageSection}>
<div style={{ width: '26vw', height: '26vw', marginRight: '10px' }}> <div className={styles.imagePreview}>
<img style={{ width: '100%', height: '100%', borderRadius: '10px', objectFit: 'cover' }} src={previewUrl} /> <img src={previewUrl} alt="Preview" className={styles.previewImage} />
</div>
<div className={styles.imageActions}>
<button
onClick={handleChangeImage}
className={styles.actionButton}
>
{isBeingEdit ? 'Ganti Gambar' : 'Tambah Gambar'}
</button>
{isBeingEdit && (
<button
onClick={handleDelete}
className={`${styles.actionButton} ${styles.deleteButton}`}
>
Hapus
</button>
)}
</div>
<input <input
type="file" type="file"
ref={fileInputRef} ref={fileInputRef}
@@ -106,113 +139,78 @@ const ItemConfig = ({
style={{ display: "none" }} style={{ display: "none" }}
/> />
</div> </div>
<div style={{ width: '72%', height: '26vw', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<div onClick={() => handleChangeImage()} style={{ width: '140px', marginRight: '10px', height: '40px', alignContent: 'center', textAlign: 'center', borderRadius: '10px', border: '1px solid #60d37e', color: '#60d37e', backgroundColor: 'white', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
{isBeingEdit ? 'Ganti gambar' : 'Tambah gambar'}
</div>
<div onClick={handleDelete} style={{ width: '76px', height: '40px', alignContent: 'center', textAlign: 'center', borderRadius: '10px', border: '1px solid rgb(211 96 96)', color: 'rgb(211 96 96)', backgroundColor: 'white', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
Hapus
</div>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', color: 'black', marginTop: '10px' }}> <div className={styles.formSection}>
<p style={{ marginBottom: '5px', fontWeight: '500' }}>Nama item</p> <div className={styles.bannerRow}>
{saveStatus === 'success' && (
<span className={`${styles.banner} ${styles.bannerSuccess}`}>Perubahan disimpan</span>
)}
{saveStatus === 'error' && (
<span className={`${styles.banner} ${styles.bannerError}`}>Gagal menyimpan perubahan</span>
)}
</div>
<div className={styles.formGroup}>
<label className={styles.formLabel}>Nama Item</label>
<input <input
value={itemName} value={itemName}
style={{ className={styles.formInput}
padding: '10px',
borderRadius: '8px',
border: '1px solid #ccc',
fontSize: '14px',
width: '100%',
marginBottom: '15px',
outline: 'none',
transition: 'all 0.3s ease',
boxSizing: 'border-box', // Make sure the padding doesn't cause overflow
}}
onChange={(e)=>setItemName(e.target.value)} onChange={(e)=>setItemName(e.target.value)}
onFocus={(e) => e.target.style.borderColor = '#60d37e'} placeholder="Masukkan nama item"
onBlur={(e) => e.target.style.borderColor = '#ccc'}
/> />
</div> </div>
<div style={{ display: 'flex', color: 'black', justifyContent: 'space-between' }}> <div className={styles.formRow}>
<div style={{ width: '48%' }}> <div className={styles.formGroup}>
<p style={{ marginBottom: '5px', fontWeight: '500', marginTop: 0 }}>Harga</p> <label className={styles.formLabel}>Harga</label>
<input <input
value={itemPrice} value={itemPrice}
style={{ className={styles.formInput}
padding: '10px',
borderRadius: '8px',
border: '1px solid #ccc',
fontSize: '14px',
width: '100%',
marginBottom: '15px',
outline: 'none',
transition: 'all 0.3s ease',
boxSizing: 'border-box',
}}
onChange={(e)=>setItemPrice(e.target.value)} onChange={(e)=>setItemPrice(e.target.value)}
onFocus={(e) => e.target.style.borderColor = '#60d37e'} placeholder="Rp 0"
onBlur={(e) => e.target.style.borderColor = '#ccc'}
/> />
</div> </div>
<div style={{ width: '48%' }}> <div className={styles.formGroup}>
<p style={{ marginBottom: '5px', fontWeight: '500', marginTop: 0 }}>Harga promo</p> <label className={styles.formLabel}>Harga Promo</label>
<input <input
value={itemPromoPrice} value={itemPromoPrice}
placeholder="Opsional" className={styles.formInput}
style={{
padding: '10px',
borderRadius: '8px',
border: '1px solid #ccc',
fontSize: '14px',
width: '100%',
marginBottom: '15px',
outline: 'none',
transition: 'all 0.3s ease',
boxSizing: 'border-box',
}}
onChange={(e)=>setItemPromoPrice(e.target.value)} onChange={(e)=>setItemPromoPrice(e.target.value)}
onFocus={(e) => e.target.style.borderColor = '#60d37e'} placeholder="Opsional"
onBlur={(e) => e.target.style.borderColor = '#ccc'}
/> />
</div> </div>
</div> </div>
<div style={{ display: 'flex', flexDirection: 'column', color: 'black' }}> <div className={styles.formGroup}>
<p style={{ marginBottom: '5px', fontWeight: '500', marginTop: 0 }}>Deskripsi</p> <label className={styles.formLabel}>Deskripsi</label>
<textarea <textarea
ref={textareaRef} ref={textareaRef}
style={{ className={styles.formTextarea}
padding: '10px', placeholder="Tambahkan deskripsi item..."
borderRadius: '8px',
border: '1px solid #ccc',
fontSize: '14px',
width: '100%',
marginBottom: '15px',
outline: 'none',
transition: 'all 0.3s ease',
boxSizing: 'border-box',
resize: 'none', // Prevent manual resize that could cause overflow
}}
placeholder="Tambah deskripsi..."
value={itemDescription} value={itemDescription}
onChange={(e)=>setItemDescription(e.target.value)} onChange={(e)=>setItemDescription(e.target.value)}
onFocus={(e) => e.target.style.borderColor = '#60d37e'}
onBlur={(e) => e.target.style.borderColor = '#ccc'}
/> />
</div> </div>
<div style={{ width: '100%', height: '35px', display: 'flex', justifyContent: 'center', alignItems: 'center' }}> <div className={styles.formActions}>
<div onClick={() => {isBeingEdit ? handleUpdate() : handleCreate()} } style={{ width: '100%', height: '40px', alignContent: 'center', textAlign: 'center', borderRadius: '10px', border: '1px solid #60d37e', color: '#60d37e', backgroundColor: 'white', display: 'flex', justifyContent: 'center', alignItems: 'center' }}> <button
{isBeingEdit? 'Simpan' : 'Buat Item'} onClick={cancelEdit}
</div> className={`${styles.formButton} ${styles.cancelButton}`}
disabled={saving}
>
Batal
</button>
<button
onClick={() => { isBeingEdit ? handleUpdate() : handleCreate() }}
className={`${styles.formButton} ${styles.saveButton}`}
disabled={saving}
>
{saving ? 'Menyimpan…' : (isBeingEdit ? 'Simpan Perubahan' : 'Buat Item')}
</button>
</div> </div>
</div> </div>
</div> </div>
</div>,
document.body
); );
}; };

View File

@@ -8,7 +8,7 @@
bottom: 0; bottom: 0;
display: flex; display: flex;
flex-direction: column-reverse; flex-direction: column-reverse;
z-index: 200; z-index: 2147483600 !important; /* above watermark and page layers */
background-color: rgba(0, 0, 0, 0.38); /* #00000061 */ background-color: rgba(0, 0, 0, 0.38); /* #00000061 */
} }

View File

@@ -17,6 +17,7 @@ import {
import ItemType from "./ItemType.js"; import ItemType from "./ItemType.js";
import { createItemType, updateItemDeletionStatus } from "../helpers/itemHelper.js"; import { createItemType, updateItemDeletionStatus } from "../helpers/itemHelper.js";
import ItemConfig from "./ItemConfig.js" import ItemConfig from "./ItemConfig.js"
import { ArrowUp, ArrowDown, Pencil, Save, X } from 'lucide-react';
const ItemLister = ({ const ItemLister = ({
index, index,
@@ -73,6 +74,22 @@ const ItemLister = ({
const [randomKey, setRandomKey] = useState(0); const [randomKey, setRandomKey] = useState(0);
// Dummy items for style preview on cafe page (non-invoice, non-edit)
const showGrid = !isEdit && !forInvoice;
const dummyCount = showGrid ? Math.max(0, 6 - items.length) : 0;
const dummyItems = Array.from({ length: dummyCount }, (_, i) => ({
itemId: `dummy-${itemTypeId}-${i}`,
name: `Sample ${i + 1}`,
price: 25000 + i * 5000,
promoPrice: i % 2 === 1 ? 20000 + i * 3000 : 0,
qty: 0,
description: 'Contoh deskripsi singkat item.',
image: getImageUrl(`uploads/samples/sample (${(i % 16) + 1}).png`),
availability: true,
selectedImage: null,
}));
const displayItems = items; // no dummy items on cafe page
const handlePlusClick = (itemId) => { const handlePlusClick = (itemId) => {
const updatedItems = items.map((item) => { const updatedItems = items.map((item) => {
if (item.itemId === itemId) { if (item.itemId === itemId) {
@@ -598,7 +615,7 @@ const ItemLister = ({
return ( return (
<> <>
{(items.length > 0 || {(items.length > 0 ||
(user && (user.cafeId == shopId || user.userId == shopOwnerId))) && ( (user && (user.cafeId == shopId || user.user_id == shopOwnerId))) && (
<div <div
key={itemTypeId} key={itemTypeId}
className={`${styles["item-lister"]} ${isEdit ? styles["fullscreen"] : "" className={`${styles["item-lister"]} ${isEdit ? styles["fullscreen"] : ""
@@ -608,7 +625,15 @@ const ItemLister = ({
{(isEdit && isFirstStep || !isEdit) && {(isEdit && isFirstStep || !isEdit) &&
<div className={styles["title-container"]}> <div className={styles["title-container"]}>
{isEdit && <ItemType blank={true} imageUrl={previewUrl} />} {isEdit && (
<div className={styles["image-preview"]}>
<img
src={previewUrl}
alt="Category Preview"
className={styles["preview-image"]}
/>
</div>
)}
<input <input
className={`${styles.title} ${isEdit ? styles.border : styles.noborder className={`${styles.title} ${isEdit ? styles.border : styles.noborder
}`} }`}
@@ -618,87 +643,49 @@ const ItemLister = ({
disabled={!isEdit} disabled={!isEdit}
/> />
{isEditMode && !isEdit && ( {isEditMode && !isEdit && (
<> <div className={styles.titleActions}>
<div <button
style={{ className={styles.iconBtn}
width: '32px', onClick={() => index === 0 ? null : moveItemTypeUp(itemTypeId)}
height: '32px', // Add a height to the div disabled={index === 0}
display: 'flex', // Use flexbox aria-label="Naikkan kategori"
justifyContent: 'center', // Center horizontally
alignItems: 'center', // Center vertically
cursor: 'pointer'
}}
onClick={index == 0 ? null : () => moveItemTypeUp(itemTypeId)} // Move onClick here for the whole div
> >
<svg <ArrowUp size={20} />
viewBox="0 0 16 16" </button>
xmlns="http://www.w3.org/2000/svg" <button
fill="#000000" className={styles.iconBtn}
style={{ width: '100%', height: '100%' }} // Ensure SVG fits the div onClick={() => index === indexTotal - 1 ? null : moveItemTypeDown(itemTypeId)}
disabled={index === indexTotal - 1}
aria-label="Turunkan kategori"
> >
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g> <ArrowDown size={20} />
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g> </button>
<g id="SVGRepo_iconCarrier"> <button
<path d="m 1 11 c 0 -0.265625 0.105469 -0.519531 0.292969 -0.707031 l 6 -6 c 0.390625 -0.390625 1.023437 -0.390625 1.414062 0 l 6 6 c 0.1875 0.1875 0.292969 0.441406 0.292969 0.707031 s -0.105469 0.519531 -0.292969 0.707031 c -0.390625 0.390625 -1.023437 0.390625 -1.414062 0 l -5.292969 -5.292969 l -5.292969 5.292969 c -0.390625 0.390625 -1.023437 0.390625 -1.414062 0 c -0.1875 -0.1875 -0.292969 -0.441406 -0.292969 -0.707031 z m 0 0" fill={index === 0 ? "gray" : "#2e3436"}></path> className={styles.iconBtn}
</g> onClick={toggleEditTypeItem}
</svg> aria-label="Edit kategori"
>
<Pencil size={20} />
</button>
</div> </div>
<div )}
style={{ {isEditMode && isEdit && (
width: '32px', <div className={styles.titleActions}>
height: '32px', // Add a height to the div <button
display: 'flex', // Use flexbox className={styles.iconBtn}
justifyContent: 'center', // Center horizontally onClick={handleSaveType}
alignItems: 'center', // Center vertically aria-label="Simpan"
cursor: 'pointer'
}}
onClick={index == indexTotal - 1 ? null : () => moveItemTypeDown(itemTypeId)} // Move onClick here for the whole div
> >
<svg <Save size={20} />
viewBox="0 0 16 16" </button>
xmlns="http://www.w3.org/2000/svg" <button
fill="#000000" className={styles.iconBtn}
style={{ width: '100%', height: '100%' }} // Ensure SVG fits the div onClick={resetItems}
aria-label="Batal"
> >
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g> <X size={20} />
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g> </button>
<g id="SVGRepo_iconCarrier">
<path d="m 1 5 c 0 -0.265625 0.105469 -0.519531 0.292969 -0.707031 c 0.390625 -0.390625 1.023437 -0.390625 1.414062 0 l 5.292969 5.292969 l 5.292969 -5.292969 c 0.390625 -0.390625 1.023437 -0.390625 1.414062 0 c 0.1875 0.1875 0.292969 0.441406 0.292969 0.707031 s -0.105469 0.519531 -0.292969 0.707031 l -6 6 c -0.390625 0.390625 -1.023437 0.390625 -1.414062 0 l -6 -6 c -0.1875 -0.1875 -0.292969 -0.441406 -0.292969 -0.707031 z m 0 0" fill={index === indexTotal - 1 ? "gray" : "#2e3436"}></path>
</g>
</svg>
</div> </div>
<div
style={{
width: '32px',
height: '32px', // Add a height to the div
display: 'flex', // Use flexbox
justifyContent: 'center', // Center horizontally
alignItems: 'center', // Center vertically
cursor: 'pointer'
}}
onClick={toggleEditTypeItem} // Move onClick here for the whole div
>
<svg
fill="#000000"
viewBox="0 0 32 32"
style={{ fillRule: 'evenodd', clipRule: 'evenodd', strokeLinejoin: 'round', strokeMiterlimit: 2 }}
version="1.1"
xmlSpace="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlnsSerif="http://www.serif.com/"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path d="M12.965,5.462c0,-0 -2.584,0.004 -4.979,0.008c-3.034,0.006 -5.49,2.467 -5.49,5.5l0,13.03c0,1.459 0.579,2.858 1.611,3.889c1.031,1.032 2.43,1.611 3.889,1.611l13.003,0c3.038,-0 5.5,-2.462 5.5,-5.5c0,-2.405 0,-5.004 0,-5.004c0,-0.828 -0.672,-1.5 -1.5,-1.5c-0.827,-0 -1.5,0.672 -1.5,1.5l0,5.004c0,1.381 -1.119,2.5 -2.5,2.5l-13.003,0c-0.663,-0 -1.299,-0.263 -1.768,-0.732c-0.469,-0.469 -0.732,-1.105 -0.732,-1.768l0,-13.03c0,-1.379 1.117,-2.497 2.496,-2.5c2.394,-0.004 4.979,-0.008 4.979,-0.008c0.828,-0.002 1.498,-0.675 1.497,-1.503c-0.001,-0.828 -0.675,-1.499 -1.503,-1.497Z"></path>
<path d="M20.046,6.411l-6.845,6.846c-0.137,0.137 -0.232,0.311 -0.271,0.501l-1.081,5.152c-0.069,0.329 0.032,0.671 0.268,0.909c0.237,0.239 0.577,0.343 0.907,0.277l5.194,-1.038c0.193,-0.039 0.371,-0.134 0.511,-0.274l6.845,-6.845l-5.528,-5.528Zm1.415,-1.414l5.527,5.528l1.112,-1.111c1.526,-1.527 1.526,-4.001 -0,-5.527c-0.001,-0 -0.001,-0.001 -0.001,-0.001c-1.527,-1.526 -4.001,-1.526 -5.527,-0l-1.111,1.111Z"></path>
</g>
</svg>
</div>
</>
)} )}
</div> </div>
} }
@@ -712,6 +699,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage) handleImageChange(previewUrl, selectedImage)
} }
imageUrl={getImageUrl("uploads/assets/addnew.png")} imageUrl={getImageUrl("uploads/assets/addnew.png")}
compact={false}
/> />
{/* {typeImage != null && !previewUrl.includes(typeImage) && ( {/* {typeImage != null && !previewUrl.includes(typeImage) && (
<ItemType <ItemType
@@ -729,6 +717,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage) handleImageChange(previewUrl, selectedImage)
} }
imageUrl={getImageUrl("uploads/assets/beverage4.jpg")} imageUrl={getImageUrl("uploads/assets/beverage4.jpg")}
compact={false}
/> />
<ItemType <ItemType
rectangular={true} rectangular={true}
@@ -736,6 +725,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage) handleImageChange(previewUrl, selectedImage)
} }
imageUrl={getImageUrl("uploads/assets/beverage1.png")} imageUrl={getImageUrl("uploads/assets/beverage1.png")}
compact={false}
/> />
<ItemType <ItemType
rectangular={true} rectangular={true}
@@ -743,6 +733,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage) handleImageChange(previewUrl, selectedImage)
} }
imageUrl={getImageUrl("uploads/assets/beverage2.png")} imageUrl={getImageUrl("uploads/assets/beverage2.png")}
compact={false}
/> />
<ItemType <ItemType
rectangular={true} rectangular={true}
@@ -750,6 +741,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage) handleImageChange(previewUrl, selectedImage)
} }
imageUrl={getImageUrl("uploads/assets/beverage3.png")} imageUrl={getImageUrl("uploads/assets/beverage3.png")}
compact={false}
/> />
<ItemType <ItemType
rectangular={true} rectangular={true}
@@ -757,6 +749,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage) handleImageChange(previewUrl, selectedImage)
} }
imageUrl={getImageUrl("uploads/assets/snack5.png")} imageUrl={getImageUrl("uploads/assets/snack5.png")}
compact={false}
/> />
<ItemType <ItemType
rectangular={true} rectangular={true}
@@ -764,6 +757,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage) handleImageChange(previewUrl, selectedImage)
} }
imageUrl={getImageUrl("uploads/assets/dessert1.png")} imageUrl={getImageUrl("uploads/assets/dessert1.png")}
compact={false}
/> />
<ItemType <ItemType
rectangular={true} rectangular={true}
@@ -771,6 +765,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage) handleImageChange(previewUrl, selectedImage)
} }
imageUrl={getImageUrl("uploads/assets/dessert2.jpg")} imageUrl={getImageUrl("uploads/assets/dessert2.jpg")}
compact={false}
/> />
<ItemType <ItemType
rectangular={true} rectangular={true}
@@ -778,6 +773,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage) handleImageChange(previewUrl, selectedImage)
} }
imageUrl={getImageUrl("uploads/assets/food4.jpg")} imageUrl={getImageUrl("uploads/assets/food4.jpg")}
compact={false}
/> />
<ItemType <ItemType
rectangular={true} rectangular={true}
@@ -785,6 +781,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage) handleImageChange(previewUrl, selectedImage)
} }
imageUrl={getImageUrl("uploads/assets/food1.png")} imageUrl={getImageUrl("uploads/assets/food1.png")}
compact={false}
/> />
<ItemType <ItemType
rectangular={true} rectangular={true}
@@ -792,6 +789,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage) handleImageChange(previewUrl, selectedImage)
} }
imageUrl={getImageUrl("uploads/assets/food2.jpg")} imageUrl={getImageUrl("uploads/assets/food2.jpg")}
compact={false}
/> />
<ItemType <ItemType
@@ -800,6 +798,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage) handleImageChange(previewUrl, selectedImage)
} }
imageUrl={getImageUrl("uploads/assets/food3.png")} imageUrl={getImageUrl("uploads/assets/food3.png")}
compact={false}
/> />
<ItemType <ItemType
rectangular={true} rectangular={true}
@@ -807,6 +806,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage) handleImageChange(previewUrl, selectedImage)
} }
imageUrl={getImageUrl("uploads/assets/packet1.png")} imageUrl={getImageUrl("uploads/assets/packet1.png")}
compact={false}
/> />
<ItemType <ItemType
rectangular={true} rectangular={true}
@@ -814,6 +814,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage) handleImageChange(previewUrl, selectedImage)
} }
imageUrl={getImageUrl("uploads/assets/packet2.png")} imageUrl={getImageUrl("uploads/assets/packet2.png")}
compact={false}
/> />
<ItemType <ItemType
rectangular={true} rectangular={true}
@@ -821,6 +822,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage) handleImageChange(previewUrl, selectedImage)
} }
imageUrl={getImageUrl("uploads/assets/snack1.png")} imageUrl={getImageUrl("uploads/assets/snack1.png")}
compact={false}
/> />
<ItemType <ItemType
rectangular={true} rectangular={true}
@@ -828,6 +830,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage) handleImageChange(previewUrl, selectedImage)
} }
imageUrl={getImageUrl("uploads/assets/snack2.png")} imageUrl={getImageUrl("uploads/assets/snack2.png")}
compact={false}
/> />
<ItemType <ItemType
rectangular={true} rectangular={true}
@@ -835,6 +838,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage) handleImageChange(previewUrl, selectedImage)
} }
imageUrl={getImageUrl("uploads/assets/snack3.png")} imageUrl={getImageUrl("uploads/assets/snack3.png")}
compact={false}
/> />
<ItemType <ItemType
rectangular={true} rectangular={true}
@@ -842,6 +846,7 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage) handleImageChange(previewUrl, selectedImage)
} }
imageUrl={getImageUrl("uploads/assets/snack4.png")} imageUrl={getImageUrl("uploads/assets/snack4.png")}
compact={false}
/> />
{Array.from({ length: 16 }, (_, index) => { {Array.from({ length: 16 }, (_, index) => {
const sampleNumber = index + 1; // To get numbers from 1 to 16 const sampleNumber = index + 1; // To get numbers from 1 to 16
@@ -853,33 +858,53 @@ const ItemLister = ({
handleImageChange(previewUrl, selectedImage) handleImageChange(previewUrl, selectedImage)
} }
imageUrl={getImageUrl(`uploads/samples/sample (${sampleNumber}).png`)} imageUrl={getImageUrl(`uploads/samples/sample (${sampleNumber}).png`)}
compact={false}
/> />
); );
})} })}
</div> </div>
<button onClick={() => setIsFirstStep(false)} style={{ width: '100%', height: '40px', borderRadius: '20px' }}>selanjutnya</button> <button onClick={() => setIsFirstStep(false)} className={styles["add-item-button"]}>
Selanjutnya
</button>
</> </>
)} )}
{(isEdit && !isFirstStep || !isEdit) && {(isEdit && !isFirstStep || !isEdit) &&
<div key={randomKey}> <div key={randomKey}>
{isEdit && <div style={{ display: 'flex', justifyContent: 'flex-start' }}><div style={{ marginTop: '49px', marginRight: '10px', marginLeft: '10px' }} onClick={() => setIsFirstStep(true)}><svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 512 512"><path d="M48,256c0,114.87,93.13,208,208,208s208-93.13,208-208S370.87,48,256,48,48,141.13,48,256Zm212.65-91.36a16,16,0,0,1,.09,22.63L208.42,240H342a16,16,0,0,1,0,32H208.42l52.32,52.73A16,16,0,1,1,238,347.27l-79.39-80a16,16,0,0,1,0-22.54l79.39-80A16,16,0,0,1,260.65,164.64Z" /></svg></div> {isEdit && (
<h2 className={styles["item-list-title"]}>{items && items.length < 1 ? 'Buat item' : 'Daftar item'}</h2></div>} <div className={styles["settings-section"]}>
<div className={styles["item-list"]}> <h3 className={styles["settings-title"]}>Pengaturan Kategori</h3>
<div className={styles["setting-row"]}>
<span className={styles["setting-label"]}>Visibilitas Kategori</span>
<div className={styles["switch-container"]}>
<span className={styles["switch-label"]}>
{isVisible ? "Tampil" : "Tersembunyi"}
</span>
<Switch
onChange={() => setIsVisible(!isVisible)}
checked={isVisible}
offColor="#cccccc"
onColor="#6B8F71"
uncheckedIcon={false}
checkedIcon={false}
height={24}
width={48}
handleDiameter={20}
/>
</div>
</div>
</div>
)}
<div className={`${styles["item-list"]} ${(!isEdit && !forInvoice) ? styles["item-grid"] : ""}`}>
{user && ( {user && (
user.userId == shopOwnerId || user.cafeId == shopId) && user.user_id == shopOwnerId || user.cafeId == shopId) &&
isEditMode && ( isEditMode && (
<> <>
{!isAddingNewItem && ( {!isAddingNewItem && (
<button <button
className={styles["add-item-button"]} className={styles["add-item-button"]}
onClick={toggleAddNewItem} onClick={toggleAddNewItem}
style={{
display: "inline-block",
height: "120px",
fontSize: "20px",
}}
> >
Tambah item + + Tambah Item Baru
</button> </button>
)} )}
{isAddingNewItem && ( {isAddingNewItem && (
@@ -888,7 +913,7 @@ const ItemLister = ({
cancelEdit={() => toggleAddNewItem()} cancelEdit={() => toggleAddNewItem()}
handleCreateItem={onCreateItem} handleCreateItem={onCreateItem}
/> />
<Item blank={true} handleCreateItem={onCreateItem} /> <Item blank={true} handleCreateItem={onCreateItem} hideDetails={!showGrid} />
</> </>
)} )}
</> </>
@@ -915,48 +940,29 @@ const ItemLister = ({
{isEditMode && isEditItem != item.itemId && ( {isEditMode && isEditItem != item.itemId && (
<div className={styles["editModeLayout"]}> <div className={styles["editModeLayout"]}>
<div style={{ display: 'flex', alignItems: 'center', height: '40px', marginLeft: '7.5vw' }}> <div>
{isEditMode && ( {isEditMode && (
<div className={styles["switch-container"]}>
<span className={styles["switch-label"]}>
{item.availability ? "Tersedia" : "Tidak Tersedia"}
</span>
<Switch <Switch
onChange={() => handleChange(item.itemId)} onChange={() => handleChange(item.itemId)}
checked={item.availability} checked={item.availability}
offColor="#cccccc"
onColor="#6B8F71"
uncheckedIcon={false}
checkedIcon={false}
height={20}
width={40}
handleDiameter={16}
/> />
</div>
)} )}
<h3>
&nbsp;{item.availability ? "Tersedia" : "Tidak tersedia"}
</h3>
</div> </div>
<div onClick={() => editItem(item.itemId)} style={{ display: 'flex', alignItems: 'center', height: '40px', marginRight: '7.5vw' }}> <div onClick={() => editItem(item.itemId)}>
<div <Pencil size={18} />
style={{
width: '32px',
height: '32px', // Add a height to the div
display: 'flex', // Use flexbox
justifyContent: 'center', // Center horizontally
alignItems: 'center', // Center vertically
cursor: 'pointer'
}}
>
<svg
fill="white"
viewBox="0 0 32 32"
style={{ fillRule: 'evenodd', clipRule: 'evenodd', strokeLinejoin: 'round', strokeMiterlimit: 2 }}
version="1.1"
xmlSpace="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlnsSerif="http://www.serif.com/"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path d="M12.965,5.462c0,-0 -2.584,0.004 -4.979,0.008c-3.034,0.006 -5.49,2.467 -5.49,5.5l0,13.03c0,1.459 0.579,2.858 1.611,3.889c1.031,1.032 2.43,1.611 3.889,1.611l13.003,0c3.038,-0 5.5,-2.462 5.5,-5.5c0,-2.405 0,-5.004 0,-5.004c0,-0.828 -0.672,-1.5 -1.5,-1.5c-0.827,-0 -1.5,0.672 -1.5,1.5l0,5.004c0,1.381 -1.119,2.5 -2.5,2.5l-13.003,0c-0.663,-0 -1.299,-0.263 -1.768,-0.732c-0.469,-0.469 -0.732,-1.105 -0.732,-1.768l0,-13.03c0,-1.379 1.117,-2.497 2.496,-2.5c2.394,-0.004 4.979,-0.008 4.979,-0.008c0.828,-0.002 1.498,-0.675 1.497,-1.503c-0.001,-0.828 -0.675,-1.499 -1.503,-1.497Z"></path>
<path d="M20.046,6.411l-6.845,6.846c-0.137,0.137 -0.232,0.311 -0.271,0.501l-1.081,5.152c-0.069,0.329 0.032,0.671 0.268,0.909c0.237,0.239 0.577,0.343 0.907,0.277l5.194,-1.038c0.193,-0.039 0.371,-0.134 0.511,-0.274l6.845,-6.845l-5.528,-5.528Zm1.415,-1.414l5.527,5.528l1.112,-1.111c1.526,-1.527 1.526,-4.001 -0,-5.527c-0.001,-0 -0.001,-0.001 -0.001,-0.001c-1.527,-1.526 -4.001,-1.526 -5.527,-0l-1.111,1.111Z"></path>
</g>
</svg>
</div>
<h3>Edit item</h3>
</div> </div>
</div> </div>
)} )}
@@ -971,6 +977,7 @@ const ItemLister = ({
qty={item.qty} qty={item.qty}
imageUrl={item.image} imageUrl={item.image}
imageFile={item.selectedImage} imageFile={item.selectedImage}
hideDetails={!showGrid}
onPlusClick={() => handlePlusClick(item.itemId)} onPlusClick={() => handlePlusClick(item.itemId)}
onNegativeClick={() => handleNegativeClick(item.itemId)} onNegativeClick={() => handleNegativeClick(item.itemId)}
onRemoveClick={() => handleRemoveClick(item.itemId)} onRemoveClick={() => handleRemoveClick(item.itemId)}
@@ -990,13 +997,6 @@ const ItemLister = ({
<div <div
key={item.itemId}> key={item.itemId}>
{isEditItem == item.itemId && ( {isEditItem == item.itemId && (
// <button
// className={styles["add-item-button"]}
// onClick={() => editItem(0)}
// style={{ display: "inline-block" }}
// >
// batal
// </button>
<ItemConfig <ItemConfig
isBeingEdit={true} isBeingEdit={true}
name={item.name} name={item.name}
@@ -1022,23 +1022,30 @@ const ItemLister = ({
<div className={styles["itemWrapper"]}> <div className={styles["itemWrapper"]}>
{(isEditMode && isEditItem != item.itemId || item.willBeDeleted) && ( {(isEditMode && isEditItem != item.itemId || item.willBeDeleted) && (
<div className={styles["editModeLayout"]}> <div className={styles["editModeLayout"]}>
<div style={{ display: 'flex', alignItems: 'center', height: '40px', marginLeft: '7.5vw' }}> <div>
{!item.willBeDeleted && isEditMode && ( {!item.willBeDeleted && isEditMode && (
<div className={styles["switch-container"]}>
<span className={styles["switch-label"]}>
{item.availability ? "Tersedia" : "Tidak Tersedia"}
</span>
<Switch <Switch
onChange={() => handleChange(item.itemId)} onChange={() => handleChange(item.itemId)}
checked={item.availability} checked={item.availability}
offColor="#cccccc"
onColor="#6B8F71"
uncheckedIcon={false}
checkedIcon={false}
height={20}
width={40}
handleDiameter={16}
/> />
</div>
)} )}
{item.willBeDeleted ? {item.willBeDeleted && (
<span style={{ backgroundColor: 'black', padding: '6px 12px', borderRadius: '20px' }}>
<h3 style={{ backgroundColor: 'black', padding: '13px 26px' }}>
Ditandai untuk dihapus Ditandai untuk dihapus
</h3> </span>
: )}
<h3>
&nbsp;{item.availability ? "Tersedia" : "Tidak tersedia"}
</h3>
}
</div> </div>
<div onClick={() => { <div onClick={() => {
@@ -1047,38 +1054,8 @@ const ItemLister = ({
} else { } else {
handleItemDeletionToggle(item.itemId, false); handleItemDeletionToggle(item.itemId, false);
} }
}} }}>
style={{ display: 'flex', alignItems: 'center', height: '40px', marginRight: '7.5vw' }}> <Pencil size={18} />
{!item.willBeDeleted && <div
style={{
width: '32px',
height: '32px', // Add a height to the div
display: 'flex', // Use flexbox
justifyContent: 'center', // Center horizontally
alignItems: 'center', // Center vertically
cursor: 'pointer'
}}
>
<svg
fill="white"
viewBox="0 0 32 32"
style={{ fillRule: 'evenodd', clipRule: 'evenodd', strokeLinejoin: 'round', strokeMiterlimit: 2 }}
version="1.1"
xmlSpace="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlnsSerif="http://www.serif.com/"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path d="M12.965,5.462c0,-0 -2.584,0.004 -4.979,0.008c-3.034,0.006 -5.49,2.467 -5.49,5.5l0,13.03c0,1.459 0.579,2.858 1.611,3.889c1.031,1.032 2.43,1.611 3.889,1.611l13.003,0c3.038,-0 5.5,-2.462 5.5,-5.5c0,-2.405 0,-5.004 0,-5.004c0,-0.828 -0.672,-1.5 -1.5,-1.5c-0.827,-0 -1.5,0.672 -1.5,1.5l0,5.004c0,1.381 -1.119,2.5 -2.5,2.5l-13.003,0c-0.663,-0 -1.299,-0.263 -1.768,-0.732c-0.469,-0.469 -0.732,-1.105 -0.732,-1.768l0,-13.03c0,-1.379 1.117,-2.497 2.496,-2.5c2.394,-0.004 4.979,-0.008 4.979,-0.008c0.828,-0.002 1.498,-0.675 1.497,-1.503c-0.001,-0.828 -0.675,-1.499 -1.503,-1.497Z"></path>
<path d="M20.046,6.411l-6.845,6.846c-0.137,0.137 -0.232,0.311 -0.271,0.501l-1.081,5.152c-0.069,0.329 0.032,0.671 0.268,0.909c0.237,0.239 0.577,0.343 0.907,0.277l5.194,-1.038c0.193,-0.039 0.371,-0.134 0.511,-0.274l6.845,-6.845l-5.528,-5.528Zm1.415,-1.414l5.527,5.528l1.112,-1.111c1.526,-1.527 1.526,-4.001 -0,-5.527c-0.001,-0 -0.001,-0.001 -0.001,-0.001c-1.527,-1.526 -4.001,-1.526 -5.527,-0l-1.111,1.111Z"></path>
</g>
</svg>
</div>
}
<h3>{item.willBeDeleted ? 'Batalkan' : 'Edit item'}</h3>
</div> </div>
</div> </div>
)} )}
@@ -1088,6 +1065,8 @@ const ItemLister = ({
last={index === indexTotal - 1 && indexx === items.length - 1} last={index === indexTotal - 1 && indexx === items.length - 1}
forCart={forCart} forCart={forCart}
forInvoice={forInvoice} forInvoice={forInvoice}
portrait={showGrid}
hideDetails={!showGrid}
name={item.name} name={item.name}
price={item.price} price={item.price}
promoPrice={item.promoPrice} promoPrice={item.promoPrice}
@@ -1097,9 +1076,9 @@ const ItemLister = ({
itemTypeId ? getImageUrl(item.image) : item.image itemTypeId ? getImageUrl(item.image) : item.image
} }
imageFile={item.selectedImage} imageFile={item.selectedImage}
onPlusClick={() => handlePlusClick(item.itemId)} onPlusClick={() => (String(item.itemId).startsWith('dummy-') ? undefined : handlePlusClick(item.itemId))}
onNegativeClick={() => handleNegativeClick(item.itemId)} onNegativeClick={() => (String(item.itemId).startsWith('dummy-') ? undefined : handleNegativeClick(item.itemId))}
onRemoveClick={() => handleRemoveClick(item.itemId)} onRemoveClick={() => (String(item.itemId).startsWith('dummy-') ? undefined : handleRemoveClick(item.itemId))}
isBeingEdit={isEditItem == item.itemId} isBeingEdit={isEditItem == item.itemId}
isAvailable={item.availability} isAvailable={item.availability}
handleUpdateItem={(name, price, image, description, promoPrice) => handleUpdateItem={(name, price, image, description, promoPrice) =>
@@ -1113,7 +1092,7 @@ const ItemLister = ({
{user && {user &&
user.roleId == 1 && user.roleId == 1 &&
user.userId == shopOwnerId && user.user_id == shopOwnerId &&
isEdit && ( isEdit && (
<> <>
{/* <button {/* <button
@@ -1128,33 +1107,6 @@ const ItemLister = ({
</div> </div>
</div> </div>
} }
{isEdit && (
<div className={styles.PaymentOption}>
<div className={styles.TotalContainer}>
<span>Pengaturan</span>
<span></span>
</div>
<div className={styles.OptionContainer}>
<span>sembunyikan semua</span>
<span>
<Switch
onChange={() => setIsVisible(!isVisible)}
checked={!isVisible}
/>
</span>
</div>
<button onClick={handleSaveType} className={styles.PayButton}>
{false ? (
<ColorRing height="50" width="50" color="white" />
) : (
"Simpan"
)}
</button>
<div className={styles.Pay2Button} onClick={resetItems}>
Kembali
</div>
</div>
)}
</div> </div>
)} )}
</> </>

View File

@@ -2,80 +2,88 @@
.item-lister { .item-lister {
width: 100%; width: 100%;
padding: 10px; /* Adjust padding as needed */ padding: 16px;
box-sizing: border-box; /* Ensure padding doesn't affect width */ box-sizing: border-box;
white-space: break-spaces; white-space: break-spaces;
background-color: #ffffff;
border-radius: 16px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
margin-bottom: 20px;
position: relative;
z-index: 0 !important; /* ensure behind modal overlay */
} }
.fullscreen { .fullscreen {
position: fixed; /* Keep the container fixed */ position: fixed;
top: 0; /* Adjust the top position as needed */ top: 0;
bottom: 0; /* Occupy the full height of the viewport */ bottom: 0;
left: 0; /* Align to the left */ left: 0;
right: 0; /* Align to the right */ right: 0;
background-color: white; /* Background color */ background-color: white;
z-index: 1000; /* Layering */ z-index: 100; /* keep above page, below modal overlays (>=200) */
overflow-y: auto; /* Allow vertical scrolling */ overflow-y: auto;
} }
.grid-container { .grid-container {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
gap: 10px; gap: 16px;
/* padding: 10px; */ height: calc(52vw - 20px);
/* max-height: calc(3 * (25vw - 20px) + 20px); */ padding: 16px;
overflow-y: auto; background-color: #f8f9fa;
height: calc(49vw - 20px); border-radius: 12px;
margin-bottom: 24px;
} }
@media (min-height: 0px) { @media (min-height: 0px) {
.grid-container { .grid-container {
height: 27vh; height: 30vh;
} }
} }
@media (min-height: 630px) { @media (min-height: 630px) {
.grid-container { .grid-container {
height: 27vh; height: 30vh;
} }
} }
@media (min-height: 636px) { @media (min-height: 636px) {
.grid-container { .grid-container {
height: 29vh; height: 32vh;
} }
} }
@media (min-height: 650px) { @media (min-height: 650px) {
.grid-container { .grid-container {
height: 34vh; height: 38vh;
} }
} }
@media (min-height: 705px) { @media (min-height: 705px) {
.grid-container { .grid-container {
height: 37vh; height: 41vh;
} }
} }
@media (min-height: 735px) { @media (min-height: 735px) {
.grid-container { .grid-container {
height: 38vh; height: 42vh;
} }
} }
@media (min-height: 759px) { @media (min-height: 759px) {
.grid-container { .grid-container {
height: 40vh; height: 44vh;
} }
} }
@media (min-height: 819px) { @media (min-height: 819px) {
.grid-container { .grid-container {
height: 44vh; height: 48vh;
} }
} }
@media (min-height: 830px) { @media (min-height: 830px) {
.grid-container { .grid-container {
height: 47vh; height: 51vh;
} }
} }
@media (min-height: 892px) { @media (min-height: 892px) {
.grid-container { .grid-container {
height: 49vh; height: 53vh;
} }
} }
@@ -83,46 +91,166 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 16px 20px;
background-color: #f8f9fa;
border-radius: 12px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
position: relative;
z-index: 0 !important; /* ensure behind modal overlay */
}
.titleActions {
display: inline-flex;
align-items: center;
gap: 10px;
}
.iconBtn {
width: 40px;
height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid #e6e6e6;
background: #ffffff;
color: #2d2d2d;
border-radius: 10px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
position: relative;
z-index: 0 !important; /* ensure behind modal overlay */
}
.iconBtn:disabled {
opacity: 0.5;
cursor: default;
}
.iconBtn:hover:not(:disabled) {
background: var(--brand-sage-50, #F0F6F2);
border-color: var(--brand-sage, #6B8F71);
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
} }
.title { .title {
background-color: transparent; background-color: transparent;
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500; font-weight: 600;
font-style: normal; font-style: normal;
font-size: 20px; font-size: 22px;
color: rgba(88, 55, 50, 1); color: rgba(88, 55, 50, 1);
text-align: left; text-align: left;
width: calc(70% - 10px); width: calc(70% - 12px);
padding-left: 10px; padding-left: 12px;
text-transform: capitalize; text-transform: capitalize;
border: none;
outline: none;
position: relative;
z-index: 151;
} }
.edit-typeItem-button { .edit-typeItem-button {
margin-left: auto; /* Push the button to the right */ margin-left: auto;
padding: 8px 16px; /* Adjust padding as needed */ padding: 10px 16px;
font-size: 14px; font-size: 14px;
background-color: #007bff; background-color: #007bff;
color: #fff; color: #fff;
border: none; border: none;
border-radius: 4px; border-radius: 8px;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
position: relative;
z-index: 152;
}
.edit-typeItem-button:hover {
background-color: #0069d9;
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
} }
.add-item-button { .add-item-button {
margin-top: 10px; margin: 0; /* follow parent gap for top/bottom spacing */
padding: 8px 16px; /* Adjust padding as needed */ display: inline-block;
font-size: 14px; width: 275px; /* requested size */
background-color: #359d42d1; height: 275px; /* requested size */
color: #fff; padding: 0; /* match image padding (none) */
border: none; border: none;
border-radius: 4px; background: transparent; /* card drawn via ::before */
color: transparent; /* no visible text */
border-radius: 12px; /* match portrait image radius */
cursor: pointer; cursor: pointer;
transition: none;
box-shadow: none;
position: relative;
z-index: 1; /* behind modal overlay */
}
.add-item-button::before {
content: "";
position: absolute;
inset: 0; /* fill container */
border-radius: 12px; /* match portrait image radius */
background:
/* vertical bar */
linear-gradient(var(--brand-sage, #6B8F71), var(--brand-sage, #6B8F71)) center/5px 20% no-repeat,
/* horizontal bar */
linear-gradient(var(--brand-sage, #6B8F71), var(--brand-sage, #6B8F71)) center/20% 5px no-repeat,
/* base */
var(--brand-sage-50, #F0F6F2);
border: 2px dashed var(--brand-sage, #6B8F71);
box-shadow: 0 4px 12px rgba(0,0,0,0.10);
}
.add-item-button:focus-visible {
outline: 2px solid var(--brand-sage, #6B8F71);
outline-offset: 2px;
border-radius: 12px;
}
/* Overlay text inside the left tile area */
.add-item-button::after { content: none; }
.add-item-button:hover {
background-color: #ffffff;
box-shadow: none;
} }
.item-list { .item-list {
display: flex; display: flex;
flex-direction: column; /* Display items in a column */ flex-direction: column;
gap: 16px;
position: relative;
z-index: 0 !important; /* ensure behind modal overlay */
}
/* Generic switch row styling reused in multiple spots */
.switch-container {
display: inline-flex;
align-items: center;
gap: 8px;
}
.switch-label {
font-size: 13px;
font-weight: 600;
color: #333;
}
/* Grid layout for portrait cards on cafe page */
.item-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
@media (min-width: 600px) {
.item-grid {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
}
@media (min-width: 992px) {
.item-grid {
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}
} }
.border { .border {
@@ -132,40 +260,91 @@
.noborder { .noborder {
border: 1px solid #ffffff00; border: 1px solid #ffffff00;
} }
.itemWrapper { .itemWrapper {
position: relative; position: relative;
background-color: #ffffff;
border-radius: 12px;
box-shadow: 0 1px 5px rgba(0,0,0,0.08);
transition: all 0.2s ease;
overflow: hidden;
position: relative;
z-index: 150;
} }
.itemWrapper:hover { /* remove hover effect */ }
.editModeLayout { .editModeLayout {
border-radius: 4px; /* Turn full-width bar into subtle corner controls over image */
position: absolute; position: absolute;
z-index: 100; inset: 0;
background-color: #0000008c; z-index: 155; /* above itemWrapper */
width: 100%; pointer-events: none; /* only children are interactive */
top: 7px; }
bottom: -4px;
display: flex; /* Left badge: availability switch + label */
flex-direction: row; .editModeLayout > div:first-child {
position: absolute;
top: 16px;
left: 16px;
display: inline-flex;
align-items: center; align-items: center;
justify-content: space-between; gap: 8px;
font-size: 14px; background: rgba(0, 0, 0, 0.55);
color: #fff;
border-radius: 999px;
padding: 6px 10px;
pointer-events: auto;
backdrop-filter: blur(2px);
}
/* Right badge: edit/unmark button */
.editModeLayout > div:last-child {
position: absolute;
top: 16px;
right: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: rgba(0, 0, 0, 0.55);
color: #fff;
border-radius: 999px;
pointer-events: auto;
cursor: pointer;
transition: background 0.2s ease, transform 0.2s ease;
backdrop-filter: blur(2px);
}
.editModeLayout > div:last-child:hover {
background: rgba(0, 0, 0, 0.7);
transform: translateY(-1px);
}
/* When label exists, keep it compact and readable */
.editModeLayout .switch-label {
font-size: 12px;
font-weight: 600;
color: #fff;
white-space: nowrap;
} }
.PaymentOption { .PaymentOption {
overflow-x: hidden; overflow-x: hidden;
background-color: #e9e9e9; background-color: #ffffff;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
font-size: calc(10px + 2vmin); font-size: calc(12px + 2vmin);
color: rgba(88, 55, 50, 1); color: rgba(88, 55, 50, 1);
border-radius: 15px 15px 0 0; border-radius: 20px 20px 0 0;
position: fixed; position: fixed;
bottom: 0; bottom: 0;
right: 0; right: 0;
left: 0; left: 0;
z-index: 300; /* Menurunkan z-index agar tidak menutupi material list */
z-index: 300; box-shadow: 0 -2px 20px rgba(0, 0, 0, 0.1);
padding: 20px;
} }
.TotalContainer { .TotalContainer {
@@ -176,20 +355,28 @@
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600; font-weight: 600;
font-style: normal; font-style: normal;
font-size: 1.5em; font-size: 1.8em;
padding: 10px 0; padding: 15px 0;
border-bottom: 1px solid #e6e6e6;
} }
.OptionContainer { .OptionContainer {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
width: 80vw; width: 80vw;
margin: 0 auto; margin: 0 auto;
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600; font-weight: 500;
font-style: normal; font-style: normal;
font-size: 0.9em; font-size: 1.1em;
padding: 10px 0; padding: 15px 0;
align-items: center;
} }
.OptionContainer span:first-child {
color: #555;
}
.PayButton { .PayButton {
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500; font-weight: 500;
@@ -197,7 +384,7 @@
font-size: 32px; font-size: 32px;
width: 80vw; width: 80vw;
height: 70px; height: 56px;
border-radius: 50px; border-radius: 50px;
background-color: rgba(88, 55, 50, 1); background-color: rgba(88, 55, 50, 1);
color: white; color: white;
@@ -205,14 +392,32 @@
margin: 0px auto; margin: 0px auto;
cursor: pointer; cursor: pointer;
margin-bottom: 23px; margin-bottom: 23px;
transition: all 0.2s ease;
box-shadow: 0 4px 12px rgba(88, 55, 50, 0.2);
position: relative;
z-index: 301;
}
.PayButton:hover {
background-color: rgba(70, 45, 40, 1);
box-shadow: 0 6px 16px rgba(88, 55, 50, 0.3);
} }
.Pay2Button { .Pay2Button {
text-align: center; text-align: center;
color: rgba(88, 55, 50, 1); color: rgba(88, 55, 50, 1);
font-size: 1em; font-size: 1.2em;
margin-bottom: 25px; font-weight: 500;
margin-bottom: 10px;
cursor: pointer; cursor: pointer;
padding: 12px;
border-radius: 8px;
transition: all 0.2s ease;
position: relative;
z-index: 301;
}
.Pay2Button:hover {
background-color: #f0f0f0;
} }
.item-list-title { .item-list-title {
@@ -225,3 +430,24 @@
color: black; color: black;
text-align: left; text-align: left;
} }
/* Harga dalam item list - dikurangi ukurannya agar proporsional */
.itemPriceList {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 700;
font-size: 14px;
color: #1c1d1d;
margin: 2px 0;
white-space: nowrap;
}
.itemPriceList.promo {
color: #e53935;
font-size: 13px;
}
.itemPriceList.original {
color: #727272;
font-size: 12px;
text-decoration: line-through;
}

View File

@@ -1,5 +1,6 @@
import React, { useRef, useEffect, useState } from "react"; import React, { useRef, useEffect, useState } from "react";
import styles from "./ItemType.module.css"; import styles from "./ItemType.module.css";
import { Coffee, CupSoda, CakeSlice, Utensils, Grid2X2, Plus } from 'lucide-react';
export default function ItemType({ export default function ItemType({
onClick, onClick,
@@ -9,6 +10,8 @@ export default function ItemType({
imageUrl, imageUrl,
selected, selected,
rectangular, rectangular,
compact,
noIcon, // New prop to remove icons
}) { }) {
const inputRef = useRef(null); const inputRef = useRef(null);
const [namee, setName] = useState(name); const [namee, setName] = useState(name);
@@ -57,42 +60,64 @@ export default function ItemType({
onCreate(namee, selectedImage); onCreate(namee, selectedImage);
}; };
const formatName = (val) => {
if (!val || typeof val !== 'string') return val;
return val
.toLowerCase()
.replace(/\b\w/g, (c) => c.toUpperCase());
};
const iconImageUrl = imageUrl === 'uploads/assets/All.png' ? 'icon:all' : imageUrl;
// If noIcon is true, we render a button-like element without icons
if (noIcon) {
return ( return (
<div <div
className={ className={`${styles["item-type"]} ${compact ? styles["item-type-compact"] : ""}`}
styles[ style={{ zIndex: blank ? 301 : "inherit" }}
namee >
? "item-type" <div
: rectangular onClick={onClick}
? "item-type-rectangular" className={`${styles["item-type-rect"]} ${compact ? styles["item-type-rect-compact"] : ""} ${selected ? styles["selected"] : ""}`}
: "item-type-nomargin" style={{
] backgroundColor: selected ? 'var(--brand-sage, #6B8F71)' : '#ffffff',
borderColor: selected ? 'var(--brand-sage, #6B8F71)' : '#e6e6e6',
color: selected ? '#ffffff' : '#2d2d2d',
}}
>
<span>
{formatName(namee)}
</span>
</div>
</div>
);
} }
return (
<div
className={`${styles["item-type"]} ${compact ? styles["item-type-compact"] : ""}`}
style={{ zIndex: blank ? 301 : "inherit" }} style={{ zIndex: blank ? 301 : "inherit" }}
> >
<div <div
onClick={ onClick={
rectangular ? (blank ? null : () => onClick(imageUrl)) : onClick rectangular ? (blank ? null : () => onClick(iconImageUrl)) : onClick
} }
className={styles["item-type-rect"]} className={`${styles["item-type-rect"]} ${compact ? styles["item-type-rect-compact"] : ""} ${selected ? styles["selected"] : ""}`}
style={{ style={{
top: selected ? "-10px" : "initial", backgroundColor: selected ? 'var(--brand-sage, #6B8F71)' : '#ffffff',
borderColor: selected ? 'var(--brand-sage, #6B8F71)' : '#e6e6e6',
color: selected ? '#ffffff' : '#2d2d2d',
}} }}
> >
{imageUrl != 'uploads/assets/All.png' ? {iconImageUrl === 'uploads/assets/All.png' ? (
<img <svg version="1.0" xmlns="http://www.w3.org/2000/svg"
src={previewUrl}
alt={namee}
className={styles["item-type-image"]}
/>
:<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="100%" height="100%" viewBox="0 0 800.000000 800.000000" width="100%" height="100%" viewBox="0 0 800.000000 800.000000"
preserveAspectRatio="xMidYMid meet"> preserveAspectRatio="xMidYMid meet">
<metadata> <metadata>
Created by potrace 1.16, written by Peter Selinger 2001-2019 Created by potrace 1.16, written by Peter Selinger 2001-2019
</metadata> </metadata>
<g transform="translate(0.000000,800.000000) scale(0.100000,-0.100000)" <g transform="translate(0.000000,800.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none"> fill="currentColor" stroke="none">
<path d="M3708 7165 c-3 -4 -44 -10 -90 -15 -266 -28 -530 -91 -753 -180 -11 <path d="M3708 7165 c-3 -4 -44 -10 -90 -15 -266 -28 -530 -91 -753 -180 -11
-4 -42 -16 -70 -26 -27 -9 -129 -57 -225 -106 -186 -94 -188 -95 -262 -145 -4 -42 -16 -70 -26 -27 -9 -129 -57 -225 -106 -186 -94 -188 -95 -262 -145
-26 -18 -52 -33 -58 -33 -5 0 -24 -13 -42 -28 -18 -16 -53 -43 -78 -61 -124 -26 -18 -52 -33 -58 -33 -5 0 -24 -13 -42 -28 -18 -16 -53 -43 -78 -61 -124
@@ -138,7 +163,17 @@ c261 0 329 -3 352 -14z m1237 -2 c52 -35 54 -49 54 -379 0 -348 -2 -360 -69
58 40 59 387 60 178 0 328 -4 342 -10z"/> 58 40 59 387 60 178 0 328 -4 342 -10z"/>
</g> </g>
</svg> </svg>
} ) : (iconImageUrl && typeof iconImageUrl === 'string' && iconImageUrl.startsWith('icon:')) ? (
<div style={{width:'100%',height:'100%',display:'flex',alignItems:'center',justifyContent:'center'}}>
<LucideCategoryIcon name={namee} iconKey={(iconImageUrl || '').split(':')[1]} compact={compact} />
</div>
) : (
<img
src={previewUrl}
alt={namee}
className={styles["item-type-image"]}
/>
)}
{blank && rectangular && ( {blank && rectangular && (
<div className={styles["item-type-image-container"]}> <div className={styles["item-type-image-container"]}>
<input <input
@@ -152,18 +187,42 @@ c261 0 329 -3 352 -14z m1237 -2 c52 -35 54 -49 54 -379 0 -348 -2 -360 -69
)} )}
</div> </div>
{!rectangular && !blank && ( {!rectangular && !blank && (
<input <div className={`${styles["item-type-name"]} ${compact ? styles["item-type-name-compact"] : ""}`} style={{ color: selected ? '#2d2d2d' : '#333' }}>
ref={inputRef} {formatName(namee)}
className={`${styles["item-type-name"]} ${styles.noborder}`} </div>
value={namee}
onChange={handleNameChange}
disabled={true}
style={{
top: selected ? "-5px" : "initial",
borderBottom: selected ? "1px solid #000" : "none",
}}
/>
)} )}
</div> </div>
); );
} }
function LucideCategoryIcon({ name, iconKey, compact }) {
const key = pickIconKey(name, iconKey);
const size = compact ? '65%' : '56%';
switch (key) {
case 'coffee':
return <Coffee color={'currentColor'} size={size} strokeWidth={2} />;
case 'drink':
return <CupSoda color={'currentColor'} size={size} strokeWidth={2} />;
case 'dessert':
return <CakeSlice color={'currentColor'} size={size} strokeWidth={2} />;
case 'food':
return <Utensils color={'currentColor'} size={size} strokeWidth={2} />;
case 'all':
return <Grid2X2 color={'currentColor'} size={size} strokeWidth={2} />;
case 'plus':
return <Plus color={'currentColor'} size={size} strokeWidth={2} />;
default:
return <Utensils color={'currentColor'} size={size} strokeWidth={2} />;
}
}
function pickIconKey(name, iconKey) {
const n = (name || '').toLowerCase();
if (iconKey === 'plus') return 'plus';
if (iconKey === 'all') return 'all';
if (/(kopi|coffee|espresso|latte|americano|kapal|brew)/.test(n)) return 'coffee';
if (/(teh|tea|drink|minum|soda|juice|jus|milk|susu|lemon)/.test(n)) return 'drink';
if (/(dessert|cake|kue|manis|ice|es krim|ice-cream)/.test(n)) return 'dessert';
if (/(food|makan|snack|cemilan|nasi|mie|noodle|soup|sup|ayam|daging|ikan|roti|sandwich|burger|pizza)/.test(n)) return 'food';
return 'food';
}

View File

@@ -1,7 +1,7 @@
.item-type { .item-type {
width: calc(25vw - 20px); width: auto;
height: calc(30vw - 20px); height: auto;
margin: 1px 10px 0px; margin: 0 6px;
overflow: visible; overflow: visible;
text-align: center; text-align: center;
align-items: center; align-items: center;
@@ -10,55 +10,107 @@
justify-content: center; justify-content: center;
position: relative; position: relative;
} }
.item-type-rectangular {
width: calc(25vw - 20px); /* Compact version of item-type */
height: calc(25vw - 20px); .item-type-compact {
overflow: visible; margin: 0 4px;
text-align: center;
align-items: center;
display: flex;
flex-direction: column;
justify-content: start;
position: relative; /* Ensure absolute positioning inside works */
} }
.item-type-nomargin {
width: calc(25vw - 20px); .item-type-rectangular {
height: calc(39vw - 20px); width: calc(30vw - 24px);
height: calc(30vw - 24px);
overflow: visible; overflow: visible;
text-align: center; text-align: center;
align-items: center; align-items: center;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
position: relative; /* Ensure absolute positioning inside works */ position: relative;
} }
.item-type-nomargin {
width: calc(30vw - 24px);
height: calc(45vw - 24px);
overflow: visible;
text-align: center;
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
}
.item-type-rect { .item-type-rect {
position: relative; position: relative;
height: 13vw; height: 48px; /* Fixed height for better touch targets */
width: 13vw; min-width: 100px; /* Minimum width */
padding: 0 20px; /* Horizontal padding */
object-fit: cover; object-fit: cover;
border-radius: 15px; border-radius: 12px; /* Square rounded corners */
background-color: #fff; background-color: #fff;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
border: 1px solid #e6e6e6;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-size: 16px;
color: #2d2d2d;
cursor: pointer;
}
/* Compact version of item-type-rect */
.item-type-rect-compact {
height: 42px; /* Slightly smaller for compact version */
min-width: 90px;
padding: 0 18px;
border-radius: 10px;
font-size: 14px;
}
.item-type-rect:hover {
background-color: var(--brand-sage-100, #E9F3ED);
border-color: var(--brand-sage, #6B8F71);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
}
.item-type-rect.selected {
background-color: var(--brand-sage, #6B8F71);
color: #ffffff;
border-color: var(--brand-sage, #6B8F71);
box-shadow: 0 2px 6px rgba(107, 143, 113, 0.2);
} }
.item-type-name { .item-type-name {
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500; font-weight: 500;
font-style: normal; font-style: normal;
font-size: 14px; font-size: 16px;
color: #333; color: #2d2d2d;
width: calc(25vw - 30px); width: auto;
text-align: center; text-align: center;
background-color: transparent; background-color: transparent;
position: relative; /* Needed for positioning the button */ position: relative;
margin-top: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 160px;
}
/* Compact version of item-type-name */
.item-type-name-compact {
font-size: 14px;
max-width: 140px;
} }
.item-type-image { .item-type-image {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
border-radius: 15px; border-radius: 12px;
} }
.item-type-image-container { .item-type-image-container {
@@ -68,6 +120,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.item-type-image-input { .item-type-image-input {
opacity: 0; opacity: 0;
position: absolute; position: absolute;
@@ -79,12 +132,12 @@
.item-type-create { .item-type-create {
position: absolute; position: absolute;
top: 76%; /* Position below the input */ top: 76%;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
margin-top: 10px; /* Space between input and button */ margin-top: 12px;
width: 20vw; width: 24vw;
text-align: center; /* Center button text */ text-align: center;
} }
.border { .border {
@@ -94,3 +147,17 @@
.noborder { .noborder {
border: 1px solid #ffffff00; border: 1px solid #ffffff00;
} }
/* No icon styles */
.no-icon {
height: 48px;
min-width: 100px;
padding: 0 20px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 500;
font-family: "Plus Jakarta Sans", sans-serif;
}

View File

@@ -1,23 +1,95 @@
/* ItemTypeLister.css */
/* New clean, intuitive category bar */
.item-type-lister { .item-type-lister {
width: 100vw; width: 100%;
overflow-x: auto; overflow-x: auto;
white-space: nowrap; white-space: nowrap;
padding: 3px 0px; padding: 8px 0; /* Reduced padding for more compact design */
margin-bottom: -5px; margin-bottom: 8px; /* Reduced margin for more compact design */
scrollbar-width: thin;
display: flex;
justify-content: center;
align-items: center;
} }
.item-type-lister::-webkit-scrollbar {
height: 6px;
}
.item-type-lister::-webkit-scrollbar-thumb {
background-color: #c5c5c5;
border-radius: 3px;
}
.category-bar {
display: flex;
align-items: center;
gap: 6px; /* Reduced gap for more compact design */
overflow-x: auto;
padding: 6px 10px; /* Reduced padding */
-ms-overflow-style: none;
scrollbar-width: none;
justify-content: center;
width: 100%;
}
.category-bar::-webkit-scrollbar { display: none; }
/* Legacy horizontal tile list container (used for tile UI) */
.item-type-list { .item-type-list {
display: inline-flex; display: inline-flex;
-ms-overflow-style: none; /* IE and Edge */ -ms-overflow-style: none;
scrollbar-width: none; /* Firefox */ scrollbar-width: none;
overflow-y: hidden; overflow-y: hidden;
gap: 6px; /* Added gap for consistent spacing */
justify-content: center;
align-items: center;
width: 100%;
} }
.item-type { .category-chip {
display: inline-block; flex: 0 0 auto;
margin-right: 20px; display: inline-flex;
/* Space between items */ align-items: center;
gap: 6px;
height: 32px; /* Reduced height for more compact design */
padding: 0 14px; /* Reduced padding for more compact design */
border-radius: 999px;
border: 1px solid #e6e6e6;
background: #ffffff;
color: #2d2d2d;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-size: 13px; /* Slightly smaller font */
cursor: pointer;
user-select: none;
transition: all 0.2s ease;
justify-content: center;
} }
.category-chip:hover {
border-color: #d0d0d0;
background-color: #f8f8f8;
}
.category-chip.selected {
background: #73a585;
color: #ffffff;
border-color: #73a585;
}
.category-chip .chip-icon {
width: 16px; /* Reduced icon size */
height: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.add-item-chip {
background: #f4f7f5;
border-color: #dfe7e2;
color: #4a6b5a;
}
.add-item-chip:hover { background: #eaf1ed; }
.rect-creator { .rect-creator {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
@@ -25,7 +97,7 @@
top: 0; top: 0;
right: 0; right: 0;
background-color: white; background-color: white;
z-index: 300; z-index: 0; /* align with item lister */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@@ -55,13 +127,123 @@
bottom: 0; bottom: 0;
align-self: center; /* Center the button horizontally */ align-self: center; /* Center the button horizontally */
} }
.item-type-name {
font-family: "Plus Jakarta Sans", sans-serif; /* Legacy styles kept for ItemType grid if needed elsewhere */
font-style: normal;
height: 20vw; /* Compact centered item type list without icon tiles */
font-size: 1.5rem; .compact-centered-list {
font-weight: 500; display: flex;
color: black; justify-content: center;
text-transform: capitalize; align-items: center;
z-index: 301; width: 100%;
padding: 8px 0;
overflow-x: auto;
}
.compact-item-type {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
height: 36px;
padding: 0 16px;
border-radius: 999px;
border: 1px solid #e6e6e6;
background: #ffffff;
color: #2d2d2d;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-size: 14px;
cursor: pointer;
user-select: none;
transition: all 0.2s ease;
margin: 0 4px;
white-space: nowrap;
}
.compact-item-type:hover {
border-color: #d0d0d0;
background-color: #f8f8f8;
}
.compact-item-type.selected {
background: #73a585;
color: #ffffff;
border-color: #73a585;
}
.compact-add-item {
background: #f4f7f5;
border-color: #dfe7e2;
color: #4a6b5a;
}
.compact-add-item:hover {
background: #eaf1ed;
}
/* Responsive design */
@media (max-width: 768px) {
.item-type-lister {
padding: 6px 0;
margin-bottom: 6px;
}
.category-bar {
gap: 4px;
padding: 4px 8px;
}
.item-type-list {
gap: 4px;
}
.category-chip {
height: 28px;
padding: 0 12px;
font-size: 12px;
}
.category-chip .chip-icon {
width: 14px;
height: 14px;
}
.compact-item-type {
height: 32px;
padding: 0 14px;
font-size: 13px;
margin: 0 3px;
}
}
@media (max-width: 480px) {
.item-type-lister {
padding: 4px 0;
margin-bottom: 4px;
}
.category-bar {
gap: 3px;
padding: 3px 6px;
}
.item-type-list {
gap: 3px;
}
.category-chip {
height: 26px;
padding: 0 10px;
font-size: 11px;
}
.category-chip .chip-icon {
width: 12px;
height: 12px;
}
.compact-item-type {
height: 30px;
padding: 0 12px;
font-size: 12px;
margin: 0 2px;
}
} }

View File

@@ -1,8 +1,7 @@
import React, { useState, useRef, useEffect } from "react"; import React, { useState, useRef, useEffect } from "react";
import smoothScroll from "smooth-scroll-into-view-if-needed"; import smoothScroll from "smooth-scroll-into-view-if-needed";
import "./ItemTypeLister.css"; import "./ItemTypeLister.css";
import ItemType from "./ItemType"; import { createItem } from "../helpers/itemHelper.js";
import { createItem, createItemType } from "../helpers/itemHelper.js";
import { getImageUrl } from "../helpers/itemHelper"; import { getImageUrl } from "../helpers/itemHelper";
import ItemLister from "./ItemLister"; import ItemLister from "./ItemLister";
@@ -22,31 +21,13 @@ const ItemTypeLister = ({
const newItemDivRef = useRef(null); const newItemDivRef = useRef(null);
const [items, setItems] = useState([]); const [items, setItems] = useState([]);
const [itemTypeName, setItemTypeName] = useState("");
const handleCreateItem = (name, price, selectedImage, previewUrl, description, promoPrice) => {
console.log(previewUrl);
const newItem = {
itemId: items.length + 1,
name,
price,
selectedImage,
image: previewUrl,
availability: true,
description,
promoPrice
};
// Update the items state with the new item
setItems((prevItems) => [...prevItems, newItem]);
};
// Effect to handle changes to isAddingNewItem // Effect to handle changes to isAddingNewItem
useEffect(() => { useEffect(() => {
if (isAddingNewItem && newItemDivRef.current) { if (isAddingNewItem && newItemDivRef.current) {
// Use smooth-scroll-into-view-if-needed to scroll to the target div // Use smooth-scroll-into-view-if-needed to scroll to the target div
smoothScroll(newItemDivRef.current, { smoothScroll(newItemDivRef.current, {
behavior: "smooth", behavior: "smooth",
block: "start", // Adjust this based on your needs block: "start",
inline: "nearest", inline: "nearest",
}); });
} else { } else {
@@ -54,11 +35,11 @@ const ItemTypeLister = ({
smoothScroll(node, { smoothScroll(node, {
behavior: "smooth", behavior: "smooth",
block: "start", // Adjust this based on your needs block: "start",
inline: "nearest", inline: "nearest",
}); });
} }
}, [isAddingNewItem]); // Dependency array includes isAddingNewItem }, [isAddingNewItem]);
const toggleAddNewItem = () => { const toggleAddNewItem = () => {
setIsAddingNewItem((prev) => !prev); setIsAddingNewItem((prev) => !prev);
@@ -67,53 +48,31 @@ const ItemTypeLister = ({
document.body.style.overflow = !isAddingNewItem ? "hidden" : "auto"; document.body.style.overflow = !isAddingNewItem ? "hidden" : "auto";
}; };
async function handleCreate(name, selectedImage) { // Removed legacy image upload logic used by the old tile view
createItemType(shopId, name, selectedImage);
}
const [selectedImage, setSelectedImage] = useState(null); const canManage = user && (user.user_id == shopOwnerId || user.cafeId == shopId);
const [previewUrl, setPreviewUrl] = useState("");
const [imageUrl, setImaguUrl] = useState("");
useEffect(() => { const formatName = (name) => {
// if (selectedImage) { if (!name) return name;
// const reader = new FileReader(); return name
// reader.onloadend = () => { .toLowerCase()
// setPreviewUrl(reader.result); .replace(/\b\w/g, (c) => c.toUpperCase());
// };
// reader.readAsDataURL(selectedImage);
// } else {
// setPreviewUrl(getImageUrl(imageUrl));
setPreviewUrl(selectedImage);
// }
}, [selectedImage, imageUrl]);
const handleImageChange = (e) => {
setSelectedImage(e);
}; };
return ( return (
<div className="item-type-lister" style={{ overflowX: isAddingNewItem ? 'hidden' : 'auto' }}>
<div className="compact-centered-list">
<div ref={newItemDivRef} className="compact-item-type-list" style={{ display: 'inline-flex' }}>
{isEditMode && !isAddingNewItem && canManage && (
<div <div
className="item-type-lister" className="compact-item-type compact-add-item"
style={{ overflowX: isAddingNewItem ? "hidden" : "" }}
>
<div
ref={newItemDivRef}
className="item-type-list"
style={{ display: isAddingNewItem ? "inline-flex" : "inline-flex" }}
>
{isEditMode &&
!isAddingNewItem &&
user && (
user.userId == shopOwnerId || user.cafeId == shopId) && (
<ItemType
onClick={toggleAddNewItem} onClick={toggleAddNewItem}
name={"buat baru"} >
imageUrl={getImageUrl("uploads/assets/addnew.png")} Buat baru
/> </div>
)} )}
{user &&(
user.userId == shopOwnerId || user.cafeId == shopId) && {canManage && isAddingNewItem && (
isAddingNewItem && (
<>
<ItemLister <ItemLister
shopId={shopId} shopId={shopId}
shopOwnerId={shopOwnerId} shopOwnerId={shopOwnerId}
@@ -128,29 +87,27 @@ const ItemTypeLister = ({
alwaysEdit={true} alwaysEdit={true}
handleUnEdit={toggleAddNewItem} handleUnEdit={toggleAddNewItem}
/> />
</>
)} )}
{itemTypes && itemTypes.length > 0 && ( {itemTypes && itemTypes.length > 0 && (
<ItemType <div
name={"semua"} className={`compact-item-type ${filterId === 0 ? 'selected' : ''}`}
onClick={() => onFilterChange(0)} onClick={() => onFilterChange(0)}
imageUrl={"uploads/assets/All.png"} >
/> Semua
</div>
)} )}
{itemTypes &&
itemTypes.map( {itemTypes && itemTypes.map((itemType) => (
(itemType) => <div
(
itemType.itemList.length > 0 || (user && (user.userId == shopOwnerId || user.cafeId == shopId))) && (
<ItemType
key={itemType.itemTypeId} key={itemType.itemTypeId}
name={itemType.name} className={`compact-item-type ${filterId === itemType.itemTypeId ? 'selected' : ''}`}
imageUrl={getImageUrl(itemType.image)}
onClick={() => onFilterChange(itemType.itemTypeId)} onClick={() => onFilterChange(itemType.itemTypeId)}
selected={filterId === itemType.itemTypeId} >
/> {formatName(itemType.name)}
) </div>
)} ))}
</div>
</div> </div>
</div> </div>
); );

View File

@@ -1,11 +1,12 @@
import React, {useState, useEffect} from "react"; import React, {useState, useEffect} from "react";
import { createPortal } from "react-dom";
import styles from "./Modal.module.css"; import styles from "./Modal.module.css";
import AccountUpdatePage from "../components/AccountUpdatePage.js"; import AccountUpdatePage from "../components/AccountUpdatePage.js";
import CreateClerk from "../pages/CreateClerk" import CreateClerk from "../pages/CreateClerk"
import CreateCafe from "../pages/CreateCafe" import CreateCafe from "../pages/CreateCafe"
import CreateTenant from "../pages/CreateTenant" import CreateTenant from "../pages/CreateTenant"
import TablesPage from "./TablesPage.js"; import IdentifyCafeModal from "./IdentifyCafeModal.js";
import PaymentOptions from "./PaymentOptions.js"; import PaymentOptions from "./PaymentOptions.js";
import Transaction from "../pages/Transaction"; import Transaction from "../pages/Transaction";
import Transaction_item from "../pages/Transaction_item"; import Transaction_item from "../pages/Transaction_item";
@@ -75,9 +76,9 @@ const Modal = ({ user, shop, isOpen, onClose, modalContent, deviceType, setModal
event.stopPropagation(); event.stopPropagation();
}; };
if(modalContent == '') handleOverlayClick(); if(modalContent == '') handleOverlayClick();
return ( return createPortal(
<div key={updateKey} onClick={handleOverlayClick} className={styles.modalOverlay}> <div key={updateKey} onClick={handleOverlayClick} className={styles.modalOverlay}>
<div className={styles.modalContent} onClick={handleContentClick}> <div className={`${styles.modalContent} ${(modalContent === 'edit_tables' || modalContent === 'payment_option' || modalContent === 'create_clerk') ? styles.modalContentWide : ''}`} onClick={handleContentClick}>
{modalContent === "edit_account" && <AccountUpdatePage user={user} />} {modalContent === "edit_account" && <AccountUpdatePage user={user} />}
{modalContent === "reset-password" && <ResetPassword />} {modalContent === "reset-password" && <ResetPassword />}
@@ -86,9 +87,9 @@ const Modal = ({ user, shop, isOpen, onClose, modalContent, deviceType, setModal
{modalContent === "create_clerk" && <CreateClerk shopId={shop.cafeId} />} {modalContent === "create_clerk" && <CreateClerk shopId={shop.cafeId} />}
{modalContent === "create_kedai" && <CreateCafe shopId={shop.cafeId} />} {modalContent === "create_kedai" && <CreateCafe shopId={shop.cafeId} />}
{modalContent === "create_tenant" && <CreateTenant shopId={shop.cafeId} />} {modalContent === "create_tenant" && <CreateTenant shopId={shop.cafeId} />}
{modalContent === "edit_tables" && <TablesPage shop={shop} />} {modalContent === "edit_tables" && <IdentifyCafeModal shop={shop} />}
{modalContent === "new_transaction" && ( {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" && ( {modalContent === "transaction_canceled" && (
<Transaction propsShopId={shop.cafeId} /> <Transaction propsShopId={shop.cafeId} />
@@ -132,7 +133,8 @@ const Modal = ({ user, shop, isOpen, onClose, modalContent, deviceType, setModal
{modalContent === "message" && <Message handleYes={onModalYesFunction} handleNo={handleNo}/>} {modalContent === "message" && <Message handleYes={onModalYesFunction} handleNo={handleNo}/>}
{modalContent === "player-prompt" && <PlayerPrompt cafeId={shop.cafeId} setModal={setModal} handleClose={handleOverlayClick} welcomePageConfig={shop.welcomePageConfig}/>} {modalContent === "player-prompt" && <PlayerPrompt cafeId={shop.cafeId} setModal={setModal} handleClose={handleOverlayClick} welcomePageConfig={shop.welcomePageConfig}/>}
</div> </div>
</div> </div>,
document.body
); );
}; };

View File

@@ -2,37 +2,295 @@
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
right: -1px; right: 0;
bottom: 0; bottom: 0;
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.7);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 1000; z-index: 2147483647 !important; /* ensure above any app layers */
padding: 20px;
} }
.modalContent { .modalContent {
width: 80vw; width: 100%;
max-height: 80vh; max-width: 500px;
max-height: 90vh;
background: #ffffff;
border-radius: 16px;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
animation: modalAppear 0.3s ease-out;
position: relative; position: relative;
overflow: visible; /* Add this line to enable scrolling */ z-index: 10000; /* ensure above any page overlays */
}
.modalContentWide {
max-width: 920px;
}
@keyframes modalAppear {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.imageSection {
padding: 24px 24px 16px 24px;
background: #f8f9fa;
border-bottom: 1px solid #e6e6e6;
}
.imagePreview {
width: 100%;
height: 200px;
border-radius: 12px;
overflow: hidden;
margin-bottom: 20px;
background: #e9ecef;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.previewImage {
width: 100%;
height: 100%;
object-fit: cover;
}
.imageActions {
display: flex;
gap: 12px;
}
.actionButton {
flex: 1;
padding: 12px 16px;
border-radius: 10px;
border: none;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.actionButton:not(.deleteButton) {
background-color: var(--brand-sage, #6B8F71);
color: white;
box-shadow: 0 2px 6px rgba(107, 143, 113, 0.2);
}
.actionButton:not(.deleteButton):hover {
background-color: #5a7a60;
box-shadow: 0 4px 10px rgba(107, 143, 113, 0.3);
}
.deleteButton {
background-color: #ff4d4d;
color: white;
box-shadow: 0 2px 6px rgba(255, 77, 77, 0.2);
}
.deleteButton:hover {
background-color: #ff1a1a;
box-shadow: 0 4px 10px rgba(255, 77, 77, 0.3);
}
.formSection {
padding: 24px;
overflow-y: auto;
flex: 1;
}
.formGroup {
margin-bottom: 20px;
}
.formRow {
display: flex;
gap: 16px;
}
.formRow .formGroup {
flex: 1;
margin-bottom: 0;
}
.formLabel {
display: block;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600;
font-size: 14px;
color: #333;
margin-bottom: 8px;
}
.formInput {
width: 100%;
padding: 12px 16px;
border-radius: 10px;
border: 1px solid #ddd;
background: #ffffff;
font-family: "Plus Jakarta Sans", sans-serif;
font-size: 15px;
color: #333;
transition: all 0.2s ease;
box-sizing: border-box;
}
.formInput:focus {
outline: none;
border-color: var(--brand-sage, #6B8F71);
box-shadow: 0 0 0 3px rgba(107, 143, 113, 0.1);
}
.formTextarea {
width: 100%;
padding: 12px 16px;
border-radius: 10px;
border: 1px solid #ddd;
background: #ffffff;
font-family: "Plus Jakarta Sans", sans-serif;
font-size: 15px;
color: #333;
transition: all 0.2s ease;
box-sizing: border-box;
min-height: 100px;
resize: vertical;
}
.formTextarea:focus {
outline: none;
border-color: var(--brand-sage, #6B8F71);
box-shadow: 0 0 0 3px rgba(107, 143, 113, 0.1);
}
.formActions {
display: flex;
gap: 12px;
margin-top: 10px;
}
.bannerRow {
display: flex;
justify-content: flex-end;
margin-bottom: 10px;
}
.banner {
padding: 8px 12px;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
}
.bannerSuccess {
color: #155724;
background: #d4edda;
border: 1px solid #c3e6cb;
}
.bannerError {
color: #721c24;
background: #f8d7da;
border: 1px solid #f5c6cb;
}
.formButton {
flex: 1;
padding: 14px 16px;
border-radius: 10px;
border: none;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600;
font-size: 15px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.cancelButton {
background-color: #f0f0f0;
color: #333;
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
}
.cancelButton:hover {
background-color: #e0e0e0;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.saveButton {
background-color: var(--brand-sage, #6B8F71);
color: white;
box-shadow: 0 2px 6px rgba(107, 143, 113, 0.2);
}
.saveButton:hover {
background-color: #5a7a60;
box-shadow: 0 4px 10px rgba(107, 143, 113, 0.3);
}
.closeButton { .closeButton {
position: absolute; position: absolute;
top: 10px; top: 16px;
right: 10px; right: 16px;
background: none; background: none;
border: none; border: none;
font-size: 24px; font-size: 24px;
cursor: pointer; cursor: pointer;
color: #333; color: #999;
padding: 0; width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s ease;
z-index: 1001; /* Menambah z-index agar selalu di atas modal */
} }
.closeButton:hover { .closeButton:hover {
color: #f44336; /* Change color on hover for better UX */ background: #f0f0f0;
color: #333;
}
/* Responsive design */
@media (max-width: 600px) {
.modalContent {
margin: 10px;
max-height: 95vh;
}
.imageSection {
padding: 20px;
}
.formSection {
padding: 20px;
}
.formRow {
flex-direction: column;
gap: 0;
}
.formActions {
flex-direction: column;
}
} }

View File

@@ -31,14 +31,14 @@
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7); text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
border-radius: 0 0 15px 15px; border-radius: 0 0 15px 15px;
z-index: 1; z-index: 0; /* align with item lister */
} }
.current-name { .current-name {
white-space: nowrap; white-space: nowrap;
pointer-events: none; pointer-events: none;
position: relative; position: relative;
z-index: 2; z-index: 1;
text-align: left; text-align: left;
margin: 35px 30px; margin: 35px 30px;
font-size: 16px; font-size: 16px;
@@ -70,7 +70,7 @@
.current-artist { .current-artist {
pointer-events: none; pointer-events: none;
position: relative; position: relative;
z-index: 2; z-index: 1;
text-align: left; text-align: left;
margin: -32px 30px; margin: -32px 30px;
font-size: 18px; font-size: 18px;
@@ -83,7 +83,7 @@
.progress-container { .progress-container {
pointer-events: none; pointer-events: none;
position: relative; position: relative;
z-index: 2; z-index: 1;
text-align: left; text-align: left;
margin: 12px 30px; margin: 12px 30px;
} }
@@ -318,4 +318,3 @@
.search-button.clicked { .search-button.clicked {
background-color: #d0c7b3; /* The color when clicked */ background-color: #d0c7b3; /* The color when clicked */
} }

View File

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

View File

@@ -1,18 +1,13 @@
import React, { useState, useRef, useEffect } from "react"; 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 { getImageUrl } from "../helpers/itemHelper";
import { import { getCafe, saveCafeDetails } from "../helpers/cafeHelpers";
getCafe,
saveCafeDetails,
setConfirmationStatus,
setOpenBillAvailability
} from "../helpers/cafeHelpers";
import Switch from "react-switch"; // Import the Switch component import Switch from "react-switch"; // Import the Switch component
import styles from "./PaymentOptions.module.css";
const SetPaymentQr = ({ shopId, const SetPaymentQr = ({ shopId, qrCodeUrl }) => {
qrCodeUrl }) => { const [qrPosition, setQrPosition] = useState([50, 50]); // legacy kept for API compatibility
const [qrPosition, setQrPosition] = useState([50, 50]); const [qrSize, setQrSize] = useState(50); // legacy kept for API compatibility
const [qrSize, setQrSize] = useState(50);
const [qrPayment, setQrPayment] = useState(); const [qrPayment, setQrPayment] = useState();
const [qrPaymentFile, setQrPaymentFile] = useState(); const [qrPaymentFile, setQrPaymentFile] = useState();
const [qrCodeDetected, setQrCodeDetected] = useState(false); const [qrCodeDetected, setQrCodeDetected] = useState(false);
@@ -20,10 +15,15 @@ const SetPaymentQr = ({ shopId,
const [isQRISavailable, setIsQRISavailable] = useState(0); const [isQRISavailable, setIsQRISavailable] = useState(0);
const qrPaymentInputRef = useRef(null); const qrPaymentInputRef = useRef(null);
const qrCodeContainerRef = useRef(null); const qrCodeContainerRef = useRef(null);
const [qrCodeData, setQrCodeData] = useState(null);
const [cafe, setCafe] = useState({}); const [cafe, setCafe] = useState({});
const [isConfigQRIS, setIsConfigQRIS] = useState(false); const [isConfigQRIS, setIsConfigQRIS] = useState(false);
const [isOpenBillAvailable, setIsOpenBillAvailable] = useState(false); const [isOpenBillAvailable, setIsOpenBillAvailable] = useState(false);
const [saving, setSaving] = useState(false);
const [saveStatus, setSaveStatus] = useState(null); // 'success' | 'error'
const [copied, setCopied] = useState(false);
useEffect(() => { useEffect(() => {
const fetchCafe = async () => { const fetchCafe = async () => {
@@ -45,12 +45,6 @@ const SetPaymentQr = ({ shopId,
fetchCafe(); fetchCafe();
}, [shopId]); }, [shopId]);
// Detect QR code when qrPayment updates
useEffect(() => {
if (qrPayment && isConfigQRIS) {
detectQRCodeFromContainer();
}
}, [qrPayment, isConfigQRIS]);
// Handle file input change // Handle file input change
const handleFileChange = (e) => { const handleFileChange = (e) => {
@@ -62,29 +56,54 @@ const SetPaymentQr = ({ shopId,
} }
}; };
// Detect QR code from the container useEffect(() => {
if (qrPayment && isConfigQRIS) {
detectQRCodeFromContainer();
}
}, [qrPayment, isConfigQRIS]);
const detectQRCodeFromContainer = () => { const detectQRCodeFromContainer = () => {
const container = qrCodeContainerRef.current; const container = qrCodeContainerRef.current;
const canvas = document.createElement("canvas"); if (!container) return;
const context = canvas.getContext("2d");
const img = new Image(); const img = new Image();
img.crossOrigin = "Anonymous"; 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.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 // Save cafe details
const handleSave = async () => { const handleSave = async () => {
setSaving(true);
setSaveStatus(null);
let qrPaymentFileCache; let qrPaymentFileCache;
console.log(qrPaymentFile) console.log(qrPaymentFile)
if(qrPaymentFile != null) if(qrPaymentFile != null)
@@ -101,107 +120,101 @@ const SetPaymentQr = ({ shopId,
try { try {
const response = await saveCafeDetails(cafe.cafeId, details); const response = await saveCafeDetails(cafe.cafeId, details);
setIsNeedConfirmationState(response.needsConfirmation ? 1 : 0);
setIsNeedConfirmationState(response.needsConfirmation ? 1 : 0); // Update state after saving setIsQRISavailable(response.isQRISavailable ? 1 : 0);
setIsQRISavailable(response.isQRISavailable ? 1 : 0); // Update state after saving setIsOpenBillAvailable(response.isOpenBillAvailable ? 1 : 0);
setIsOpenBillAvailable(response.isOpenBillAvailable ? 1 : 0); // Update state after saving setSaveStatus('success');
console.log("Cafe details saved:", response); console.log("Cafe details saved:", response);
} catch (error) { } catch (error) {
console.error("Error saving cafe details:", error); console.error("Error saving cafe details:", error);
setSaveStatus('error');
} finally {
setSaving(false);
} }
}; };
const copyQrData = async () => {
if (!qrCodeData) return;
try {
await navigator.clipboard.writeText(qrCodeData);
setCopied(true);
setTimeout(()=>setCopied(false), 1200);
} catch {}
};
return ( return (
<div style={styles.container}> <div className={styles.container}>
<h3 style={styles.title}>Konfigurasi pembayaran</h3> <h3 className={styles.title}>Konfigurasi pembayaran</h3>
<div style={styles.switchContainer}> <div className={styles.section}>
<p style={styles.uploadMessage}> <div className={styles.sectionHeader}>
Pembayaran QRIS. <div>
</p> <div className={styles.sectionTitle}>Pembayaran QRIS</div>
<div className={styles.sectionDesc}>Aktifkan agar pelanggan dapat membayar via QRIS. Kasir tetap perlu verifikasi rekening.</div>
</div>
<Switch
onChange={(checked) => setIsQRISavailable(checked ? 1 : 0)}
checked={isQRISavailable === 1}
offColor="#888"
onColor="#4CAF50"
uncheckedIcon={false}
checkedIcon={false}
height={25}
width={50}
/>
</div>
{isConfigQRIS ? <div className={styles.row}>
<button
type="button"
className={`${styles.button} ${styles.configButton}`}
onClick={() => isQRISavailable === 1 && setIsConfigQRIS(true)}
disabled={isQRISavailable !== 1}
>
Konfigurasi QRIS
</button>
</div>
{isConfigQRIS && (
<> <>
<div <div
id="qr-code-container" id="qr-code-container"
ref={qrCodeContainerRef} ref={qrCodeContainerRef}
className={styles.imageBox}
onClick={() => qrPaymentInputRef.current.click()} onClick={() => qrPaymentInputRef.current.click()}
style={{ style={{ backgroundImage: `url(${qrPayment})` }}
...styles.qrCodeContainer,
backgroundImage: `url(${qrPayment})`,
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
backgroundSize: "contain",
}}
> >
<input <input type="file" accept="image/*" ref={qrPaymentInputRef} style={{ display: 'none' }} onChange={handleFileChange} />
type="file"
accept="image/*"
ref={qrPaymentInputRef}
style={{ display: "none" }}
onChange={handleFileChange}
/>
</div> </div>
<div style={styles.uploadMessage}> <div className={styles.smallNote}>Klik area untuk unggah/ganti gambar QR</div>
<p>Klik untuk ganti background</p> <div className={styles.detectRow}>
<div className={`${styles.tag} ${qrCodeDetected ? styles.tagOk : styles.tagBad}`}>
{qrCodeDetected && qrPayment !== 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPsNr0TPq8dHT3nBwDQ6OHQQTrqzVFoeBOmuWfgyErrLbJi6f6CnnYhpNHEvkJ_2X-kyI&usqp=CAU' ? 'QR terdeteksi' : 'Tidak ada QR terdeteksi'}
</div> </div>
<div style={styles.resultMessage}> <button className={styles.button} onClick={() => qrPaymentInputRef.current.click()}>{qrPayment ? 'Ganti' : 'Unggah'}</button>
{qrCodeDetected && qrPayment !== 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPsNr0TPq8dHT3nBwDQ6OHQQTrqzVFoeBOmuWfgyErrLbJi6f6CnnYhpNHEvkJ_2X-kyI&usqp=CAU' ? <p>QR terdeteksi</p> : <p>Tidak ada qr terdeteksi</p>}
{qrCodeDetected && qrPayment !== 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPsNr0TPq8dHT3nBwDQ6OHQQTrqzVFoeBOmuWfgyErrLbJi6f6CnnYhpNHEvkJ_2X-kyI&usqp=CAU' ? <div
onClick={() => qrPaymentInputRef.current.click()} style={styles.uploadButton}>Ganti</div> : <div
onClick={() => qrPaymentInputRef.current.click()} style={styles.uploadButton}>Unggah</div>}
</div> </div>
{qrCodeDetected && (
<div onClick={() => setIsConfigQRIS(false)} <div className={styles.copyRow}>
<input className={styles.linkField} readOnly value={qrCodeData || ''} />
style={{ <button className={styles.button} onClick={copyQrData}>{copied ? 'Disalin' : 'Salin'}</button>
...styles.qrisConfigButton,
width: '100%',
marginLeft: "0",
}}
>Terapkan</div>
</>
:
<>
<p style={styles.description}>
Aktifkan fitur agar pelanggan dapat menggunakan opsi pembayaran QRIS, namun kasir anda perlu memeriksa rekening untuk memastikan pembayaran.
</p>
<div style={{ display: 'flex' }}>
<Switch
onChange={(checked) => setIsQRISavailable(checked ? 1 : 0)}
checked={isQRISavailable === 1} // Convert to boolean
offColor="#888"
onColor="#4CAF50"
uncheckedIcon={false}
checkedIcon={false}
height={25}
width={50}
/>
<div
onClick={() => setIsConfigQRIS(true)}
style={{
...styles.qrisConfigButton,
backgroundColor: isQRISavailable == 1 ? styles.qrisConfigButton.backgroundColor : 'gray',
}}
>
Konfigurasi QRIS
</div> </div>
)}
<div className={styles.actionsRight}>
<button className={`${styles.button} ${styles.primary}`} onClick={() => setIsConfigQRIS(false)}>Terapkan</button>
</div> </div>
</> </>
} )}
</div>
<div className={styles.section}>
<div className={styles.sectionHeader}>
<div>
<div className={styles.sectionTitle}>Open bill</div>
<div className={styles.sectionDesc}>Izinkan pelanggan menambah pesanan dalam satu sesi dan bayar di akhir.</div>
</div> </div>
<div style={styles.switchContainer}>
<p style={styles.uploadMessage}>
Open bill
</p>
<p style={styles.description}>
Aktifkan fitur agar pelanggan dapat menambahkan pesanan selama sesi berlangsung tanpa perlu melakukan transaksi baru dan hanya membayar di akhir.
</p>
<Switch <Switch
onChange={(checked) => setIsOpenBillAvailable(checked ? 1 : 0)} onChange={(checked) => setIsOpenBillAvailable(checked ? 1 : 0)}
checked={isOpenBillAvailable === 1} // Convert to boolean checked={isOpenBillAvailable === 1}
offColor="#888" offColor="#888"
onColor="#4CAF50" onColor="#4CAF50"
uncheckedIcon={false} uncheckedIcon={false}
@@ -210,17 +223,17 @@ const SetPaymentQr = ({ shopId,
width={50} width={50}
/> />
</div> </div>
</div>
<div style={styles.switchContainer}> <div className={styles.section}>
<p style={styles.uploadMessage}> <div className={styles.sectionHeader}>
Pengecekan ganda <div>
</p> <div className={styles.sectionTitle}>Pengecekan ganda</div>
<p style={styles.description}> <div className={styles.sectionDesc}>Kasir memeriksa kembali ketersediaan item sebelum pembayaran.</div>
Nyalakan agar kasir memeriksa kembali ketersediaan produk sebelum pelanggan membayar. </div>
</p>
<Switch <Switch
onChange={(checked) => setIsNeedConfirmationState(checked ? 1 : 0)} onChange={(checked) => setIsNeedConfirmationState(checked ? 1 : 0)}
checked={isNeedConfirmationState === 1} // Convert to boolean checked={isNeedConfirmationState === 1}
offColor="#888" offColor="#888"
onColor="#4CAF50" onColor="#4CAF50"
uncheckedIcon={false} uncheckedIcon={false}
@@ -229,112 +242,19 @@ const SetPaymentQr = ({ shopId,
width={50} width={50}
/> />
</div> </div>
</div>
<div style={styles.buttonContainer}> <div className={styles.footer}>
<button onClick={handleSave} style={styles.saveButton}> <div>
Simpan {saveStatus === 'success' && <span className={`${styles.banner} ${styles.bannerSuccess}`}>Simpan berhasil</span>}
{saveStatus === 'error' && <span className={`${styles.banner} ${styles.bannerError}`}>Gagal menyimpan</span>}
</div>
<button className={`${styles.button} ${styles.primary}`} onClick={handleSave} disabled={saving}>
{saving ? 'Menyimpan…' : 'Simpan'}
</button> </button>
</div> </div>
</div> </div>
); );
}; };
// Styles
const styles = {
container: {
position: 'relative',
overflowY: 'auto',
overflowX: 'hidden',
maxHeight: '80vh',
width: '100%',
backgroundColor: "white",
padding: "20px",
borderRadius: "8px",
boxShadow: "0 2px 10px rgba(0, 0, 0, 0.1)",
textAlign: "center", // Center text and children
},
title: {
marginBottom: "20px",
fontWeight: "bold",
},
qrCodeContainer: {
backgroundColor: '#999999',
borderRadius: '20px',
position: "relative",
width: "100%",
height: "200px",
backgroundSize: "contain",
overflow: "hidden",
margin: "0 auto", // Center the QR code container
marginTop: '10px'
},
uploadMessage: {
fontWeight: 600,
textAlign: "left",
},
qrisConfigButton: {
borderRadius: '15px',
backgroundColor: '#28a745',
width: '144px',
textAlign: 'center',
color: 'white',
lineHeight: '24px',
marginLeft: '14px',
},
uploadButton: {
paddingRight: '10px',
backgroundColor: '#28a745',
borderRadius: '30px',
color: 'white',
fontWeight: 700,
height: '36px',
lineHeight: '36px',
paddingLeft: '10px',
paddingHeight: '10px',
},
resultMessage: {
marginTop: "-24px",
textAlign: "left",
display: 'flex',
justifyContent: 'space-between'
},
buttonContainer: {
marginTop: "20px",
textAlign: "left",
},
saveButton: {
padding: "10px 20px",
fontSize: "16px",
backgroundColor: "#28a745",
color: "#fff",
border: "none",
borderRadius: "30px",
cursor: "pointer",
transition: "background-color 0.3s",
},
switchContainer: {
textAlign: "left",
},
description: {
margin: "10px 0",
fontSize: "14px",
color: "#666",
},
sliderContainer: {
marginBottom: "20px",
},
label: {
display: "block",
marginBottom: "10px",
},
sliderWrapper: {
display: "flex",
alignItems: "center",
},
input: {
flex: "1",
margin: "0 10px",
},
};
export default SetPaymentQr; export default SetPaymentQr;

View File

@@ -0,0 +1,171 @@
/* PaymentOptions.module.css */
.container {
position: relative;
overflow-y: auto;
overflow-x: hidden;
max-height: 80vh;
width: 100%;
background: #fff;
padding: 20px;
border-radius: 8px;
box-sizing: border-box;
}
.title {
margin: 0 0 16px 0;
font-size: 18px;
font-weight: 700;
}
.section {
background: #fafafa;
border: 1px solid #e6e6e6;
border-radius: 12px;
padding: 16px;
margin-bottom: 14px;
}
.sectionHeader {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.sectionTitle {
font-weight: 700;
font-size: 15px;
margin-bottom: 4px;
}
.sectionDesc {
font-size: 13px;
color: #666;
}
.row {
display: flex;
align-items: center;
gap: 12px;
margin-top: 10px;
}
.button {
padding: 10px 14px;
border-radius: 10px;
border: 1px solid #ddd;
background: #fff;
cursor: pointer;
}
.button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.primary {
background: #28a745;
border: none;
color: #fff;
}
.configButton {
font-weight: 600;
}
.imageBox {
background-color: #999999;
border-radius: 12px;
position: relative;
width: 100%;
height: 220px;
background-repeat: no-repeat;
background-position: center;
background-size: contain;
overflow: hidden;
margin: 10px 0 6px 0;
}
.smallNote {
font-size: 12px;
color: #777;
}
.detectRow {
margin-top: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
.tag {
padding: 6px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
}
.tagOk {
color: #155724;
background: #d4edda;
border: 1px solid #c3e6cb;
}
.tagBad {
color: #721c24;
background: #f8d7da;
border: 1px solid #f5c6cb;
}
.copyRow {
display: flex;
gap: 8px;
align-items: center;
margin-top: 8px;
}
.linkField {
flex: 1;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 10px;
font-size: 14px;
}
.actionsRight {
display: flex;
justify-content: flex-end;
margin-top: 8px;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 14px;
}
.banner {
padding: 8px 12px;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
}
.bannerSuccess {
color: #155724;
background: #d4edda;
border: 1px solid #c3e6cb;
}
.bannerError {
color: #721c24;
background: #f8d7da;
border: 1px solid #f5c6cb;
}
@media (max-width: 720px) {
.container { padding: 16px; }
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
.watermark { .watermark {
z-index: 5; z-index: 0; /* align with item lister */
margin-top: 30px; margin-top: 30px;
background-color: rgb(222, 237, 100); background-color: rgb(222, 237, 100);
font-weight: 700; font-weight: 700;
@@ -28,7 +28,7 @@
/* Media query for desktop */ /* Media query for desktop */
@media (min-width: 768px) { /* Adjust the min-width as needed */ @media (min-width: 768px) { /* Adjust the min-width as needed */
.watermark { .watermark {
z-index: 5; z-index: 0; /* align with item lister */
margin-top: 30px; margin-top: 30px;
background-color: rgb(222, 237, 100); background-color: rgb(222, 237, 100);
font-weight: 700; font-weight: 700;

View File

@@ -106,10 +106,10 @@ export async function getCafeByIdentifier(cafeIdentifyName) {
return -1; return -1;
} }
} }
export async function getOwnedCafes(userId) { export async function getOwnedCafes(user_id) {
try { try {
const response = await fetch( 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", method: "POST",
headers: { headers: {

View File

@@ -5,9 +5,95 @@ body {
sans-serif; sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
background-color: #e9e9e9;
position: relative;
} }
code { code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace; monospace;
} }
/* Ensure proper scrolling behavior */
html, body {
width: 100%;
min-height: 100%;
overflow-x: hidden;
overflow-y: auto;
}
#root {
width: 100%;
min-height: 100%;
overflow: visible;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* Ensure proper z-index stacking */
* {
z-index: auto;
}
/* Responsive design adjustments */
@media (max-width: 768px) {
body {
font-size: 14px;
}
.container {
max-height: 75vh; /* Menaikkan tinggi maksimum pada mobile */
}
.materialCard {
margin-bottom: 12px;
}
.materialSummary {
padding: 14px 16px;
}
.materialName {
font-size: 15px;
}
.materialStock {
font-size: 13px;
margin-right: 12px;
}
.detailGrid {
grid-template-columns: 1fr;
gap: 12px;
}
.formGrid {
grid-template-columns: 1fr;
gap: 16px;
}
/* Memastikan MaterialList tetap dapat di-scroll */
.MaterialList-module__container {
max-height: 80vh;
overflow-y: auto;
}
}

View File

@@ -1,6 +1,6 @@
// src/CafePage.js // src/CafePage.js
import React, { useState, useEffect } from "react"; import React, { useState, useEffect, useRef } from "react";
import { import {
useParams, useParams,
useSearchParams, useSearchParams,
@@ -55,6 +55,7 @@ function CafePage({
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const token = searchParams.get("token"); const token = searchParams.get("token");
const { shopIdentifier, tableCode } = useParams(); const { shopIdentifier, tableCode } = useParams();
// Send params to parent immediately (original behavior)
sendParam({ shopIdentifier, tableCode }); sendParam({ shopIdentifier, tableCode });
const { const {
@@ -76,18 +77,63 @@ function CafePage({
const [config, setConfig] = useState({}); const [config, setConfig] = useState({});
const [beingEditedType, setBeingEditedType] = useState(0); const [beingEditedType, setBeingEditedType] = useState(0);
// Sticky floating cart bar animations and visibility
const [cartBump, setCartBump] = useState(false);
const [barIntro, setBarIntro] = useState(false);
const [historyPulse, setHistoryPulse] = useState(false);
const prevShowRef = useRef(false);
const prevTxnRef = useRef(null);
const showBar = !isEditMode && (user.username || cartItemsLength > 0);
const checkWelcomePageConfig = () => { useEffect(() => {
const parsedConfig = JSON.parse(welcomePageConfig); if (cartItemsLength > 0) {
if (parsedConfig.isWelcomePageActive == "true") { setCartBump(true);
const clicked = sessionStorage.getItem("getStartedClicked"); const t = setTimeout(() => setCartBump(false), 450);
if (!clicked) { return () => clearTimeout(t);
sessionStorage.setItem("getStartedClicked", true);
document.body.style.overflow = "hidden";
setIsStarted(true);
} }
}, [cartItemsLength]);
useEffect(() => {
if (showBar && !prevShowRef.current) {
setBarIntro(true);
const t = setTimeout(() => setBarIntro(false), 500);
prevShowRef.current = true;
return () => clearTimeout(t);
} }
}; if (!showBar) prevShowRef.current = false;
}, [showBar]);
useEffect(() => {
if (lastTransaction && lastTransaction !== prevTxnRef.current) {
setHistoryPulse(true);
const t = setTimeout(() => setHistoryPulse(false), 900);
prevTxnRef.current = lastTransaction;
return () => clearTimeout(t);
}
}, [lastTransaction]);
// 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(() => { useEffect(() => {
if (welcomePageConfig) { if (welcomePageConfig) {
@@ -100,16 +146,16 @@ function CafePage({
isActive: parsedConfig.isWelcomePageActive === "true", isActive: parsedConfig.isWelcomePageActive === "true",
}); });
} }
checkWelcomePageConfig(); // checkWelcomePageConfig();
}, [welcomePageConfig]); }, [welcomePageConfig]);
useEffect(() => { useEffect(() => {
function fetchData() { function fetchData() {
console.log(user.userId == shopOwnerId) console.log(user.user_id == shopOwnerId)
setModal("create_item"); setModal("create_item");
} }
console.log(getLocalStorage('auth')) console.log(getLocalStorage('auth'))
if (getLocalStorage("auth") != null) { if (getLocalStorage("auth") != null) {
const executeFetch = async () => { const executeFetch = async () => {
@@ -118,7 +164,7 @@ function CafePage({
} }
console.log(user) console.log(user)
console.log('open') 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(); executeFetch();
} }
@@ -234,6 +280,8 @@ function CafePage({
removeConnectedGuestSides={removeConnectedGuestSides} removeConnectedGuestSides={removeConnectedGuestSides}
setIsEditMode={(e) => setIsEditMode(e)} setIsEditMode={(e) => setIsEditMode(e)}
isEditMode={isEditMode} isEditMode={isEditMode}
zIndexLevel={9000}
rectZIndex={9000}
/> />
<MusicPlayer <MusicPlayer
socket={socket} socket={socket}
@@ -307,29 +355,33 @@ function CafePage({
} }
/> />
))} ))}
{!isEditMode && (user.username || cartItemsLength > 0) && {showBar &&
<div style={{ marginTop: '10px', height: '40px', position: 'sticky', bottom: '40px', display: 'flex', justifyContent: 'center', alignItems: 'center', textAlign: 'center' }}> <div className={`StickyCartBar${barIntro ? ' intro' : ''}`}>
{(lastTransaction != null || cartItemsLength > 0) && {(lastTransaction != null || cartItemsLength > 0) &&
<div onClick={goToCart} style={{ backgroundColor: '#73a585', width: user.username ? '55vw' : '70vw', height: '40px', borderRadius: '30px', display: 'flex', justifyContent: 'space-between', padding: '0 20px' }}> <div role="button" tabIndex={0} aria-label="Buka keranjang" onKeyDown={(e)=>{ if(e.key==='Enter'||e.key===' ') { e.preventDefault(); goToCart(); } }} onClick={goToCart} className={`cartBtn${cartBump ? ' bump' : ''}`}>
<div style={{ display: 'flex', flexWrap: 'wrap', alignContent: 'center' }}>{lastTransaction != null && '+'}{cartItemsLength} item</div> <div className="summary">
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', width: '130px' }}> {(lastTransaction != null) && <span>+</span>}
<span>{cartItemsLength} item</span>
</div>
<div className="summary" style={{ gap: 6 }}>
{((lastTransaction == null || lastTransaction?.payment_type != 'paylater')) ? {((lastTransaction == null || lastTransaction?.payment_type != 'paylater')) ?
<span style={{ whiteSpace: 'nowrap' }}>Rp{totalPrice}</span> <span className="value">{`Rp ${Number(totalPrice || 0).toLocaleString('id-ID')}`}</span>
: :
<span style={{ whiteSpace: 'nowrap' }}>Open bill</span> <span className="value">Open bill</span>
} }
<div style={{ display: 'flex', alignItems: 'center', marginLeft: '5px', width: '20px' }}> <div className="icon">
<svg viewBox="0 0 34 34" style={{ fill: 'white', marginTop: '4px' }}> <svg viewBox="0 0 34 34" style={{ fill: 'white', marginTop: '4px' }}>
<path d="M9.79175 24.75C8.09591 24.75 6.72383 26.1375 6.72383 27.8333C6.72383 29.5292 8.09591 30.9167 9.79175 30.9167C11.4876 30.9167 12.8751 29.5292 12.8751 27.8333C12.8751 26.1375 11.4876 24.75 9.79175 24.75ZM0.541748 0.0833435V3.16668H3.62508L9.17508 14.8679L7.09383 18.645C6.84717 19.0767 6.70842 19.5854 6.70842 20.125C6.70842 21.8208 8.09591 23.2083 9.79175 23.2083H28.2917V20.125H10.4392C10.2234 20.125 10.0538 19.9554 10.0538 19.7396L10.1001 19.5546L11.4876 17.0417H22.973C24.1292 17.0417 25.1467 16.4096 25.6709 15.4538L31.1901 5.44834C31.3134 5.23251 31.3751 4.97043 31.3751 4.70834C31.3751 3.86043 30.6813 3.16668 29.8334 3.16668H7.03217L5.583 0.0833435H0.541748ZM25.2084 24.75C23.5126 24.75 22.1405 26.1375 22.1405 27.8333C22.1405 29.5292 23.5126 30.9167 25.2084 30.9167C26.9042 30.9167 28.2917 29.5292 28.2917 27.8333C28.2917 26.1375 26.9042 24.75 25.2084 24.75Z"></path> <path d="M9.79175 24.75C8.09591 24.75 6.72383 26.1375 6.72383 27.8333C6.72383 29.5292 8.09591 30.9167 9.79175 30.9167C11.4876 30.9167 12.8751 29.5292 12.8751 27.8333C12.8751 26.1375 11.4876 24.75 9.79175 24.75ZM0.541748 0.0833435V3.16668H3.62508L9.17508 14.8679L7.09383 18.645C6.84717 19.0767 6.70842 19.5854 6.70842 20.125C6.70842 21.8208 8.09591 23.2083 9.79175 23.2083H28.2917V20.125H10.4392C10.2234 20.125 10.0538 19.9554 10.0538 19.7396L10.1001 19.5546L11.4876 17.0417H22.973C24.1292 17.0417 25.1467 16.4096 25.6709 15.4538L31.1901 5.44834C31.3134 5.23251 31.3751 4.97043 31.3751 4.70834C31.3751 3.86043 30.6813 3.16668 29.8334 3.16668H7.03217L5.583 0.0833435H0.541748ZM25.2084 24.75C23.5126 24.75 22.1405 26.1375 22.1405 27.8333C22.1405 29.5292 23.5126 30.9167 25.2084 30.9167C26.9042 30.9167 28.2917 29.5292 28.2917 27.8333C28.2917 26.1375 26.9042 24.75 25.2084 24.75Z"></path>
</svg> </svg>
{cartItemsLength > 0 && <span className={`badge${cartBump ? ' pop' : ''}`}>{cartItemsLength > 9 ? '9+' : cartItemsLength}</span>}
</div> </div>
</div> </div>
</div> </div>
} }
{user.username && {user.username &&
<div onClick={goToTransactions} style={{ backgroundColor: '#73a585', width: '15vw', height: '40px', borderRadius: '30px', display: 'flex', justifyContent: 'center', marginLeft: lastTransaction != null || cartItemsLength > 0 ? '6px' : '0px' }}> <div role="button" tabIndex={0} aria-label="Lihat riwayat" onKeyDown={(e)=>{ if(e.key==='Enter'||e.key===' ') { e.preventDefault(); goToTransactions(); } }} onClick={goToTransactions} className={`historyBtn${historyPulse ? ' pulse' : ''}`}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '38px', marginRight: '5px' }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 38, marginRight: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', marginLeft: '5px', width: '20px' }}> <div className="icon">
<svg viewBox="0 0 512 512"> <svg viewBox="0 0 512 512">
<g <g
transform="translate(0 460) scale(0.09 -0.09)" transform="translate(0 460) scale(0.09 -0.09)"
@@ -377,3 +429,4 @@ function CafePage({
} }
export default CafePage; export default CafePage;

View File

@@ -1,5 +1,6 @@
import React, { useRef, useEffect, useState } from "react"; import React, { useRef, useEffect, useState } from "react";
import styles from "./Invoice.module.css"; import styles from "./Invoice.module.css";
import cartStyles from "./CartPage.module.css";
import { useParams } from "react-router-dom"; // Changed from useSearchParams to useLocation import { useParams } from "react-router-dom"; // Changed from useSearchParams to useLocation
import { ThreeDots, ColorRing } from "react-loader-spinner"; import { ThreeDots, ColorRing } from "react-loader-spinner";
@@ -272,6 +273,8 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
socketId socketId
); );
localStorage.removeItem('lastTransaction') localStorage.removeItem('lastTransaction')
// Dispatch the custom event
window.dispatchEvent(new Event("localStorageUpdated"));
} }
else else
@@ -382,7 +385,13 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
return ( return (
<div className={styles.Invoice} style={{ height: (getItemsByCafeId(shopId).length > 0 ? '' : '100vh'), minHeight: (getItemsByCafeId(shopId).length > 0 ? '100vh' : '') }}> <div className={styles.Invoice} style={{ height: (getItemsByCafeId(shopId).length > 0 ? '' : '100vh'), minHeight: (getItemsByCafeId(shopId).length > 0 ? '100vh' : '') }}>
<div onClick={goToShop} style={{ marginLeft: '22px', marginTop: '49px', marginRight: '10px', display: 'flex', flexWrap: 'nowrap', alignItems: 'center', fontSize: '25px' }} ><svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 512 512"><path d="M48,256c0,114.87,93.13,208,208,208s208-93.13,208-208S370.87,48,256,48,48,141.13,48,256Zm212.65-91.36a16,16,0,0,1,.09,22.63L208.42,240H342a16,16,0,0,1,0,32H208.42l52.32,52.73A16,16,0,1,1,238,347.27l-79.39-80a16,16,0,0,1,0-22.54l79.39-80A16,16,0,0,1,260.65,164.64Z" /></svg>Keranjang</div> <div className={cartStyles.header}>
<div className={cartStyles.backBtn} onClick={goToShop} aria-label="Kembali">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 512 512"><path fill="#fff" d="M48,256c0,114.87,93.13,208,208,208s208-93.13,208-208S370.87,48,256,48,48,141.13,48,256Zm212.65-91.36a16,16,0,0,1,.09,22.63L208.42,240H342a16,16,0,0,1,0,32H208.42l52.32,52.73A16,16,0,1,1,238,347.27l-79.39-80a16,16,0,0,1,0-22.54l79.39-80A16,16,0,0,1,260.65,164.64Z" /></svg>
</div>
<div className={cartStyles.title}>Keranjang</div>
</div>
<div className={cartStyles.container}>
{(transactionData == null && getItemsByCafeId(shopId).length < 1) ? {(transactionData == null && getItemsByCafeId(shopId).length < 1) ?
<div style={{ height: '75vh', display: 'flex', justifyContent: 'center', flexDirection: 'column', alignContent: 'center', alignItems: 'center' }}> <div style={{ height: '75vh', display: 'flex', justifyContent: 'center', flexDirection: 'column', alignContent: 'center', alignItems: 'center' }}>
@@ -441,20 +450,20 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
</div> </div>
)} )}
<div className={styles.NoteContainer}>
<span>Catatan :</span>
<span></span>
</div> </div>
}
<div className={styles.NoteContainer}> {getItemsByCafeId(shopId).length > 0 && (
<div className={`${styles.RoundedRectangle} ${cartStyles.sectionCard}`}>
<div className={cartStyles.sectionTitle}>Catatan Untuk Kasir</div>
<div className={cartStyles.divider}></div>
<textarea <textarea
ref={textareaRef} ref={textareaRef}
className={styles.NoteInput} className={styles.NoteInput}
placeholder="Tambahkan catatan..." placeholder="Contoh: tanpa gula, ekstra es, dsb."
/> />
</div> </div>
</div> )}
}
{transactionData && {transactionData &&
<div className={styles.RoundedRectangle} style={{ backgroundColor: '#c3c3c3', fontSize: '15px', display: 'flex', justifyContent: 'space-between' }}> <div className={styles.RoundedRectangle} style={{ backgroundColor: '#c3c3c3', fontSize: '15px', display: 'flex', justifyContent: 'space-between' }}>
@@ -574,5 +583,6 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
) )
} }
</div> </div>
</div>
); );
} }

View File

@@ -22,6 +22,7 @@
.CheckoutContainer { .CheckoutContainer {
bottom: 0px; bottom: 0px;
position: fixed; position: fixed;
z-index: 100; /* Menurunkan z-index agar tidak menutupi material list */
} }
.EmailContainer { .EmailContainer {

View File

@@ -0,0 +1,68 @@
.header {
position: sticky;
top: 0;
z-index: 5;
background: var(--brand-sage, #6B8F71);
color: #fff;
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.backBtn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 999px;
background: rgba(255,255,255,0.15);
border: 1px solid rgba(255,255,255,0.25);
cursor: pointer;
}
.title {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 700;
font-size: 18px;
}
.container {
padding: 12px;
}
.sectionCard {
background: #fff;
border-radius: 14px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
padding: 12px;
margin: 12px 8px;
}
.sectionTitle {
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 700;
font-size: 16px;
color: #2d2d2d;
margin: 4px 0 10px 4px;
}
.rowBetween {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.hint {
font-size: 12px;
color: #666;
}
.divider {
height: 1px;
background: #eee;
margin: 8px 0 12px 0;
}

View File

@@ -1,119 +1,153 @@
import React, { useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { createClerks } from '../helpers/userHelpers'; // Adjust the import path as needed import { createClerks, getClerks } from '../helpers/userHelpers';
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import styles from './CreateClerk.module.css';
const CreateClerk = ({ shopId }) => { const CreateClerk = ({ shopId }) => {
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [message, setMessage] = useState(''); const [saving, setSaving] = useState(false);
const [banner, setBanner] = useState(null); // { type: 'success'|'error', text: string }
const [clerks, setClerks] = useState([]);
const location = useLocation(); const location = useLocation();
const queryParams = new URLSearchParams(location.search); const queryParams = new URLSearchParams(location.search);
const cafeIdParam = queryParams.get("cafeId"); const cafeIdParam = queryParams.get("cafeId");
const effectiveShopId = useMemo(()=> shopId || cafeIdParam, [shopId, cafeIdParam]);
useEffect(()=>{
const load = async ()=>{
if (!effectiveShopId) return;
try {
const data = await getClerks(effectiveShopId);
if (data && Array.isArray(data)) setClerks(data);
} catch (e) {}
};
load();
}, [effectiveShopId]);
const generatePassword = () => {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%';
let pwd = '';
for (let i = 0; i < 12; i++) pwd += chars[Math.floor(Math.random()*chars.length)];
setPassword(pwd);
};
const handleSubmit = async (event) => { const handleSubmit = async (event) => {
event.preventDefault(); event.preventDefault();
setLoading(true); setSaving(true);
setMessage(''); setBanner(null);
// Basic validation // Basic validation
if (!username || !password) { if (!username || !password) {
setMessage('Username and password are required'); setBanner({ type: 'error', text: 'Username dan password wajib diisi' });
setLoading(false); setSaving(false);
return;
}
if (username.length < 3) {
setBanner({ type: 'error', text: 'Username minimal 3 karakter' });
setSaving(false);
return;
}
if (password.length < 6) {
setBanner({ type: 'error', text: 'Password minimal 6 karakter' });
setSaving(false);
return; return;
} }
try { try {
const create = await createClerks(shopId || cafeIdParam, username, password); const create = await createClerks(effectiveShopId, username, password);
if (create) {
if (create) setMessage('Clerk created successfully'); setBanner({ type: 'success', text: 'Kasir berhasil ditambahkan' });
else setMessage('Failed to create clerk'); // Refresh list
try {
const data = await getClerks(effectiveShopId);
if (data && Array.isArray(data)) setClerks(data);
} catch {}
// Clear form
setUsername('');
setPassword('');
} else {
setBanner({ type: 'error', text: 'Gagal menambahkan kasir' });
}
} catch (error) { } catch (error) {
setMessage('Error creating clerk'); setBanner({ type: 'error', text: 'Terjadi kesalahan saat menambahkan kasir' });
} finally { } finally {
setLoading(false); setSaving(false);
} }
}; };
return ( return (
<div style={styles.container}> <div className={styles.container}>
<h2 style={styles.header}>Tambah Kasir</h2> <div className={styles.header}>
<form onSubmit={handleSubmit} style={styles.form}> <h2 className={styles.title}>Tambah Kasir</h2>
{banner && (
<div className={`${styles.banner} ${banner.type === 'success' ? styles.bannerSuccess : styles.bannerError}`}>
{banner.text}
</div>
)}
</div>
<div className={styles.section}>
<div className={styles.sectionTitle}>Form kasir baru</div>
<form onSubmit={handleSubmit}>
<div className={styles.formRow}>
<div className={styles.field}>
<label className={styles.label}>Username</label>
<input <input
className={styles.input}
type="text" type="text"
placeholder="Username" placeholder="kasir_baru"
value={username} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
style={styles.input}
/> />
</div>
<div className={styles.field}>
<label className={styles.label}>Password</label>
<div className={styles.pwdRow}>
<input <input
type="password" className={styles.input}
placeholder="Password" type={showPassword ? 'text' : 'password'}
placeholder="••••••"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
style={styles.input}
/> />
<button type="submit" style={styles.button} disabled={loading}> <button type="button" className={styles.button} onClick={()=>setShowPassword(!showPassword)}>
{loading ? 'Creating...' : 'Create Clerk'} {showPassword ? 'Sembunyikan' : 'Tampilkan'}
</button> </button>
{message && ( <button type="button" className={styles.button} onClick={generatePassword}>
<p style={{ ...styles.message, color: message.includes('success') ? 'green' : 'red' }}> Generate
{message} </button>
</p> </div>
)} </div>
</div>
<div className={styles.footer}>
<button className={`${styles.button} ${styles.primary}`} type="submit" disabled={saving}>
{saving ? 'Menambahkan…' : 'Tambah Kasir'}
</button>
</div>
</form> </form>
</div> </div>
<div className={styles.section}>
<div className={styles.sectionTitle}>Daftar kasir</div>
<div className={styles.list}>
{clerks && clerks.length > 0 ? (
clerks.map((c) => (
<div key={c.user_id || c.username} className={`${styles.listItem} ${styles.muted}`}>
<span>@{c.username}</span>
{/* Tempatkan tombol hapus jika API tersedia */}
</div>
))
) : (
<div className={styles.listItem}>Belum ada kasir</div>
)}
</div>
</div>
</div>
); );
}; };
// Basic styling to make it mobile-friendly with a white background
const styles = {
container: {
backgroundColor: '#fff',
width: '100%',
maxWidth: '350px',
margin: '0 auto',
padding: '20px',
boxShadow: '0 4px 10px rgba(0, 0, 0, 0.1)',
borderRadius: '8px',
boxSizing: 'border-box',
},
header: {
textAlign: 'center',
marginBottom: '20px',
fontSize: '20px',
color: '#333',
},
form: {
display: 'flex',
flexDirection: 'column',
gap: '15px',
},
input: {
padding: '12px',
fontSize: '16px',
borderRadius: '8px',
border: '1px solid #ccc',
width: '100%',
boxSizing: 'border-box',
backgroundColor: '#f9f9f9',
},
button: {
padding: '12px',
fontSize: '16px',
borderRadius: '8px',
border: 'none',
backgroundColor: '#28a745',
color: 'white',
cursor: 'pointer',
width: '100%',
},
message: {
textAlign: 'center',
marginTop: '10px',
},
};
export default CreateClerk; export default CreateClerk;

View File

@@ -0,0 +1,137 @@
/* CreateClerk.module.css */
.container {
background: #fff;
width: 100%;
max-width: 720px;
margin: 0 auto;
padding: 20px;
border-radius: 12px;
box-sizing: border-box;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.title {
font-size: 18px;
font-weight: 700;
margin: 0;
}
.section {
background: #fafafa;
border: 1px solid #e6e6e6;
border-radius: 12px;
padding: 16px;
margin-bottom: 14px;
}
.sectionTitle {
margin: 0 0 10px 0;
font-weight: 700;
font-size: 15px;
}
.formRow {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
.label {
font-size: 13px;
color: #555;
}
.input {
padding: 12px;
font-size: 14px;
border-radius: 10px;
border: 1px solid #ddd;
background: #fff;
}
.pwdRow {
display: flex;
gap: 8px;
}
.button {
padding: 10px 14px;
border-radius: 10px;
border: 1px solid #ddd;
background: #fff;
cursor: pointer;
}
.button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.primary {
background: #28a745;
border: none;
color: #fff;
}
.footer {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.banner {
padding: 8px 12px;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
}
.bannerSuccess {
color: #155724;
background: #d4edda;
border: 1px solid #c3e6cb;
}
.bannerError {
color: #721c24;
background: #f8d7da;
border: 1px solid #f5c6cb;
}
.list {
max-height: 220px;
overflow: auto;
border: 1px solid #e6e6e6;
border-radius: 8px;
padding: 8px;
}
.listItem {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 10px;
border-radius: 6px;
}
.muted {
background: #f0f0f0;
}
@media (max-width: 720px) {
.formRow { grid-template-columns: 1fr; }
}

View File

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

View File

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

View File

@@ -7,6 +7,8 @@
font-size: calc(10px + 2vmin); font-size: calc(10px + 2vmin);
color: rgba(88, 55, 50, 1); color: rgba(88, 55, 50, 1);
background-color: #e9e9e9; background-color: #e9e9e9;
position: relative;
z-index: 50; /* Memastikan berada di bawah material list */
} }
.Invoice-title { .Invoice-title {
@@ -45,6 +47,8 @@
bottom: 0; bottom: 0;
right: 0; right: 0;
left: 0; left: 0;
z-index: 100; /* Menurunkan z-index lebih jauh agar tidak menutupi material list */
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
} }
.PaymentOptionMargin { .PaymentOptionMargin {
@@ -159,6 +163,7 @@
padding: 10px 0; padding: 10px 0;
margin-bottom: 7px; margin-bottom: 7px;
} }
.OrderTypeContainer { .OrderTypeContainer {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View File

@@ -110,7 +110,7 @@ const LinktreePage = ({ data, setModal }) => {
</div> </div>
<div className={styles.linktreeForm}> <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}> <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> </button>
</div> </div>
<div className={styles.footer}> <div className={styles.footer}>

View File

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

View File

@@ -1,12 +1,4 @@
import React, { useState, useRef, useEffect } from "react"; import React, { useState, useEffect } from "react";
import jsQR from "jsqr";
import { getImageUrl } from "../helpers/itemHelper";
import {
getCafe,
saveCafeDetails,
setConfirmationStatus,
} from "../helpers/cafeHelpers";
import { import {
getMaterials, getMaterials,
createMaterial, createMaterial,
@@ -17,347 +9,497 @@ import {
getMaterialMutations, getMaterialMutations,
} from "../helpers/materialMutationHelpers"; } from "../helpers/materialMutationHelpers";
import Switch from "react-switch"; // Import the Switch component import styles from './MaterialList.module.css';
import Carousel from '../components/Carousel'
import styles from './MaterialList.module.css'; // Import the CSS Module
const SetPaymentQr = ({ cafeId }) => { const MaterialList = ({ cafeId }) => {
// All your state and logic goes here (unchanged) // State declarations
const [materials, setMaterials] = useState([]); const [materials, setMaterials] = useState([]);
const [mutations, setMutations] = useState([]); const [mutations, setMutations] = useState([]);
const [newMaterialName, setNewMaterialName] = useState("");
const [newMaterialUnit, setNewMaterialUnit] = useState("kilogram");
const [newMaterialImage, setNewMaterialImage] = useState(null);
const [deleting, setDeleting] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [showForm, setShowForm] = useState(false);
const [selectedMaterialIndex, setSelectedMaterialIndex] = useState(-1); // Add material form state
const [latestMutation, setLatestMutation] = useState([]); const [showAddForm, setShowAddForm] = useState(false);
const [currentQuantity, setCurrentQuantity] = useState(-1); const [newMaterialName, setNewMaterialName] = useState("");
const [currentPrice, setCurrentPrice] = useState(0); const [newMaterialUnit, setNewMaterialUnit] = useState("kilogram");
const [newPrice, setNewPrice] = useState(0);
const [quantityChange, setQuantityChange] = useState(0); // Edit material state
const [editingMaterialId, setEditingMaterialId] = useState(null);
const [newQuantity, setNewQuantity] = useState(0);
const [newPrice, setNewPrice] = useState("");
// Expand state for each material
const [expandedMaterials, setExpandedMaterials] = useState({});
// View state
const [sortOrder, setSortOrder] = useState("desc"); const [sortOrder, setSortOrder] = useState("desc");
const [isEditCurrentPrice, setIsEditCurrentPrice] = useState(false); const [isViewingHistory, setIsViewingHistory] = useState({});
const [isViewingHistory, setIsViewingHistory] = useState(false);
// Format currency helper
const formatCurrency = (value) => {
if (!value) return "0";
return parseInt(value.toString().replace(/\./g, "")).toLocaleString('id-ID');
};
const convertToInteger = (formattedValue) => { const convertToInteger = (formattedValue) => {
// Remove dots and convert to integer return parseInt(formattedValue.replace(/\./g, ""), 10) || 0;
return parseInt(formattedValue.replace(/\./g, ""), 10);
}; };
const formatCurrency = (value) => { // Handle price input change
if (!value) return ""; const handlePriceChange = (e) => {
// Remove existing formatting (dots) and format again
const numericValue = value.toString().replace(/\D/g, ""); // Keep only digits
return numericValue.replace(/\B(?=(\d{3})+(?!\d))/g, "."); // Add dot as thousands separator
};
const handleChange = (e) => {
const formattedValue = formatCurrency(e.target.value); const formattedValue = formatCurrency(e.target.value);
setNewPrice(formattedValue); setNewPrice(formattedValue);
}; };
// Fetch materials and mutations
useEffect(() => { useEffect(() => {
const fetchMaterials = async () => { const fetchData = async () => {
try { try {
const data = await getMaterials(cafeId); const [materialsData, mutationsData] = await Promise.all([
setMaterials(data); getMaterials(cafeId),
console.log(data) getMaterialMutations(cafeId)
setError(null); ]);
if (data.length > 0) {
setSelectedMaterialIndex(0);
}
} catch (error) {
console.error("Error fetching materials:", error);
setError("Failed to fetch materials.");
}
};
const fetchMutations = async () => { setMaterials(materialsData);
try { setMutations(mutationsData);
const data = await getMaterialMutations(cafeId); setError(null);
setMutations(data);
} catch (err) { } catch (err) {
setError(err.message); console.error("Error fetching data:", err);
setError("Gagal memuat data bahan baku.");
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
fetchMaterials(); if (cafeId) {
fetchMutations(); fetchData();
}
}, [cafeId]); }, [cafeId]);
const filteredMutations = mutations.filter((mutation) => mutation.materialId === materials[selectedMaterialIndex]?.materialId) || []; // Handle create material
const handleCreateMaterial = async (e) => {
e.preventDefault();
if (!newMaterialName.trim()) return;
const sortedMutations = filteredMutations setLoading(true);
.filter((mutation) => mutation.materialId === materials[selectedMaterialIndex].materialId) try {
.sort((a, b) => { const formData = new FormData();
formData.append("name", newMaterialName);
formData.append("unit", newMaterialUnit);
await createMaterial(cafeId, formData);
// Reset form
setNewMaterialName("");
setNewMaterialUnit("kilogram");
setShowAddForm(false);
// Refresh materials list
const data = await getMaterials(cafeId);
setMaterials(data);
setError(null);
} catch (err) {
console.error("Error creating material:", err);
setError("Gagal menambah bahan baku.");
} finally {
setLoading(false);
}
};
// Handle delete material
const handleDeleteMaterial = async (materialId) => {
if (!window.confirm("Apakah Anda yakin ingin menghapus bahan baku ini?")) return;
setLoading(true);
try {
await deleteMaterial(materialId);
// Refresh materials list
const data = await getMaterials(cafeId);
setMaterials(data);
// Close expanded view if deleted material was expanded
setExpandedMaterials(prevState => {
const newState = { ...prevState };
delete newState[materialId];
return newState;
});
setError(null);
} catch (err) {
console.error("Error deleting material:", err);
setError("Gagal menghapus bahan baku.");
} finally {
setLoading(false);
}
};
// Handle update stock
const handleUpdateStock = async (materialId) => {
if (newQuantity === 0 && !convertToInteger(newPrice)) return;
setLoading(true);
try {
const material = materials.find(m => m.materialId === materialId);
const currentQuantity = material ? material.currentStock : 0;
const finalQuantity = currentQuantity + newQuantity;
const formData = new FormData();
formData.append("newStock", finalQuantity);
formData.append("priceAtp", convertToInteger(newPrice));
formData.append("reason", `Update stok: ${newQuantity > 0 ? '+' : ''}${newQuantity}`);
await createMaterialMutation(materialId, formData);
// Refresh data
const [materialsData, mutationsData] = await Promise.all([
getMaterials(cafeId),
getMaterialMutations(cafeId)
]);
setMaterials(materialsData);
setMutations(mutationsData);
// Reset form
setNewQuantity(0);
setNewPrice("");
setEditingMaterialId(null);
setError(null);
} catch (err) {
console.error("Error updating stock:", err);
setError("Gagal memperbarui stok.");
} finally {
setLoading(false);
}
};
// Get filtered and sorted mutations for a material
const getFilteredMutations = (materialId) => {
const filtered = mutations.filter(m => m.materialId === materialId);
return filtered.sort((a, b) => {
if (sortOrder === "asc") { if (sortOrder === "asc") {
return new Date(a.createdAt) - new Date(b.createdAt); return new Date(a.createdAt) - new Date(b.createdAt);
} else { } else {
return new Date(b.createdAt) - new Date(a.createdAt); return new Date(b.createdAt) - new Date(a.createdAt);
} }
}); });
const handleCreateMaterial = async (e) => {
e.preventDefault();
setLoading(true);
const formData = new FormData();
formData.append("name", newMaterialName);
formData.append("unit", newMaterialUnit);
if (newMaterialImage) {
formData.append("image", newMaterialImage);
}
try {
await createMaterial(cafeId, formData);
setNewMaterialName("");
setNewMaterialUnit("kilogram");
setNewMaterialImage(null);
setShowForm(false);
const data = await getMaterials(cafeId);
setMaterials(data);
setError(null);
if (data.length > 0) {
setSelectedMaterialIndex(0);
}
} catch (error) {
console.error("Error creating material:", error);
setError("Failed to create material.");
} finally {
setLoading(false);
}
}; };
const handleDeleteMaterial = async (materialId) => { // Format date for display
setDeleting(materialId);
try {
await deleteMaterial(materialId);
const updatedMaterials = materials.filter(
(material) => material.materialId !== materialId
);
setMaterials(updatedMaterials);
setError(null);
if (selectedMaterialIndex === materialId) {
setSelectedMaterialIndex(
updatedMaterials.length > 0 ? updatedMaterials[0].materialId : null
);
}
} catch (error) {
console.error("Error deleting material:", error);
setError("Failed to delete material.");
} finally {
setDeleting(null);
}
};
const handleQuantityChange = (change) => {
setQuantityChange((prev) => prev + change);
if (quantityChange + change < 1) setNewPrice(currentPrice);
setIsEditCurrentPrice(false);
};
useEffect(() => {
setQuantityChange(0);
if (materials.length > 0 || selectedMaterialIndex > -1) {
const materialMutations = mutations.filter(
(mutation) => mutation.materialId === materials[selectedMaterialIndex]?.materialId
);
console.log(materialMutations)
if (materialMutations.length > 0) {
const latestMutation = materialMutations.reduce(
(latest, current) =>
new Date(current.createdAt) > new Date(latest.createdAt)
? current
: latest,
materialMutations[0]
);
setLatestMutation(latestMutation);
setCurrentQuantity(latestMutation.newStock);
setCurrentPrice(formatCurrency(latestMutation.priceAtp));
setNewPrice(formatCurrency(latestMutation.priceAtp));
} else {
setCurrentQuantity(0); // Default value if no mutations exist
setLatestMutation({ newStock: 0 });
setCurrentPrice(0);
setNewPrice(0);
}
}
setIsViewingHistory(false);
}, [materials, mutations, selectedMaterialIndex]);
const handleUpdateStock = async () => {
setLoading(true);
try {
const newprice = convertToInteger(newPrice)
const newStock = currentQuantity + quantityChange;
const formData = new FormData();
formData.append("newStock", newStock);
formData.append("priceAtp", newprice);
formData.append("reason", "Stock update");
await createMaterialMutation(materials[selectedMaterialIndex].materialId, formData);
setQuantityChange(0);
const updatedMutations = await getMaterialMutations(cafeId);
setMutations(updatedMutations);
setCurrentQuantity(newStock);
setError(null);
} catch (error) {
console.error("Error updating stock:", error);
setError("Failed to update stock.");
} finally {
setLoading(false);
}
};
const currentMaterial = materials.find(
(material) => material.materialId === selectedMaterialIndex
);
const formatDate = (timestamp) => { const formatDate = (timestamp) => {
const date = new Date(timestamp); const date = new Date(timestamp);
return date.toLocaleString(); return date.toLocaleString('id-ID', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}; };
// Toggle material expansion
const toggleExpand = (materialId) => {
setExpandedMaterials(prevState => {
const newState = {
...prevState,
[materialId]: !prevState[materialId]
};
// Reset form when expanding/collapsing
if (!prevState[materialId]) {
setNewQuantity(0);
setNewPrice("");
setEditingMaterialId(null);
}
return newState;
});
};
// Toggle history view for a material
const toggleHistoryView = (materialId) => {
setIsViewingHistory(prevState => ({
...prevState,
[materialId]: !prevState[materialId]
}));
};
// Handle quantity change
const handleQuantityChange = (change) => {
setNewQuantity(prev => Math.max(0, prev + change));
};
if (loading && materials.length === 0) {
return (
<div className={styles.container}>
<div className={styles.loading}>
<div className={styles.loadingSpinner}></div>
<div>Memuat data bahan baku...</div>
</div>
</div>
);
}
return ( return (
<div className={styles.container}> <div className={styles.container}>
{loading ? ( <div className={styles.header}>
<></> <h2 className={styles.title}>Manajemen Bahan Baku</h2>
) : ( <button
<> className={styles.addButton}
<h3 className={styles.title}>Bahan baku</h3> onClick={() => setShowAddForm(!showAddForm)}
<Carousel items={materials} onSelect={(e) => setSelectedMaterialIndex(e)} selectedIndex={selectedMaterialIndex} /> >
{selectedMaterialIndex !== -1 ? ( {showAddForm ? 'Batal' : '+ Tambah Bahan Baku'}
<> </button>
<div className={styles.switchContainer}>
<h3>Stok sekarang {currentQuantity}</h3>
</div> </div>
<div className={styles.stokContainer}> <div className={styles.content}>
<button onClick={() => handleQuantityChange(currentQuantity + quantityChange > 0 ? -1 : 0)} className={styles.stockButton}> {error && (
<div className={styles.error} style={{
padding: '12px',
backgroundColor: '#fff0f0',
color: '#dc3545',
borderRadius: '8px',
marginBottom: '20px'
}}>
{error}
</div>
)}
{/* Add Material Form */}
{showAddForm && (
<div className={styles.addMaterialForm}>
<h3 className={styles.sectionTitle}>Tambah Bahan Baku Baru</h3>
<form onSubmit={handleCreateMaterial}>
<div className={styles.formGrid}>
<div className={styles.formGroup}>
<label className={styles.formLabel}>Nama Bahan Baku</label>
<input
type="text"
className={styles.formInput}
value={newMaterialName}
onChange={(e) => setNewMaterialName(e.target.value)}
placeholder="Masukkan nama bahan baku"
required
/>
</div>
<div className={styles.formGroup}>
<label className={styles.formLabel}>Satuan</label>
<select
className={styles.formInput}
value={newMaterialUnit}
onChange={(e) => setNewMaterialUnit(e.target.value)}
>
<option value="gram">gram</option>
<option value="ons">ons</option>
<option value="kilogram">kilogram</option>
<option value="kuintal">kuintal</option>
<option value="liter">liter</option>
<option value="piece">piece</option>
<option value="meter">meter</option>
<option value="pack">pack</option>
<option value="sachet">sachet</option>
<option value="box">box</option>
</select>
</div>
</div>
<div className={styles.formActions}>
<button
type="button"
className={styles.cancelButton}
onClick={() => {
setShowAddForm(false);
setNewMaterialName("");
setNewMaterialUnit("kilogram");
}}
>
Batal
</button>
<button
type="submit"
className={styles.saveButton}
disabled={!newMaterialName.trim()}
>
Tambah Bahan Baku
</button>
</div>
</form>
</div>
)}
{/* Materials List */}
{materials.length === 0 ? (
<div className={styles.emptyState}>
<div className={styles.emptyStateIcon}>📭</div>
<div className={styles.emptyStateText}>Belum ada bahan baku yang terdaftar</div>
<button
className={styles.addButton}
onClick={() => setShowAddForm(true)}
>
+ Tambah Bahan Baku Pertama
</button>
</div>
) : (
<div className={styles.materialsList}>
{materials.map((material) => (
<div key={material.materialId} className={styles.materialCard}>
{/* Material Summary (Always Visible) */}
<div
className={styles.materialSummary}
onClick={() => toggleExpand(material.materialId)}
>
<h3 className={styles.materialName}>{material.name}</h3>
<div className={styles.materialStock}>
{material.currentStock} {material.unit}
</div>
<div className={`${styles.expandIcon} ${expandedMaterials[material.materialId] ? styles.expanded : ''}`}>
</div>
</div>
{/* Material Detail (Expandable) */}
{expandedMaterials[material.materialId] && (
<div className={styles.materialDetail}>
<div className={styles.detailGrid}>
<div className={styles.detailItem}>
<span className={styles.detailLabel}>Stok Saat Ini</span>
<span className={styles.detailValue}>{material.currentStock} {material.unit}</span>
</div>
<div className={styles.detailItem}>
<span className={styles.detailLabel}>Harga per {material.unit}</span>
<span className={styles.detailValue}>Rp {formatCurrency(material.currentPrice || 0)}</span>
</div>
</div>
<div className={styles.detailActions}>
<button
className={styles.detailButton}
onClick={() => {
setEditingMaterialId(material.materialId);
}}
>
Update Stok
</button>
<button
className={styles.detailButton}
onClick={() => handleDeleteMaterial(material.materialId)}
>
🗑 Hapus
</button>
</div>
{/* Stock Update Form */}
{editingMaterialId === material.materialId && (
<div className={styles.stockUpdateForm}>
<h4 className={styles.formTitle}>Update Stok</h4>
<div className={styles.quantityControls}>
<button
className={styles.quantityButton}
onClick={() => handleQuantityChange(-1)}
disabled={newQuantity <= 0}
>
- -
</button> </button>
<p>{currentQuantity + quantityChange}</p> <div className={styles.quantityDisplay}>
<button onClick={() => handleQuantityChange(1)} className={styles.stockButton}> {newQuantity > 0 ? `+${newQuantity}` : newQuantity}
</div>
<button
className={styles.quantityButton}
onClick={() => handleQuantityChange(1)}
>
+ +
</button> </button>
</div> </div>
<div className={styles.uploadMessage}>
<p>harga per {materials && materials[selectedMaterialIndex]?.unit} sekarang</p> <div className={styles.formGroup}>
</div> <label className={styles.formLabel}>Harga per {material.unit}</label>
<div className={styles.resultMessage}>
<input <input
className={styles.resultMessageInput} // Replace inline style with CSS module class type="text"
disabled={!isEditCurrentPrice || quantityChange < 1} className={styles.formInput}
value={newPrice} value={newPrice}
onChange={handleChange} onChange={handlePriceChange}
placeholder="Enter amount" placeholder="Masukkan harga"
/> />
<div onClick={() => quantityChange < 1 ? null : setIsEditCurrentPrice(!isEditCurrentPrice)} className={quantityChange < 1 ? styles.changeButtonDisabled : styles.changeButtonEnabled}>
{isEditCurrentPrice ? 'Terapkan' : 'Ganti'}
</div> </div>
</div>
<div className={styles.buttonContainer}> <div className={styles.formActions}>
<button onClick={handleUpdateStock} className={styles.saveButton}> <button
Laporkan {quantityChange > 0 ? 'penambahan' : 'stok sekarang'} {quantityChange < 1 ? currentQuantity + quantityChange : quantityChange} {materials[selectedMaterialIndex]?.unit} className={styles.cancelButton}
onClick={() => {
setEditingMaterialId(null);
setNewQuantity(0);
setNewPrice("");
}}
>
Batal
</button>
<button
className={styles.saveButton}
onClick={() => handleUpdateStock(material.materialId)}
disabled={newQuantity === 0 && !convertToInteger(newPrice)}
>
Simpan Perubahan
</button> </button>
</div> </div>
<div className={styles.historyTab}> </div>
<h3 onClick={() => setIsViewingHistory(!isViewingHistory)}> {isViewingHistory ? '˅' : '˃'} Riwayat stok</h3> )}
{selectedMaterialIndex !== -1 && isViewingHistory && !loading && (
{/* History Section */}
<div className={styles.historySection}>
<h4
className={styles.historyTitle}
onClick={() => toggleHistoryView(material.materialId)}
>
{isViewingHistory[material.materialId] ? '▲' : '▼'} Riwayat Stok
</h4>
{isViewingHistory[material.materialId] && (
<> <>
<div className={styles.sorter} onClick={() => setSortOrder(sortOrder == 'asc' ? 'desc' : 'asc')}> <div className={styles.sortControls}>
Urutkan: {sortOrder === 'asc' ? "terlama" : "terbaru"} <div style={{ transform: 'rotate(90deg)' }}>&lt;&gt;</div> <button
className={styles.sortButton}
onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}
>
Urutkan: {sortOrder === 'asc' ? 'Terlama' : 'Terbaru'}
</button>
</div> </div>
<div className={styles.historyContainer}>
<div className={styles.mutationContainer}> <div className={styles.historyItems}>
{sortedMutations.length > 0 ? ( {getFilteredMutations(material.materialId).length > 0 ? (
sortedMutations.map((mutation) => ( getFilteredMutations(material.materialId).map((mutation) => (
<div key={mutation.id} className={styles.mutationCard}> <div key={mutation.id} className={styles.historyItem}>
<div style={{ width: '42px', backgroundColor: '#b9b9b9', borderRadius: '10px', padding: '3px', paddingBottom: '0' }}> <div className={styles.historyIcon}>📊</div>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <div className={styles.historyContent}>
<g id="SVGRepo_bgCarrier" stroke-width="0"></g> <div className={styles.historyDate}>{formatDate(mutation.createdAt)}</div>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g> <div className={styles.historyDetails}>
<g id="SVGRepo_iconCarrier"> Stok: {mutation.newStock} {material.unit} |
<g id="Interface / Book"> Perubahan: {mutation.newStock - mutation.oldStock > 0 ? '+' : ''}{mutation.newStock - mutation.oldStock} |
<path id="Vector" d="M5 19.5002V6.2002C5 5.08009 5 4.51962 5.21799 4.0918C5.40973 3.71547 5.71547 3.40973 6.0918 3.21799C6.51962 3 7.08009 3 8.2002 3H17.4002C17.9602 3 18.2407 3 18.4546 3.10899C18.6427 3.20487 18.7948 3.35774 18.8906 3.5459C18.9996 3.75981 19 4.04005 19 4.6001V16.4001C19 16.9601 18.9996 17.2398 18.8906 17.4537C18.7948 17.6419 18.6429 17.7952 18.4548 17.8911C18.2411 18 17.961 18 17.402 18H7.25C6.00736 18 5 19.0074 5 20.25C5 20.6642 5.33579 21 5.75 21H16.402C16.961 21 17.2411 21 17.4548 20.8911C17.6429 20.7952 17.7948 20.642 17.8906 20.4538C17.9996 20.2399 18 19.9601 18 19.4V18" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> Harga: Rp {formatCurrency(mutation.priceAtp)}
</g>
</g>
</svg>
</div> </div>
<div className={styles.mutationTitle}>
<h4>{formatDate(mutation.createdAt)}</h4>
<p>Total stok: {mutation.newStock} || +{mutation.newStock - mutation.oldStock} || {formatCurrency((mutation.newStock - mutation.oldStock) * mutation.priceAtp)}</p>
</div> </div>
</div> </div>
)) ))
) : ( ) : (
<p>Tidak ada laporan perubahan stok.</p> <div className={styles.emptyState} style={{ padding: '20px' }}>
)} <div>Belum ada riwayat perubahan stok</div>
</div> </div>
)}
</div> </div>
</> </>
)} )}
</div> </div>
</>
) : (
<>
<div className={styles.description}>
<div style={{ marginRight: "5px", fontSize: "1.2em" }}></div>
<h6 style={{ margin: 0, textAlign: "left", fontSize: '12px' }}>
Fitur ini mempermudah mengelola biaya dan memantau pengeluaran bahan.
</h6>
</div> </div>
<div className={styles.switchContainer}>
<h3>Buat bahan baru</h3>
</div>
<div className={styles.resultMessage}>
<input
className={styles.resultMessageInput}
value={newMaterialName}
onChange={(event) => setNewMaterialName(event.target.value)}
placeholder="Masukkan nama barang"
style={{width: '100%', height: '31px'}}
/>
</div>
<select
id="materialUnit"
value={newMaterialUnit}
onChange={(e) => setNewMaterialUnit(e.target.value)}
className={styles.unit}
style={{height: '37px'}}
>
<option value="gram">Satuan: gram</option>
<option value="ons">Satuan: ons</option>
<option value="kilogram">Satuan: kilogram</option>
<option value="kuintal">Satuan: kuintal</option>
<option value="liter">Satuan: liter</option>
<option value="piece">Satuan: piece</option>
<option value="meter">Satuan: meter</option>
<option value="pack">Satuan: pack</option>
<option value="sachet">Satuan: sachet</option>
<option value="box">Satuan: box</option>
</select>
<div className={styles.buttonContainer}>
<button className={styles.saveButton} onClick={handleCreateMaterial}>
Buat bahan baku
</button>
</div>
</>
)} )}
</> </div>
))}
</div>
)} )}
</div> </div>
</div>
); );
}; };
export default SetPaymentQr; export default MaterialList;

View File

@@ -1,149 +1,556 @@
/* SetPaymentQr.module.css */ /* MaterialList.module.css */
.container { .container {
width: 100%; width: 100%;
min-height: 47vh; background-color: #ffffff;
background-color: white; border-radius: 16px;
padding: 20px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border-radius: 8px; padding: 0;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); margin-bottom: 24px;
text-align: center; transition: all 0.3s ease;
} position: relative;
z-index: 200; /* Menaikkan z-index agar berada di atas cartbar */
max-height: 70vh; /* Membatasi tinggi maksimum */
overflow-y: auto; /* Memungkinkan scrolling vertikal */
}
.title { .container:hover {
margin-bottom: 20px; box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
font-weight: bold; }
}
.uploadMessage { .header {
font-weight: 600; padding: 24px;
text-align: left; border-bottom: 1px solid #e6e6e6;
}
.changeButtonEnabled {
padding-right: 10px;
background-color: green;
border-radius: 30px;
color: white;
font-weight: 700;
height: 36px;
line-height: 36px;
padding-left: 10px;
padding-height: 10px;
}
.changeButtonDisabled {
padding-right: 10px;
background-color: #a1a1a1;
border-radius: 30px;
color: white;
font-weight: 700;
height: 36px;
line-height: 36px;
padding-left: 10px;
padding-height: 10px;
}
.resultMessage {
margin-top: -13px;
text-align: left;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
}
.resultMessage input {
padding-left: 8px;
width: 180px;
}
.stokContainer {
display: flex;
justify-content: space-evenly;
align-items: center; align-items: center;
margin-top: -20px; position: sticky;
margin-bottom: -15px; top: 0;
text-align: left; background-color: #ffffff;
} z-index: 201; /* Memastikan header berada di atas container */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02);
}
.buttonContainer { .title {
margin-top: 11px; margin: 0;
text-align: left; font-weight: 700;
} font-size: 24px;
color: #333;
}
.stockButton { .addButton {
padding: 10px 20px;
font-size: 3.5vw;
background-color: #28a745; background-color: #28a745;
color: #fff; color: white;
border: none; border: none;
border-radius: 30px; border-radius: 12px;
padding: 12px 24px;
font-size: 16px;
font-weight: 600;
cursor: pointer; cursor: pointer;
transition: background-color 0.3s; transition: all 0.2s ease;
}
.saveButton {
width: 100%;
padding: 10px 20px;
font-size: 3.5vw;
background-color: #28a745;
color: #fff;
border: none;
border-radius: 30px;
cursor: pointer;
transition: background-color 0.3s;
}
.switchContainer {
margin-top: 20px;
text-align: left;
}
.historyTab {
text-align: left;
}
.historyContainer {
text-align: left;
max-height: 15vh;
overflow-y: auto;
}
.description {
display: flex; display: flex;
margin: 10px 0; align-items: center;
gap: 8px;
box-shadow: 0 2px 6px rgba(40, 167, 69, 0.2);
position: relative;
z-index: 202; /* Memastikan tombol berada di atas header */
}
.addButton:hover {
background-color: #218838;
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(40, 167, 69, 0.3);
}
.addButton:disabled {
background-color: #a1a1a1;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.content {
padding: 24px;
position: relative;
z-index: 200;
}
/* Materials List Styles */
.materialsList {
display: flex;
flex-direction: column;
gap: 12px;
}
.materialCard {
background: #ffffff;
border-radius: 12px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
border: 1px solid #eee;
position: relative;
z-index: 200;
}
.materialCard:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: #28a745;
}
.materialSummary {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
cursor: pointer;
position: relative;
z-index: 201;
}
.materialName {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0;
flex: 1;
}
.materialStock {
font-size: 14px; font-size: 14px;
color: #666; color: #666;
} margin: 0 16px 0 0;
}
.unit { .expandIcon {
margin-top: 11px; font-size: 18px;
width: 100%; color: #28a745;
height: 31px; transition: transform 0.2s ease;
} }
.sorter { .expandIcon.expanded {
border: 1px solid #c3c3c3; transform: rotate(180deg);
padding: 5px; }
/* Material Detail Styles */
.materialDetail {
padding: 0 20px 20px 20px;
border-top: 1px solid #eee;
background: #f9f9f9;
border-radius: 0 0 12px 12px;
position: relative;
z-index: 200;
}
.detailGrid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 20px;
}
.detailItem {
background: #ffffff;
padding: 16px;
border-radius: 10px; border-radius: 10px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.detailLabel {
font-size: 13px;
color: #666;
margin-bottom: 4px;
}
.detailValue {
font-size: 18px;
font-weight: 600;
color: #333;
}
.detailActions {
display: flex; display: flex;
justify-content: space-between; gap: 12px;
margin-top: -10px; margin-top: 16px;
margin-bottom: 10px; }
}
.mutationCard { .detailButton {
flex: 1;
padding: 12px 16px;
border-radius: 10px;
border: none;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
display: flex; display: flex;
margin-bottom: 7px; align-items: center;
margin-top: 7px; justify-content: center;
gap: 8px;
}
.updateButton {
background-color: #28a745;
color: white;
box-shadow: 0 2px 6px rgba(40, 167, 69, 0.2);
}
.updateButton:hover {
background-color: #218838;
box-shadow: 0 4px 10px rgba(40, 167, 69, 0.3);
}
.deleteButton {
background-color: #dc3545;
color: white;
box-shadow: 0 2px 6px rgba(220, 53, 69, 0.2);
}
.deleteButton:hover {
background-color: #c82333;
box-shadow: 0 4px 10px rgba(220, 53, 69, 0.3);
}
/* Stock Update Form */
.stockUpdateForm {
background: #ffffff;
border-radius: 12px;
padding: 20px;
margin-top: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
position: relative;
z-index: 201;
}
.formTitle {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0 0 20px 0;
}
.quantityControls {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 20px;
}
.quantityButton {
width: 40px;
height: 40px;
border-radius: 50%;
border: 1px solid #ddd;
background: #ffffff;
font-size: 20px;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.quantityButton:hover {
border-color: #28a745;
background: #f0f8f1;
color: #28a745;
}
.quantityDisplay {
font-size: 20px;
font-weight: 600;
min-width: 80px;
text-align: center;
}
.formGroup {
margin-bottom: 20px;
}
.formLabel {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
font-size: 14px;
}
.formInput {
width: 100%;
padding: 12px 16px;
border-radius: 10px;
border: 1px solid #ddd;
font-size: 16px;
transition: all 0.2s ease;
background: #ffffff;
}
.formInput:focus {
outline: none;
border-color: #28a745;
box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.1);
}
.formActions {
display: flex;
gap: 12px;
}
.formButton {
flex: 1;
padding: 14px 16px;
border-radius: 10px;
border: none;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.saveButton {
background-color: #28a745;
color: white;
box-shadow: 0 2px 6px rgba(40, 167, 69, 0.2);
}
.saveButton:hover {
background-color: #218838;
box-shadow: 0 4px 10px rgba(40, 167, 69, 0.3);
}
.cancelButton {
background-color: #f0f0f0;
color: #333;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
}
.cancelButton:hover {
background-color: #e0e0e0;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
/* Add Material Form */
.addMaterialForm {
background: #f8f9fa;
border-radius: 12px;
padding: 24px;
margin-top: 24px;
position: relative;
z-index: 201;
}
.formSection {
margin-bottom: 24px;
}
.sectionTitle {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0 0 20px 0;
padding-bottom: 12px;
border-bottom: 1px solid #e6e6e6;
}
.formGrid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.fullWidth {
grid-column: 1 / -1;
}
/* Empty State */
.emptyState {
text-align: center;
padding: 60px 20px;
color: #999;
}
.emptyStateIcon {
font-size: 48px;
margin-bottom: 16px;
color: #ccc;
}
.emptyStateText {
font-size: 18px;
margin-bottom: 24px;
color: #666;
}
/* History Section */
.historySection {
margin-top: 24px;
border-top: 1px solid #e6e6e6;
padding-top: 20px;
}
.historyTitle {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0 0 16px 0;
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
}
.historyTitle:hover {
color: #28a745;
}
.historyItems {
display: flex;
flex-direction: column;
gap: 12px;
max-height: 250px;
overflow-y: auto;
padding-right: 8px;
}
.historyItems::-webkit-scrollbar {
width: 6px;
}
.historyItems::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.historyItems::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 10px;
}
.historyItem {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 16px;
background: #ffffff;
border-radius: 10px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.historyIcon {
width: 36px;
height: 36px;
background: #b9b9b9;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 16px;
}
.historyContent {
flex: 1;
}
.historyDate {
font-size: 13px;
font-weight: 600;
color: #333;
margin: 0 0 2px 0;
}
.historyDetails {
font-size: 13px;
color: #666;
margin: 0;
}
.sortControls {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
}
.sortButton {
background: #f0f0f0;
border: none;
border-radius: 20px;
padding: 6px 14px;
font-size: 13px;
color: #555;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 6px;
}
.sortButton:hover {
background: #e0e0e0;
}
/* Loading State */
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.loadingSpinner {
font-size: 32px;
margin-bottom: 16px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Responsive Design */
@media (max-width: 768px) {
.header {
padding: 16px;
flex-direction: column;
gap: 16px;
align-items: stretch;
} }
.mutationTitle { .content {
margin-left: 5px; padding: 16px;
}
.mutationTitle h4 {
margin: 0;
} }
.mutationTitle p { .detailGrid {
margin: 0; grid-template-columns: 1fr;
gap: 12px;
} }
.formGrid {
grid-template-columns: 1fr;
gap: 16px;
}
.historyItems {
max-height: 200px;
}
.materialSummary {
padding: 14px 16px;
}
.materialName {
font-size: 15px;
}
.materialStock {
font-size: 13px;
margin-right: 12px;
}
.container {
max-height: 75vh; /* Menaikkan tinggi maksimum pada mobile */
}
}

View File

@@ -82,7 +82,7 @@ const RoundedRectangle = ({
}; };
const percentageStyle = { const percentageStyle = {
fontSize: "16px", fontSize: "14px",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
textAlign: "right", textAlign: "right",
@@ -282,11 +282,11 @@ const App = ({ forCafe = true, cafeId = -1,
if (amount >= 1_000_000_000) { if (amount >= 1_000_000_000) {
// Format for billions // Format for billions
const billions = amount / 1_000_000_000; 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) { } else if (amount >= 1_000_000) {
// Format for millions // Format for millions
const millions = amount / 1_000_000; 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) { } else if (amount >= 1_000) {
// Format for thousands // Format for thousands
const thousands = amount / 1_000; const thousands = amount / 1_000;
@@ -326,7 +326,7 @@ const App = ({ forCafe = true, cafeId = -1,
setSelectedCafeId(-1); setSelectedCafeId(-1);
} else if (otherCafes.length === 1) { } else if (otherCafes.length === 1) {
updatedFullTexts = [ 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 // Only add the "Buat Bisnis" option for user.roleId == 1
...(user.roleId == 1 ? [["Buat Bisnis", -1]] : []) ...(user.roleId == 1 ? [["Buat Bisnis", -1]] : [])
]; ];
@@ -335,7 +335,7 @@ const App = ({ forCafe = true, cafeId = -1,
} else { } else {
updatedFullTexts = [ updatedFullTexts = [
["semua", 0], // First entry is "semua" ["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 // Only add "Buat Bisnis +" option for user.roleId == 1
...(user.roleId == 1 ? [["Buat Bisnis +", -1]] : []) ...(user.roleId == 1 ? [["Buat Bisnis +", -1]] : [])
]; ];
@@ -411,10 +411,10 @@ const App = ({ forCafe = true, cafeId = -1,
console.log(analytics) console.log(analytics)
if (user && user.roleId === 0 && 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){ if(selectedItem[1] != 0 && selectedItem[1] != -1){
const filteredData = analytics.items.filter( const filteredData = analytics.items.filter(
(data) => data.userId === nextSelectedId (data) => data.user_id === nextSelectedId
); );
// Extract coupons from the filtered data // Extract coupons from the filtered data

View File

@@ -0,0 +1,76 @@
/* StickyCartBar.module.css */
.bar {
position: sticky;
bottom: 40px;
z-index: 120; /* above items, below modal */
display: flex;
justify-content: center;
align-items: center;
margin-top: 10px;
pointer-events: none; /* allow buttons to define interaction */
}
.row {
display: flex;
gap: 8px;
width: 100%;
max-width: 980px;
padding: 0 12px;
pointer-events: auto;
}
.mainBtn {
flex: 1;
height: 44px;
border-radius: 999px;
background: var(--brand-primary, #73a585);
color: #fff;
border: none;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
cursor: pointer;
box-shadow: 0 6px 18px rgba(115,165,133,0.35);
}
.summary {
display: flex;
align-items: center;
gap: 6px;
font-weight: 600;
}
.value {
white-space: nowrap;
font-weight: 700;
}
.cartIcon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
}
.historyBtn {
width: 48px;
height: 44px;
border-radius: 999px;
background: var(--brand-primary, #73a585);
color: #fff;
border: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 6px 18px rgba(115,165,133,0.35);
}
@media (max-width: 480px) {
.mainBtn { height: 42px; }
.historyBtn { height: 42px; width: 46px; }
}

View File

@@ -11,7 +11,7 @@ import { getTables } from "../helpers/tableHelper";
import TableCanvas from "../components/TableCanvas"; import TableCanvas from "../components/TableCanvas";
import { useSearchParams } from "react-router-dom"; 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(); const { shopId, tableId } = useParams();
if (sendParam) sendParam({ shopId, tableId }); if (sendParam) sendParam({ shopId, tableId });
@@ -231,13 +231,29 @@ export default function Transactions({ propsShopId, sendParam, deviceType, handl
</div> </div>
))} ))}
</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"]}> <h2 className={styles["Transactions-detail"]}>
{transaction.serving_type === "pickup" {transaction.serving_type === "pickup"
? "Ambil sendiri" ? "Ambil sendiri"
: `Diantar ke ${transaction.Table ? transaction.Table.tableNo : "N/A" : `Diantar ke ${transaction.Table ? transaction.Table.tableNo : "N/A"
}`} }`}
</h2> </h2>
{transaction.notes != null && ( {transaction.notes != '' && (
<> <>
<div className={styles.NoteContainer}> <div className={styles.NoteContainer}>
<span>Note :</span> <span>Note :</span>

View File

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

View File

@@ -8,100 +8,121 @@ import {
declineTransaction, declineTransaction,
getTransactionsFromCafe, getTransactionsFromCafe,
} from "../helpers/transactionHelpers"; } 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 ButtonWithReplica from "../components/ButtonWithReplica";
import TableCanvas from "../components/TableCanvas";
import dayjs from 'dayjs'; dayjs.extend(utc);
import utc from 'dayjs/plugin/utc'; dayjs.extend(timezone);
import timezone from 'dayjs/plugin/timezone';
export default function Transactions({ shop, shopId, propsShopId, sendParam, deviceType, paymentUrl, setModal, newTransaction }) {
export default function Transactions({ shop, shopId, propsShopId, sendParam, deviceType, paymentUrl }) {
const { shopIdentifier, tableId } = useParams(); const { shopIdentifier, tableId } = useParams();
if (sendParam) sendParam({ shopIdentifier, tableId }); if (sendParam) sendParam({ shopIdentifier, tableId });
dayjs.extend(utc);
dayjs.extend(timezone);
const [transactions, setTransactions] = useState([]); const [transactions, setTransactions] = useState([]);
const [isPaymentLoading, setIsPaymentLoading] = useState(false); const [isPaymentLoading, setIsPaymentLoading] = useState(false);
const [isPaymentOpen, setIsPaymentOpen] = useState(false); const [isPaymentOpen, setIsPaymentOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [matchedItems, setMatchedItems] = useState([]);
const [loading, setLoading] = useState(false);
// Currency formatter (thousand separators, with Rp prefix)
const formatRp = (value) => `Rp ${new Intl.NumberFormat('id-ID').format(Math.round(Number(value || 0)))}`;
const getStatusClass = (t) => {
if (t.confirmed === 3 || t.is_paid) return styles.statusSuccess;
if (t.confirmed === -1 || t.confirmed === -2) return styles.statusCancelled;
return styles.statusNeutral;
};
useEffect(() => {
setMatchedItems(searchAndAggregateItems(transactions, searchTerm));
}, [searchTerm, transactions]);
useEffect(() => { useEffect(() => {
const fetchTransactions = async () => { const fetchTransactions = async () => {
if (deviceType == 'clerk') {
try { try {
let response;
response = await getTransactionsFromCafe(shopId || propsShopId, 5, false); // response = await getMyTransactions(shopId || propsShopId, 5);
console.log(response) // setMyTransactions(response);
if (response) { setLoading(true);
setTransactions(response); let response = await getTransactionsFromCafe(shopId || propsShopId, -1, false);
return;
} setLoading(false);
if (response) setTransactions(response);
} catch (error) { } catch (error) {
console.error("Error fetching transactions:", error); console.error("Error fetching transactions:", error);
} }
}
else {
try {
let response;
response = await getMyTransactions(shopId || propsShopId, 5);
console.log(response)
const combinedTransactions = [];
response.forEach(cafe => {
const { cafeId, name: cafeName, transactions } = cafe;
transactions.forEach(transaction => {
const newTransaction = {
...transaction,
cafeId,
cafeName,
DetailedTransactions: transaction.detailedTransactions // Rename here
}; };
delete newTransaction.detailedTransactions; // Remove the old key console.log(deviceType)
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);
}
}
};
fetchTransactions(); fetchTransactions();
}, [deviceType]); }, [deviceType, newTransaction]);
const calculateTotalPrice = (detailedTransactions) => { const calculateTotalPrice = (detailedTransactions) => {
return detailedTransactions.reduce((total, dt) => { return detailedTransactions.reduce((total, dt) => {
return total + dt.qty * (dt.promoPrice ? dt.promoPrice : dt.price); return total + dt.qty * (dt.promoPrice ? dt.promoPrice : dt.price);
}, 0); }, 0);
}; };
const calculateAllTransactionsTotal = (transactions) => {
const calculateAllTransactionsTotal = (transactions) => { return transactions
return transactions.reduce((grandTotal, transaction) => { .filter(transaction => transaction.confirmed > 1) // Filter transactions where confirmed > 1
.reduce((grandTotal, transaction) => {
return grandTotal + calculateTotalPrice(transaction.DetailedTransactions); return grandTotal + calculateTotalPrice(transaction.DetailedTransactions);
}, 0); }, 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) => { const handleConfirm = async (transactionId) => {
setIsPaymentLoading(true); setIsPaymentLoading(true);
try { try {
const c = await confirmTransaction(transactionId); const result = await confirmTransaction(transactionId);
if (c) { if (result) {
// Update the confirmed status locally setTransactions(prev =>
setTransactions((prevTransactions) => prev.map(t =>
prevTransactions.map((transaction) => t.transactionId === transactionId ? result : t
transaction.transactionId === transactionId
? { ...transaction, confirmed: 1 } // Set to confirmed
: transaction
) )
); );
} }
@@ -115,14 +136,11 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
const handleDecline = async (transactionId) => { const handleDecline = async (transactionId) => {
setIsPaymentLoading(true); setIsPaymentLoading(true);
try { try {
const c = await declineTransaction(transactionId); const result = await declineTransaction(transactionId);
if (c) { if (result) {
// Update the confirmed status locally setTransactions(prev =>
setTransactions((prevTransactions) => prev.map(t =>
prevTransactions.map((transaction) => t.transactionId === transactionId ? result : t
transaction.transactionId === transactionId
? c // Set to confirmed
: transaction
) )
); );
} }
@@ -133,94 +151,97 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
} }
}; };
if (loading)
return (
<div className="Loader">
<div className="LoaderChild">
<ThreeDots />
<h1></h1>
</div>
</div>
);
return ( return (
<div className={styles.Transactions}> <div className={styles.Transactions}>
<div style={{ marginTop: "30px" }}></div> <h2 className={styles["Transactions-title"]}>
<h2 className={styles["Transactions-title"]}>Daftar transaksi Transaksi selesai {formatRp(calculateAllTransactionsTotal(transactions))}
Rp {calculateAllTransactionsTotal(transactions)} </h2> </h2>
<div style={{ marginTop: "30px" }}></div>
{/* <TableCanvas tables={tables} selectedTable={selectedTable} /> */} <input
<div className={styles.TransactionListContainer} style={{ padding: '0 20px 0 20px' }}> type="text"
placeholder="Cari nama item..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{ border: '0px', height: '36px', borderRadius: '12px', margin: '6px auto 10px', width: '88%', padding: '0 8px', fontSize: '14px' }}
/>
{/* Existing Transactions List (keep all your JSX below unchanged) */}
<div className={styles.TransactionListContainer}>
{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>{formatRp(item.totalPrice)}</span>
</div>
</div>
))}
{transactions && {transactions &&
transactions.map((transaction) => ( transactions.map((transaction) => (
<div <div
key={transaction.transactionId} key={transaction.transactionId}
className={styles.RoundedRectangle} className={`${styles.RoundedRectangle} ${!transaction.is_paid ? styles.unpaid : ''}`}
style={{ overflow: 'hidden' }}
> >
<div className={styles['receipt-header']}> <div className={styles['receipt-header']}>
{transaction.confirmed === 1 ? ( {transaction.confirmed === 1 ? (
<ColorRing className={styles['receipt-logo']} /> <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' }}> <div style={{ display: 'flex', justifyContent: 'center', margin: '12px 0' }}>
<svg <svg width="60" height="60" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
style={{ width: '60px', transform: 'Rotate(45deg)' }} <path d="M18.3 5.7a1 1 0 0 0-1.4 0L12 10.6 7.1 5.7A1 1 0 0 0 5.7 7.1L10.6 12l-4.9 4.9a1 1 0 1 0 1.4 1.4L12 13.4l4.9 4.9a1 1 0 0 0 1.4-1.4L13.4 12l4.9-4.9a1 1 0 0 0 0-1.4z" fill="#E45454"/>
clipRule="evenodd"
fillRule="evenodd"
strokeLinejoin="round"
strokeMiterlimit="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12.002 2c5.518 0 9.998 4.48 9.998 9.998 0 5.517-4.48 9.997-9.998 9.997-5.517 0-9.997-4.48-9.997-9.997 0-5.518 4.48-9.998 9.997-9.998zm0 1.5c-4.69 0-8.497 3.808-8.497 8.498s3.807 8.497 8.497 8.497 8.498-3.807 8.498-8.497-3.808-8.498-8.498-8.498zm-.747 7.75h-3.5c-.414 0-.75.336-.75.75s.336.75.75.75h3.5v3.5c0 .414.336.75.75.75s.75-.336.75-.75v-3.5h3.5c.414 0 .75-.336.75-.75s-.336-.75-.75-.75h-3.5v-3.5c0-.414-.336-.75-.75-.75s-.75.336-.75.75z"
fillRule="nonzero"
/>
</svg> </svg>
</div> </div>
) : transaction.confirmed === 2 ? ( ) : transaction.confirmed === 2 && !transaction.is_paid ? (
<ColorRing className={styles['receipt-logo']} /> <ColorRing className={styles['receipt-logo']} />
) : transaction.confirmed === 3 ? ( ) : transaction.confirmed === 3 || transaction.is_paid ? (
<div> <div style={{ display: 'flex', justifyContent: 'center', margin: '12px 0' }}>
<svg <svg width="60" height="60" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
height="60px" <path d="M9 16.2 4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4z" fill="#54B265"/>
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> </svg>
</div> </div>
) : ( ) : (
<ColorRing className={styles['receipt-logo']} /> <ColorRing className={styles['receipt-logo']} />
)} )}
<div className={styles['receipt-info']}> <div className={styles['receipt-info']}>
{deviceType == 'clerk' ? {deviceType == 'clerk' ?
<h3>{transaction.confirmed === 1 ? ( <h3 className={getStatusClass(transaction)}>{transaction.confirmed === 1 && !transaction.is_paid ? (
"Silahkan Cek Pembayaran" "Silahkan Cek Pembayaran"
) : transaction.confirmed === -1 ? ( ) : transaction.confirmed === -1 && !transaction.is_paid ? (
"Dibatalkan Oleh Kasir" "Dibatalkan Oleh Kasir"
) : transaction.confirmed === -2 ? ( ) : transaction.confirmed === -2 && !transaction.is_paid ? (
"Dibatalkan Oleh Pelanggan" "Dibatalkan Oleh Pelanggan"
) : transaction.confirmed === 2 ? ( ) : transaction.confirmed === 2 && !transaction.is_paid ? (
"Sedang Diproses" "Sedang Diproses"
) : transaction.confirmed === 3 ? ( ) : transaction.confirmed === 3 || transaction.is_paid ? (
"Transaksi Sukses" "Transaksi Sukses"
) : ( ) : (
"Silahkan Cek Ketersediaan" "Silahkan Cek Ketersediaan"
)}</h3> )}</h3>
: :
<h3>{transaction.confirmed === 1 ? ( <h3 className={getStatusClass(transaction)}>{transaction.confirmed === 1 ? (
(transaction.payment_type == 'cash' ? 'Silahkan Bayar Ke Kasir' : "Silahkan Bayar Dengan QRIS") (transaction.payment_type == 'cash' ? 'Silahkan Bayar Ke Kasir' : "Silahkan Bayar Dengan QRIS")
) : transaction.confirmed === -1 ? ( ) : transaction.confirmed === -1 ? (
"Dibatalkan Oleh Kasir" "Dibatalkan Oleh Kasir"
@@ -258,16 +279,28 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
<ul> <ul>
{transaction.DetailedTransactions.map((detail) => ( {transaction.DetailedTransactions.map((detail) => (
<li key={detail.detailedTransactionId}> <li key={detail.detailedTransactionId}>
<span>{detail.Item.name}</span> - {detail.qty < 1 ? 'tidak tersedia' : `${detail.qty} x Rp <span>{detail.Item.name}</span> - {detail.qty < 1 ? 'tidak tersedia' : `${detail.qty} x ${formatRp(detail.promoPrice ? detail.promoPrice : detail.price)}`}
${detail.promoPrice ? detail.promoPrice : detail.price}`}
</li> </li>
))} ))}
</ul> </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"]}> <h2 className={styles["Transactions-detail"]}>
{transaction.serving_type === "pickup" {transaction.serving_type === "pickup"
? "Self pickup" ? "Self pickup"
: `Serve to ${transaction.Table ? transaction.Table.tableNo : "N/A" : `Serve to ${transaction.Table ? transaction.Table.tableNo : "N/A"}`}
}`}
</h2> </h2>
{transaction.notes && ( {transaction.notes && (
@@ -290,12 +323,13 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
<div className={styles.TotalContainer}> <div className={styles.TotalContainer}>
<span>Total:</span> <span>Total:</span>
<span> <span>
Rp {calculateTotalPrice(transaction.DetailedTransactions)} {formatRp(calculateTotalPrice(transaction.DetailedTransactions))}
</span> </span>
</div> </div>
<div className={styles.TotalContainer}> <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)) && (
<div className={styles.ActionRow}>
<button <button
className={styles.PayButton} className={styles.PayButton}
onClick={() => handleConfirm(transaction.transactionId)} onClick={() => handleConfirm(transaction.transactionId)}
@@ -303,25 +337,30 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
> >
{ {
isPaymentLoading ? ( isPaymentLoading ? (
<ColorRing height="50" width="50" color="white" /> <ColorRing height="28" width="28" color="white" />
) : transaction.confirmed === 1 ? ( ) : transaction.confirmed === 1 ? (
"Konfirmasi Telah Bayar" "Konfirmasi"
) : transaction.confirmed === 2 ? ( ) : transaction.confirmed === 2 ? (
"Confirm item is ready" "Confirm item is ready"
) : ( ) : (
"Confirm availability" "Confirm availability"
) )
} }
</button> </button>
} <button
className={styles.DeclineButton}
onClick={() => handleDecline(transaction.transactionId)}
disabled={isPaymentLoading}
>
{isPaymentLoading ? '...' : 'Batalkan'}
</button>
</div>
)}
{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 <ButtonWithReplica
paymentUrl={paymentUrl} paymentUrl={paymentUrl}
price={ price={formatRp(calculateTotalPrice(transaction.DetailedTransactions))}
"Rp" + calculateTotalPrice(transaction.DetailedTransactions)
}
disabled={isPaymentLoading} disabled={isPaymentLoading}
isPaymentLoading={isPaymentLoading} isPaymentLoading={isPaymentLoading}
handleClick={() => handleConfirm(transaction.transactionId)} handleClick={() => handleConfirm(transaction.transactionId)}
@@ -344,40 +383,30 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
} }
</div> </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 <button
className={styles.PayButton} className={styles.DeclineButton}
onClick={() => handleDecline(transaction.transactionId)} onClick={() => handleDecline(transaction.transactionId)}
disabled={ disabled={
transaction.confirmed === -1 || transaction.confirmed === -1 ||
transaction.confirmed === 3 || transaction.confirmed === 3 ||
isPaymentLoading isPaymentLoading
} // Disable button if confirmed (1) or declined (-1) or }
> >
{isPaymentLoading ? ( {isPaymentLoading ? '...' : 'Batalkan'}
<ColorRing height="50" width="50" color="white" />
) : transaction.confirmed === -1 ? (
"Ditolak" // Display "Declined" if the transaction is declined (-1)
) : transaction.confirmed === -2 ? (
"Dibatalkan" // Display "Declined" if the transaction is declined (-1)
) : (
"Batalkan" // Display "Confirm availability" if the transaction is not confirmed (0)
)}
</button> </button>
: ) : (
((transaction.confirmed >= 0 && transaction.confirmed < 2 && transaction.payment_type != 'paylater/cash' && transaction.payment_type != 'paylater/cashless' || isPaymentOpen) && (transaction.confirmed >= 0 && transaction.confirmed < 2 && (transaction.payment_type != 'paylater/cash' && transaction.payment_type != 'paylater/cashless' || isPaymentOpen)) && (
<h5 <button
className={`${styles.DeclineButton}`} className={styles.DeclineButton}
onClick={() => onClick={() => isPaymentOpen ? setIsPaymentOpen(false) : handleDecline(transaction.transactionId)}
isPaymentOpen
? setIsPaymentOpen(false)
: handleDecline(transaction.transactionId)
}
> >
{isPaymentOpen ? "kembali" : "batalkan"} {isPaymentOpen ? 'Kembali' : 'Batalkan'}
</h5> </button>
) )
} )
)}
</div> </div>
))} ))}
</div> </div>

View File

@@ -13,41 +13,43 @@
} }
.Transactions { .Transactions {
overflow-x: hidden; overflow-x: hidden;
background-color: white; background-color: #f5f7f6;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
font-size: calc(10px + 2vmin); font-size: calc(10px + 2vmin);
color: rgba(88, 55, 50, 1); color: rgba(40, 40, 40, 1);
background-color: #e9e9e9;
} }
.Transactions-title { .Transactions-title {
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500; font-weight: 700;
font-style: normal; font-style: normal;
font-size: 6vw; font-size: clamp(18px, 3.6vw, 24px);
color: black; color: var(--brand-sage, #6B8F71);
text-align: left; text-align: left;
margin-left: 20px; margin: 16px 16px 8px 16px;
margin-top: 57px;
} }
.Transactions-detail { .Transactions-detail {
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500; font-weight: 500;
font-style: normal; font-style: normal;
font-size: 15px; font-size: 13px;
color: rgba(88, 55, 50, 1); color: #555;
text-align: left; text-align: left;
margin-left: 20px; margin-left: 16px;
margin-top: 17px; margin-top: 12px;
} }
.TransactionListContainer { .TransactionListContainer {
overflow-y: auto; /* Enables vertical scrolling */ overflow-y: auto; /* Enables vertical scrolling */
background-color: #dbdbdb; background-color: transparent;
overflow: visible; overflow: visible;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 12px;
padding: 8px 12px 16px;
} }
.TotalContainer { .TotalContainer {
@@ -57,14 +59,14 @@
/* width: 100%; */ /* width: 100%; */
margin: 0 auto; margin: 0 auto;
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600; font-weight: 700;
font-style: normal; font-style: normal;
font-size: 15px; font-size: 13px;
/* padding: 10px; */ /* padding: 10px; */
box-sizing: border-box; box-sizing: border-box;
margin-bottom: 17px; margin-bottom: 12px;
margin-left: 20px; margin-left: 16px;
margin-right: 20px; margin-right: 16px;
} }
.PaymentContainer { .PaymentContainer {
@@ -83,61 +85,66 @@
.PayButton { .PayButton {
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500; font-weight: 600;
font-style: normal; font-style: normal;
font-size: 12px; /* Adjusted for better readability */ font-size: 12px;
padding: 12px 16px; /* Added padding for a better look */ padding: 10px 14px;
border-radius: 50px; border-radius: 10px; /* square rounded */
background-color: rgba(88, 55, 50, 1); background-color: var(--brand-primary, #73a585);
color: white; color: white;
border: none; border: none;
margin: 0 auto;
cursor: pointer; cursor: pointer;
display: block; /* Centering the button */ display: inline-flex;
align-items: center;
justify-content: center;
text-align: center; text-align: center;
} }
.DeclineButton { .DeclineButton {
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
z-index: 201; font-weight: 600;
position: relative;
font-weight: 500;
font-style: normal; font-style: normal;
font-size: 15px; font-size: 12px;
padding: 12px 24px; /* Add some padding for spacing */ padding: 10px 14px;
color: rgba(88, 55, 50, 1); color: #444;
background-color: transparent; /* No background */ background-color: #f0f0f0;
border: none; /* No border */ border: 1px solid #e0e0e0;
margin: 0 auto; /* Center horizontally */ border-radius: 10px; /* square rounded */
cursor: pointer; cursor: pointer;
display: block; /* Center the text horizontally */ display: inline-flex;
text-align: center; /* Center the text within the button */ align-items: center;
justify-content: center;
} }
.DeclineButton.active { .DeclineButton.active { opacity: 0.9; }
position: relative;
z-index: 201; /* Row for primary/secondary action buttons */
font-family: "Plus Jakarta Sans", sans-serif; .ActionRow {
font-weight: 500; display: flex;
font-style: normal; gap: 8px;
font-size: 20px; width: 100%;
padding: 12px 24px; /* Add some padding for spacing */ justify-content: center;
color: rgba(88, 55, 50, 1);
background-color: transparent; /* No background */
border: none; /* No border */
margin: 0 auto; /* Center horizontally */
cursor: pointer;
display: block; /* Center the text horizontally */
text-align: center; /* Center the text within the button */
margin-bottom: 23px; /* Space at the bottom to match the PayButton */
} }
.RoundedRectangle { .RoundedRectangle {
position: relative; position: relative;
border-radius: 20px; border-radius: 16px;
padding: 15px; /* Adjusted for better spacing */ padding: 12px 12px 10px;
margin: 12px; margin: 0;
background-color: #f9f9f9; background-color: #ffffff;
border: 1px solid var(--brand-sage-100, #E9F3ED);
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.06);
display: flex;
flex-direction: column;
min-height: 280px; /* base height, grows with content */
} }
/* Unpaid tickets: taller to keep content inside ticket design */
.unpaid {
min-height: 380px;
}
/* Self pickup accent: subtle brand accent */
/* removed pickup accent and badge per request */
.expression { .expression {
width: 100%; width: 100%;
} }
@@ -201,20 +208,28 @@
} }
.receipt-logo { .receipt-logo {
width: 80px; width: 60px;
height: 80px; height: 60px;
border-radius: 50%; /* Circular logo */ border-radius: 50%; /* Circular logo */
object-fit: cover; object-fit: cover;
margin-bottom: 10px; margin-bottom: 8px;
} }
.receipt-info h3 { .receipt-info h3 {
font-size: 16px; font-size: 13px;
margin: 5px 0; font-weight: 700;
color: var(--brand-sage, #6B8F71);
margin: 4px 0 2px;
} }
/* Status-colors override */
.receipt-info h3.statusNeutral { color: var(--brand-sage, #6B8F71); }
.receipt-info h3.statusSuccess { color: #54B265; }
.receipt-info h3.statusCancelled { color: #E45454; }
.receipt-info p { .receipt-info p {
font-size: 14px; font-size: 12px;
color: #666;
margin: 2px 0; margin: 2px 0;
} }
/* Dotted line with circular cutouts */ /* Dotted line with circular cutouts */
@@ -222,39 +237,61 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin: 15px 0; margin: 12px 0 10px;
} }
.dotted-line .line { .dotted-line .line {
border-top: 13px dotted #dbdbdb; border-top: 10px dotted #e9e9e9;
width: 100%; width: 100%;
margin: 0 18px; margin: 0 18px;
} }
.dotted-line .circle-left { .dotted-line .circle-left {
left: -25px; left: -18px;
position: absolute; position: absolute;
width: 50px; width: 36px;
height: 50px; height: 36px;
border-radius: 50%; border-radius: 50%;
background-color: #dbdbdb; background-color: #e9e9e9;
display: flex; /* Use flexbox to center the inner circle */ display: flex; /* Use flexbox to center the inner circle */
justify-content: center; /* Center horizontally */ justify-content: center; /* Center horizontally */
align-items: center; /* Center vertically */ align-items: center; /* Center vertically */
} }
.dotted-line .circle-right { .dotted-line .circle-right {
right: -25px; right: -18px;
position: absolute; position: absolute;
width: 50px; width: 36px;
height: 50px; height: 36px;
border-radius: 50%; border-radius: 50%;
background-color: #dbdbdb; background-color: #e9e9e9;
display: flex; /* Use flexbox to center the inner circle */ display: flex; /* Use flexbox to center the inner circle */
justify-content: center; /* Center horizontally */ justify-content: center; /* Center horizontally */
align-items: center; /* Center vertically */ align-items: center; /* Center vertically */
} }
/* No scroll body for items per request */
/* Compact list text */
.RoundedRectangle ul {
list-style: none;
padding: 0 12px 6px;
margin: 0;
}
.RoundedRectangle ul li {
font-size: 12px;
color: #333;
padding: 3px 0;
}
/* Grid density tweaks */
@media (min-width: 640px) {
.TransactionListContainer { gap: 14px; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
}
@media (min-width: 1024px) {
.TransactionListContainer { gap: 16px; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
}
.inner-circle { .inner-circle {
width: 80%; width: 80%;
height: 80%; height: 80%;
@@ -389,10 +426,11 @@
.addNewItem{ .addNewItem{
width: 100%; width: 100%;
height: 27px; height: 28px;
background-color: rgb(115, 165, 133); background-color: var(--brand-primary, rgb(115, 165, 133));
border-radius: 11px; border-radius: 10px; /* square rounded */
text-align: center; text-align: center;
color: white; color: white;
line-height: 27px; line-height: 28px;
font-size: 12px; /* compact */
} }

View File

@@ -7,6 +7,8 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
text-align: center; text-align: center;
padding: 20px;
box-sizing: border-box;
} }
.image-container { .image-container {
@@ -15,6 +17,11 @@
overflow: hidden; overflow: hidden;
border-radius: 50%; border-radius: 50%;
margin-bottom: 20px; margin-bottom: 20px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background-color: #f0f0f0;
} }
.fileInput { .fileInput {
@@ -24,38 +31,134 @@
.circular-image { .circular-image {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain; object-fit: cover;
border-radius: 50%;
} }
.welcoming-text { .welcoming-text {
font-size: 24px; font-size: 24px;
margin-bottom: 20px; margin-bottom: 20px;
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600;
color: #333;
text-align: center;
max-width: 80%;
} }
.get-started-button { .get-started-button {
padding: 10px 20px; padding: 12px 24px;
border: none; border: none;
border-radius: 25px; border-radius: 25px;
background-color: #007bff; /* Bootstrap primary color */ background-color: #28a745; /* Bootstrap primary color */
color: white; color: white;
cursor: pointer; cursor: pointer;
font-size: 16px; font-size: 16px;
transition: background-color 0.3s; transition: background-color 0.3s;
font-family: "Plus Jakarta Sans", sans-serif; font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600;
box-shadow: 0 2px 6px rgba(40, 167, 69, 0.2);
} }
.get-started-button:hover { .get-started-button:hover {
background-color: #0056b3; /* Darker shade on hover */ background-color: #218838; /* Darker shade on hover */
box-shadow: 0 4px 10px rgba(40, 167, 69, 0.3);
}
.get-started-button:active {
transform: translateY(1px);
} }
/* Fullscreen styles */ /* Fullscreen styles */
.fullscreen { .fullscreen {
position: fixed; position: fixed;
top: 50%; top: 0;
left: 50%; left: 0;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
transform: translate(-50%, -50%); z-index: 1000;
z-index: 300; background-color: inherit;
}
.fullscreen .image-container {
width: 200px;
height: 200px;
}
.fullscreen .welcoming-text {
font-size: 32px;
margin-bottom: 30px;
}
.fullscreen .get-started-button {
padding: 16px 32px;
font-size: 20px;
}
/* Responsive design */
@media (max-width: 768px) {
.welcome-page {
padding: 15px;
}
.image-container {
width: 120px;
height: 120px;
}
.welcoming-text {
font-size: 20px;
margin-bottom: 16px;
}
.get-started-button {
padding: 10px 20px;
font-size: 14px;
}
.fullscreen .image-container {
width: 150px;
height: 150px;
}
.fullscreen .welcoming-text {
font-size: 24px;
margin-bottom: 20px;
}
.fullscreen .get-started-button {
padding: 12px 24px;
font-size: 16px;
}
}
@media (max-width: 480px) {
.image-container {
width: 100px;
height: 100px;
}
.welcoming-text {
font-size: 18px;
margin-bottom: 12px;
}
.get-started-button {
padding: 8px 16px;
font-size: 12px;
}
.fullscreen .image-container {
width: 120px;
height: 120px;
}
.fullscreen .welcoming-text {
font-size: 20px;
margin-bottom: 16px;
}
.fullscreen .get-started-button {
padding: 10px 20px;
font-size: 14px;
}
} }

View File

@@ -1,5 +1,5 @@
// WelcomePage.js // WelcomePage.js
import React,{useRef} from "react"; import React, { useRef } from "react";
import "./WelcomePage.css"; import "./WelcomePage.css";
const WelcomePage = ({ const WelcomePage = ({
@@ -15,6 +15,15 @@ const WelcomePage = ({
const handleImageClick = () => { const handleImageClick = () => {
fileInputRef.current.click(); fileInputRef.current.click();
}; };
// SVG Icon for camera
const CameraIcon = () => (
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 19H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2z" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<circle cx="12" cy="13" r="4" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
return ( return (
<div <div
className={`welcome-page ${isFullscreen ? "fullscreen" : ""}`} // Corrected the className syntax className={`welcome-page ${isFullscreen ? "fullscreen" : ""}`} // Corrected the className syntax
@@ -29,10 +38,13 @@ const WelcomePage = ({
className="image-container" className="image-container"
> >
{!isFullscreen && {!isFullscreen &&
<div onClick={handleImageClick} style={{width: '100%', height: '100%', position:'absolute'}}> <div onClick={handleImageClick} style={{width: '100%', height: '100%', position:'absolute', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer'}}>
<h1 style={{textAlign:'left'}}> <div style={{textAlign: 'center', color: 'white'}}>
{image ? "Click To Change Image" : "Click To Add Image"} <CameraIcon />
<h1 style={{textAlign:'center', fontSize: '16px', margin: '10px 0 0 0'}}>
{image ? "Klik untuk mengganti gambar" : "Klik untuk menambahkan gambar"}
</h1> </h1>
</div>
<input <input
ref={fileInputRef} style={{display: 'none', width:'100%', height: '100%'}}type="file" accept="image/*" onChange={onImageChange} /> ref={fileInputRef} style={{display: 'none', width:'100%', height: '100%'}}type="file" accept="image/*" onChange={onImageChange} />
</div> </div>
@@ -43,7 +55,7 @@ const WelcomePage = ({
{welcomingText} {welcomingText}
</h1> </h1>
<button className="get-started-button" onClick={onGetStarted}> <button className="get-started-button" onClick={onGetStarted}>
Get Started Mulai
</button> </button>
</div> </div>
); );

View File

@@ -1,56 +1,395 @@
/* WelcomePageEditor.css */ /* WelcomePageEditor.css */
.welcome-page-editor { .welcome-page-editor {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; width: 100%;
justify-content: center; height: 100vh;
padding: 20px; background-color: #ffffff;
border: 1px solid #ddd; border-radius: 16px;
border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
background-color: rgb(207, 207, 207); overflow: hidden; /* contain scroll inside editor-content */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); font-family: "Plus Jakarta Sans", sans-serif;
overflow: hidden;
} }
h2 { .editor-header {
margin-bottom: 20px; padding: 24px;
border-bottom: 1px solid #e6e6e6;
background-color: #f8f9fa;
}
.editor-title {
margin: 0;
font-weight: 700;
font-size: 24px; font-size: 24px;
color: #333; color: #333;
} }
input[type="file"] { .editor-content {
margin-bottom: 20px; display: flex;
} flex: 1; /* take remaining height */
textarea {
width: 100%; width: 100%;
height: 100px; /* Adjust as needed */ overflow-y: auto; /* enable vertical scroll in editor-content */
padding: 10px; overflow-x: hidden;
border: 1px solid #ccc; justify-content: center;
border-radius: 4px; align-items: flex-start;
resize: none; padding: 16px;
margin-bottom: 20px; box-sizing: border-box;
font-size: 16px;
} }
textarea:focus { .config-panel {
border-color: #007bff; /* Highlight border color on focus */ width: 100%;
max-width: 960px;
padding: 24px;
border-right: none;
background-color: #fafafa;
overflow-y: visible;
display: flex;
flex-direction: column;
gap: 24px;
box-sizing: border-box;
}
.config-section {
background-color: #ffffff;
border-radius: 12px;
padding: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.section-title {
margin: 0 0 16px 0;
font-weight: 600;
font-size: 18px;
color: #333;
display: flex;
align-items: center;
gap: 10px;
}
.config-group {
margin-bottom: 20px;
}
.config-label {
display: block;
margin-bottom: 8px;
font-weight: 500;
font-size: 14px;
color: #555;
}
.config-input {
width: 100%;
padding: 12px 16px;
border-radius: 10px;
border: 1px solid #ddd;
font-size: 15px;
color: #333;
transition: all 0.2s ease;
box-sizing: border-box;
}
.config-input:focus {
outline: none; outline: none;
border-color: #28a745;
box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.1);
} }
label { .color-input {
margin-bottom: 20px; height: 45px;
padding: 8px;
cursor: pointer;
}
.textarea-input {
min-height: 80px;
resize: vertical;
font-family: "Plus Jakarta Sans", sans-serif;
}
.switch-container {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid #eee;
}
.switch-label {
font-weight: 500;
font-size: 15px;
color: #333;
}
.save-button {
width: 100%;
padding: 14px 16px;
border-radius: 10px;
border: none;
background-color: #28a745;
color: white;
font-weight: 600;
font-size: 16px; font-size: 16px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 6px rgba(40, 167, 69, 0.2);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.save-button:hover {
background-color: #218838;
box-shadow: 0 4px 10px rgba(40, 167, 69, 0.3);
}
.save-button:disabled {
background-color: #a1a1a1;
cursor: not-allowed;
box-shadow: none;
}
.preview-button {
width: 100%;
padding: 12px 16px;
border-radius: 10px;
border: 1px solid #ddd;
background-color: #ffffff;
color: #333;
font-weight: 600;
font-size: 15px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.preview-button:hover {
background-color: #f2f2f2;
}
.loading-spinner {
width: 20px;
height: 20px;
border: 2px solid #ffffff;
border-top: 2px solid transparent;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.preview-panel {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
.preview-header {
padding: 16px 24px;
border-bottom: 1px solid #e6e6e6;
background-color: #f8f9fa;
display: flex;
justify-content: space-between;
align-items: center;
}
.preview-title {
margin: 0;
font-weight: 600;
font-size: 18px;
color: #333;
}
.preview-container {
flex: 1;
padding: 24px;
overflow: auto;
display: flex;
justify-content: center;
align-items: center;
} }
.welcome-preview { .welcome-preview {
width: 100%; width: 100%;
height: 100%; max-width: 400px;
border: 1px dashed #ccc; /* Preview border style */ border-radius: 16px;
border-radius: 8px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
padding: 20px; overflow: hidden;
}
.fullscreen-toggle {
position: absolute;
bottom: 24px;
right: 24px;
width: 60px;
height: 60px;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.7);
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
z-index: 10;
}
.fullscreen-toggle:hover {
background-color: rgba(0, 0, 0, 0.9);
transform: scale(1.05);
}
.toggle-icon {
font-size: 24px;
transform: rotate(45deg);
}
/* Top actions row */
.top-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-bottom: 8px;
}
.top-actions .preview-button {
width: auto;
padding: 10px 14px;
}
/* Theme & color redesign */
.hex-input {
max-width: 140px;
}
.theme-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.color-row {
display: flex;
gap: 12px;
align-items: center;
}
.swatches {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.swatch {
width: 28px;
height: 28px;
border-radius: 6px;
border: 1px solid transparent;
cursor: pointer;
}
.contrast-card {
margin-top: 16px;
border-radius: 12px;
border: 1px solid #e6e6e6;
}
.contrast-inner {
padding: 16px;
}
.contrast-title {
font-size: 14px;
color: #666;
margin-bottom: 8px;
font-weight: 600;
}
.contrast-preview {
font-size: 18px;
font-weight: 600;
}
@media (max-width: 992px) {
.theme-grid {
grid-template-columns: 1fr;
}
}
.inline-preview {
margin-top: 12px;
background: #fff;
border: 1px solid #e6e6e6;
border-radius: 12px;
overflow: hidden;
}
.inline-preview-content {
padding: 16px;
min-height: 320px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background-color: #ffffff; /* Background for preview area */ box-sizing: border-box;
}
/* Responsive design */
@media (max-width: 992px) {
.editor-content {
flex-direction: column;
padding: 12px;
}
.config-panel {
width: 100%;
border-right: none;
border-bottom: none;
max-height: none;
}
.preview-panel {
height: 50vh;
}
}
@media (max-width: 768px) {
.editor-header {
padding: 16px;
}
.editor-title {
font-size: 20px;
}
.config-panel {
padding: 16px;
}
.config-section {
padding: 16px;
}
.section-title {
font-size: 16px;
}
.preview-container {
padding: 16px;
}
.inline-preview-content {
min-height: 260px;
padding: 12px;
}
} }

View File

@@ -14,6 +14,7 @@ const WelcomePageEditor = ({ cafeId, welcomePageConfig }) => {
const [isWelcomePageActive, setIsWelcomePageActive] = useState(false); // State for the switch const [isWelcomePageActive, setIsWelcomePageActive] = useState(false); // State for the switch
const [loading, setLoading] = useState(false); // Loading state const [loading, setLoading] = useState(false); // Loading state
const [isFullscreen, setIsFullscreen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false);
const [showPreview, setShowPreview] = useState(false);
// Load existing welcome page configuration when component mounts // Load existing welcome page configuration when component mounts
useEffect(() => { useEffect(() => {
@@ -51,6 +52,21 @@ const WelcomePageEditor = ({ cafeId, welcomePageConfig }) => {
setTextColor(e.target.value); setTextColor(e.target.value);
}; };
// Extra handlers for HEX input fields
const handleBackgroundHexInput = (e) => {
let v = e.target.value;
if (!v.startsWith('#')) v = '#' + v.replace(/#/g, '');
if (v.length > 7) v = v.slice(0, 7);
setBackgroundColor(v);
};
const handleTextHexInput = (e) => {
let v = e.target.value;
if (!v.startsWith('#')) v = '#' + v.replace(/#/g, '');
if (v.length > 7) v = v.slice(0, 7);
setTextColor(v);
};
const handleSave = async () => { const handleSave = async () => {
setLoading(true); setLoading(true);
@@ -72,85 +88,236 @@ const WelcomePageEditor = ({ cafeId, welcomePageConfig }) => {
} }
}; };
// SVG Icons
const CoffeeIcon = () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 8h1.5a2.5 2.5 0 0 1 0 5H15m0-5H13m-1 0H6a3 3 0 0 0-3 3v1.5a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V11a3 3 0 0 0-3-3h-5.5m-1 0V5a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1V2m-5 0v1a1 1 0 0 0-1 1v1m0 0v1a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
const PaletteIcon = () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 3v4a1 1 0 0 0 1 1h4m-9 4a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm6 10v-2a3 3 0 0 0-3-3h-4a3 3 0 0 0-3 3v2h10ZM16 21v-2a3 3 0 0 0-3-3h-2a3 3 0 0 0-3 3v2h8Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
const TypeIcon = () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 7V4h16v3M9 20h6M12 4v16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
const SaveIcon = () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M17 21v-8H7v8M7 3v5h8M12 11h6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
const EyeIcon = () => (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8Z" stroke="currentColor" strokeWidth="2"/>
<path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" stroke="currentColor" strokeWidth="2"/>
<path d="M22 12c-1.5-4-5-6-10-6S4 8 2 12" stroke="currentColor" strokeWidth="2"/>
</svg>
);
return ( return (
<div <div className="welcome-page-editor">
className="welcome-page-editor" <div className="editor-header">
style={{ width: "80vw", height: "80vh" }} <h2 className="editor-title">Konfigurasi Halaman Selamat Datang</h2>
</div>
<div className="editor-content">
{/* Configuration Panel */}
<div className="config-panel">
{/* Top Preview Toggle */}
<div className="top-actions">
<button
type="button"
className="preview-button"
onClick={() => setShowPreview(!showPreview)}
> >
<h2>Edit Welcome Page</h2> <EyeIcon /> {showPreview ? 'Tutup Pratinjau' : 'Preview'}
<div style={{ display: "flex", flexDirection: "column" }}>
<textarea
value={welcomingText}
onChange={handleTextChange}
placeholder="Enter welcoming text..."
style={{ height: "20px", resize: "none" }} // Reduced height
/>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<label>
Background Color:
<input
type="color"
value={backgroundColor}
onChange={handleColorChange}
/>
</label>
<label>
Text Color:
<input
type="color"
value={textColor}
onChange={handleTextColorChange}
/>
</label>
</div>
<div style={{ display: "flex", alignItems: "center" }}>
<label style={{ marginRight: "10px" }}>Is Welcome Page Active:</label>
<Switch
onChange={() => setIsWelcomePageActive(!isWelcomePageActive)}
checked={isWelcomePageActive}
offColor="#888"
onColor="#0a0"
uncheckedIcon={false}
checkedIcon={false}
/>
</div>
<button onClick={handleSave} disabled={loading}>
{loading ? "Saving..." : "Save Configuration"}
</button> </button>
</div> </div>
<div
style={{ width: "100%", height: "100%", position: "relative", flex: 1, borderRadius: '15px' }} {showPreview && (
> <div className="inline-preview">
<div className="inline-preview-content">
<WelcomePage <WelcomePage
image={image} image={image}
welcomingText={welcomingText} welcomingText={welcomingText}
backgroundColor={backgroundColor} backgroundColor={backgroundColor}
textColor={textColor} textColor={textColor}
onGetStarted={() => setIsFullscreen(false)} onGetStarted={() => setShowPreview(false)}
isFullscreen={isFullscreen} isFullscreen={false}
onImageChange={handleImageChange} onImageChange={handleImageChange}
/> />
<div style={{ position: "absolute", bottom: 0, right: 0 }}> </div>
<svg </div>
width="100" // Adjust size as needed )}
height="100" // Adjust size as needed <div className="config-section">
style={{ position: "absolute", bottom: 0, right: 0 }} <h3 className="section-title">
onClick={() => setIsFullscreen(true)} <CoffeeIcon />
Konten Utama
</h3>
<div className="config-group">
<label className="config-label">Teks Selamat Datang</label>
<textarea
className="config-input textarea-input"
value={welcomingText}
onChange={handleTextChange}
placeholder="Masukkan teks selamat datang..."
/>
</div>
<div className="config-group">
<label className="config-label">Gambar Latar Belakang</label>
<input
type="file"
accept="image/*"
onChange={handleImageChange}
className="config-input"
/>
{image && (
<div style={{ marginTop: '12px', textAlign: 'center' }}>
<img
src={image}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '150px',
borderRadius: '8px',
border: '1px solid #ddd'
}}
/>
</div>
)}
</div>
</div>
<div className="config-section">
<h3 className="section-title">
<PaletteIcon />
Tema & Warna
</h3>
<div className="theme-grid">
<div className="color-field">
<label className="config-label">Warna Latar Belakang</label>
<div className="color-row">
<input
type="color"
className="config-input color-input"
value={backgroundColor}
onChange={handleColorChange}
/>
<input
type="text"
className="config-input hex-input"
value={backgroundColor}
onChange={handleBackgroundHexInput}
maxLength={7}
placeholder="#FFFFFF"
/>
</div>
<div className="swatches" aria-label="Background presets">
{['#ffffff','#f8f9fa','#fff3cd','#e8f5e9','#e3f2fd','#212529'].map((c) => (
<button
key={c}
className="swatch"
style={{ backgroundColor: c, borderColor: c.toLowerCase()==='#ffffff'?'#ddd':'transparent' }}
onClick={() => setBackgroundColor(c)}
title={c}
type="button"
/>
))}
</div>
</div>
<div className="color-field">
<label className="config-label">Warna Teks</label>
<div className="color-row">
<input
type="color"
className="config-input color-input"
value={textColor}
onChange={handleTextColorChange}
/>
<input
type="text"
className="config-input hex-input"
value={textColor}
onChange={handleTextHexInput}
maxLength={7}
placeholder="#000000"
/>
</div>
<div className="swatches" aria-label="Text presets">
{['#000000','#212529','#343a40','#6c757d','#ffffff','#28a745'].map((c) => (
<button
key={c}
className="swatch"
style={{ backgroundColor: c, borderColor: c.toLowerCase()==='#ffffff'?'#ddd':'transparent' }}
onClick={() => setTextColor(c)}
title={c}
type="button"
/>
))}
</div>
</div>
</div>
<div className="contrast-card" style={{ backgroundColor }}>
<div className="contrast-inner">
<div className="contrast-title">Pratinjau Kontrast</div>
<div className="contrast-preview" style={{ color: textColor }}>{welcomingText || 'Contoh Teks'}</div>
</div>
</div>
</div>
<div className="config-section">
<h3 className="section-title">
<TypeIcon />
Pengaturan
</h3>
<div className="switch-container">
<span className="switch-label">Aktifkan Halaman Selamat Datang</span>
<Switch
onChange={() => setIsWelcomePageActive(!isWelcomePageActive)}
checked={isWelcomePageActive}
offColor="#cccccc"
onColor="#28a745"
uncheckedIcon={false}
checkedIcon={false}
height={24}
width={48}
handleDiameter={20}
/>
</div>
</div>
<button
className="save-button"
onClick={handleSave}
disabled={loading}
> >
<g transform="rotate(45 50 50)"> {loading ? (
<circle cx="50" cy="50" r="40" fill="rgba(0, 0, 0, 0.5)" /> <>
<text <div className="loading-spinner"></div>
x="50" Menyimpan...
y="50" </>
textAnchor="middle" ) : (
dominantBaseline="middle" <>
fontSize="24" <SaveIcon />
fill="white" // Adjust text color as needed Simpan Konfigurasi
> </>
&lt;&gt; )}
</text> </button>
</g> {/* Pratinjau dipicu oleh tombol atas; section khusus dihapus */}
</svg>
</div> </div>
</div> </div>
</div> </div>