Compare commits
18 Commits
e51987443c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2add1c5090 | ||
|
|
3fbe0bb870 | ||
|
|
dae7fb9221 | ||
|
|
6ed982d6ef | ||
|
|
df203447a9 | ||
|
|
f58b40c70d | ||
|
|
6127415e37 | ||
|
|
b28c6ed0fe | ||
|
|
67cf759b31 | ||
|
|
53e091d3a4 | ||
|
|
3a431b1b14 | ||
|
|
69a07be3cd | ||
|
|
b012517568 | ||
|
|
3e35468f2c | ||
|
|
df7c4f737c | ||
|
|
b726ae6919 | ||
|
|
da317f83c9 | ||
|
|
f6482d24d2 |
58
note/detailUI.md
Normal file
58
note/detailUI.md
Normal 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
|
||||
11075
package-lock.json
generated
11075
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,12 +10,15 @@
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"apexcharts": "^5.3.4",
|
||||
"caniuse-lite": "^1.0.30001690",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"html-to-image": "^1.11.11",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jsqr": "^1.4.0",
|
||||
"lucide-react": "^0.541.0",
|
||||
"qr-scanner": "^1.4.2",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"react": "^18.3.1",
|
||||
"react-apexcharts": "^1.7.0",
|
||||
|
||||
@@ -28,6 +28,15 @@
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>KedaiMaster</title>
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-2SKSCVFB2N"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){ dataLayer.push(arguments); }
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-2SKSCVFB2N');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
151
src/App.css
151
src/App.css
@@ -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=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,
|
||||
body {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
.App {
|
||||
/* overflow-x: hidden; */
|
||||
}
|
||||
|
||||
.Cafe {
|
||||
@@ -29,7 +36,7 @@ body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
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);
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -56,6 +63,142 @@ body {
|
||||
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 {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
124
src/App.js
124
src/App.js
@@ -71,6 +71,12 @@ function App() {
|
||||
const [depth, setDepth] = useState(-1);
|
||||
const [queue, setQueue] = useState([]);
|
||||
|
||||
const [newTransaction, setNewTransaction] = useState({});
|
||||
|
||||
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const tokenParams = queryParams.get("token");
|
||||
if(tokenParams) localStorage.setItem('auth', tokenParams)
|
||||
|
||||
const validTransactionStates = [
|
||||
'new_transaction',
|
||||
@@ -104,37 +110,42 @@ function App() {
|
||||
const init = async () => {
|
||||
await checkLastTransaction();
|
||||
};
|
||||
|
||||
|
||||
init(); // call the async function
|
||||
|
||||
|
||||
const handleStorageChange = () => {
|
||||
calculateTotalsFromLocalStorage();
|
||||
|
||||
if (!localStorage.getItem("lastTransaction")) {
|
||||
setLastTransaction(null);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
window.addEventListener("localStorageUpdated", handleStorageChange);
|
||||
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("localStorageUpdated", handleStorageChange);
|
||||
};
|
||||
}, [shopId]);
|
||||
|
||||
|
||||
const checkLastTransaction = async () => {
|
||||
const { totalCount, totalPrice } = calculateTotals(shopId);
|
||||
setTotalItemsCount(totalCount);
|
||||
setTotalPrice(totalPrice);
|
||||
|
||||
|
||||
const lastTransactionStr = localStorage.getItem("lastTransaction");
|
||||
|
||||
|
||||
if (!lastTransactionStr) return;
|
||||
|
||||
|
||||
const lastTransaction = JSON.parse(lastTransactionStr);
|
||||
console.log(lastTransaction);
|
||||
|
||||
|
||||
if (!lastTransaction || !lastTransaction.transactionId) {
|
||||
localStorage.removeItem("lastTransaction");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
setModal('transaction_confirmed', { transactionId: lastTransaction.transactionId })
|
||||
const myLastTransaction = await checkIsMyTransaction(lastTransaction.transactionId);
|
||||
console.log(myLastTransaction)
|
||||
if (myLastTransaction.isMyTransaction) {
|
||||
@@ -143,7 +154,7 @@ function App() {
|
||||
localStorage.removeItem("lastTransaction");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleSetParam = async ({ shopIdentifier, tableCode }) => {
|
||||
setShopIdentifier(shopIdentifier);
|
||||
@@ -219,7 +230,7 @@ function App() {
|
||||
});
|
||||
} else {
|
||||
socket.emit("checkUserToken", {
|
||||
token: getLocalStorage("auth"),
|
||||
token: getLocalStorage("auth") || tokenParams,
|
||||
shopId,
|
||||
});
|
||||
}
|
||||
@@ -236,24 +247,23 @@ function App() {
|
||||
});
|
||||
|
||||
socket.on("transaction_confirmed", async (data) => {
|
||||
console.log("transaction notification: " + data);
|
||||
console.log(JSON.stringify(data));
|
||||
setModal("transaction_confirmed", data);
|
||||
|
||||
localStorage.setItem('cart', []);
|
||||
|
||||
const startTime = Date.now(); // Capture the start time
|
||||
const timeout = 10000; // 10 seconds timeout in milliseconds
|
||||
// const startTime = Date.now(); // Capture the start time
|
||||
// const timeout = 10000; // 10 seconds timeout in milliseconds
|
||||
|
||||
calculateTotalsFromLocalStorage();
|
||||
|
||||
while (localStorage.getItem("lastTransaction") === null) {
|
||||
if (Date.now() - startTime > timeout) {
|
||||
return; // Exit the function and don't proceed further
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
|
||||
}
|
||||
// while (localStorage.getItem("lastTransaction") === null) {
|
||||
// if (Date.now() - startTime > timeout) {
|
||||
// return; // Exit the function and don't proceed further
|
||||
// }
|
||||
|
||||
// await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
|
||||
// }
|
||||
|
||||
// If 'lastTransaction' exists, proceed
|
||||
const lastTransaction = JSON.parse(localStorage.getItem("lastTransaction"));
|
||||
@@ -270,17 +280,23 @@ function App() {
|
||||
setModal("transaction_success", data);
|
||||
|
||||
// If 'lastTransaction' exists, proceed
|
||||
localStorage.removeItem("lastTransaction");
|
||||
|
||||
if (lastTransaction != null) {
|
||||
if (localStorage.getItem("lastTransaction")) {
|
||||
setLastTransaction(null);
|
||||
console.log('remove last transaction')
|
||||
localStorage.removeItem("lastTransaction");
|
||||
window.dispatchEvent(new Event("localStorageUpdated"));
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("transaction_end", async (data) => {
|
||||
console.log("transaction notification");
|
||||
setModal("transaction_end", data);
|
||||
|
||||
// If 'lastTransaction' exists, proceed
|
||||
if (localStorage.getItem("lastTransaction")) {
|
||||
setLastTransaction(null);
|
||||
localStorage.removeItem("lastTransaction");
|
||||
window.dispatchEvent(new Event("localStorageUpdated"));
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("payment_claimed", async (data) => {
|
||||
@@ -289,8 +305,15 @@ function App() {
|
||||
});
|
||||
|
||||
socket.on("transaction_failed", async (data) => {
|
||||
console.log("transaction notification");
|
||||
console.log(JSON.stringify(data));
|
||||
setModal("transaction_failed", data);
|
||||
|
||||
// If 'lastTransaction' exists, proceed
|
||||
if (localStorage.getItem("lastTransaction")) {
|
||||
setLastTransaction(null);
|
||||
localStorage.removeItem("lastTransaction");
|
||||
window.dispatchEvent(new Event("localStorageUpdated"));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -324,7 +347,7 @@ function App() {
|
||||
setDeviceType("guestDevice");
|
||||
} else {
|
||||
console.log(data)
|
||||
if(data.data.user.cafeId != null) navigate(`/${data.data.user.cafeIdentityName}`, { replace: true });
|
||||
if (data.data.user.cafeId != null) navigate(`/${data.data.user.cafeIdentityName}`, { replace: true });
|
||||
setUser(data.data.user);
|
||||
if (data.data.latestOpenBillTransaction != null) localStorage.setItem('lastTransaction', JSON.stringify(data.data.latestOpenBillTransaction))
|
||||
if (
|
||||
@@ -378,30 +401,32 @@ function App() {
|
||||
};
|
||||
}, [socket, shopId]);
|
||||
|
||||
async function checkIfStillViewingOtherTransaction() {
|
||||
async function checkIfStillViewingOtherTransaction(data) {
|
||||
|
||||
console.log("transaction notification");
|
||||
console.log(modalContent);
|
||||
|
||||
let response;
|
||||
response = await getTransactionsFromCafe(shopId, 0, true);
|
||||
transactionList.current = response;
|
||||
console.log(response);
|
||||
|
||||
// Get current URL's search parameters inside the socket event handler
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
let transaction_info = searchParams.get("transactionId") || ''; // Get transactionId or set it to empty string
|
||||
console.log(transaction_info); // Log the updated transaction_info
|
||||
|
||||
|
||||
const depthh = transactionList.current.findIndex(
|
||||
if(response[0].transactionId != transaction_info) transactionList.current = response;
|
||||
|
||||
let depthh = transactionList.current.findIndex(
|
||||
item => item.transactionId.toString() === transaction_info.toString()
|
||||
);
|
||||
|
||||
setDepth(depthh);
|
||||
if (transaction_info != response[0].transactionId)
|
||||
setDepth(depthh);
|
||||
else setModal("new_transaction", data);
|
||||
|
||||
console.log(transaction_info == response[0].transactionId)
|
||||
// If transaction_info is an empty string, set the modal
|
||||
if (transaction_info == '' || transaction_info == response[0].transactionId) return false;
|
||||
if (transaction_info.toString() == '') return false;
|
||||
else return true;
|
||||
}
|
||||
|
||||
@@ -409,12 +434,15 @@ function App() {
|
||||
// This will ensure that searchParams and transaction_info get updated on each render
|
||||
socket.on("transaction_created", async (data) => {
|
||||
console.log("transaction notification");
|
||||
const isViewingOtherTransaction = await checkIfStillViewingOtherTransaction();
|
||||
setNewTransaction(data);
|
||||
|
||||
if(!location.pathname.endsWith('/transactions')){
|
||||
const isViewingOtherTransaction = await checkIfStillViewingOtherTransaction(data);
|
||||
// If transaction_info is an empty string, set the modal
|
||||
if (!isViewingOtherTransaction) {
|
||||
setModal("new_transaction", data);
|
||||
}
|
||||
|
||||
}
|
||||
// Show browser notification
|
||||
let permission = Notification.permission;
|
||||
if (permission !== "granted") return;
|
||||
@@ -429,12 +457,15 @@ function App() {
|
||||
socket.on("transaction_canceled", async (data) => {
|
||||
console.log("transaction notification");
|
||||
|
||||
const isViewingOtherTransaction = await checkIfStillViewingOtherTransaction();
|
||||
setNewTransaction(data);
|
||||
if(location.pathname != '/transactions'){
|
||||
const isViewingOtherTransaction = await checkIfStillViewingOtherTransaction(data);
|
||||
// If transaction_info is an empty string, set the modal
|
||||
if (!isViewingOtherTransaction) {
|
||||
setModal("new_transaction", data);
|
||||
navigate(`?transactionId=${data.transactionId}`, { replace: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up the socket event listener on unmount or when dependencies change
|
||||
@@ -444,24 +475,13 @@ function App() {
|
||||
};
|
||||
}, [socket, shopId, location]); // Ensure location is in the dependencies to respond to changes in the URL
|
||||
|
||||
// useEffect(() => {
|
||||
// if (user.cafeId != null && user.cafeId !== shopId) {
|
||||
// // Preserve existing query parameters
|
||||
// const currentParams = new URLSearchParams(location.search).toString();
|
||||
|
||||
// // Navigate to the new cafeId while keeping existing params
|
||||
// navigate(`/${user.cafeId}?${currentParams}`, { replace: true });
|
||||
// }
|
||||
|
||||
// }, [user, shopId]);
|
||||
|
||||
function handleMoveToTransaction(direction, from) {
|
||||
console.log(direction);
|
||||
console.log(from);
|
||||
|
||||
// Find the current index based on the 'from' transactionId
|
||||
const currentIndex = transactionList.current.findIndex(item => item.transactionId == from);
|
||||
|
||||
|
||||
// Determine the new transactionId based on the direction
|
||||
let newTransactionId;
|
||||
if (direction === 'next') {
|
||||
@@ -472,7 +492,7 @@ function App() {
|
||||
: from; // If already at the end, stay on the current transactionId
|
||||
} else if (direction === 'previous') {
|
||||
|
||||
setDepth(currentIndex -1);
|
||||
setDepth(currentIndex - 1);
|
||||
// If we're not at the first transaction, get the previous transactionId
|
||||
newTransactionId = currentIndex > 0
|
||||
? transactionList.current[currentIndex - 1].transactionId
|
||||
@@ -771,6 +791,8 @@ function App() {
|
||||
sendParam={handleSetParam}
|
||||
deviceType={deviceType}
|
||||
paymentUrl={shop.qrPayment}
|
||||
setModal={setModal}
|
||||
newTransaction={newTransaction}
|
||||
/>
|
||||
{/* <Footer
|
||||
shopId={shopIdentifier}
|
||||
|
||||
@@ -44,10 +44,10 @@ const App = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const shopId = window.location.pathname.split('/')[1]; // Get shopId from the URL
|
||||
const userId = localStorage.getItem('userId');
|
||||
const user_id = localStorage.getItem('user_id');
|
||||
|
||||
// Connect to Socket.IO if userId is present
|
||||
// if (userId) {
|
||||
// Connect to Socket.IO if user_id is present
|
||||
// if (user_id) {
|
||||
// connectSocket(shopId, 1);
|
||||
// }
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { useLocation } from "react-router-dom";
|
||||
import { useNavigationHelpers } from "../helpers/navigationHelpers";
|
||||
import Switch from "react-switch";
|
||||
|
||||
// Restore original gradient background for header container when shopName exists
|
||||
const HeaderBarbackground = styled.div`
|
||||
${({ shopName }) =>
|
||||
shopName &&
|
||||
@@ -19,10 +20,10 @@ const HeaderBar = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 15px;
|
||||
padding: 12px 14px;
|
||||
color: black;
|
||||
background-color: #ffffff;
|
||||
z-index: 200;
|
||||
z-index: ${(props) => (props.zIndexLevel !== undefined ? props.zIndexLevel : 200)};
|
||||
border: 1px solid #00000000;
|
||||
margin: 20px 12px;
|
||||
border-radius: 13px;
|
||||
@@ -33,30 +34,15 @@ const HeaderBar = styled.div`
|
||||
const Title = styled.h2`
|
||||
margin: 0;
|
||||
font-family: "Plus Jakarta Sans", sans-serif;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-size:${(props) => (props.HeaderSize)};
|
||||
color: rgba(88, 55, 50, 1);
|
||||
text-transform: uppercase;
|
||||
color: rgba(45, 45, 45, 1);
|
||||
`;
|
||||
|
||||
const ProfileName = styled.h2`
|
||||
position: absolute;
|
||||
font-family: "Plus Jakarta Sans", sans-serif;
|
||||
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;
|
||||
`;
|
||||
// SubTitle removed per redesign (no subtitle below cafe name)
|
||||
|
||||
// Deprecated the animated ProfileName in favor of a cleaner layout
|
||||
|
||||
const nn = keyframes`
|
||||
0% {
|
||||
@@ -103,22 +89,17 @@ const ss = keyframes`
|
||||
}
|
||||
`;
|
||||
|
||||
const ProfileImage = styled.img`
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
object-fit: contain;
|
||||
cursor: pointer;
|
||||
z-index: 199;
|
||||
animation: ${(props) => {
|
||||
if (props.animate === "grow") return g;
|
||||
if (props.animate === "shrink") return s;
|
||||
return "none";
|
||||
}}
|
||||
0.5s forwards;
|
||||
const CafeAvatar = styled.img`
|
||||
width: clamp(32px, 5vw, 56px);
|
||||
height: clamp(32px, 5vw, 56px);
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
background: #f2f2f2;
|
||||
margin-left: 8px; /* extra left padding so it’s not too tight */
|
||||
`;
|
||||
|
||||
// User initial avatar removed; only cafe image is shown on the left
|
||||
|
||||
const g = keyframes`
|
||||
0% {
|
||||
top: 0px;
|
||||
@@ -149,62 +130,43 @@ const s = keyframes`
|
||||
}
|
||||
`;
|
||||
|
||||
/* Replace bubble-like animation with subtle fade/slide */
|
||||
const grow = keyframes`
|
||||
0% {
|
||||
right: 12px;
|
||||
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;
|
||||
}
|
||||
0% { opacity: 0; transform: translateY(-6px) scale(0.98); }
|
||||
100% { opacity: 1; transform: translateY(0) scale(1); }
|
||||
`;
|
||||
|
||||
const shrink = keyframes`
|
||||
0% {
|
||||
right: 12px;
|
||||
width: 300px;
|
||||
height: auto;
|
||||
border-radius: 20px;
|
||||
}
|
||||
100% {
|
||||
right: 28px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
0% { opacity: 1; transform: translateY(0) scale(1); }
|
||||
100% { opacity: 0; transform: translateY(-6px) scale(0.98); }
|
||||
`;
|
||||
const Rectangle = styled.div`
|
||||
overflow-y: hidden;
|
||||
overflow-y: auto;
|
||||
position: absolute;
|
||||
top: 39px;
|
||||
right: 12px;
|
||||
width: 200px;
|
||||
max-height: 87vh; /* or another appropriate value */
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
width: 240px;
|
||||
max-height: 75vh;
|
||||
background-color: white;
|
||||
z-index: 198;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
animation: ${(props) => (props.animate === "grow" ? grow : shrink)} 0.5s
|
||||
forwards;
|
||||
padding: 10px;
|
||||
z-index: ${(props) => (props.baseZIndex !== undefined ? props.baseZIndex : 198)};
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 12px;
|
||||
animation: ${(props) => (props.animate === "grow" ? grow : shrink)} 0.2s forwards;
|
||||
padding: 10px 12px;
|
||||
box-sizing: border-box;
|
||||
overflow-x: hidden;
|
||||
font-size: 14px;
|
||||
color: #393939;
|
||||
backdrop-filter: blur(2px);
|
||||
`;
|
||||
|
||||
const ChildContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
padding-top: 70px;
|
||||
align-items: stretch;
|
||||
flex-wrap: nowrap;
|
||||
`;
|
||||
|
||||
const ChildWrapper = styled.div`
|
||||
@@ -213,25 +175,88 @@ const ChildWrapper = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
const Child = styled.div`
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
font-family: "Plus Jakarta Sans", sans-serif;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
font-family: "Plus Jakarta Sans", sans-serif;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
padding: ${(props) => (props.hasChildren ? '8px 0 4px' : '8px 4px')};
|
||||
${(props) =>
|
||||
props.hasChildren
|
||||
? `
|
||||
margin-top: 14px;
|
||||
border-top: 0.5px solid #a5a5a5;
|
||||
margin-top: 10px;
|
||||
border-top: 0.5px solid #e9e9e9;
|
||||
height: auto;
|
||||
`
|
||||
: `
|
||||
display: flex;
|
||||
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 = ({
|
||||
HeaderText,
|
||||
@@ -251,7 +276,9 @@ const Header = ({
|
||||
removeConnectedGuestSides,
|
||||
setIsEditMode,
|
||||
isEditMode,
|
||||
HeaderMargin = '25px'
|
||||
HeaderMargin = '25px',
|
||||
zIndexLevel,
|
||||
rectZIndex
|
||||
}) => {
|
||||
const { goToLogin, goToGuestSideLogin, goToAdminCafes } =
|
||||
useNavigationHelpers(shopId, tableCode);
|
||||
@@ -261,10 +288,10 @@ const Header = ({
|
||||
const [guestSideOf, setGuestSideOf] = useState(null);
|
||||
const location = useLocation();
|
||||
|
||||
const handleImageClick = () => {
|
||||
const toggleMenu = () => {
|
||||
if (showRectangle) {
|
||||
setAnimate("shrink");
|
||||
setTimeout(() => setShowRectangle(false), 500);
|
||||
setTimeout(() => setShowRectangle(false), 200);
|
||||
} else {
|
||||
setAnimate("grow");
|
||||
setShowRectangle(true);
|
||||
@@ -274,15 +301,14 @@ const Header = ({
|
||||
const handleClickOutside = (event) => {
|
||||
if (rectangleRef.current && !rectangleRef.current.contains(event.target)) {
|
||||
setAnimate("shrink");
|
||||
setTimeout(() => setShowRectangle(false), 500);
|
||||
rectangleRef.current.style.overflow = "hidden";
|
||||
setTimeout(() => setShowRectangle(false), 200);
|
||||
}
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
if (showRectangle) {
|
||||
setAnimate("shrink");
|
||||
setTimeout(() => setShowRectangle(false), 500);
|
||||
setTimeout(() => setShowRectangle(false), 200);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -321,52 +347,62 @@ const Header = ({
|
||||
// Otherwise, use the possessive function
|
||||
return `${cafeName}'s menu`;
|
||||
};
|
||||
return (
|
||||
|
||||
const formatCafeName = (name) => {
|
||||
if (!name) return name;
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
};
|
||||
return (
|
||||
<HeaderBarbackground shopName={shopName}>
|
||||
<HeaderBar HeaderMargin={HeaderMargin} shopName={shopName}>
|
||||
<Title HeaderSize={HeaderSize}>
|
||||
{shopName == null
|
||||
? HeaderText == null
|
||||
? "kedaimaster"
|
||||
: HeaderText
|
||||
: shopName}
|
||||
</Title>
|
||||
<div style={{ visibility: showProfile ? "visible" : "hidden" }}>
|
||||
<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"}
|
||||
alt="Profile"
|
||||
onClick={user.username !== undefined ? handleImageClick : null}
|
||||
animate={showRectangle && animate}
|
||||
/>
|
||||
<ProfileName animate={showRectangle && animate}>
|
||||
{showProfile && user.username !== undefined ? user.username : "guest"}
|
||||
</ProfileName>
|
||||
{showRectangle && (
|
||||
<Rectangle ref={rectangleRef} animate={animate}>
|
||||
<ChildContainer>
|
||||
<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}>
|
||||
{formatCafeName(
|
||||
shopName == null
|
||||
? HeaderText == null
|
||||
? "kedaimaster"
|
||||
: HeaderText
|
||||
: shopName
|
||||
)}
|
||||
</Title>
|
||||
</CenterGroup>
|
||||
|
||||
<RightGroup style={{ visibility: showProfile ? "visible" : "hidden", position: 'relative' }}>
|
||||
<HamburgerButton onClick={toggleMenu} aria-label="Open menu">
|
||||
<HamburgerIcon />
|
||||
</HamburgerButton>
|
||||
{showRectangle && (
|
||||
<Rectangle ref={rectangleRef} animate={animate} baseZIndex={rectZIndex !== undefined ? rectZIndex : zIndexLevel}>
|
||||
<ChildContainer>
|
||||
{guestSideOfClerk && guestSideOfClerk.clerkUsername && (
|
||||
<Child hasChildren>
|
||||
this is the guest side of {guestSideOfClerk.clerkUsername}
|
||||
</Child>
|
||||
)}
|
||||
{user.username !== undefined && (
|
||||
<Child onClick={() => setModal("edit_account")}>
|
||||
Kelola akun
|
||||
</Child>
|
||||
<CategoryLabel>Kelola akun</CategoryLabel>
|
||||
)}
|
||||
{user.roleId == 0 && (
|
||||
<Child onClick={()=>setModal('create_coupon', {})}>Buat Voucher</Child>)}
|
||||
{shopId && user.roleId == 1 && (
|
||||
<Child onClick={goToAdminCafes}>Dashboard</Child>)}
|
||||
{shopId &&
|
||||
user.userId == shopOwnerId &&
|
||||
user.user_id == shopOwnerId &&
|
||||
user.username !== undefined &&
|
||||
user.roleId === 1 && (
|
||||
<>
|
||||
<Child hasChildren>
|
||||
<Child>
|
||||
{shopName}
|
||||
</Child>
|
||||
<CategoryLabel>
|
||||
{formatCafeName(shopName)}
|
||||
</CategoryLabel>
|
||||
<Child>
|
||||
Mode pengembangan
|
||||
<Switch
|
||||
@@ -381,7 +417,7 @@ const Header = ({
|
||||
</Child>
|
||||
|
||||
<Child hasChildren>
|
||||
<Child>Konfigurasi</Child>
|
||||
<CategoryLabel>Konfigurasi</CategoryLabel>
|
||||
<Child onClick={() => setModal("welcome_config")}>
|
||||
Desain kafe
|
||||
</Child>
|
||||
@@ -393,7 +429,7 @@ const Header = ({
|
||||
</Child>
|
||||
</Child>
|
||||
<Child hasChildren>
|
||||
<Child>Kasir</Child>
|
||||
<CategoryLabel>Kasir</CategoryLabel>
|
||||
<Child onClick={() => setModal("create_clerk")}>
|
||||
+ Tambah
|
||||
</Child>
|
||||
@@ -420,7 +456,7 @@ const Header = ({
|
||||
user.cafeId == shopId &&
|
||||
user.roleId === 2 && (
|
||||
<Child hasChildren>
|
||||
<Child>{shopName}</Child>
|
||||
<CategoryLabel>{formatCafeName(shopName)}</CategoryLabel>
|
||||
|
||||
<Child>
|
||||
Mode pengembangan
|
||||
@@ -435,7 +471,7 @@ const Header = ({
|
||||
</Child>
|
||||
|
||||
<Child hasChildren>
|
||||
<Child>Konfigurasi</Child>
|
||||
<CategoryLabel>Konfigurasi</CategoryLabel>
|
||||
<Child onClick={() => setModal("welcome_config")}>
|
||||
Desain kafe
|
||||
</Child>
|
||||
@@ -478,11 +514,12 @@ const Header = ({
|
||||
{user.username !== undefined && (
|
||||
<Child hasChildren ><Child onClick={isLogout}>Logout</Child></Child>
|
||||
)}
|
||||
</ChildContainer>
|
||||
</Rectangle>
|
||||
)}
|
||||
</div>
|
||||
</HeaderBar></HeaderBarbackground>
|
||||
</ChildContainer>
|
||||
</Rectangle>
|
||||
)}
|
||||
</RightGroup>
|
||||
</HeaderBar>
|
||||
</HeaderBarbackground>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
348
src/components/IdentifyCafeModal.js
Normal file
348
src/components/IdentifyCafeModal.js
Normal 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>
|
||||
);
|
||||
}
|
||||
299
src/components/IdentifyCafeModal.module.css
Normal file
299
src/components/IdentifyCafeModal.module.css
Normal 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; }
|
||||
}
|
||||
@@ -4,6 +4,8 @@ const Item = ({
|
||||
blank,
|
||||
forCart,
|
||||
forInvoice,
|
||||
portrait,
|
||||
hideDetails,
|
||||
name: initialName,
|
||||
description: initialDescription,
|
||||
price: initialPrice,
|
||||
@@ -53,10 +55,10 @@ const Item = ({
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
handleCreateItem(itemName, itemPrice, selectedImage, previewUrl);
|
||||
handleCreateItem(itemName, itemPrice, selectedImage, previewUrl, itemDescription, promoPrice);
|
||||
};
|
||||
const handleUpdate = () => {
|
||||
handleUpdateItem(itemName, itemPrice, selectedImage, previewUrl);
|
||||
handleUpdateItem(itemName, itemPrice, selectedImage, itemDescription, promoPrice);
|
||||
};
|
||||
|
||||
const handleRemoveClick = () => {
|
||||
@@ -74,6 +76,11 @@ const Item = ({
|
||||
}
|
||||
};
|
||||
|
||||
const formatCurrency = (value) => {
|
||||
const num = Number(value) || 0;
|
||||
return num.toLocaleString('id-ID');
|
||||
};
|
||||
|
||||
const handlePriceChange = (event) => {
|
||||
setItemPrice(event.target.value);
|
||||
};
|
||||
@@ -89,245 +96,73 @@ const Item = ({
|
||||
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 (
|
||||
<div className={`${!last && !forInvoice ? styles.notLast : ""}`}>
|
||||
<div className={`${styles.item} ${forInvoice ? styles.itemInvoice : ""} `}>
|
||||
<div className={`${(!portrait && !last && !forInvoice) ? styles.notLast : ""}`}>
|
||||
<div className={`${styles.item} ${forInvoice ? styles.itemInvoice : ""} ${portrait ? styles.itemPortrait : ""} `}>
|
||||
{!forInvoice && (
|
||||
// <div className={styles.imageContainer}>
|
||||
<img
|
||||
src={
|
||||
previewUrl
|
||||
}
|
||||
onError={({ currentTarget }) => {
|
||||
currentTarget.onerror = null; // prevents looping
|
||||
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";
|
||||
}}
|
||||
alt={itemName}
|
||||
style={{
|
||||
filter: !isAvailable ? "grayscale(100%)" : "none",
|
||||
}}
|
||||
className={styles.imageContainer}
|
||||
/>
|
||||
// </div>
|
||||
<div className={styles.imageWrap}>
|
||||
<img
|
||||
src={previewUrl}
|
||||
onError={({ currentTarget }) => {
|
||||
currentTarget.onerror = null;
|
||||
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";
|
||||
}}
|
||||
alt={itemName}
|
||||
style={{ filter: !isAvailable ? "grayscale(100%)" : "none" }}
|
||||
className={styles.imageContainer}
|
||||
/>
|
||||
{promoPrice && promoPrice != 0 && promoPrice != '' && (
|
||||
<div className={styles.promoPill}>Promo</div>
|
||||
)}
|
||||
{portrait && (
|
||||
<>
|
||||
<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.qtyInvoice}>{itemQty}</p>
|
||||
<p className={styles.itemPriceInvoice}>Rp {formatCurrency(itemQty * (promoPrice > 0 ? promoPrice : itemPrice))}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.itemDetails}>
|
||||
{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 && (
|
||||
<>
|
||||
<p className={styles.multiplySymbol}>x</p>
|
||||
<p className={styles.qtyInvoice}>{itemQty}</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>
|
||||
)}
|
||||
|
||||
{!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 && (
|
||||
<div className={styles.remove} onClick={handleRemoveClick}>
|
||||
ⓧ
|
||||
</div>
|
||||
)}
|
||||
{/* {blank && (
|
||||
<button className={styles.createItem} onClick={handleCreate}>
|
||||
Create Item
|
||||
</button>
|
||||
)} */}
|
||||
</div>
|
||||
{itemDescription && itemDescription != 'undefined' && itemDescription?.length &&
|
||||
<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>
|
||||
}
|
||||
{null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,32 +1,100 @@
|
||||
.itemContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* gap: 10px; */
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-left: 5px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
color: rgba(88, 55, 50, 1);
|
||||
font-size: 32px;
|
||||
box-sizing: border-box; /* Include padding and border in the element's total width */
|
||||
width: 100%; /* Ensure the item does not exceed the parent's width */
|
||||
overflow: hidden; /* Prevent internal overflow */
|
||||
padding-top: 10px;
|
||||
margin-bottom: 5px;
|
||||
width: 100%;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.08);
|
||||
box-sizing: border-box;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.item:not(.itemInvoice) {
|
||||
/* border-top: 2px solid #00000017; */
|
||||
.item:hover { /* remove hover effect on list items */ }
|
||||
|
||||
/* 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{
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #00000017;
|
||||
.imageWrap {
|
||||
position: relative;
|
||||
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 {
|
||||
@@ -37,10 +105,14 @@
|
||||
margin-top: 0px;
|
||||
margin-bottom: 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 {
|
||||
margin-bottom: 0; /* Remove margin-bottom for the last child */
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.itemImage {
|
||||
@@ -48,29 +120,30 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
.item:not(.itemPortrait) .imageContainer {
|
||||
position: relative;
|
||||
width: 26vw;
|
||||
height: 26vw;
|
||||
border-radius: 12px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 10px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
bottom: 15px;
|
||||
top: 16px;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
bottom: 16px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 20px;
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
font-size: 3.3vw;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.overlay:hover {
|
||||
@@ -81,20 +154,18 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.itemDetails {
|
||||
.infoRow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
flex-grow: 1;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.itemInvoiceDetails {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
margin-left: 10px;
|
||||
margin-left: 12px;
|
||||
margin-top: -15px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
@@ -102,50 +173,47 @@
|
||||
.itemName {
|
||||
font-family: "Plus Jakarta Sans", sans-serif;
|
||||
font-style: normal;
|
||||
width: calc(100% - 15px); /* Adjust the width to prevent overflow */
|
||||
font-size: 5vw;
|
||||
font-weight: 500;
|
||||
width: calc(100% - 20px);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-top: 0;
|
||||
margin: 0 5px;
|
||||
color: rgba(88, 55, 50, 1);
|
||||
margin: 0 6px;
|
||||
color: #333;
|
||||
background-color: transparent;
|
||||
text-transform: capitalize;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.itemInvoiceName {
|
||||
width: calc(260% - 15px);
|
||||
width: calc(260% - 20px);
|
||||
background-color: transparent;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 500;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.multiplySymbol {
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.qtyInvoice {
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.itemPrice {
|
||||
font-family: "Plus Jakarta Sans", sans-serif;
|
||||
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;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.itemPriceInvoice {
|
||||
font-family: "Plus Jakarta Sans", sans-serif;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-left: 5px;
|
||||
color: #d9c61c;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
margin-left: 6px;
|
||||
color: #6B8F71;
|
||||
text-align: right;
|
||||
margin-top: 22px;
|
||||
}
|
||||
@@ -153,75 +221,404 @@
|
||||
.itemQty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9rem;
|
||||
margin-left: 5px;
|
||||
color: #a8c7a9;
|
||||
fill: #a8c7a9;
|
||||
height: 40px;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.itemQtyValue {
|
||||
font-family: "Plus Jakarta Sans", sans-serif;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
margin-top: 19px;
|
||||
margin-left: 1px;
|
||||
margin-right: 1px;
|
||||
width: 25px;
|
||||
font-weight: 700;
|
||||
margin-top: 22px;
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
width: 32px;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.itemQtyInput {
|
||||
font-family: "Plus Jakarta Sans", sans-serif;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
width: 30px; /* Adjust the width to prevent overflow */
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 700;
|
||||
width: 40px;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.addButton {
|
||||
background-color: #ffffff;
|
||||
border: 2px solid #a8c7a9;
|
||||
/* border: none; */
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
width: 87px;
|
||||
height: 32px;
|
||||
margin-left: 5px;
|
||||
margin-top: 5px;
|
||||
border-radius: 20px;
|
||||
background-color: var(--brand-sage, #6B8F71);
|
||||
border: 1px solid var(--brand-sage, #6B8F71);
|
||||
color: #ffffff;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
min-width: 90px;
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
border-radius: 10px;
|
||||
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 {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.disabled {
|
||||
color: gray;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.plusNegative {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
margin: 2.5px 0 -0.5px 0px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
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 {
|
||||
width: 84px;
|
||||
height: 21px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
position: absolute;
|
||||
transform: rotate(45deg);
|
||||
left: -33px;
|
||||
top: 21px;
|
||||
right: 16px;
|
||||
top: 50%;
|
||||
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 {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
margin-top: -10px;
|
||||
margin-right: 10px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
position: absolute;
|
||||
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 {
|
||||
@@ -233,7 +630,7 @@
|
||||
.itemInvoice .itemName,
|
||||
.itemInvoice .itemPrice,
|
||||
.itemInvoice .itemQty .qtyInvoice .multiplySymbol {
|
||||
font-size: 0.9rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.blank {
|
||||
@@ -246,6 +643,98 @@
|
||||
|
||||
.createItem {
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
right: 15px;
|
||||
left: 20px;
|
||||
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 */ }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import styles from "./Modal.module.css";
|
||||
import { getImageUrl } from "../helpers/itemHelper.js";
|
||||
|
||||
@@ -23,9 +24,10 @@ const ItemConfig = ({
|
||||
const [itemDescription, setItemDescription] = useState(initialDescription);
|
||||
const fileInputRef = useRef(null);
|
||||
const textareaRef = useRef(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saveStatus, setSaveStatus] = useState(null); // 'success' | 'error'
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
// Prevent scrolling when modal is open
|
||||
document.body.style.overflow = "hidden";
|
||||
if(selectedImage){
|
||||
@@ -80,139 +82,135 @@ const ItemConfig = ({
|
||||
}
|
||||
}, [textareaRef.current]);
|
||||
|
||||
const handleCreate = () => {
|
||||
console.log(itemPromoPrice)
|
||||
handleCreateItem(itemName, itemPrice, selectedImage, previewUrl, itemDescription, itemPromoPrice);
|
||||
document.body.style.overflow = "auto";
|
||||
const handleCreate = async () => {
|
||||
setSaving(true);
|
||||
setSaveStatus(null);
|
||||
try {
|
||||
await Promise.resolve(handleCreateItem(itemName, itemPrice, selectedImage, previewUrl, itemDescription, itemPromoPrice));
|
||||
setSaveStatus('success');
|
||||
} catch (e) {
|
||||
setSaveStatus('error');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
const handleUpdate = () => {
|
||||
console.log(itemName, itemPrice, selectedImage, itemDescription, itemPromoPrice)
|
||||
handleUpdateItem(itemName, itemPrice, selectedImage, itemDescription, itemPromoPrice);
|
||||
document.body.style.overflow = "auto";
|
||||
const handleUpdate = async () => {
|
||||
setSaving(true);
|
||||
setSaveStatus(null);
|
||||
try {
|
||||
await Promise.resolve(handleUpdateItem(itemName, itemPrice, selectedImage, itemDescription, itemPromoPrice));
|
||||
setSaveStatus('success');
|
||||
} catch (e) {
|
||||
setSaveStatus('error');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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={handleContentClick} style={{ display: 'flex', flexDirection: 'column', padding: '15px', backgroundColor: 'white', borderRadius: '20px 20px 0 0', overflowY: 'auto' }}>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<div style={{ width: '26vw', height: '26vw', marginRight: '10px' }}>
|
||||
<img style={{ width: '100%', height: '100%', borderRadius: '10px', objectFit: 'cover' }} src={previewUrl} />
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
className={styles.fileInput}
|
||||
onChange={handleFileChange}
|
||||
accept="image/*"
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
return createPortal(
|
||||
<div onClick={handleOverlayClick} className={styles.modalOverlay}>
|
||||
<div onClick={handleContentClick} className={styles.modalContent}>
|
||||
<div className={styles.imageSection}>
|
||||
<div className={styles.imagePreview}>
|
||||
<img src={previewUrl} alt="Preview" className={styles.previewImage} />
|
||||
</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 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>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', color: 'black', marginTop: '10px' }}>
|
||||
<p style={{ marginBottom: '5px', fontWeight: '500' }}>Nama item</p>
|
||||
<input
|
||||
value={itemName}
|
||||
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', // Make sure the padding doesn't cause overflow
|
||||
}}
|
||||
onChange={(e)=>setItemName(e.target.value)}
|
||||
onFocus={(e) => e.target.style.borderColor = '#60d37e'}
|
||||
onBlur={(e) => e.target.style.borderColor = '#ccc'}
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
className={styles.fileInput}
|
||||
onChange={handleFileChange}
|
||||
accept="image/*"
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', color: 'black', justifyContent: 'space-between' }}>
|
||||
<div style={{ width: '48%' }}>
|
||||
<p style={{ marginBottom: '5px', fontWeight: '500', marginTop: 0 }}>Harga</p>
|
||||
<div className={styles.formSection}>
|
||||
<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
|
||||
value={itemPrice}
|
||||
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)=>setItemPrice(e.target.value)}
|
||||
onFocus={(e) => e.target.style.borderColor = '#60d37e'}
|
||||
onBlur={(e) => e.target.style.borderColor = '#ccc'}
|
||||
value={itemName}
|
||||
className={styles.formInput}
|
||||
onChange={(e)=>setItemName(e.target.value)}
|
||||
placeholder="Masukkan nama item"
|
||||
/>
|
||||
</div>
|
||||
<div style={{ width: '48%' }}>
|
||||
<p style={{ marginBottom: '5px', fontWeight: '500', marginTop: 0 }}>Harga promo</p>
|
||||
<input
|
||||
value={itemPromoPrice}
|
||||
placeholder="Opsional"
|
||||
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)}
|
||||
onFocus={(e) => e.target.style.borderColor = '#60d37e'}
|
||||
onBlur={(e) => e.target.style.borderColor = '#ccc'}
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.formLabel}>Harga</label>
|
||||
<input
|
||||
value={itemPrice}
|
||||
className={styles.formInput}
|
||||
onChange={(e)=>setItemPrice(e.target.value)}
|
||||
placeholder="Rp 0"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.formLabel}>Harga Promo</label>
|
||||
<input
|
||||
value={itemPromoPrice}
|
||||
className={styles.formInput}
|
||||
onChange={(e)=>setItemPromoPrice(e.target.value)}
|
||||
placeholder="Opsional"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.formLabel}>Deskripsi</label>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className={styles.formTextarea}
|
||||
placeholder="Tambahkan deskripsi item..."
|
||||
value={itemDescription}
|
||||
onChange={(e)=>setItemDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', color: 'black' }}>
|
||||
<p style={{ marginBottom: '5px', fontWeight: '500', marginTop: 0 }}>Deskripsi</p>
|
||||
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
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',
|
||||
resize: 'none', // Prevent manual resize that could cause overflow
|
||||
}}
|
||||
placeholder="Tambah deskripsi..."
|
||||
value={itemDescription}
|
||||
onChange={(e)=>setItemDescription(e.target.value)}
|
||||
onFocus={(e) => e.target.style.borderColor = '#60d37e'}
|
||||
onBlur={(e) => e.target.style.borderColor = '#ccc'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ width: '100%', height: '35px', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<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' }}>
|
||||
{isBeingEdit? 'Simpan' : 'Buat Item'}
|
||||
<div className={styles.formActions}>
|
||||
<button
|
||||
onClick={cancelEdit}
|
||||
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>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
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 */
|
||||
}
|
||||
|
||||
@@ -98,4 +98,4 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import ItemType from "./ItemType.js";
|
||||
import { createItemType, updateItemDeletionStatus } from "../helpers/itemHelper.js";
|
||||
import ItemConfig from "./ItemConfig.js"
|
||||
import { ArrowUp, ArrowDown, Pencil, Save, X } from 'lucide-react';
|
||||
|
||||
const ItemLister = ({
|
||||
index,
|
||||
@@ -73,6 +74,22 @@ const ItemLister = ({
|
||||
|
||||
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 updatedItems = items.map((item) => {
|
||||
if (item.itemId === itemId) {
|
||||
@@ -598,7 +615,7 @@ const ItemLister = ({
|
||||
return (
|
||||
<>
|
||||
{(items.length > 0 ||
|
||||
(user && (user.cafeId == shopId || user.userId == shopOwnerId))) && (
|
||||
(user && (user.cafeId == shopId || user.user_id == shopOwnerId))) && (
|
||||
<div
|
||||
key={itemTypeId}
|
||||
className={`${styles["item-lister"]} ${isEdit ? styles["fullscreen"] : ""
|
||||
@@ -608,7 +625,15 @@ const ItemLister = ({
|
||||
|
||||
{(isEdit && isFirstStep || !isEdit) &&
|
||||
<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
|
||||
className={`${styles.title} ${isEdit ? styles.border : styles.noborder
|
||||
}`}
|
||||
@@ -618,87 +643,49 @@ const ItemLister = ({
|
||||
disabled={!isEdit}
|
||||
/>
|
||||
{isEditMode && !isEdit && (
|
||||
<>
|
||||
<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={index == 0 ? null : () => moveItemTypeUp(itemTypeId)} // Move onClick here for the whole div
|
||||
<div className={styles.titleActions}>
|
||||
<button
|
||||
className={styles.iconBtn}
|
||||
onClick={() => index === 0 ? null : moveItemTypeUp(itemTypeId)}
|
||||
disabled={index === 0}
|
||||
aria-label="Naikkan kategori"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="#000000"
|
||||
style={{ width: '100%', height: '100%' }} // Ensure SVG fits the div
|
||||
>
|
||||
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
|
||||
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<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>
|
||||
</g>
|
||||
</svg>
|
||||
</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={index == indexTotal - 1 ? null : () => moveItemTypeDown(itemTypeId)} // Move onClick here for the whole div
|
||||
<ArrowUp size={20} />
|
||||
</button>
|
||||
<button
|
||||
className={styles.iconBtn}
|
||||
onClick={() => index === indexTotal - 1 ? null : moveItemTypeDown(itemTypeId)}
|
||||
disabled={index === indexTotal - 1}
|
||||
aria-label="Turunkan kategori"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="#000000"
|
||||
style={{ width: '100%', height: '100%' }} // Ensure SVG fits the div
|
||||
>
|
||||
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
|
||||
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
|
||||
<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
|
||||
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
|
||||
<ArrowDown size={20} />
|
||||
</button>
|
||||
<button
|
||||
className={styles.iconBtn}
|
||||
onClick={toggleEditTypeItem}
|
||||
aria-label="Edit kategori"
|
||||
>
|
||||
<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>
|
||||
|
||||
</>
|
||||
<Pencil size={20} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{isEditMode && isEdit && (
|
||||
<div className={styles.titleActions}>
|
||||
<button
|
||||
className={styles.iconBtn}
|
||||
onClick={handleSaveType}
|
||||
aria-label="Simpan"
|
||||
>
|
||||
<Save size={20} />
|
||||
</button>
|
||||
<button
|
||||
className={styles.iconBtn}
|
||||
onClick={resetItems}
|
||||
aria-label="Batal"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
@@ -712,6 +699,7 @@ const ItemLister = ({
|
||||
handleImageChange(previewUrl, selectedImage)
|
||||
}
|
||||
imageUrl={getImageUrl("uploads/assets/addnew.png")}
|
||||
compact={false}
|
||||
/>
|
||||
{/* {typeImage != null && !previewUrl.includes(typeImage) && (
|
||||
<ItemType
|
||||
@@ -729,6 +717,7 @@ const ItemLister = ({
|
||||
handleImageChange(previewUrl, selectedImage)
|
||||
}
|
||||
imageUrl={getImageUrl("uploads/assets/beverage4.jpg")}
|
||||
compact={false}
|
||||
/>
|
||||
<ItemType
|
||||
rectangular={true}
|
||||
@@ -736,6 +725,7 @@ const ItemLister = ({
|
||||
handleImageChange(previewUrl, selectedImage)
|
||||
}
|
||||
imageUrl={getImageUrl("uploads/assets/beverage1.png")}
|
||||
compact={false}
|
||||
/>
|
||||
<ItemType
|
||||
rectangular={true}
|
||||
@@ -743,6 +733,7 @@ const ItemLister = ({
|
||||
handleImageChange(previewUrl, selectedImage)
|
||||
}
|
||||
imageUrl={getImageUrl("uploads/assets/beverage2.png")}
|
||||
compact={false}
|
||||
/>
|
||||
<ItemType
|
||||
rectangular={true}
|
||||
@@ -750,6 +741,7 @@ const ItemLister = ({
|
||||
handleImageChange(previewUrl, selectedImage)
|
||||
}
|
||||
imageUrl={getImageUrl("uploads/assets/beverage3.png")}
|
||||
compact={false}
|
||||
/>
|
||||
<ItemType
|
||||
rectangular={true}
|
||||
@@ -757,6 +749,7 @@ const ItemLister = ({
|
||||
handleImageChange(previewUrl, selectedImage)
|
||||
}
|
||||
imageUrl={getImageUrl("uploads/assets/snack5.png")}
|
||||
compact={false}
|
||||
/>
|
||||
<ItemType
|
||||
rectangular={true}
|
||||
@@ -764,6 +757,7 @@ const ItemLister = ({
|
||||
handleImageChange(previewUrl, selectedImage)
|
||||
}
|
||||
imageUrl={getImageUrl("uploads/assets/dessert1.png")}
|
||||
compact={false}
|
||||
/>
|
||||
<ItemType
|
||||
rectangular={true}
|
||||
@@ -771,6 +765,7 @@ const ItemLister = ({
|
||||
handleImageChange(previewUrl, selectedImage)
|
||||
}
|
||||
imageUrl={getImageUrl("uploads/assets/dessert2.jpg")}
|
||||
compact={false}
|
||||
/>
|
||||
<ItemType
|
||||
rectangular={true}
|
||||
@@ -778,6 +773,7 @@ const ItemLister = ({
|
||||
handleImageChange(previewUrl, selectedImage)
|
||||
}
|
||||
imageUrl={getImageUrl("uploads/assets/food4.jpg")}
|
||||
compact={false}
|
||||
/>
|
||||
<ItemType
|
||||
rectangular={true}
|
||||
@@ -785,6 +781,7 @@ const ItemLister = ({
|
||||
handleImageChange(previewUrl, selectedImage)
|
||||
}
|
||||
imageUrl={getImageUrl("uploads/assets/food1.png")}
|
||||
compact={false}
|
||||
/>
|
||||
<ItemType
|
||||
rectangular={true}
|
||||
@@ -792,6 +789,7 @@ const ItemLister = ({
|
||||
handleImageChange(previewUrl, selectedImage)
|
||||
}
|
||||
imageUrl={getImageUrl("uploads/assets/food2.jpg")}
|
||||
compact={false}
|
||||
/>
|
||||
|
||||
<ItemType
|
||||
@@ -800,6 +798,7 @@ const ItemLister = ({
|
||||
handleImageChange(previewUrl, selectedImage)
|
||||
}
|
||||
imageUrl={getImageUrl("uploads/assets/food3.png")}
|
||||
compact={false}
|
||||
/>
|
||||
<ItemType
|
||||
rectangular={true}
|
||||
@@ -807,6 +806,7 @@ const ItemLister = ({
|
||||
handleImageChange(previewUrl, selectedImage)
|
||||
}
|
||||
imageUrl={getImageUrl("uploads/assets/packet1.png")}
|
||||
compact={false}
|
||||
/>
|
||||
<ItemType
|
||||
rectangular={true}
|
||||
@@ -814,6 +814,7 @@ const ItemLister = ({
|
||||
handleImageChange(previewUrl, selectedImage)
|
||||
}
|
||||
imageUrl={getImageUrl("uploads/assets/packet2.png")}
|
||||
compact={false}
|
||||
/>
|
||||
<ItemType
|
||||
rectangular={true}
|
||||
@@ -821,6 +822,7 @@ const ItemLister = ({
|
||||
handleImageChange(previewUrl, selectedImage)
|
||||
}
|
||||
imageUrl={getImageUrl("uploads/assets/snack1.png")}
|
||||
compact={false}
|
||||
/>
|
||||
<ItemType
|
||||
rectangular={true}
|
||||
@@ -828,6 +830,7 @@ const ItemLister = ({
|
||||
handleImageChange(previewUrl, selectedImage)
|
||||
}
|
||||
imageUrl={getImageUrl("uploads/assets/snack2.png")}
|
||||
compact={false}
|
||||
/>
|
||||
<ItemType
|
||||
rectangular={true}
|
||||
@@ -835,6 +838,7 @@ const ItemLister = ({
|
||||
handleImageChange(previewUrl, selectedImage)
|
||||
}
|
||||
imageUrl={getImageUrl("uploads/assets/snack3.png")}
|
||||
compact={false}
|
||||
/>
|
||||
<ItemType
|
||||
rectangular={true}
|
||||
@@ -842,6 +846,7 @@ const ItemLister = ({
|
||||
handleImageChange(previewUrl, selectedImage)
|
||||
}
|
||||
imageUrl={getImageUrl("uploads/assets/snack4.png")}
|
||||
compact={false}
|
||||
/>
|
||||
{Array.from({ length: 16 }, (_, index) => {
|
||||
const sampleNumber = index + 1; // To get numbers from 1 to 16
|
||||
@@ -853,33 +858,53 @@ const ItemLister = ({
|
||||
handleImageChange(previewUrl, selectedImage)
|
||||
}
|
||||
imageUrl={getImageUrl(`uploads/samples/sample (${sampleNumber}).png`)}
|
||||
compact={false}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</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) &&
|
||||
<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>
|
||||
<h2 className={styles["item-list-title"]}>{items && items.length < 1 ? 'Buat item' : 'Daftar item'}</h2></div>}
|
||||
<div className={styles["item-list"]}>
|
||||
{isEdit && (
|
||||
<div className={styles["settings-section"]}>
|
||||
<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.userId == shopOwnerId || user.cafeId == shopId) &&
|
||||
user.user_id == shopOwnerId || user.cafeId == shopId) &&
|
||||
isEditMode && (
|
||||
<>
|
||||
{!isAddingNewItem && (
|
||||
<button
|
||||
className={styles["add-item-button"]}
|
||||
onClick={toggleAddNewItem}
|
||||
style={{
|
||||
display: "inline-block",
|
||||
height: "120px",
|
||||
fontSize: "20px",
|
||||
}}
|
||||
>
|
||||
Tambah item +
|
||||
+ Tambah Item Baru
|
||||
</button>
|
||||
)}
|
||||
{isAddingNewItem && (
|
||||
@@ -888,7 +913,7 @@ const ItemLister = ({
|
||||
cancelEdit={() => toggleAddNewItem()}
|
||||
handleCreateItem={onCreateItem}
|
||||
/>
|
||||
<Item blank={true} handleCreateItem={onCreateItem} />
|
||||
<Item blank={true} handleCreateItem={onCreateItem} hideDetails={!showGrid} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
@@ -915,48 +940,29 @@ const ItemLister = ({
|
||||
{isEditMode && isEditItem != item.itemId && (
|
||||
|
||||
<div className={styles["editModeLayout"]}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', height: '40px', marginLeft: '7.5vw' }}>
|
||||
<div>
|
||||
{isEditMode && (
|
||||
<Switch
|
||||
onChange={() => handleChange(item.itemId)}
|
||||
checked={item.availability}
|
||||
/>
|
||||
<div className={styles["switch-container"]}>
|
||||
<span className={styles["switch-label"]}>
|
||||
{item.availability ? "Tersedia" : "Tidak Tersedia"}
|
||||
</span>
|
||||
<Switch
|
||||
onChange={() => handleChange(item.itemId)}
|
||||
checked={item.availability}
|
||||
offColor="#cccccc"
|
||||
onColor="#6B8F71"
|
||||
uncheckedIcon={false}
|
||||
checkedIcon={false}
|
||||
height={20}
|
||||
width={40}
|
||||
handleDiameter={16}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<h3>
|
||||
{item.availability ? "Tersedia" : "Tidak tersedia"}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div onClick={() => editItem(item.itemId)} style={{ display: 'flex', alignItems: 'center', height: '40px', marginRight: '7.5vw' }}>
|
||||
<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>Edit item</h3>
|
||||
<div onClick={() => editItem(item.itemId)}>
|
||||
<Pencil size={18} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -971,6 +977,7 @@ const ItemLister = ({
|
||||
qty={item.qty}
|
||||
imageUrl={item.image}
|
||||
imageFile={item.selectedImage}
|
||||
hideDetails={!showGrid}
|
||||
onPlusClick={() => handlePlusClick(item.itemId)}
|
||||
onNegativeClick={() => handleNegativeClick(item.itemId)}
|
||||
onRemoveClick={() => handleRemoveClick(item.itemId)}
|
||||
@@ -990,13 +997,6 @@ const ItemLister = ({
|
||||
<div
|
||||
key={item.itemId}>
|
||||
{isEditItem == item.itemId && (
|
||||
// <button
|
||||
// className={styles["add-item-button"]}
|
||||
// onClick={() => editItem(0)}
|
||||
// style={{ display: "inline-block" }}
|
||||
// >
|
||||
// batal
|
||||
// </button>
|
||||
<ItemConfig
|
||||
isBeingEdit={true}
|
||||
name={item.name}
|
||||
@@ -1022,23 +1022,30 @@ const ItemLister = ({
|
||||
<div className={styles["itemWrapper"]}>
|
||||
{(isEditMode && isEditItem != item.itemId || item.willBeDeleted) && (
|
||||
<div className={styles["editModeLayout"]}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', height: '40px', marginLeft: '7.5vw' }}>
|
||||
<div>
|
||||
{!item.willBeDeleted && isEditMode && (
|
||||
<Switch
|
||||
onChange={() => handleChange(item.itemId)}
|
||||
checked={item.availability}
|
||||
/>
|
||||
<div className={styles["switch-container"]}>
|
||||
<span className={styles["switch-label"]}>
|
||||
{item.availability ? "Tersedia" : "Tidak Tersedia"}
|
||||
</span>
|
||||
<Switch
|
||||
onChange={() => handleChange(item.itemId)}
|
||||
checked={item.availability}
|
||||
offColor="#cccccc"
|
||||
onColor="#6B8F71"
|
||||
uncheckedIcon={false}
|
||||
checkedIcon={false}
|
||||
height={20}
|
||||
width={40}
|
||||
handleDiameter={16}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{item.willBeDeleted ?
|
||||
|
||||
<h3 style={{ backgroundColor: 'black', padding: '13px 26px' }}>
|
||||
{item.willBeDeleted && (
|
||||
<span style={{ backgroundColor: 'black', padding: '6px 12px', borderRadius: '20px' }}>
|
||||
Ditandai untuk dihapus
|
||||
</h3>
|
||||
:
|
||||
<h3>
|
||||
{item.availability ? "Tersedia" : "Tidak tersedia"}
|
||||
</h3>
|
||||
}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div onClick={() => {
|
||||
@@ -1047,38 +1054,8 @@ const ItemLister = ({
|
||||
} else {
|
||||
handleItemDeletionToggle(item.itemId, false);
|
||||
}
|
||||
}}
|
||||
style={{ display: 'flex', alignItems: 'center', height: '40px', marginRight: '7.5vw' }}>
|
||||
{!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>
|
||||
}}>
|
||||
<Pencil size={18} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -1088,6 +1065,8 @@ const ItemLister = ({
|
||||
last={index === indexTotal - 1 && indexx === items.length - 1}
|
||||
forCart={forCart}
|
||||
forInvoice={forInvoice}
|
||||
portrait={showGrid}
|
||||
hideDetails={!showGrid}
|
||||
name={item.name}
|
||||
price={item.price}
|
||||
promoPrice={item.promoPrice}
|
||||
@@ -1097,9 +1076,9 @@ const ItemLister = ({
|
||||
itemTypeId ? getImageUrl(item.image) : item.image
|
||||
}
|
||||
imageFile={item.selectedImage}
|
||||
onPlusClick={() => handlePlusClick(item.itemId)}
|
||||
onNegativeClick={() => handleNegativeClick(item.itemId)}
|
||||
onRemoveClick={() => handleRemoveClick(item.itemId)}
|
||||
onPlusClick={() => (String(item.itemId).startsWith('dummy-') ? undefined : handlePlusClick(item.itemId))}
|
||||
onNegativeClick={() => (String(item.itemId).startsWith('dummy-') ? undefined : handleNegativeClick(item.itemId))}
|
||||
onRemoveClick={() => (String(item.itemId).startsWith('dummy-') ? undefined : handleRemoveClick(item.itemId))}
|
||||
isBeingEdit={isEditItem == item.itemId}
|
||||
isAvailable={item.availability}
|
||||
handleUpdateItem={(name, price, image, description, promoPrice) =>
|
||||
@@ -1113,7 +1092,7 @@ const ItemLister = ({
|
||||
|
||||
{user &&
|
||||
user.roleId == 1 &&
|
||||
user.userId == shopOwnerId &&
|
||||
user.user_id == shopOwnerId &&
|
||||
isEdit && (
|
||||
<>
|
||||
{/* <button
|
||||
@@ -1128,33 +1107,6 @@ const ItemLister = ({
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -2,80 +2,88 @@
|
||||
|
||||
.item-lister {
|
||||
width: 100%;
|
||||
padding: 10px; /* Adjust padding as needed */
|
||||
box-sizing: border-box; /* Ensure padding doesn't affect width */
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
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 {
|
||||
position: fixed; /* Keep the container fixed */
|
||||
top: 0; /* Adjust the top position as needed */
|
||||
bottom: 0; /* Occupy the full height of the viewport */
|
||||
left: 0; /* Align to the left */
|
||||
right: 0; /* Align to the right */
|
||||
background-color: white; /* Background color */
|
||||
z-index: 1000; /* Layering */
|
||||
overflow-y: auto; /* Allow vertical scrolling */
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: white;
|
||||
z-index: 100; /* keep above page, below modal overlays (>=200) */
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 10px;
|
||||
/* padding: 10px; */
|
||||
/* max-height: calc(3 * (25vw - 20px) + 20px); */
|
||||
overflow-y: auto;
|
||||
height: calc(49vw - 20px);
|
||||
gap: 16px;
|
||||
height: calc(52vw - 20px);
|
||||
padding: 16px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
@media (min-height: 0px) {
|
||||
.grid-container {
|
||||
height: 27vh;
|
||||
height: 30vh;
|
||||
}
|
||||
}
|
||||
@media (min-height: 630px) {
|
||||
.grid-container {
|
||||
height: 27vh;
|
||||
height: 30vh;
|
||||
}
|
||||
}
|
||||
@media (min-height: 636px) {
|
||||
.grid-container {
|
||||
height: 29vh;
|
||||
height: 32vh;
|
||||
}
|
||||
}
|
||||
@media (min-height: 650px) {
|
||||
.grid-container {
|
||||
height: 34vh;
|
||||
height: 38vh;
|
||||
}
|
||||
}
|
||||
@media (min-height: 705px) {
|
||||
.grid-container {
|
||||
height: 37vh;
|
||||
height: 41vh;
|
||||
}
|
||||
}
|
||||
@media (min-height: 735px) {
|
||||
.grid-container {
|
||||
height: 38vh;
|
||||
height: 42vh;
|
||||
}
|
||||
}
|
||||
@media (min-height: 759px) {
|
||||
.grid-container {
|
||||
height: 40vh;
|
||||
height: 44vh;
|
||||
}
|
||||
}
|
||||
@media (min-height: 819px) {
|
||||
.grid-container {
|
||||
height: 44vh;
|
||||
height: 48vh;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-height: 830px) {
|
||||
.grid-container {
|
||||
height: 47vh;
|
||||
height: 51vh;
|
||||
}
|
||||
}
|
||||
@media (min-height: 892px) {
|
||||
.grid-container {
|
||||
height: 49vh;
|
||||
height: 53vh;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,46 +91,166 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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 {
|
||||
background-color: transparent;
|
||||
font-family: "Plus Jakarta Sans", sans-serif;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-size: 20px;
|
||||
color: rgba(88, 55, 50, 1);
|
||||
text-align: left;
|
||||
width: calc(70% - 10px);
|
||||
padding-left: 10px;
|
||||
text-transform: capitalize;
|
||||
font-family: "Plus Jakarta Sans", sans-serif;
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-size: 22px;
|
||||
color: rgba(88, 55, 50, 1);
|
||||
text-align: left;
|
||||
width: calc(70% - 12px);
|
||||
padding-left: 12px;
|
||||
text-transform: capitalize;
|
||||
border: none;
|
||||
outline: none;
|
||||
position: relative;
|
||||
z-index: 151;
|
||||
}
|
||||
|
||||
.edit-typeItem-button {
|
||||
margin-left: auto; /* Push the button to the right */
|
||||
padding: 8px 16px; /* Adjust padding as needed */
|
||||
margin-left: auto;
|
||||
padding: 10px 16px;
|
||||
font-size: 14px;
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
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 {
|
||||
margin-top: 10px;
|
||||
padding: 8px 16px; /* Adjust padding as needed */
|
||||
font-size: 14px;
|
||||
background-color: #359d42d1;
|
||||
color: #fff;
|
||||
margin: 0; /* follow parent gap for top/bottom spacing */
|
||||
display: inline-block;
|
||||
width: 275px; /* requested size */
|
||||
height: 275px; /* requested size */
|
||||
padding: 0; /* match image padding (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;
|
||||
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 {
|
||||
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 {
|
||||
@@ -132,40 +260,91 @@
|
||||
.noborder {
|
||||
border: 1px solid #ffffff00;
|
||||
}
|
||||
|
||||
.itemWrapper {
|
||||
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;
|
||||
}
|
||||
.editModeLayout {
|
||||
border-radius: 4px;
|
||||
.itemWrapper:hover { /* remove hover effect */ }
|
||||
|
||||
.editModeLayout {
|
||||
/* Turn full-width bar into subtle corner controls over image */
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
background-color: #0000008c;
|
||||
width: 100%;
|
||||
top: 7px;
|
||||
bottom: -4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
inset: 0;
|
||||
z-index: 155; /* above itemWrapper */
|
||||
pointer-events: none; /* only children are interactive */
|
||||
}
|
||||
|
||||
/* Left badge: availability switch + label */
|
||||
.editModeLayout > div:first-child {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
gap: 8px;
|
||||
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 {
|
||||
overflow-x: hidden;
|
||||
background-color: #e9e9e9;
|
||||
background-color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
font-size: calc(12px + 2vmin);
|
||||
color: rgba(88, 55, 50, 1);
|
||||
border-radius: 15px 15px 0 0;
|
||||
|
||||
border-radius: 20px 20px 0 0;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
|
||||
z-index: 300;
|
||||
z-index: 300; /* Menurunkan z-index agar tidak menutupi material list */
|
||||
box-shadow: 0 -2px 20px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.TotalContainer {
|
||||
@@ -176,20 +355,28 @@
|
||||
font-family: "Plus Jakarta Sans", sans-serif;
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-size: 1.5em;
|
||||
padding: 10px 0;
|
||||
font-size: 1.8em;
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.OptionContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 80vw;
|
||||
margin: 0 auto;
|
||||
font-family: "Plus Jakarta Sans", sans-serif;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-size: 0.9em;
|
||||
padding: 10px 0;
|
||||
font-size: 1.1em;
|
||||
padding: 15px 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.OptionContainer span:first-child {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.PayButton {
|
||||
font-family: "Plus Jakarta Sans", sans-serif;
|
||||
font-weight: 500;
|
||||
@@ -197,7 +384,7 @@
|
||||
font-size: 32px;
|
||||
|
||||
width: 80vw;
|
||||
height: 70px;
|
||||
height: 56px;
|
||||
border-radius: 50px;
|
||||
background-color: rgba(88, 55, 50, 1);
|
||||
color: white;
|
||||
@@ -205,14 +392,32 @@
|
||||
margin: 0px auto;
|
||||
cursor: pointer;
|
||||
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 {
|
||||
text-align: center;
|
||||
color: rgba(88, 55, 50, 1);
|
||||
font-size: 1em;
|
||||
margin-bottom: 25px;
|
||||
font-size: 1.2em;
|
||||
font-weight: 500;
|
||||
margin-bottom: 10px;
|
||||
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 {
|
||||
@@ -224,4 +429,25 @@
|
||||
font-size: 6vw;
|
||||
color: black;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
import styles from "./ItemType.module.css";
|
||||
import { Coffee, CupSoda, CakeSlice, Utensils, Grid2X2, Plus } from 'lucide-react';
|
||||
|
||||
export default function ItemType({
|
||||
onClick,
|
||||
@@ -9,6 +10,8 @@ export default function ItemType({
|
||||
imageUrl,
|
||||
selected,
|
||||
rectangular,
|
||||
compact,
|
||||
noIcon, // New prop to remove icons
|
||||
}) {
|
||||
const inputRef = useRef(null);
|
||||
const [namee, setName] = useState(name);
|
||||
@@ -57,42 +60,64 @@ export default function ItemType({
|
||||
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 (
|
||||
<div
|
||||
className={`${styles["item-type"]} ${compact ? styles["item-type-compact"] : ""}`}
|
||||
style={{ zIndex: blank ? 301 : "inherit" }}
|
||||
>
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={`${styles["item-type-rect"]} ${compact ? styles["item-type-rect-compact"] : ""} ${selected ? styles["selected"] : ""}`}
|
||||
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[
|
||||
namee
|
||||
? "item-type"
|
||||
: rectangular
|
||||
? "item-type-rectangular"
|
||||
: "item-type-nomargin"
|
||||
]
|
||||
}
|
||||
className={`${styles["item-type"]} ${compact ? styles["item-type-compact"] : ""}`}
|
||||
style={{ zIndex: blank ? 301 : "inherit" }}
|
||||
>
|
||||
<div
|
||||
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={{
|
||||
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' ?
|
||||
<img
|
||||
src={previewUrl}
|
||||
alt={namee}
|
||||
className={styles["item-type-image"]}
|
||||
/>
|
||||
:<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
{iconImageUrl === 'uploads/assets/All.png' ? (
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="100%" height="100%" viewBox="0 0 800.000000 800.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.16, written by Peter Selinger 2001-2019
|
||||
</metadata>
|
||||
<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
|
||||
-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
|
||||
@@ -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"/>
|
||||
</g>
|
||||
</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 && (
|
||||
<div className={styles["item-type-image-container"]}>
|
||||
<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>
|
||||
{!rectangular && !blank && (
|
||||
<input
|
||||
ref={inputRef}
|
||||
className={`${styles["item-type-name"]} ${styles.noborder}`}
|
||||
value={namee}
|
||||
onChange={handleNameChange}
|
||||
disabled={true}
|
||||
style={{
|
||||
top: selected ? "-5px" : "initial",
|
||||
borderBottom: selected ? "1px solid #000" : "none",
|
||||
}}
|
||||
/>
|
||||
<div className={`${styles["item-type-name"]} ${compact ? styles["item-type-name-compact"] : ""}`} style={{ color: selected ? '#2d2d2d' : '#333' }}>
|
||||
{formatName(namee)}
|
||||
</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';
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
.item-type {
|
||||
width: calc(25vw - 20px);
|
||||
height: calc(30vw - 20px);
|
||||
margin: 1px 10px 0px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin: 0 6px;
|
||||
overflow: visible;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
@@ -10,55 +10,107 @@
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
.item-type-rectangular {
|
||||
width: calc(25vw - 20px);
|
||||
height: calc(25vw - 20px);
|
||||
overflow: visible;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: start;
|
||||
position: relative; /* Ensure absolute positioning inside works */
|
||||
|
||||
/* Compact version of item-type */
|
||||
.item-type-compact {
|
||||
margin: 0 4px;
|
||||
}
|
||||
.item-type-nomargin {
|
||||
width: calc(25vw - 20px);
|
||||
height: calc(39vw - 20px);
|
||||
|
||||
.item-type-rectangular {
|
||||
width: calc(30vw - 24px);
|
||||
height: calc(30vw - 24px);
|
||||
overflow: visible;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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 {
|
||||
position: relative;
|
||||
height: 13vw;
|
||||
width: 13vw;
|
||||
height: 48px; /* Fixed height for better touch targets */
|
||||
min-width: 100px; /* Minimum width */
|
||||
padding: 0 20px; /* Horizontal padding */
|
||||
object-fit: cover;
|
||||
border-radius: 15px;
|
||||
border-radius: 12px; /* Square rounded corners */
|
||||
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 {
|
||||
font-family: "Plus Jakarta Sans", sans-serif;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
width: calc(25vw - 30px);
|
||||
font-size: 16px;
|
||||
color: #2d2d2d;
|
||||
width: auto;
|
||||
text-align: center;
|
||||
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 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 15px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.item-type-image-container {
|
||||
@@ -68,6 +120,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.item-type-image-input {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
@@ -79,12 +132,12 @@
|
||||
|
||||
.item-type-create {
|
||||
position: absolute;
|
||||
top: 76%; /* Position below the input */
|
||||
top: 76%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin-top: 10px; /* Space between input and button */
|
||||
width: 20vw;
|
||||
text-align: center; /* Center button text */
|
||||
margin-top: 12px;
|
||||
width: 24vw;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.border {
|
||||
@@ -94,3 +147,17 @@
|
||||
.noborder {
|
||||
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;
|
||||
}
|
||||
@@ -1,23 +1,95 @@
|
||||
/* ItemTypeLister.css */
|
||||
|
||||
/* New clean, intuitive category bar */
|
||||
.item-type-lister {
|
||||
width: 100vw;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
padding: 3px 0px;
|
||||
margin-bottom: -5px;
|
||||
padding: 8px 0; /* Reduced padding for more compact design */
|
||||
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 {
|
||||
display: inline-flex;
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
overflow-y: hidden;
|
||||
gap: 6px; /* Added gap for consistent spacing */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.item-type {
|
||||
display: inline-block;
|
||||
margin-right: 20px;
|
||||
/* Space between items */
|
||||
.category-chip {
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
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 {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
@@ -25,7 +97,7 @@
|
||||
top: 0;
|
||||
right: 0;
|
||||
background-color: white;
|
||||
z-index: 300;
|
||||
z-index: 0; /* align with item lister */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -55,13 +127,123 @@
|
||||
bottom: 0;
|
||||
align-self: center; /* Center the button horizontally */
|
||||
}
|
||||
.item-type-name {
|
||||
font-family: "Plus Jakarta Sans", sans-serif;
|
||||
font-style: normal;
|
||||
height: 20vw;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
color: black;
|
||||
text-transform: capitalize;
|
||||
z-index: 301;
|
||||
|
||||
/* Legacy styles kept for ItemType grid if needed elsewhere */
|
||||
|
||||
/* Compact centered item type list without icon tiles */
|
||||
.compact-centered-list {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import smoothScroll from "smooth-scroll-into-view-if-needed";
|
||||
import "./ItemTypeLister.css";
|
||||
import ItemType from "./ItemType";
|
||||
import { createItem, createItemType } from "../helpers/itemHelper.js";
|
||||
import { createItem } from "../helpers/itemHelper.js";
|
||||
import { getImageUrl } from "../helpers/itemHelper";
|
||||
import ItemLister from "./ItemLister";
|
||||
|
||||
@@ -22,31 +21,13 @@ const ItemTypeLister = ({
|
||||
const newItemDivRef = useRef(null);
|
||||
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
|
||||
useEffect(() => {
|
||||
if (isAddingNewItem && newItemDivRef.current) {
|
||||
// Use smooth-scroll-into-view-if-needed to scroll to the target div
|
||||
smoothScroll(newItemDivRef.current, {
|
||||
behavior: "smooth",
|
||||
block: "start", // Adjust this based on your needs
|
||||
block: "start",
|
||||
inline: "nearest",
|
||||
});
|
||||
} else {
|
||||
@@ -54,11 +35,11 @@ const ItemTypeLister = ({
|
||||
|
||||
smoothScroll(node, {
|
||||
behavior: "smooth",
|
||||
block: "start", // Adjust this based on your needs
|
||||
block: "start",
|
||||
inline: "nearest",
|
||||
});
|
||||
}
|
||||
}, [isAddingNewItem]); // Dependency array includes isAddingNewItem
|
||||
}, [isAddingNewItem]);
|
||||
|
||||
const toggleAddNewItem = () => {
|
||||
setIsAddingNewItem((prev) => !prev);
|
||||
@@ -67,93 +48,69 @@ const ItemTypeLister = ({
|
||||
document.body.style.overflow = !isAddingNewItem ? "hidden" : "auto";
|
||||
};
|
||||
|
||||
async function handleCreate(name, selectedImage) {
|
||||
createItemType(shopId, name, selectedImage);
|
||||
}
|
||||
// Removed legacy image upload logic used by the old tile view
|
||||
|
||||
const canManage = user && (user.user_id == shopOwnerId || user.cafeId == shopId);
|
||||
|
||||
const [selectedImage, setSelectedImage] = useState(null);
|
||||
const [previewUrl, setPreviewUrl] = useState("");
|
||||
const [imageUrl, setImaguUrl] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
// if (selectedImage) {
|
||||
// const reader = new FileReader();
|
||||
// reader.onloadend = () => {
|
||||
// setPreviewUrl(reader.result);
|
||||
// };
|
||||
// reader.readAsDataURL(selectedImage);
|
||||
// } else {
|
||||
// setPreviewUrl(getImageUrl(imageUrl));
|
||||
setPreviewUrl(selectedImage);
|
||||
// }
|
||||
}, [selectedImage, imageUrl]);
|
||||
const handleImageChange = (e) => {
|
||||
setSelectedImage(e);
|
||||
const formatName = (name) => {
|
||||
if (!name) return name;
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="item-type-lister"
|
||||
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
|
||||
<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
|
||||
className="compact-item-type compact-add-item"
|
||||
onClick={toggleAddNewItem}
|
||||
name={"buat baru"}
|
||||
imageUrl={getImageUrl("uploads/assets/addnew.png")}
|
||||
>
|
||||
Buat baru
|
||||
</div>
|
||||
)}
|
||||
|
||||
{canManage && isAddingNewItem && (
|
||||
<ItemLister
|
||||
shopId={shopId}
|
||||
shopOwnerId={shopOwnerId}
|
||||
user={user}
|
||||
typeName={""}
|
||||
setShopItems={setShopItems}
|
||||
itemList={items}
|
||||
isEditMode={true}
|
||||
handleCreateItem={(itemTypeId, name, price, selectedImage, description, promoPrice) => createItem(shopId, name, price, selectedImage, itemTypeId, description, promoPrice)}
|
||||
beingEditedType={beingEditedType}
|
||||
setBeingEditedType={setBeingEditedType}
|
||||
alwaysEdit={true}
|
||||
handleUnEdit={toggleAddNewItem}
|
||||
/>
|
||||
)}
|
||||
{user &&(
|
||||
user.userId == shopOwnerId || user.cafeId == shopId) &&
|
||||
isAddingNewItem && (
|
||||
<>
|
||||
<ItemLister
|
||||
shopId={shopId}
|
||||
shopOwnerId={shopOwnerId}
|
||||
user={user}
|
||||
typeName={""}
|
||||
setShopItems={setShopItems}
|
||||
itemList={items}
|
||||
isEditMode={true}
|
||||
handleCreateItem={(itemTypeId, name, price, selectedImage, description, promoPrice) => createItem(shopId, name, price, selectedImage, itemTypeId, description, promoPrice)}
|
||||
beingEditedType={beingEditedType}
|
||||
setBeingEditedType={setBeingEditedType}
|
||||
alwaysEdit={true}
|
||||
handleUnEdit={toggleAddNewItem}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{itemTypes && itemTypes.length > 0 && (
|
||||
<ItemType
|
||||
name={"semua"}
|
||||
onClick={() => onFilterChange(0)}
|
||||
imageUrl={"uploads/assets/All.png"}
|
||||
/>
|
||||
)}
|
||||
{itemTypes &&
|
||||
itemTypes.map(
|
||||
(itemType) =>
|
||||
(
|
||||
itemType.itemList.length > 0 || (user && (user.userId == shopOwnerId || user.cafeId == shopId))) && (
|
||||
<ItemType
|
||||
key={itemType.itemTypeId}
|
||||
name={itemType.name}
|
||||
imageUrl={getImageUrl(itemType.image)}
|
||||
onClick={() => onFilterChange(itemType.itemTypeId)}
|
||||
selected={filterId === itemType.itemTypeId}
|
||||
/>
|
||||
)
|
||||
|
||||
{itemTypes && itemTypes.length > 0 && (
|
||||
<div
|
||||
className={`compact-item-type ${filterId === 0 ? 'selected' : ''}`}
|
||||
onClick={() => onFilterChange(0)}
|
||||
>
|
||||
Semua
|
||||
</div>
|
||||
)}
|
||||
|
||||
{itemTypes && itemTypes.map((itemType) => (
|
||||
<div
|
||||
key={itemType.itemTypeId}
|
||||
className={`compact-item-type ${filterId === itemType.itemTypeId ? 'selected' : ''}`}
|
||||
onClick={() => onFilterChange(itemType.itemTypeId)}
|
||||
>
|
||||
{formatName(itemType.name)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemTypeLister;
|
||||
export default ItemTypeLister;
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, {useState, useEffect} from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import styles from "./Modal.module.css";
|
||||
|
||||
import AccountUpdatePage from "../components/AccountUpdatePage.js";
|
||||
import CreateClerk from "../pages/CreateClerk"
|
||||
import CreateCafe from "../pages/CreateCafe"
|
||||
import CreateTenant from "../pages/CreateTenant"
|
||||
import TablesPage from "./TablesPage.js";
|
||||
import IdentifyCafeModal from "./IdentifyCafeModal.js";
|
||||
import PaymentOptions from "./PaymentOptions.js";
|
||||
import Transaction from "../pages/Transaction";
|
||||
import Transaction_item from "../pages/Transaction_item";
|
||||
@@ -75,9 +76,9 @@ const Modal = ({ user, shop, isOpen, onClose, modalContent, deviceType, setModal
|
||||
event.stopPropagation();
|
||||
};
|
||||
if(modalContent == '') handleOverlayClick();
|
||||
return (
|
||||
return createPortal(
|
||||
<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 === "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_kedai" && <CreateCafe 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" && (
|
||||
<Transaction propsShopId={shop.cafeId} handleMoveToTransaction={handleMoveToTransaction} depth={depth} shopImg={shopImg} />
|
||||
<Transaction propsShopId={shop.cafeId} handleMoveToTransaction={handleMoveToTransaction} depth={depth} shopImg={shopImg} setModal={setModal}/>
|
||||
)}
|
||||
{modalContent === "transaction_canceled" && (
|
||||
<Transaction propsShopId={shop.cafeId} />
|
||||
@@ -132,7 +133,8 @@ const Modal = ({ user, shop, isOpen, onClose, modalContent, deviceType, setModal
|
||||
{modalContent === "message" && <Message handleYes={onModalYesFunction} handleNo={handleNo}/>}
|
||||
{modalContent === "player-prompt" && <PlayerPrompt cafeId={shop.cafeId} setModal={setModal} handleClose={handleOverlayClick} welcomePageConfig={shop.welcomePageConfig}/>}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,37 +2,295 @@
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: -1px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
z-index: 2147483647 !important; /* ensure above any app layers */
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modalContent {
|
||||
width: 80vw;
|
||||
max-height: 80vh;
|
||||
width: 100%;
|
||||
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;
|
||||
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;
|
||||
align-items: 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 {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: #333;
|
||||
padding: 0;
|
||||
color: #999;
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
|
||||
|
||||
border-radius: 0 0 15px 15px;
|
||||
z-index: 1;
|
||||
z-index: 0; /* align with item lister */
|
||||
}
|
||||
|
||||
.current-name {
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: 1;
|
||||
text-align: left;
|
||||
margin: 35px 30px;
|
||||
font-size: 16px;
|
||||
@@ -70,7 +70,7 @@
|
||||
.current-artist {
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: 1;
|
||||
text-align: left;
|
||||
margin: -32px 30px;
|
||||
font-size: 18px;
|
||||
@@ -83,7 +83,7 @@
|
||||
.progress-container {
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: 1;
|
||||
text-align: left;
|
||||
margin: 12px 30px;
|
||||
}
|
||||
@@ -318,4 +318,3 @@
|
||||
.search-button.clicked {
|
||||
background-color: #d0c7b3; /* The color when clicked */
|
||||
}
|
||||
|
||||
|
||||
@@ -479,7 +479,7 @@ export function MusicPlayer({ socket, shopId, user, shopOwnerId, isSpotifyNeedLo
|
||||
className={`expandable-container ${expanded ? "expanded" : ""}`}
|
||||
ref={expandableContainerRef}
|
||||
>
|
||||
{user.cafeId == shopId || user.userId == shopOwnerId && (
|
||||
{user.cafeId == shopId || user.user_id == shopOwnerId && (
|
||||
<>
|
||||
<div className="auth-box">
|
||||
<div
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import jsQR from "jsqr";
|
||||
import QrScanner from "qr-scanner"; // Import qr-scanner
|
||||
import { getImageUrl } from "../helpers/itemHelper";
|
||||
import {
|
||||
getCafe,
|
||||
saveCafeDetails,
|
||||
setConfirmationStatus,
|
||||
setOpenBillAvailability
|
||||
} from "../helpers/cafeHelpers";
|
||||
import { getCafe, saveCafeDetails } from "../helpers/cafeHelpers";
|
||||
import Switch from "react-switch"; // Import the Switch component
|
||||
import styles from "./PaymentOptions.module.css";
|
||||
|
||||
const SetPaymentQr = ({ shopId,
|
||||
qrCodeUrl }) => {
|
||||
const [qrPosition, setQrPosition] = useState([50, 50]);
|
||||
const [qrSize, setQrSize] = useState(50);
|
||||
const SetPaymentQr = ({ shopId, qrCodeUrl }) => {
|
||||
const [qrPosition, setQrPosition] = useState([50, 50]); // legacy kept for API compatibility
|
||||
const [qrSize, setQrSize] = useState(50); // legacy kept for API compatibility
|
||||
const [qrPayment, setQrPayment] = useState();
|
||||
const [qrPaymentFile, setQrPaymentFile] = useState();
|
||||
const [qrCodeDetected, setQrCodeDetected] = useState(false);
|
||||
@@ -20,10 +15,15 @@ const SetPaymentQr = ({ shopId,
|
||||
const [isQRISavailable, setIsQRISavailable] = useState(0);
|
||||
const qrPaymentInputRef = useRef(null);
|
||||
const qrCodeContainerRef = useRef(null);
|
||||
|
||||
const [qrCodeData, setQrCodeData] = useState(null);
|
||||
const [cafe, setCafe] = useState({});
|
||||
|
||||
const [isConfigQRIS, setIsConfigQRIS] = useState(false);
|
||||
const [isOpenBillAvailable, setIsOpenBillAvailable] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saveStatus, setSaveStatus] = useState(null); // 'success' | 'error'
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCafe = async () => {
|
||||
@@ -45,12 +45,6 @@ const SetPaymentQr = ({ shopId,
|
||||
fetchCafe();
|
||||
}, [shopId]);
|
||||
|
||||
// Detect QR code when qrPayment updates
|
||||
useEffect(() => {
|
||||
if (qrPayment && isConfigQRIS) {
|
||||
detectQRCodeFromContainer();
|
||||
}
|
||||
}, [qrPayment, isConfigQRIS]);
|
||||
|
||||
// Handle file input change
|
||||
const handleFileChange = (e) => {
|
||||
@@ -62,29 +56,54 @@ const SetPaymentQr = ({ shopId,
|
||||
}
|
||||
};
|
||||
|
||||
// Detect QR code from the container
|
||||
useEffect(() => {
|
||||
if (qrPayment && isConfigQRIS) {
|
||||
detectQRCodeFromContainer();
|
||||
}
|
||||
}, [qrPayment, isConfigQRIS]);
|
||||
|
||||
const detectQRCodeFromContainer = () => {
|
||||
const container = qrCodeContainerRef.current;
|
||||
const canvas = document.createElement("canvas");
|
||||
const context = canvas.getContext("2d");
|
||||
if (!container) return;
|
||||
|
||||
const img = new Image();
|
||||
img.crossOrigin = "Anonymous";
|
||||
img.onload = () => {
|
||||
canvas.width = container.offsetWidth;
|
||||
canvas.height = container.offsetHeight;
|
||||
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const qrCode = jsQR(imageData.data, canvas.width, canvas.height);
|
||||
setQrCodeDetected(!!qrCode);
|
||||
if (qrCode) {
|
||||
console.log("QR Code detected:", qrCode.data);
|
||||
}
|
||||
};
|
||||
img.src = qrPayment;
|
||||
|
||||
img.onload = () => {
|
||||
// Buat canvas dari image
|
||||
const canvas = document.createElement("canvas");
|
||||
const context = canvas.getContext("2d");
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
context.drawImage(img, 0, 0);
|
||||
|
||||
// Ambil data URL dari canvas (png)
|
||||
const imageDataUrl = canvas.toDataURL();
|
||||
|
||||
QrScanner.scanImage(imageDataUrl, { returnDetailedScanResult: true })
|
||||
.then(result => {
|
||||
setQrCodeDetected(true);
|
||||
setQrCodeData(result.data);
|
||||
console.log("QR Code detected:", result.data);
|
||||
})
|
||||
.catch(() => {
|
||||
setQrCodeDetected(false);
|
||||
setQrCodeData(null);
|
||||
console.log("QR Code not detected");
|
||||
});
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
setQrCodeDetected(false);
|
||||
setQrCodeData(null);
|
||||
};
|
||||
};
|
||||
|
||||
// Save cafe details
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
setSaveStatus(null);
|
||||
let qrPaymentFileCache;
|
||||
console.log(qrPaymentFile)
|
||||
if(qrPaymentFile != null)
|
||||
@@ -101,240 +120,141 @@ const SetPaymentQr = ({ shopId,
|
||||
|
||||
try {
|
||||
const response = await saveCafeDetails(cafe.cafeId, details);
|
||||
|
||||
setIsNeedConfirmationState(response.needsConfirmation ? 1 : 0); // Update state after saving
|
||||
setIsQRISavailable(response.isQRISavailable ? 1 : 0); // Update state after saving
|
||||
setIsOpenBillAvailable(response.isOpenBillAvailable ? 1 : 0); // Update state after saving
|
||||
|
||||
setIsNeedConfirmationState(response.needsConfirmation ? 1 : 0);
|
||||
setIsQRISavailable(response.isQRISavailable ? 1 : 0);
|
||||
setIsOpenBillAvailable(response.isOpenBillAvailable ? 1 : 0);
|
||||
setSaveStatus('success');
|
||||
console.log("Cafe details saved:", response);
|
||||
} catch (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 (
|
||||
<div style={styles.container}>
|
||||
<h3 style={styles.title}>Konfigurasi pembayaran</h3>
|
||||
<div className={styles.container}>
|
||||
<h3 className={styles.title}>Konfigurasi pembayaran</h3>
|
||||
|
||||
<div style={styles.switchContainer}>
|
||||
<p style={styles.uploadMessage}>
|
||||
Pembayaran QRIS.
|
||||
</p>
|
||||
<div className={styles.section}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<div>
|
||||
<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
|
||||
id="qr-code-container"
|
||||
ref={qrCodeContainerRef}
|
||||
className={styles.imageBox}
|
||||
onClick={() => qrPaymentInputRef.current.click()}
|
||||
style={{
|
||||
...styles.qrCodeContainer,
|
||||
backgroundImage: `url(${qrPayment})`,
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundSize: "contain",
|
||||
}}
|
||||
style={{ backgroundImage: `url(${qrPayment})` }}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
ref={qrPaymentInputRef}
|
||||
style={{ display: "none" }}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<input type="file" accept="image/*" ref={qrPaymentInputRef} style={{ display: 'none' }} onChange={handleFileChange} />
|
||||
</div>
|
||||
<div style={styles.uploadMessage}>
|
||||
<p>Klik untuk ganti background</p>
|
||||
</div>
|
||||
<div style={styles.resultMessage}>
|
||||
{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 onClick={() => setIsConfigQRIS(false)}
|
||||
|
||||
style={{
|
||||
...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 className={styles.smallNote}>Klik area untuk unggah/ganti gambar QR</div>
|
||||
<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>
|
||||
|
||||
<button className={styles.button} onClick={() => qrPaymentInputRef.current.click()}>{qrPayment ? 'Ganti' : 'Unggah'}</button>
|
||||
</div>
|
||||
{qrCodeDetected && (
|
||||
<div className={styles.copyRow}>
|
||||
<input className={styles.linkField} readOnly value={qrCodeData || ''} />
|
||||
<button className={styles.button} onClick={copyQrData}>{copied ? 'Disalin' : 'Salin'}</button>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.actionsRight}>
|
||||
<button className={`${styles.button} ${styles.primary}`} onClick={() => setIsConfigQRIS(false)}>Terapkan</button>
|
||||
</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
|
||||
onChange={(checked) => setIsOpenBillAvailable(checked ? 1 : 0)}
|
||||
checked={isOpenBillAvailable === 1} // Convert to boolean
|
||||
offColor="#888"
|
||||
onColor="#4CAF50"
|
||||
uncheckedIcon={false}
|
||||
checkedIcon={false}
|
||||
height={25}
|
||||
width={50}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={styles.switchContainer}>
|
||||
<p style={styles.uploadMessage}>
|
||||
Pengecekan ganda
|
||||
</p>
|
||||
<p style={styles.description}>
|
||||
Nyalakan agar kasir memeriksa kembali ketersediaan produk sebelum pelanggan membayar.
|
||||
</p>
|
||||
<Switch
|
||||
onChange={(checked) => setIsNeedConfirmationState(checked ? 1 : 0)}
|
||||
checked={isNeedConfirmationState === 1} // Convert to boolean
|
||||
offColor="#888"
|
||||
onColor="#4CAF50"
|
||||
uncheckedIcon={false}
|
||||
checkedIcon={false}
|
||||
height={25}
|
||||
width={50}
|
||||
/>
|
||||
<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>
|
||||
<Switch
|
||||
onChange={(checked) => setIsOpenBillAvailable(checked ? 1 : 0)}
|
||||
checked={isOpenBillAvailable === 1}
|
||||
offColor="#888"
|
||||
onColor="#4CAF50"
|
||||
uncheckedIcon={false}
|
||||
checkedIcon={false}
|
||||
height={25}
|
||||
width={50}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.buttonContainer}>
|
||||
<button onClick={handleSave} style={styles.saveButton}>
|
||||
Simpan
|
||||
<div className={styles.section}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<div>
|
||||
<div className={styles.sectionTitle}>Pengecekan ganda</div>
|
||||
<div className={styles.sectionDesc}>Kasir memeriksa kembali ketersediaan item sebelum pembayaran.</div>
|
||||
</div>
|
||||
<Switch
|
||||
onChange={(checked) => setIsNeedConfirmationState(checked ? 1 : 0)}
|
||||
checked={isNeedConfirmationState === 1}
|
||||
offColor="#888"
|
||||
onColor="#4CAF50"
|
||||
uncheckedIcon={false}
|
||||
checkedIcon={false}
|
||||
height={25}
|
||||
width={50}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.footer}>
|
||||
<div>
|
||||
{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>
|
||||
</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;
|
||||
|
||||
171
src/components/PaymentOptions.module.css
Normal file
171
src/components/PaymentOptions.module.css
Normal 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
@@ -1,5 +1,5 @@
|
||||
.watermark {
|
||||
z-index: 5;
|
||||
z-index: 0; /* align with item lister */
|
||||
margin-top: 30px;
|
||||
background-color: rgb(222, 237, 100);
|
||||
font-weight: 700;
|
||||
@@ -28,7 +28,7 @@
|
||||
/* Media query for desktop */
|
||||
@media (min-width: 768px) { /* Adjust the min-width as needed */
|
||||
.watermark {
|
||||
z-index: 5;
|
||||
z-index: 0; /* align with item lister */
|
||||
margin-top: 30px;
|
||||
background-color: rgb(222, 237, 100);
|
||||
font-weight: 700;
|
||||
@@ -52,4 +52,4 @@
|
||||
|
||||
.watermarkFooter {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,10 +106,10 @@ export async function getCafeByIdentifier(cafeIdentifyName) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
export async function getOwnedCafes(userId) {
|
||||
export async function getOwnedCafes(user_id) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/cafe/get-cafe-by-ownerId/` + userId,
|
||||
`${API_BASE_URL}/cafe/get-cafe-by-ownerId/` + user_id,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
||||
@@ -5,9 +5,95 @@ body {
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
overflow-x: hidden;
|
||||
background-color: #e9e9e9;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// src/CafePage.js
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import {
|
||||
useParams,
|
||||
useSearchParams,
|
||||
@@ -55,6 +55,7 @@ function CafePage({
|
||||
const [searchParams] = useSearchParams();
|
||||
const token = searchParams.get("token");
|
||||
const { shopIdentifier, tableCode } = useParams();
|
||||
// Send params to parent immediately (original behavior)
|
||||
sendParam({ shopIdentifier, tableCode });
|
||||
|
||||
const {
|
||||
@@ -76,18 +77,63 @@ function CafePage({
|
||||
const [config, setConfig] = useState({});
|
||||
|
||||
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 = () => {
|
||||
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 (cartItemsLength > 0) {
|
||||
setCartBump(true);
|
||||
const t = setTimeout(() => setCartBump(false), 450);
|
||||
return () => clearTimeout(t);
|
||||
}
|
||||
};
|
||||
}, [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(() => {
|
||||
if (welcomePageConfig) {
|
||||
@@ -100,16 +146,16 @@ function CafePage({
|
||||
isActive: parsedConfig.isWelcomePageActive === "true",
|
||||
});
|
||||
}
|
||||
checkWelcomePageConfig();
|
||||
// checkWelcomePageConfig();
|
||||
}, [welcomePageConfig]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
function fetchData() {
|
||||
console.log(user.userId == shopOwnerId)
|
||||
console.log(user.user_id == shopOwnerId)
|
||||
setModal("create_item");
|
||||
|
||||
}
|
||||
|
||||
console.log(getLocalStorage('auth'))
|
||||
if (getLocalStorage("auth") != null) {
|
||||
const executeFetch = async () => {
|
||||
@@ -118,7 +164,7 @@ function CafePage({
|
||||
}
|
||||
console.log(user)
|
||||
console.log('open')
|
||||
if (user.length != 0 && user.userId == shopOwnerId && shopItems.length == 0) fetchData();
|
||||
if (user.length != 0 && user.user_id == shopOwnerId && shopItems.length == 0) fetchData();
|
||||
};
|
||||
executeFetch();
|
||||
}
|
||||
@@ -234,6 +280,8 @@ function CafePage({
|
||||
removeConnectedGuestSides={removeConnectedGuestSides}
|
||||
setIsEditMode={(e) => setIsEditMode(e)}
|
||||
isEditMode={isEditMode}
|
||||
zIndexLevel={9000}
|
||||
rectZIndex={9000}
|
||||
/>
|
||||
<MusicPlayer
|
||||
socket={socket}
|
||||
@@ -307,29 +355,33 @@ function CafePage({
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{!isEditMode && (user.username || cartItemsLength > 0) &&
|
||||
<div style={{ marginTop: '10px', height: '40px', position: 'sticky', bottom: '40px', display: 'flex', justifyContent: 'center', alignItems: 'center', textAlign: 'center' }}>
|
||||
{showBar &&
|
||||
<div className={`StickyCartBar${barIntro ? ' intro' : ''}`}>
|
||||
{(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 style={{ display: 'flex', flexWrap: 'wrap', alignContent: 'center' }}>{lastTransaction != null && '+'}{cartItemsLength} item</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', width: '130px' }}>
|
||||
<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 className="summary">
|
||||
{(lastTransaction != null) && <span>+</span>}
|
||||
<span>{cartItemsLength} item</span>
|
||||
</div>
|
||||
<div className="summary" style={{ gap: 6 }}>
|
||||
{((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' }}>
|
||||
<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>
|
||||
{cartItemsLength > 0 && <span className={`badge${cartBump ? ' pop' : ''}`}>{cartItemsLength > 9 ? '9+' : cartItemsLength}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{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 style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '38px', marginRight: '5px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', marginLeft: '5px', width: '20px' }}>
|
||||
<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: 38, marginRight: 6 }}>
|
||||
<div className="icon">
|
||||
<svg viewBox="0 0 512 512">
|
||||
<g
|
||||
transform="translate(0 460) scale(0.09 -0.09)"
|
||||
@@ -377,3 +429,4 @@ function CafePage({
|
||||
}
|
||||
|
||||
export default CafePage;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
import styles from "./Invoice.module.css";
|
||||
import cartStyles from "./CartPage.module.css";
|
||||
import { useParams } from "react-router-dom"; // Changed from useSearchParams to useLocation
|
||||
import { ThreeDots, ColorRing } from "react-loader-spinner";
|
||||
|
||||
@@ -272,6 +273,8 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
|
||||
socketId
|
||||
);
|
||||
localStorage.removeItem('lastTransaction')
|
||||
// Dispatch the custom event
|
||||
window.dispatchEvent(new Event("localStorageUpdated"));
|
||||
}
|
||||
else
|
||||
|
||||
@@ -382,7 +385,13 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
|
||||
return (
|
||||
<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) ?
|
||||
<div style={{ height: '75vh', display: 'flex', justifyContent: 'center', flexDirection: 'column', alignContent: 'center', alignItems: 'center' }}>
|
||||
@@ -441,21 +450,21 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.NoteContainer}>
|
||||
<span>Catatan :</span>
|
||||
<span></span>
|
||||
</div>
|
||||
|
||||
<div className={styles.NoteContainer}>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className={styles.NoteInput}
|
||||
placeholder="Tambahkan catatan..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{getItemsByCafeId(shopId).length > 0 && (
|
||||
<div className={`${styles.RoundedRectangle} ${cartStyles.sectionCard}`}>
|
||||
<div className={cartStyles.sectionTitle}>Catatan Untuk Kasir</div>
|
||||
<div className={cartStyles.divider}></div>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className={styles.NoteInput}
|
||||
placeholder="Contoh: tanpa gula, ekstra es, dsb."
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{transactionData &&
|
||||
<div className={styles.RoundedRectangle} style={{ backgroundColor: '#c3c3c3', fontSize: '15px', display: 'flex', justifyContent: 'space-between' }}>
|
||||
{transactionData.payment_type != 'paylater' ?
|
||||
@@ -573,6 +582,7 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
.CheckoutContainer {
|
||||
bottom: 0px;
|
||||
position: fixed;
|
||||
z-index: 100; /* Menurunkan z-index agar tidak menutupi material list */
|
||||
}
|
||||
|
||||
.EmailContainer {
|
||||
@@ -111,4 +112,4 @@
|
||||
font-size: 1em;
|
||||
margin-bottom: 25px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
68
src/pages/CartPage.module.css
Normal file
68
src/pages/CartPage.module.css
Normal 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;
|
||||
}
|
||||
@@ -1,119 +1,153 @@
|
||||
import React, { useState } from 'react';
|
||||
import { createClerks } from '../helpers/userHelpers'; // Adjust the import path as needed
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { createClerks, getClerks } from '../helpers/userHelpers';
|
||||
import { useLocation } from "react-router-dom";
|
||||
import styles from './CreateClerk.module.css';
|
||||
|
||||
const CreateClerk = ({ shopId }) => {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [banner, setBanner] = useState(null); // { type: 'success'|'error', text: string }
|
||||
const [clerks, setClerks] = useState([]);
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
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) => {
|
||||
event.preventDefault();
|
||||
setLoading(true);
|
||||
setMessage('');
|
||||
setSaving(true);
|
||||
setBanner(null);
|
||||
|
||||
// Basic validation
|
||||
if (!username || !password) {
|
||||
setMessage('Username and password are required');
|
||||
setLoading(false);
|
||||
setBanner({ type: 'error', text: 'Username dan password wajib diisi' });
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
const create = await createClerks(shopId || cafeIdParam, username, password);
|
||||
|
||||
if (create) setMessage('Clerk created successfully');
|
||||
else setMessage('Failed to create clerk');
|
||||
const create = await createClerks(effectiveShopId, username, password);
|
||||
if (create) {
|
||||
setBanner({ type: 'success', text: 'Kasir berhasil ditambahkan' });
|
||||
// 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) {
|
||||
setMessage('Error creating clerk');
|
||||
setBanner({ type: 'error', text: 'Terjadi kesalahan saat menambahkan kasir' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<h2 style={styles.header}>Tambah Kasir</h2>
|
||||
<form onSubmit={handleSubmit} style={styles.form}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
style={styles.input}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
style={styles.input}
|
||||
/>
|
||||
<button type="submit" style={styles.button} disabled={loading}>
|
||||
{loading ? 'Creating...' : 'Create Clerk'}
|
||||
</button>
|
||||
{message && (
|
||||
<p style={{ ...styles.message, color: message.includes('success') ? 'green' : 'red' }}>
|
||||
{message}
|
||||
</p>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<h2 className={styles.title}>Tambah Kasir</h2>
|
||||
{banner && (
|
||||
<div className={`${styles.banner} ${banner.type === 'success' ? styles.bannerSuccess : styles.bannerError}`}>
|
||||
{banner.text}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</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
|
||||
className={styles.input}
|
||||
type="text"
|
||||
placeholder="kasir_baru"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label}>Password</label>
|
||||
<div className={styles.pwdRow}>
|
||||
<input
|
||||
className={styles.input}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="••••••"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<button type="button" className={styles.button} onClick={()=>setShowPassword(!showPassword)}>
|
||||
{showPassword ? 'Sembunyikan' : 'Tampilkan'}
|
||||
</button>
|
||||
<button type="button" className={styles.button} onClick={generatePassword}>
|
||||
Generate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
<button className={`${styles.button} ${styles.primary}`} type="submit" disabled={saving}>
|
||||
{saving ? 'Menambahkan…' : 'Tambah Kasir'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</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;
|
||||
|
||||
137
src/pages/CreateClerk.module.css
Normal file
137
src/pages/CreateClerk.module.css
Normal 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; }
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ const Dashboard = ({ user, setModal }) => {
|
||||
// Create admin functionality
|
||||
createCafeOwner(newItem.email, newItem.username, newItem.password)
|
||||
.then((newitem) => {
|
||||
setItems([...items, { userId: newitem.userId, name: newitem.username }]);
|
||||
setItems([...items, { user_id: newitem.user_id, name: newitem.username }]);
|
||||
setIsCreating(false);
|
||||
setNewItem({ name: "", type: "" });
|
||||
})
|
||||
|
||||
@@ -71,9 +71,9 @@ const LinktreePage = ({ user, setModal }) => {
|
||||
|
||||
// Handle manual coupon code check
|
||||
const handleGetkCoupons = async () => {
|
||||
const result = await getUserCoupons();
|
||||
setCoupons(result.coupons);
|
||||
console.log(result)
|
||||
// const result = await getUserCoupons();
|
||||
// setCoupons(result.coupons);
|
||||
// console.log(result)
|
||||
};
|
||||
|
||||
// Handle user transactions
|
||||
@@ -95,24 +95,20 @@ const LinktreePage = ({ user, setModal }) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Handle login
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
setError(false);
|
||||
setLoading(true);
|
||||
const response = await loginUser(username, password);
|
||||
if (response.success) {
|
||||
localStorage.setItem('auth', response.token);
|
||||
console.log(response)
|
||||
window.location.href = response.cafeIdentifyName ? `/${response.cafeIdentifyName}` : '/';
|
||||
} else {
|
||||
setError(true);
|
||||
}
|
||||
} catch (error) {
|
||||
setError(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
const handleLogin = () => {
|
||||
const baseUrl = "https://kediritechnopark.com/";
|
||||
const modal = "product";
|
||||
const productId = 1;
|
||||
|
||||
const authorizedUri = "http://localhost:3000?token=";
|
||||
const unauthorizedUri = `${baseUrl}?modal=${modal}&product_id=${productId}`;
|
||||
|
||||
const url =
|
||||
`${baseUrl}?modal=${modal}&product_id=${productId}` +
|
||||
`&authorized_uri=${encodeURIComponent(authorizedUri)}` +
|
||||
`&unauthorized_uri=${encodeURIComponent(unauthorizedUri)}`;
|
||||
|
||||
window.location.href = url;
|
||||
};
|
||||
|
||||
// Handle logout
|
||||
@@ -152,7 +148,7 @@ const LinktreePage = ({ user, setModal }) => {
|
||||
try {
|
||||
if (user.roleId < 1) {
|
||||
const newOwner = await createCafeOwner(newItem.email, newItem.username, newItem.password);
|
||||
setItems([...items, { userId: newOwner.userId, name: newOwner.username }]);
|
||||
setItems([...items, { user_id: newOwner.user_id, name: newOwner.username }]);
|
||||
} else {
|
||||
const newCafe = await createCafe(newItem.name);
|
||||
setItems([...items, { cafeId: newCafe.cafeId, name: newCafe.name }]);
|
||||
@@ -202,7 +198,7 @@ const LinktreePage = ({ user, setModal }) => {
|
||||
];
|
||||
console.log(items)
|
||||
|
||||
const selectedItems = items?.items?.find(item => (item.userId || item.cafeId) === selectedItemId);
|
||||
const selectedItems = items?.items?.find(item => (item.user_id || item.cafeId) === selectedItemId);
|
||||
|
||||
// If the selected tenant is found, extract the cafes
|
||||
const selectedSubItems = selectedItems?.subItems || [];
|
||||
@@ -278,7 +274,7 @@ const LinktreePage = ({ user, setModal }) => {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
gratis 3 bulan pertama
|
||||
gratis 1 bulan pertama
|
||||
</div>
|
||||
:
|
||||
<div className={styles.mainHeading}>
|
||||
@@ -290,57 +286,20 @@ const LinktreePage = ({ user, setModal }) => {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
Gratis 3 bulan pertama
|
||||
Gratis 1 bulan pertama
|
||||
</div>
|
||||
}
|
||||
<div className={styles.subHeading}>
|
||||
Solusi berbasis web untuk memudahkan pengelolaan kedai, dengan fitur yang mempermudah pemilik, kasir, dan tamu berinteraksi.
|
||||
</div>
|
||||
|
||||
{getLocalStorage('auth') == null && (
|
||||
<div className={styles.LoginForm}>
|
||||
<div className={`${styles.FormUsername} ${inputtingPassword ? styles.animateForm : wasInputtingPassword ? styles.reverseForm : ''}`}>
|
||||
<label htmlFor="username" className={styles.usernameLabel}>---- Masuk -----------------------------</label>
|
||||
<input
|
||||
id="username"
|
||||
placeholder="username"
|
||||
maxLength="30"
|
||||
className={!error ? styles.usernameInput : styles.usernameInputError}
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<button onClick={() => { setInputtingPassword(true); setWasInputtingPassword(true) }} className={styles.claimButton}>
|
||||
<span>➜</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.FormPassword} ${inputtingPassword ? styles.animateForm : wasInputtingPassword ? styles.reverseForm : styles.idleForm}`}>
|
||||
<span>
|
||||
<label onClick={() => setInputtingPassword(false)} htmlFor="password" className={styles.usernameLabel}> <--- <-- Kembali </label>
|
||||
<label htmlFor="password" className={styles.usernameLabel}> ----- </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>
|
||||
<div className={`${styles.FormUsername} ${inputtingPassword ? styles.animateForm : wasInputtingPassword ? styles.reverseForm : ''}`}>
|
||||
<button onClick={() => handleLogin()} className={styles.claimButton}>
|
||||
<span>Masuk</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.footer}>
|
||||
<div className={styles.footerLinks}>
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: rgba(88, 55, 50, 1);
|
||||
background-color: #e9e9e9;
|
||||
position: relative;
|
||||
z-index: 50; /* Memastikan berada di bawah material list */
|
||||
}
|
||||
|
||||
.Invoice-title {
|
||||
@@ -45,6 +47,8 @@
|
||||
bottom: 0;
|
||||
right: 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 {
|
||||
@@ -159,6 +163,7 @@
|
||||
padding: 10px 0;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.OrderTypeContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -110,7 +110,7 @@ const LinktreePage = ({ data, setModal }) => {
|
||||
</div>
|
||||
<div className={styles.linktreeForm}>
|
||||
<button onClick={()=>window.open("https://api.whatsapp.com/send?phone=6281318894994&text=Saya%20ingin%20coba%20gratis%203%20bulan")} className={styles.claimButton}>
|
||||
<span>Dapatkan voucher gratis 3 bulan</span>
|
||||
<span>Dapatkan voucher gratis 1 bulan</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
|
||||
@@ -338,7 +338,6 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.footerLinks {
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import jsQR from "jsqr";
|
||||
import { getImageUrl } from "../helpers/itemHelper";
|
||||
import {
|
||||
getCafe,
|
||||
saveCafeDetails,
|
||||
setConfirmationStatus,
|
||||
} from "../helpers/cafeHelpers";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
getMaterials,
|
||||
createMaterial,
|
||||
@@ -17,347 +9,497 @@ import {
|
||||
getMaterialMutations,
|
||||
} from "../helpers/materialMutationHelpers";
|
||||
|
||||
import Switch from "react-switch"; // Import the Switch component
|
||||
import Carousel from '../components/Carousel'
|
||||
import styles from './MaterialList.module.css'; // Import the CSS Module
|
||||
import styles from './MaterialList.module.css';
|
||||
|
||||
const SetPaymentQr = ({ cafeId }) => {
|
||||
// All your state and logic goes here (unchanged)
|
||||
const MaterialList = ({ cafeId }) => {
|
||||
// State declarations
|
||||
const [materials, setMaterials] = 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 [error, setError] = useState(null);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [selectedMaterialIndex, setSelectedMaterialIndex] = useState(-1);
|
||||
const [latestMutation, setLatestMutation] = useState([]);
|
||||
const [currentQuantity, setCurrentQuantity] = useState(-1);
|
||||
const [currentPrice, setCurrentPrice] = useState(0);
|
||||
const [newPrice, setNewPrice] = useState(0);
|
||||
const [quantityChange, setQuantityChange] = useState(0);
|
||||
|
||||
// Add material form state
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const [newMaterialName, setNewMaterialName] = useState("");
|
||||
const [newMaterialUnit, setNewMaterialUnit] = useState("kilogram");
|
||||
|
||||
// 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 [isEditCurrentPrice, setIsEditCurrentPrice] = useState(false);
|
||||
const [isViewingHistory, setIsViewingHistory] = useState(false);
|
||||
const [isViewingHistory, setIsViewingHistory] = useState({});
|
||||
|
||||
// Format currency helper
|
||||
const formatCurrency = (value) => {
|
||||
if (!value) return "0";
|
||||
return parseInt(value.toString().replace(/\./g, "")).toLocaleString('id-ID');
|
||||
};
|
||||
|
||||
const convertToInteger = (formattedValue) => {
|
||||
// Remove dots and convert to integer
|
||||
return parseInt(formattedValue.replace(/\./g, ""), 10);
|
||||
return parseInt(formattedValue.replace(/\./g, ""), 10) || 0;
|
||||
};
|
||||
|
||||
const formatCurrency = (value) => {
|
||||
if (!value) return "";
|
||||
// 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) => {
|
||||
// Handle price input change
|
||||
const handlePriceChange = (e) => {
|
||||
const formattedValue = formatCurrency(e.target.value);
|
||||
setNewPrice(formattedValue);
|
||||
};
|
||||
|
||||
// Fetch materials and mutations
|
||||
useEffect(() => {
|
||||
const fetchMaterials = async () => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const data = await getMaterials(cafeId);
|
||||
setMaterials(data);
|
||||
console.log(data)
|
||||
const [materialsData, mutationsData] = await Promise.all([
|
||||
getMaterials(cafeId),
|
||||
getMaterialMutations(cafeId)
|
||||
]);
|
||||
|
||||
setMaterials(materialsData);
|
||||
setMutations(mutationsData);
|
||||
setError(null);
|
||||
if (data.length > 0) {
|
||||
setSelectedMaterialIndex(0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching materials:", error);
|
||||
setError("Failed to fetch materials.");
|
||||
}
|
||||
};
|
||||
|
||||
const fetchMutations = async () => {
|
||||
try {
|
||||
const data = await getMaterialMutations(cafeId);
|
||||
setMutations(data);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
console.error("Error fetching data:", err);
|
||||
setError("Gagal memuat data bahan baku.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchMaterials();
|
||||
fetchMutations();
|
||||
if (cafeId) {
|
||||
fetchData();
|
||||
}
|
||||
}, [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
|
||||
.filter((mutation) => mutation.materialId === materials[selectedMaterialIndex].materialId)
|
||||
.sort((a, b) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
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") {
|
||||
return new Date(a.createdAt) - new Date(b.createdAt);
|
||||
} else {
|
||||
return new Date(b.createdAt) - new Date(a.createdAt);
|
||||
}
|
||||
});
|
||||
|
||||
const handleCreateMaterial = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
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) => {
|
||||
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
|
||||
);
|
||||
// Format date for display
|
||||
const formatDate = (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 (
|
||||
<div className={styles.container}>
|
||||
{loading ? (
|
||||
<></>
|
||||
) : (
|
||||
<>
|
||||
<h3 className={styles.title}>Bahan baku</h3>
|
||||
<Carousel items={materials} onSelect={(e) => setSelectedMaterialIndex(e)} selectedIndex={selectedMaterialIndex} />
|
||||
{selectedMaterialIndex !== -1 ? (
|
||||
<>
|
||||
<div className={styles.switchContainer}>
|
||||
<h3>Stok sekarang {currentQuantity}</h3>
|
||||
</div>
|
||||
<div className={styles.header}>
|
||||
<h2 className={styles.title}>Manajemen Bahan Baku</h2>
|
||||
<button
|
||||
className={styles.addButton}
|
||||
onClick={() => setShowAddForm(!showAddForm)}
|
||||
>
|
||||
{showAddForm ? 'Batal' : '+ Tambah Bahan Baku'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.stokContainer}>
|
||||
<button onClick={() => handleQuantityChange(currentQuantity + quantityChange > 0 ? -1 : 0)} className={styles.stockButton}>
|
||||
-
|
||||
</button>
|
||||
<p>{currentQuantity + quantityChange}</p>
|
||||
<button onClick={() => handleQuantityChange(1)} className={styles.stockButton}>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.uploadMessage}>
|
||||
<p>harga per {materials && materials[selectedMaterialIndex]?.unit} sekarang</p>
|
||||
</div>
|
||||
<div className={styles.resultMessage}>
|
||||
<input
|
||||
className={styles.resultMessageInput} // Replace inline style with CSS module class
|
||||
disabled={!isEditCurrentPrice || quantityChange < 1}
|
||||
value={newPrice}
|
||||
onChange={handleChange}
|
||||
placeholder="Enter amount"
|
||||
/>
|
||||
<div onClick={() => quantityChange < 1 ? null : setIsEditCurrentPrice(!isEditCurrentPrice)} className={quantityChange < 1 ? styles.changeButtonDisabled : styles.changeButtonEnabled}>
|
||||
{isEditCurrentPrice ? 'Terapkan' : 'Ganti'}
|
||||
<div className={styles.content}>
|
||||
{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.buttonContainer}>
|
||||
<button onClick={handleUpdateStock} className={styles.saveButton}>
|
||||
Laporkan {quantityChange > 0 ? 'penambahan' : 'stok sekarang'} {quantityChange < 1 ? currentQuantity + quantityChange : quantityChange} {materials[selectedMaterialIndex]?.unit}
|
||||
|
||||
<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>
|
||||
<div className={styles.historyTab}>
|
||||
<h3 onClick={() => setIsViewingHistory(!isViewingHistory)}> {isViewingHistory ? '˅' : '˃'} Riwayat stok</h3>
|
||||
{selectedMaterialIndex !== -1 && isViewingHistory && !loading && (
|
||||
<>
|
||||
<div className={styles.sorter} onClick={() => setSortOrder(sortOrder == 'asc' ? 'desc' : 'asc')}>
|
||||
Urutkan: {sortOrder === 'asc' ? "terlama" : "terbaru"} <div style={{ transform: 'rotate(90deg)' }}><></div>
|
||||
</div>
|
||||
<div className={styles.historyContainer}>
|
||||
<div className={styles.mutationContainer}>
|
||||
{sortedMutations.length > 0 ? (
|
||||
sortedMutations.map((mutation) => (
|
||||
<div key={mutation.id} className={styles.mutationCard}>
|
||||
<div style={{ width: '42px', backgroundColor: '#b9b9b9', borderRadius: '10px', padding: '3px', paddingBottom: '0' }}>
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<g id="Interface / Book">
|
||||
<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>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</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>
|
||||
))
|
||||
) : (
|
||||
<p>Tidak ada laporan perubahan stok.</p>
|
||||
)}
|
||||
</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>
|
||||
<div className={styles.quantityDisplay}>
|
||||
{newQuantity > 0 ? `+${newQuantity}` : newQuantity}
|
||||
</div>
|
||||
<button
|
||||
className={styles.quantityButton}
|
||||
onClick={() => handleQuantityChange(1)}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.formLabel}>Harga per {material.unit}</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.formInput}
|
||||
value={newPrice}
|
||||
onChange={handlePriceChange}
|
||||
placeholder="Masukkan harga"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formActions}>
|
||||
<button
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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.sortControls}>
|
||||
<button
|
||||
className={styles.sortButton}
|
||||
onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}
|
||||
>
|
||||
Urutkan: {sortOrder === 'asc' ? 'Terlama' : 'Terbaru'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.historyItems}>
|
||||
{getFilteredMutations(material.materialId).length > 0 ? (
|
||||
getFilteredMutations(material.materialId).map((mutation) => (
|
||||
<div key={mutation.id} className={styles.historyItem}>
|
||||
<div className={styles.historyIcon}>📊</div>
|
||||
<div className={styles.historyContent}>
|
||||
<div className={styles.historyDate}>{formatDate(mutation.createdAt)}</div>
|
||||
<div className={styles.historyDetails}>
|
||||
Stok: {mutation.newStock} {material.unit} |
|
||||
Perubahan: {mutation.newStock - mutation.oldStock > 0 ? '+' : ''}{mutation.newStock - mutation.oldStock} |
|
||||
Harga: Rp {formatCurrency(mutation.priceAtp)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className={styles.emptyState} style={{ padding: '20px' }}>
|
||||
<div>Belum ada riwayat perubahan stok</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 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>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetPaymentQr;
|
||||
export default MaterialList;
|
||||
@@ -1,149 +1,556 @@
|
||||
/* SetPaymentQr.module.css */
|
||||
/* MaterialList.module.css */
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
min-height: 47vh;
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
background-color: #ffffff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
padding: 0;
|
||||
margin-bottom: 24px;
|
||||
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 */
|
||||
}
|
||||
|
||||
.container:hover {
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 24px;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: #ffffff;
|
||||
z-index: 201; /* Memastikan header berada di atas container */
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.addButton {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
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;
|
||||
color: #666;
|
||||
margin: 0 16px 0 0;
|
||||
}
|
||||
|
||||
.expandIcon {
|
||||
font-size: 18px;
|
||||
color: #28a745;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.expandIcon.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* 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;
|
||||
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;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.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;
|
||||
align-items: center;
|
||||
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;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 20px;
|
||||
font-weight: bold;
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.uploadMessage {
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
.detailGrid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.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;
|
||||
.formGrid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.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;
|
||||
.historyItems {
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.resultMessage {
|
||||
margin-top: -13px;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.materialSummary {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.resultMessage input {
|
||||
padding-left: 8px;
|
||||
width: 180px;
|
||||
.materialName {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.stokContainer {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
margin-top: -20px;
|
||||
margin-bottom: -15px;
|
||||
text-align: left;
|
||||
.materialStock {
|
||||
font-size: 13px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
margin-top: 11px;
|
||||
text-align: left;
|
||||
.container {
|
||||
max-height: 75vh; /* Menaikkan tinggi maksimum pada mobile */
|
||||
}
|
||||
|
||||
.stockButton {
|
||||
padding: 10px 20px;
|
||||
font-size: 3.5vw;
|
||||
background-color: #28a745;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 30px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.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;
|
||||
margin: 10px 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.unit {
|
||||
margin-top: 11px;
|
||||
width: 100%;
|
||||
height: 31px;
|
||||
}
|
||||
|
||||
.sorter {
|
||||
border: 1px solid #c3c3c3;
|
||||
padding: 5px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: -10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mutationCard {
|
||||
display: flex;
|
||||
margin-bottom: 7px;
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
.mutationTitle {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.mutationTitle h4 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mutationTitle p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ const RoundedRectangle = ({
|
||||
};
|
||||
|
||||
const percentageStyle = {
|
||||
fontSize: "16px",
|
||||
fontSize: "14px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
textAlign: "right",
|
||||
@@ -282,11 +282,11 @@ const App = ({ forCafe = true, cafeId = -1,
|
||||
if (amount >= 1_000_000_000) {
|
||||
// Format for billions
|
||||
const billions = amount / 1_000_000_000;
|
||||
return billions.toFixed(0) + "b"; // No decimal places for billions
|
||||
return billions.toFixed(0) + "m"; // No decimal places for billions
|
||||
} else if (amount >= 1_000_000) {
|
||||
// Format for millions
|
||||
const millions = amount / 1_000_000;
|
||||
return millions.toFixed(2).replace(/\.00$/, "") + "m"; // Two decimal places, remove trailing '.00'
|
||||
return millions.toFixed(2).replace(/\.00$/, "") + "jt"; // Two decimal places, remove trailing '.00'
|
||||
} else if (amount >= 1_000) {
|
||||
// Format for thousands
|
||||
const thousands = amount / 1_000;
|
||||
@@ -326,7 +326,7 @@ const App = ({ forCafe = true, cafeId = -1,
|
||||
setSelectedCafeId(-1);
|
||||
} else if (otherCafes.length === 1) {
|
||||
updatedFullTexts = [
|
||||
[otherCafes[0].cafeIdentifyName || otherCafes[0].username, otherCafes[0].cafeId || otherCafes[0].userId],
|
||||
[otherCafes[0].cafeIdentifyName || otherCafes[0].username, otherCafes[0].cafeId || otherCafes[0].user_id],
|
||||
// Only add the "Buat Bisnis" option for user.roleId == 1
|
||||
...(user.roleId == 1 ? [["Buat Bisnis", -1]] : [])
|
||||
];
|
||||
@@ -335,7 +335,7 @@ const App = ({ forCafe = true, cafeId = -1,
|
||||
} else {
|
||||
updatedFullTexts = [
|
||||
["semua", 0], // First entry is "semua"
|
||||
...otherCafes.map(item => [item.cafeIdentifyName || item.username, item.cafeId || item.userId]), // Map over cafes to get name and cafeId pairs
|
||||
...otherCafes.map(item => [item.cafeIdentifyName || item.username, item.cafeId || item.user_id]), // Map over cafes to get name and cafeId pairs
|
||||
// Only add "Buat Bisnis +" option for user.roleId == 1
|
||||
...(user.roleId == 1 ? [["Buat Bisnis +", -1]] : [])
|
||||
];
|
||||
@@ -411,10 +411,10 @@ const App = ({ forCafe = true, cafeId = -1,
|
||||
|
||||
console.log(analytics)
|
||||
if (user && user.roleId === 0 && analytics) {
|
||||
// Filter the analytics items based on userId
|
||||
// Filter the analytics items based on user_id
|
||||
if(selectedItem[1] != 0 && selectedItem[1] != -1){
|
||||
const filteredData = analytics.items.filter(
|
||||
(data) => data.userId === nextSelectedId
|
||||
(data) => data.user_id === nextSelectedId
|
||||
);
|
||||
|
||||
// Extract coupons from the filtered data
|
||||
|
||||
76
src/pages/StickyCartBar.module.css
Normal file
76
src/pages/StickyCartBar.module.css
Normal 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; }
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { getTables } from "../helpers/tableHelper";
|
||||
import TableCanvas from "../components/TableCanvas";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
export default function Transactions({ propsShopId, sendParam, deviceType, handleMoveToTransaction, depth, shopImg }) {
|
||||
export default function Transactions({ propsShopId, sendParam, deviceType, handleMoveToTransaction, depth, shopImg, setModal }) {
|
||||
const { shopId, tableId } = useParams();
|
||||
if (sendParam) sendParam({ shopId, tableId });
|
||||
|
||||
@@ -231,13 +231,29 @@ export default function Transactions({ propsShopId, sendParam, deviceType, handl
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!transaction.is_paid && transaction.confirmed > -1 &&
|
||||
<div
|
||||
onClick={() => {
|
||||
localStorage.setItem('lastTransaction', JSON.stringify(transaction));
|
||||
setModal("message", { captMessage: 'Silahkan tambahkan pesanan', descMessage: 'Pembayaran akan ditambahkan ke transaksi sebelumnya.' }, null, null);
|
||||
|
||||
// Dispatch the custom event
|
||||
window.dispatchEvent(new Event("localStorageUpdated"));
|
||||
}}
|
||||
className={styles["addNewItem"]}
|
||||
>
|
||||
Tambah pesanan
|
||||
</div>
|
||||
}
|
||||
|
||||
<h2 className={styles["Transactions-detail"]}>
|
||||
{transaction.serving_type === "pickup"
|
||||
? "Ambil sendiri"
|
||||
: `Diantar ke ${transaction.Table ? transaction.Table.tableNo : "N/A"
|
||||
}`}
|
||||
</h2>
|
||||
{transaction.notes != null && (
|
||||
{transaction.notes != '' && (
|
||||
<>
|
||||
<div className={styles.NoteContainer}>
|
||||
<span>Note :</span>
|
||||
|
||||
@@ -192,7 +192,7 @@ export default function Transactions({
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{transaction.payment_type != 'paylater/cash' && transaction.payment_type != 'paylater/cashless' &&
|
||||
{(transaction.payment_type != 'paylater/cash' && transaction.payment_type != 'paylater/cashless') &&
|
||||
<div
|
||||
onClick={() => {
|
||||
localStorage.setItem('lastTransaction', JSON.stringify(transaction));
|
||||
|
||||
@@ -8,100 +8,121 @@ import {
|
||||
declineTransaction,
|
||||
getTransactionsFromCafe,
|
||||
} from "../helpers/transactionHelpers";
|
||||
import { getTables } from "../helpers/tableHelper";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import { ThreeDots } from "react-loader-spinner";
|
||||
|
||||
import ButtonWithReplica from "../components/ButtonWithReplica";
|
||||
import TableCanvas from "../components/TableCanvas";
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
|
||||
export default function Transactions({ shop, shopId, propsShopId, sendParam, deviceType, paymentUrl }) {
|
||||
export default function Transactions({ shop, shopId, propsShopId, sendParam, deviceType, paymentUrl, setModal, newTransaction }) {
|
||||
const { shopIdentifier, tableId } = useParams();
|
||||
if (sendParam) sendParam({ shopIdentifier, tableId });
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
const [transactions, setTransactions] = useState([]);
|
||||
const [isPaymentLoading, setIsPaymentLoading] = useState(false);
|
||||
const [isPaymentOpen, setIsPaymentOpen] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [matchedItems, setMatchedItems] = useState([]);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 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(() => {
|
||||
const fetchTransactions = async () => {
|
||||
if (deviceType == 'clerk') {
|
||||
try {
|
||||
let response;
|
||||
response = await getTransactionsFromCafe(shopId || propsShopId, 5, false);
|
||||
console.log(response)
|
||||
if (response) {
|
||||
setTransactions(response);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching transactions:", error);
|
||||
}
|
||||
}
|
||||
else {
|
||||
try {
|
||||
let response;
|
||||
response = await getMyTransactions(shopId || propsShopId, 5);
|
||||
console.log(response)
|
||||
const combinedTransactions = [];
|
||||
try {
|
||||
|
||||
response.forEach(cafe => {
|
||||
const { cafeId, name: cafeName, transactions } = cafe;
|
||||
// response = await getMyTransactions(shopId || propsShopId, 5);
|
||||
// setMyTransactions(response);
|
||||
setLoading(true);
|
||||
let response = await getTransactionsFromCafe(shopId || propsShopId, -1, false);
|
||||
|
||||
transactions.forEach(transaction => {
|
||||
const newTransaction = {
|
||||
...transaction,
|
||||
cafeId,
|
||||
cafeName,
|
||||
DetailedTransactions: transaction.detailedTransactions // Rename here
|
||||
};
|
||||
delete newTransaction.detailedTransactions; // Remove the old key
|
||||
combinedTransactions.push(newTransaction);
|
||||
});
|
||||
});
|
||||
|
||||
// combinedTransactions now contains all transactions with cafe info and renamed key
|
||||
console.log(combinedTransactions)
|
||||
|
||||
// combinedTransactions now contains all transactions with cafe info
|
||||
setTransactions(combinedTransactions);
|
||||
} catch (error) {
|
||||
console.error("Error fetching transactions:", error);
|
||||
}
|
||||
setLoading(false);
|
||||
if (response) setTransactions(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching transactions:", error);
|
||||
}
|
||||
};
|
||||
|
||||
console.log(deviceType)
|
||||
fetchTransactions();
|
||||
}, [deviceType]);
|
||||
}, [deviceType, newTransaction]);
|
||||
|
||||
const calculateTotalPrice = (detailedTransactions) => {
|
||||
return detailedTransactions.reduce((total, dt) => {
|
||||
return total + dt.qty * (dt.promoPrice ? dt.promoPrice : dt.price);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const calculateAllTransactionsTotal = (transactions) => {
|
||||
return transactions.reduce((grandTotal, transaction) => {
|
||||
const calculateAllTransactionsTotal = (transactions) => {
|
||||
return transactions
|
||||
.filter(transaction => transaction.confirmed > 1) // Filter transactions where confirmed > 1
|
||||
.reduce((grandTotal, transaction) => {
|
||||
return grandTotal + calculateTotalPrice(transaction.DetailedTransactions);
|
||||
}, 0);
|
||||
};
|
||||
};
|
||||
const searchAndAggregateItems = (transactions, searchTerm) => {
|
||||
if (!searchTerm.trim()) return [];
|
||||
|
||||
const normalizedTerm = searchTerm.trim().toLowerCase();
|
||||
// Map with key = `${itemId}-${confirmedGroup}` to keep confirmed groups separate
|
||||
const aggregatedItems = new Map();
|
||||
|
||||
transactions.forEach(transaction => {
|
||||
// Determine confirmed group as a string key
|
||||
const confirmedGroup = transaction.confirmed >= 0 && transaction.confirmed > 1 ? 'confirmed_gt_1' : 'confirmed_le_1';
|
||||
|
||||
transaction.DetailedTransactions.forEach(detail => {
|
||||
const itemName = detail.Item.name;
|
||||
const itemNameLower = itemName.toLowerCase();
|
||||
|
||||
if (itemNameLower.includes(normalizedTerm)) {
|
||||
// Combine itemId and confirmedGroup to keep them separated
|
||||
const key = `${detail.itemId}-${confirmedGroup}`;
|
||||
|
||||
if (!aggregatedItems.has(key)) {
|
||||
aggregatedItems.set(key, {
|
||||
itemId: detail.itemId,
|
||||
name: itemName,
|
||||
totalQty: 0,
|
||||
totalPrice: 0,
|
||||
confirmedGroup, // Keep track of which group this belongs to
|
||||
});
|
||||
}
|
||||
|
||||
const current = aggregatedItems.get(key);
|
||||
current.totalQty += detail.qty;
|
||||
current.totalPrice += detail.qty * (detail.promoPrice || detail.price);
|
||||
}
|
||||
});
|
||||
});
|
||||
console.log(aggregatedItems.values())
|
||||
return Array.from(aggregatedItems.values());
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleConfirm = async (transactionId) => {
|
||||
setIsPaymentLoading(true);
|
||||
try {
|
||||
const c = await confirmTransaction(transactionId);
|
||||
if (c) {
|
||||
// Update the confirmed status locally
|
||||
setTransactions((prevTransactions) =>
|
||||
prevTransactions.map((transaction) =>
|
||||
transaction.transactionId === transactionId
|
||||
? { ...transaction, confirmed: 1 } // Set to confirmed
|
||||
: transaction
|
||||
const result = await confirmTransaction(transactionId);
|
||||
if (result) {
|
||||
setTransactions(prev =>
|
||||
prev.map(t =>
|
||||
t.transactionId === transactionId ? result : t
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -115,14 +136,11 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
|
||||
const handleDecline = async (transactionId) => {
|
||||
setIsPaymentLoading(true);
|
||||
try {
|
||||
const c = await declineTransaction(transactionId);
|
||||
if (c) {
|
||||
// Update the confirmed status locally
|
||||
setTransactions((prevTransactions) =>
|
||||
prevTransactions.map((transaction) =>
|
||||
transaction.transactionId === transactionId
|
||||
? c // Set to confirmed
|
||||
: transaction
|
||||
const result = await declineTransaction(transactionId);
|
||||
if (result) {
|
||||
setTransactions(prev =>
|
||||
prev.map(t =>
|
||||
t.transactionId === transactionId ? result : t
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -133,106 +151,109 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
|
||||
}
|
||||
};
|
||||
|
||||
if (loading)
|
||||
return (
|
||||
<div className="Loader">
|
||||
<div className="LoaderChild">
|
||||
<ThreeDots />
|
||||
<h1></h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={styles.Transactions}>
|
||||
<div style={{ marginTop: "30px" }}></div>
|
||||
<h2 className={styles["Transactions-title"]}>Daftar transaksi
|
||||
Rp {calculateAllTransactionsTotal(transactions)} </h2>
|
||||
<div style={{ marginTop: "30px" }}></div>
|
||||
{/* <TableCanvas tables={tables} selectedTable={selectedTable} /> */}
|
||||
<div className={styles.TransactionListContainer} style={{ padding: '0 20px 0 20px' }}>
|
||||
<h2 className={styles["Transactions-title"]}>
|
||||
Transaksi selesai {formatRp(calculateAllTransactionsTotal(transactions))}
|
||||
</h2>
|
||||
|
||||
<input
|
||||
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.map((transaction) => (
|
||||
<div
|
||||
key={transaction.transactionId}
|
||||
className={styles.RoundedRectangle}
|
||||
style={{ overflow: 'hidden' }}
|
||||
className={`${styles.RoundedRectangle} ${!transaction.is_paid ? styles.unpaid : ''}`}
|
||||
>
|
||||
|
||||
<div className={styles['receipt-header']}>
|
||||
{transaction.confirmed === 1 ? (
|
||||
<ColorRing className={styles['receipt-logo']} />
|
||||
) : transaction.confirmed === -1 || transaction.confirmed === -2 ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', margin: '16px 0px' }}>
|
||||
<svg
|
||||
style={{ width: '60px', transform: 'Rotate(45deg)' }}
|
||||
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"
|
||||
/>
|
||||
) : (transaction.confirmed === -1 && !transaction.is_paid) || (transaction.confirmed === -2 && !transaction.is_paid) ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', margin: '12px 0' }}>
|
||||
<svg width="60" height="60" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<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"/>
|
||||
</svg>
|
||||
</div>
|
||||
) : transaction.confirmed === 2 ? (
|
||||
) : transaction.confirmed === 2 && !transaction.is_paid ? (
|
||||
<ColorRing className={styles['receipt-logo']} />
|
||||
) : transaction.confirmed === 3 ? (
|
||||
<div>
|
||||
<svg
|
||||
height="60px"
|
||||
width="60px"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 506.4 506.4"
|
||||
xmlSpace="preserve"
|
||||
fill="#000000"
|
||||
style={{marginTop: '12px', marginBottom: '12px'}}
|
||||
>
|
||||
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
|
||||
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<circle style={{ fill: '#54B265' }} cx="253.2" cy="253.2" r="249.2" />
|
||||
<path
|
||||
style={{ fill: '#F4EFEF' }}
|
||||
d="M372.8,200.4l-11.2-11.2c-4.4-4.4-12-4.4-16.4,0L232,302.4l-69.6-69.6c-4.4-4.4-12-4.4-16.4,0 L134.4,244c-4.4,4.4-4.4,12,0,16.4l89.2,89.2c4.4,4.4,12,4.4,16.4,0l0,0l0,0l10.4-10.4l0.8-0.8l121.6-121.6 C377.2,212.4,377.2,205.2,372.8,200.4z"
|
||||
></path>
|
||||
<path d="M253.2,506.4C113.6,506.4,0,392.8,0,253.2S113.6,0,253.2,0s253.2,113.6,253.2,253.2S392.8,506.4,253.2,506.4z M253.2,8 C118,8,8,118,8,253.2s110,245.2,245.2,245.2s245.2-110,245.2-245.2S388.4,8,253.2,8z"></path>
|
||||
<path d="M231.6,357.2c-4,0-8-1.6-11.2-4.4l-89.2-89.2c-6-6-6-16,0-22l11.6-11.6c6-6,16.4-6,22,0l66.8,66.8L342,186.4 c2.8-2.8,6.8-4.4,11.2-4.4c4,0,8,1.6,11.2,4.4l11.2,11.2l0,0c6,6,6,16,0,22L242.8,352.4C239.6,355.6,235.6,357.2,231.6,357.2z M154,233.6c-2,0-4,0.8-5.6,2.4l-11.6,11.6c-2.8,2.8-2.8,8,0,10.8l89.2,89.2c2.8,2.8,8,2.8,10.8,0l132.8-132.8c2.8-2.8,2.8-8,0-10.8 l-11.2-11.2c-2.8-2.8-8-2.8-10.8,0L234.4,306c-1.6,1.6-4,1.6-5.6,0l-69.6-69.6C158,234.4,156,233.6,154,233.6z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
) : transaction.confirmed === 3 || transaction.is_paid ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', margin: '12px 0' }}>
|
||||
<svg width="60" height="60" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 16.2 4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4z" fill="#54B265"/>
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
<ColorRing className={styles['receipt-logo']} />
|
||||
)}
|
||||
|
||||
|
||||
<div className={styles['receipt-info']}>
|
||||
{deviceType == 'clerk' ?
|
||||
<h3>{transaction.confirmed === 1 ? (
|
||||
"Silahkan Cek Pembayaran"
|
||||
) : transaction.confirmed === -1 ? (
|
||||
"Dibatalkan Oleh Kasir"
|
||||
) : transaction.confirmed === -2 ? (
|
||||
"Dibatalkan Oleh Pelanggan"
|
||||
) : transaction.confirmed === 2 ? (
|
||||
"Sedang Diproses"
|
||||
) : transaction.confirmed === 3 ? (
|
||||
"Transaksi Sukses"
|
||||
) : (
|
||||
"Silahkan Cek Ketersediaan"
|
||||
)}</h3>
|
||||
:
|
||||
<h3>{transaction.confirmed === 1 ? (
|
||||
(transaction.payment_type == 'cash' ? 'Silahkan Bayar Ke Kasir' : "Silahkan Bayar Dengan QRIS")
|
||||
) : transaction.confirmed === -1 ? (
|
||||
"Dibatalkan Oleh Kasir"
|
||||
) : transaction.confirmed === -2 ? (
|
||||
"Dibatalkan Oleh Pelanggan"
|
||||
) : transaction.confirmed === 2 ? (
|
||||
"Sedang diproses"
|
||||
) : transaction.confirmed === 3 ? (
|
||||
"Transaksi Sukses"
|
||||
) : (
|
||||
"Memeriksa Ketersediaan "
|
||||
)}</h3>}
|
||||
{deviceType == 'clerk' ?
|
||||
<h3 className={getStatusClass(transaction)}>{transaction.confirmed === 1 && !transaction.is_paid ? (
|
||||
"Silahkan Cek Pembayaran"
|
||||
) : transaction.confirmed === -1 && !transaction.is_paid ? (
|
||||
"Dibatalkan Oleh Kasir"
|
||||
) : transaction.confirmed === -2 && !transaction.is_paid ? (
|
||||
"Dibatalkan Oleh Pelanggan"
|
||||
) : transaction.confirmed === 2 && !transaction.is_paid ? (
|
||||
"Sedang Diproses"
|
||||
) : transaction.confirmed === 3 || transaction.is_paid ? (
|
||||
"Transaksi Sukses"
|
||||
) : (
|
||||
"Silahkan Cek Ketersediaan"
|
||||
)}</h3>
|
||||
:
|
||||
<h3 className={getStatusClass(transaction)}>{transaction.confirmed === 1 ? (
|
||||
(transaction.payment_type == 'cash' ? 'Silahkan Bayar Ke Kasir' : "Silahkan Bayar Dengan QRIS")
|
||||
) : transaction.confirmed === -1 ? (
|
||||
"Dibatalkan Oleh Kasir"
|
||||
) : transaction.confirmed === -2 ? (
|
||||
"Dibatalkan Oleh Pelanggan"
|
||||
) : transaction.confirmed === 2 ? (
|
||||
"Sedang diproses"
|
||||
) : transaction.confirmed === 3 ? (
|
||||
"Transaksi Sukses"
|
||||
) : (
|
||||
"Memeriksa Ketersediaan "
|
||||
)}</h3>}
|
||||
|
||||
<p>Transaction ID: {transaction.transactionId}</p>
|
||||
<p>Payment Type: {transaction.payment_type}</p>
|
||||
@@ -258,16 +279,28 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
|
||||
<ul>
|
||||
{transaction.DetailedTransactions.map((detail) => (
|
||||
<li key={detail.detailedTransactionId}>
|
||||
<span>{detail.Item.name}</span> - {detail.qty < 1 ? 'tidak tersedia' : `${detail.qty} x Rp
|
||||
${detail.promoPrice ? detail.promoPrice : detail.price}`}
|
||||
<span>{detail.Item.name}</span> - {detail.qty < 1 ? 'tidak tersedia' : `${detail.qty} x ${formatRp(detail.promoPrice ? detail.promoPrice : detail.price)}`}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{!transaction.is_paid && transaction.confirmed > -1 && (
|
||||
<div
|
||||
onClick={() => {
|
||||
localStorage.setItem('lastTransaction', JSON.stringify(transaction));
|
||||
setModal("message", { captMessage: 'Silahkan tambahkan pesanan', descMessage: 'Pembayaran akan ditambahkan ke transaksi sebelumnya.' }, null, null);
|
||||
|
||||
// Dispatch the custom event
|
||||
window.dispatchEvent(new Event("localStorageUpdated"));
|
||||
}}
|
||||
className={styles["addNewItem"]}
|
||||
>
|
||||
Tambah pesanan
|
||||
</div>
|
||||
)}
|
||||
<h2 className={styles["Transactions-detail"]}>
|
||||
{transaction.serving_type === "pickup"
|
||||
? "Self pickup"
|
||||
: `Serve to ${transaction.Table ? transaction.Table.tableNo : "N/A"
|
||||
}`}
|
||||
: `Serve to ${transaction.Table ? transaction.Table.tableNo : "N/A"}`}
|
||||
</h2>
|
||||
|
||||
{transaction.notes && (
|
||||
@@ -290,38 +323,44 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
|
||||
<div className={styles.TotalContainer}>
|
||||
<span>Total:</span>
|
||||
<span>
|
||||
Rp {calculateTotalPrice(transaction.DetailedTransactions)}
|
||||
{formatRp(calculateTotalPrice(transaction.DetailedTransactions))}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.TotalContainer}>
|
||||
{(deviceType == 'clerk' && (transaction.confirmed == 0 || transaction.confirmed == 1 || transaction.confirmed == 2)) &&
|
||||
<button
|
||||
className={styles.PayButton}
|
||||
onClick={() => handleConfirm(transaction.transactionId)}
|
||||
disabled={isPaymentLoading}
|
||||
>
|
||||
{
|
||||
isPaymentLoading ? (
|
||||
<ColorRing height="50" width="50" color="white" />
|
||||
) : transaction.confirmed === 1 ? (
|
||||
"Konfirmasi Telah Bayar"
|
||||
) : transaction.confirmed === 2 ? (
|
||||
"Confirm item is ready"
|
||||
) : (
|
||||
"Confirm availability"
|
||||
)
|
||||
}
|
||||
{(deviceType == 'clerk' && !transaction.is_paid && (transaction.confirmed == 0 || transaction.confirmed == 1 || transaction.confirmed == 2)) && (
|
||||
<div className={styles.ActionRow}>
|
||||
<button
|
||||
className={styles.PayButton}
|
||||
onClick={() => handleConfirm(transaction.transactionId)}
|
||||
disabled={isPaymentLoading}
|
||||
>
|
||||
{
|
||||
isPaymentLoading ? (
|
||||
<ColorRing height="28" width="28" color="white" />
|
||||
) : transaction.confirmed === 1 ? (
|
||||
"Konfirmasi"
|
||||
) : transaction.confirmed === 2 ? (
|
||||
"Confirm item is ready"
|
||||
) : (
|
||||
"Confirm availability"
|
||||
)
|
||||
}
|
||||
</button>
|
||||
<button
|
||||
className={styles.DeclineButton}
|
||||
onClick={() => handleDecline(transaction.transactionId)}
|
||||
disabled={isPaymentLoading}
|
||||
>
|
||||
{isPaymentLoading ? '...' : 'Batalkan'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</button>
|
||||
}
|
||||
|
||||
{deviceType == 'guestDevice' && transaction.confirmed < 2 && transaction.payment_type != 'cash' && transaction.payment_type != 'paylater/cash' &&
|
||||
{deviceType == 'guestDevice' && transaction.confirmed < 2 && transaction.payment_type != 'cash' && transaction.payment_type != 'paylater/cash' &&
|
||||
<ButtonWithReplica
|
||||
paymentUrl={paymentUrl}
|
||||
price={
|
||||
"Rp" + calculateTotalPrice(transaction.DetailedTransactions)
|
||||
}
|
||||
price={formatRp(calculateTotalPrice(transaction.DetailedTransactions))}
|
||||
disabled={isPaymentLoading}
|
||||
isPaymentLoading={isPaymentLoading}
|
||||
handleClick={() => handleConfirm(transaction.transactionId)}
|
||||
@@ -344,40 +383,30 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
|
||||
}
|
||||
</div>
|
||||
|
||||
{deviceType == 'guestDevice' && transaction.confirmed >=0 && transaction.confirmed < 2 && transaction.payment_type == 'cash' ?
|
||||
<button
|
||||
className={styles.PayButton}
|
||||
onClick={() => handleDecline(transaction.transactionId)}
|
||||
disabled={
|
||||
transaction.confirmed === -1 ||
|
||||
transaction.confirmed === 3 ||
|
||||
isPaymentLoading
|
||||
} // Disable button if confirmed (1) or declined (-1) or
|
||||
>
|
||||
{isPaymentLoading ? (
|
||||
<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>
|
||||
:
|
||||
((transaction.confirmed >= 0 && transaction.confirmed < 2 && transaction.payment_type != 'paylater/cash' && transaction.payment_type != 'paylater/cashless' || isPaymentOpen) &&
|
||||
<h5
|
||||
className={`${styles.DeclineButton}`}
|
||||
onClick={() =>
|
||||
isPaymentOpen
|
||||
? setIsPaymentOpen(false)
|
||||
: handleDecline(transaction.transactionId)
|
||||
{deviceType == 'guestDevice' && (
|
||||
transaction.confirmed >= 0 && transaction.confirmed < 2 && transaction.payment_type == 'cash' ? (
|
||||
<button
|
||||
className={styles.DeclineButton}
|
||||
onClick={() => handleDecline(transaction.transactionId)}
|
||||
disabled={
|
||||
transaction.confirmed === -1 ||
|
||||
transaction.confirmed === 3 ||
|
||||
isPaymentLoading
|
||||
}
|
||||
>
|
||||
{isPaymentOpen ? "kembali" : "batalkan"}
|
||||
</h5>
|
||||
{isPaymentLoading ? '...' : 'Batalkan'}
|
||||
</button>
|
||||
) : (
|
||||
(transaction.confirmed >= 0 && transaction.confirmed < 2 && (transaction.payment_type != 'paylater/cash' && transaction.payment_type != 'paylater/cashless' || isPaymentOpen)) && (
|
||||
<button
|
||||
className={styles.DeclineButton}
|
||||
onClick={() => isPaymentOpen ? setIsPaymentOpen(false) : handleDecline(transaction.transactionId)}
|
||||
>
|
||||
{isPaymentOpen ? 'Kembali' : 'Batalkan'}
|
||||
</button>
|
||||
)
|
||||
)
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -13,41 +13,43 @@
|
||||
}
|
||||
.Transactions {
|
||||
overflow-x: hidden;
|
||||
background-color: white;
|
||||
background-color: #f5f7f6;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: rgba(88, 55, 50, 1);
|
||||
background-color: #e9e9e9;
|
||||
color: rgba(40, 40, 40, 1);
|
||||
}
|
||||
|
||||
.Transactions-title {
|
||||
font-family: "Plus Jakarta Sans", sans-serif;
|
||||
font-weight: 500;
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-size: 6vw;
|
||||
color: black;
|
||||
font-size: clamp(18px, 3.6vw, 24px);
|
||||
color: var(--brand-sage, #6B8F71);
|
||||
text-align: left;
|
||||
margin-left: 20px;
|
||||
margin-top: 57px;
|
||||
margin: 16px 16px 8px 16px;
|
||||
}
|
||||
|
||||
.Transactions-detail {
|
||||
font-family: "Plus Jakarta Sans", sans-serif;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-size: 15px;
|
||||
color: rgba(88, 55, 50, 1);
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
text-align: left;
|
||||
margin-left: 20px;
|
||||
margin-top: 17px;
|
||||
margin-left: 16px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.TransactionListContainer {
|
||||
overflow-y: auto; /* Enables vertical scrolling */
|
||||
background-color: #dbdbdb;
|
||||
background-color: transparent;
|
||||
overflow: visible;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 12px;
|
||||
padding: 8px 12px 16px;
|
||||
}
|
||||
|
||||
.TotalContainer {
|
||||
@@ -57,14 +59,14 @@
|
||||
/* width: 100%; */
|
||||
margin: 0 auto;
|
||||
font-family: "Plus Jakarta Sans", sans-serif;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-size: 15px;
|
||||
font-size: 13px;
|
||||
/* padding: 10px; */
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 17px;
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
margin-bottom: 12px;
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.PaymentContainer {
|
||||
@@ -83,61 +85,66 @@
|
||||
|
||||
.PayButton {
|
||||
font-family: "Plus Jakarta Sans", sans-serif;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-size: 12px; /* Adjusted for better readability */
|
||||
padding: 12px 16px; /* Added padding for a better look */
|
||||
border-radius: 50px;
|
||||
background-color: rgba(88, 55, 50, 1);
|
||||
font-size: 12px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px; /* square rounded */
|
||||
background-color: var(--brand-primary, #73a585);
|
||||
color: white;
|
||||
border: none;
|
||||
margin: 0 auto;
|
||||
cursor: pointer;
|
||||
display: block; /* Centering the button */
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
.DeclineButton {
|
||||
font-family: "Plus Jakarta Sans", sans-serif;
|
||||
z-index: 201;
|
||||
position: relative;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-size: 15px;
|
||||
padding: 12px 24px; /* Add some padding for spacing */
|
||||
color: rgba(88, 55, 50, 1);
|
||||
background-color: transparent; /* No background */
|
||||
border: none; /* No border */
|
||||
margin: 0 auto; /* Center horizontally */
|
||||
font-size: 12px;
|
||||
padding: 10px 14px;
|
||||
color: #444;
|
||||
background-color: #f0f0f0;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 10px; /* square rounded */
|
||||
cursor: pointer;
|
||||
display: block; /* Center the text horizontally */
|
||||
text-align: center; /* Center the text within the button */
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.DeclineButton.active {
|
||||
position: relative;
|
||||
z-index: 201;
|
||||
font-family: "Plus Jakarta Sans", sans-serif;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-size: 20px;
|
||||
padding: 12px 24px; /* Add some padding for spacing */
|
||||
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 */
|
||||
.DeclineButton.active { opacity: 0.9; }
|
||||
|
||||
/* Row for primary/secondary action buttons */
|
||||
.ActionRow {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
.RoundedRectangle {
|
||||
position: relative;
|
||||
border-radius: 20px;
|
||||
padding: 15px; /* Adjusted for better spacing */
|
||||
margin: 12px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 16px;
|
||||
padding: 12px 12px 10px;
|
||||
margin: 0;
|
||||
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 {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -201,20 +208,28 @@
|
||||
}
|
||||
|
||||
.receipt-logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%; /* Circular logo */
|
||||
object-fit: cover;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.receipt-info h3 {
|
||||
font-size: 16px;
|
||||
margin: 5px 0;
|
||||
font-size: 13px;
|
||||
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 {
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin: 2px 0;
|
||||
}
|
||||
/* Dotted line with circular cutouts */
|
||||
@@ -222,39 +237,61 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 15px 0;
|
||||
margin: 12px 0 10px;
|
||||
}
|
||||
|
||||
.dotted-line .line {
|
||||
border-top: 13px dotted #dbdbdb;
|
||||
border-top: 10px dotted #e9e9e9;
|
||||
width: 100%;
|
||||
margin: 0 18px;
|
||||
}
|
||||
|
||||
.dotted-line .circle-left {
|
||||
left: -25px;
|
||||
left: -18px;
|
||||
position: absolute;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background-color: #dbdbdb;
|
||||
background-color: #e9e9e9;
|
||||
display: flex; /* Use flexbox to center the inner circle */
|
||||
justify-content: center; /* Center horizontally */
|
||||
align-items: center; /* Center vertically */
|
||||
}
|
||||
|
||||
.dotted-line .circle-right {
|
||||
right: -25px;
|
||||
right: -18px;
|
||||
position: absolute;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background-color: #dbdbdb;
|
||||
background-color: #e9e9e9;
|
||||
display: flex; /* Use flexbox to center the inner circle */
|
||||
justify-content: center; /* Center horizontally */
|
||||
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 {
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
@@ -389,10 +426,11 @@
|
||||
|
||||
.addNewItem{
|
||||
width: 100%;
|
||||
height: 27px;
|
||||
background-color: rgb(115, 165, 133);
|
||||
border-radius: 11px;
|
||||
height: 28px;
|
||||
background-color: var(--brand-primary, rgb(115, 165, 133));
|
||||
border-radius: 10px; /* square rounded */
|
||||
text-align: center;
|
||||
color: white;
|
||||
line-height: 27px;
|
||||
}
|
||||
line-height: 28px;
|
||||
font-size: 12px; /* compact */
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
@@ -15,6 +17,11 @@
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.fileInput {
|
||||
@@ -24,38 +31,134 @@
|
||||
.circular-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.welcoming-text {
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
font-family: "Plus Jakarta Sans", sans-serif;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.get-started-button {
|
||||
padding: 10px 20px;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
background-color: #007bff; /* Bootstrap primary color */
|
||||
background-color: #28a745; /* Bootstrap primary color */
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.3s;
|
||||
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 {
|
||||
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 {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 300;
|
||||
z-index: 1000;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// WelcomePage.js
|
||||
import React,{useRef} from "react";
|
||||
import React, { useRef } from "react";
|
||||
import "./WelcomePage.css";
|
||||
|
||||
const WelcomePage = ({
|
||||
@@ -15,6 +15,15 @@ const WelcomePage = ({
|
||||
const handleImageClick = () => {
|
||||
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 (
|
||||
<div
|
||||
className={`welcome-page ${isFullscreen ? "fullscreen" : ""}`} // Corrected the className syntax
|
||||
@@ -29,10 +38,13 @@ const WelcomePage = ({
|
||||
className="image-container"
|
||||
>
|
||||
{!isFullscreen &&
|
||||
<div onClick={handleImageClick} style={{width: '100%', height: '100%', position:'absolute'}}>
|
||||
<h1 style={{textAlign:'left'}}>
|
||||
{image ? "Click To Change Image" : "Click To Add Image"}
|
||||
</h1>
|
||||
<div onClick={handleImageClick} style={{width: '100%', height: '100%', position:'absolute', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer'}}>
|
||||
<div style={{textAlign: 'center', color: 'white'}}>
|
||||
<CameraIcon />
|
||||
<h1 style={{textAlign:'center', fontSize: '16px', margin: '10px 0 0 0'}}>
|
||||
{image ? "Klik untuk mengganti gambar" : "Klik untuk menambahkan gambar"}
|
||||
</h1>
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef} style={{display: 'none', width:'100%', height: '100%'}}type="file" accept="image/*" onChange={onImageChange} />
|
||||
</div>
|
||||
@@ -43,10 +55,10 @@ const WelcomePage = ({
|
||||
{welcomingText}
|
||||
</h1>
|
||||
<button className="get-started-button" onClick={onGetStarted}>
|
||||
Get Started
|
||||
Mulai
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WelcomePage;
|
||||
export default WelcomePage;
|
||||
@@ -1,56 +1,395 @@
|
||||
/* WelcomePageEditor.css */
|
||||
|
||||
.welcome-page-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background-color: rgb(207, 207, 207);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background-color: #ffffff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden; /* contain scroll inside editor-content */
|
||||
font-family: "Plus Jakarta Sans", sans-serif;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
.editor-header {
|
||||
padding: 24px;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.editor-title {
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
.editor-content {
|
||||
display: flex;
|
||||
flex: 1; /* take remaining height */
|
||||
width: 100%;
|
||||
height: 100px; /* Adjust as needed */
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
resize: none;
|
||||
margin-bottom: 20px;
|
||||
font-size: 16px;
|
||||
overflow-y: auto; /* enable vertical scroll in editor-content */
|
||||
overflow-x: hidden;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
border-color: #007bff; /* Highlight border color on focus */
|
||||
.config-panel {
|
||||
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;
|
||||
border-color: #28a745;
|
||||
box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.1);
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: 20px;
|
||||
.color-input {
|
||||
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;
|
||||
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 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px dashed #ccc; /* Preview border style */
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
max-width: 400px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
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;
|
||||
justify-content: 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ const WelcomePageEditor = ({ cafeId, welcomePageConfig }) => {
|
||||
const [isWelcomePageActive, setIsWelcomePageActive] = useState(false); // State for the switch
|
||||
const [loading, setLoading] = useState(false); // Loading state
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
|
||||
// Load existing welcome page configuration when component mounts
|
||||
useEffect(() => {
|
||||
@@ -51,6 +52,21 @@ const WelcomePageEditor = ({ cafeId, welcomePageConfig }) => {
|
||||
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 () => {
|
||||
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 (
|
||||
<div
|
||||
className="welcome-page-editor"
|
||||
style={{ width: "80vw", height: "80vh" }}
|
||||
>
|
||||
<h2>Edit Welcome Page</h2>
|
||||
<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>
|
||||
<div className="welcome-page-editor">
|
||||
<div className="editor-header">
|
||||
<h2 className="editor-title">Konfigurasi Halaman Selamat Datang</h2>
|
||||
</div>
|
||||
<div
|
||||
style={{ width: "100%", height: "100%", position: "relative", flex: 1, borderRadius: '15px' }}
|
||||
>
|
||||
<WelcomePage
|
||||
image={image}
|
||||
welcomingText={welcomingText}
|
||||
backgroundColor={backgroundColor}
|
||||
textColor={textColor}
|
||||
onGetStarted={() => setIsFullscreen(false)}
|
||||
isFullscreen={isFullscreen}
|
||||
onImageChange={handleImageChange}
|
||||
/>
|
||||
<div style={{ position: "absolute", bottom: 0, right: 0 }}>
|
||||
<svg
|
||||
width="100" // Adjust size as needed
|
||||
height="100" // Adjust size as needed
|
||||
style={{ position: "absolute", bottom: 0, right: 0 }}
|
||||
onClick={() => setIsFullscreen(true)}
|
||||
|
||||
<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)}
|
||||
>
|
||||
<EyeIcon /> {showPreview ? 'Tutup Pratinjau' : 'Preview'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showPreview && (
|
||||
<div className="inline-preview">
|
||||
<div className="inline-preview-content">
|
||||
<WelcomePage
|
||||
image={image}
|
||||
welcomingText={welcomingText}
|
||||
backgroundColor={backgroundColor}
|
||||
textColor={textColor}
|
||||
onGetStarted={() => setShowPreview(false)}
|
||||
isFullscreen={false}
|
||||
onImageChange={handleImageChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="config-section">
|
||||
<h3 className="section-title">
|
||||
<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)">
|
||||
<circle cx="50" cy="50" r="40" fill="rgba(0, 0, 0, 0.5)" />
|
||||
<text
|
||||
x="50"
|
||||
y="50"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fontSize="24"
|
||||
fill="white" // Adjust text color as needed
|
||||
>
|
||||
<>
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
{loading ? (
|
||||
<>
|
||||
<div className="loading-spinner"></div>
|
||||
Menyimpan...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SaveIcon />
|
||||
Simpan Konfigurasi
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{/* Pratinjau dipicu oleh tombol atas; section khusus dihapus */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user