Compare commits

...

8 Commits

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

58
note/detailUI.md Normal file
View File

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

219
package-lock.json generated
View File

@@ -14,12 +14,14 @@
"@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",
@@ -3905,171 +3907,6 @@
"integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
"license": "MIT"
},
"node_modules/@mui/core-downloads-tracker": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.1.tgz",
"integrity": "sha512-+mIK1Z0BhOaQ0vCgOkT1mSrIpEHLo338h4/duuL4TBLXPvUMit732mnwJY3W40Avy30HdeSfwUAAGRkKmwRaEQ==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/material": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.1.tgz",
"integrity": "sha512-Xf6Shbo03YmcBedZMwSpEFOwpYDtU7tC+rhAHTrA9FHk0FpsDqiQ9jUa1j/9s3HLs7KWb5mDcGnlwdh9Q9KAag==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.2",
"@mui/core-downloads-tracker": "^7.3.1",
"@mui/system": "^7.3.1",
"@mui/types": "^7.4.5",
"@mui/utils": "^7.3.1",
"@popperjs/core": "^2.11.8",
"@types/react-transition-group": "^4.4.12",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1",
"react-is": "^19.1.1",
"react-transition-group": "^4.4.5"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@mui/material-pigment-css": "^7.3.1",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@mui/material-pigment-css": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/private-theming": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.1.tgz",
"integrity": "sha512-WU3YLkKXii/x8ZEKnrLKsPwplCVE11yZxUvlaaZSIzCcI3x2OdFC8eMlNy74hVeUsYQvzzX1Es/k4ARPlFvpPQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.2",
"@mui/utils": "^7.3.1",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/styled-engine": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.1.tgz",
"integrity": "sha512-Nqo6OHjvJpXJ1+9TekTE//+8RybgPQUKwns2Lh0sq+8rJOUSUKS3KALv4InSOdHhIM9Mdi8/L7LTF1/Ky6D6TQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.2",
"@emotion/cache": "^11.14.0",
"@emotion/serialize": "^1.3.3",
"@emotion/sheet": "^1.4.0",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
}
}
},
"node_modules/@mui/system": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.1.tgz",
"integrity": "sha512-mIidecvcNVpNJMdPDmCeoSL5zshKBbYPcphjuh6ZMjhybhqhZ4mX6k9zmIWh6XOXcqRQMg5KrcjnO0QstrNj3w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.2",
"@mui/private-theming": "^7.3.1",
"@mui/styled-engine": "^7.3.1",
"@mui/types": "^7.4.5",
"@mui/utils": "^7.3.1",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/types": {
"version": "7.4.5",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.5.tgz",
@@ -4638,7 +4475,6 @@
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.6.tgz",
"integrity": "sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"@svgdotjs/svg.js": "^3.2.4"
}
@@ -4648,7 +4484,6 @@
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.filter.js/-/svg.filter.js-3.0.9.tgz",
"integrity": "sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@svgdotjs/svg.js": "^3.2.4"
},
@@ -4661,7 +4496,6 @@
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.4.tgz",
"integrity": "sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==",
"license": "MIT",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Fuzzyma"
@@ -4672,7 +4506,6 @@
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.resize.js/-/svg.resize.js-2.0.5.tgz",
"integrity": "sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 14.18"
},
@@ -4686,7 +4519,6 @@
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.select.js/-/svg.select.js-4.0.3.tgz",
"integrity": "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 14.18"
},
@@ -4924,26 +4756,6 @@
"tslib": "^2.8.0"
}
},
"node_modules/@testing-library/dom": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
"@types/aria-query": "^5.0.1",
"aria-query": "5.3.0",
"dom-accessibility-api": "^0.5.9",
"lz-string": "^1.5.0",
"picocolors": "1.1.1",
"pretty-format": "^27.0.2"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@testing-library/jest-dom": {
"version": "5.17.0",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz",
@@ -5976,8 +5788,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz",
"integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@zxing/browser": {
"version": "0.0.7",
@@ -6315,7 +6126,6 @@
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-5.3.4.tgz",
"integrity": "sha512-N0gNh8uLu/BN8N+BCphNK+gZAoSoUtDDn1jFGB+3+EMcv8s6vajuP3W0g4dMLTRp6chFkjMmQK3uD8pz4ISmLA==",
"license": "SEE LICENSE IN LICENSE",
"peer": true,
"dependencies": {
"@svgdotjs/svg.draggable.js": "^3.0.4",
"@svgdotjs/svg.filter.js": "^3.0.8",
@@ -15199,6 +15009,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
"version": "0.541.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.541.0.tgz",
"integrity": "sha512-s0Vircsu5WaGv2KoJZ5+SoxiAJ3UXV5KqEM3eIFDHaHkcLIFdIWgXtZ412+Gh02UsdS7Was+jvEpBvPCWQISlg==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
@@ -20825,20 +20644,6 @@
"is-typedarray": "^1.0.0"
}
},
"node_modules/typescript": {
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/unbox-primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",

View File

@@ -10,12 +10,14 @@
"@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",

View File

@@ -1,6 +1,15 @@
@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 {
scrollbar-width: none; /* Firefox */
@@ -27,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;
}
@@ -54,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;
}

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import { useLocation } from "react-router-dom";
import { useNavigationHelpers } from "../helpers/navigationHelpers";
import 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 its 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`
@@ -214,24 +176,87 @@ const ChildWrapper = styled.div`
`;
const Child = styled.div`
width: 100%;
height: 36px;
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,28 +347,40 @@ const Header = ({
// Otherwise, use the possessive function
return `${cafeName}'s menu`;
};
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}>
<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}>
{shopName == null
{formatCafeName(
shopName == null
? HeaderText == null
? "kedaimaster"
: HeaderText
: shopName}
: 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>
</CenterGroup>
<RightGroup style={{ visibility: showProfile ? "visible" : "hidden", position: 'relative' }}>
<HamburgerButton onClick={toggleMenu} aria-label="Open menu">
<HamburgerIcon />
</HamburgerButton>
{showRectangle && (
<Rectangle ref={rectangleRef} animate={animate}>
<Rectangle ref={rectangleRef} animate={animate} baseZIndex={rectZIndex !== undefined ? rectZIndex : zIndexLevel}>
<ChildContainer>
{guestSideOfClerk && guestSideOfClerk.clerkUsername && (
<Child hasChildren>
@@ -350,9 +388,7 @@ const Header = ({
</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>)}
@@ -364,9 +400,9 @@ const Header = ({
user.roleId === 1 && (
<>
<Child hasChildren>
<Child>
{shopName}
</Child>
<CategoryLabel>
{formatCafeName(shopName)}
</CategoryLabel>
<Child>
Mode pengembangan &nbsp;
<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&nbsp;
@@ -435,7 +471,7 @@ const Header = ({
</Child>
<Child hasChildren>
<Child>Konfigurasi</Child>
<CategoryLabel>Konfigurasi</CategoryLabel>
<Child onClick={() => setModal("welcome_config")}>
Desain kafe
</Child>
@@ -481,8 +517,9 @@ const Header = ({
</ChildContainer>
</Rectangle>
)}
</div>
</HeaderBar></HeaderBarbackground>
</RightGroup>
</HeaderBar>
</HeaderBarbackground>
);
};

View File

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

View File

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

View File

@@ -4,6 +4,8 @@ const Item = ({
blank,
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}>
<div className={styles.imageWrap}>
<img
src={
previewUrl
}
src={previewUrl}
onError={({ currentTarget }) => {
currentTarget.onerror = null; // prevents looping
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",
}}
style={{ filter: !isAvailable ? "grayscale(100%)" : "none" }}
className={styles.imageContainer}
/>
// </div>
{promoPrice && promoPrice != 0 && promoPrice != '' && (
<div className={styles.promoPill}>Promo</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 && (
{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>
</>
)}
{!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>
</>
}
<p className={styles.itemPriceInvoice}>Rp {formatCurrency(itemQty * (promoPrice > 0 ? promoPrice : itemPrice))}</p>
</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>
);
};

View File

@@ -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; */
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;
width: 87px;
height: 32px;
margin-left: 5px;
margin-top: 5px;
border-radius: 20px;
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 */ }

View File

@@ -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,23 +82,54 @@ 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} />
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 className={styles.imageActions}>
<button
onClick={handleChangeImage}
className={styles.actionButton}
>
{isBeingEdit ? 'Ganti Gambar' : 'Tambah Gambar'}
</button>
{isBeingEdit && (
<button
onClick={handleDelete}
className={`${styles.actionButton} ${styles.deleteButton}`}
>
Hapus
</button>
)}
</div>
<input
type="file"
ref={fileInputRef}
@@ -106,113 +139,78 @@ const ItemConfig = ({
style={{ display: "none" }}
/>
</div>
<div style={{ width: '72%', height: '26vw', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<div onClick={() => handleChangeImage()} style={{ width: '140px', marginRight: '10px', height: '40px', alignContent: 'center', textAlign: 'center', borderRadius: '10px', border: '1px solid #60d37e', color: '#60d37e', backgroundColor: 'white', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
{isBeingEdit ? 'Ganti gambar' : 'Tambah gambar'}
</div>
<div onClick={handleDelete} style={{ width: '76px', height: '40px', alignContent: 'center', textAlign: 'center', borderRadius: '10px', border: '1px solid rgb(211 96 96)', color: 'rgb(211 96 96)', backgroundColor: 'white', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
Hapus
</div>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', color: 'black', marginTop: '10px' }}>
<p style={{ marginBottom: '5px', fontWeight: '500' }}>Nama item</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={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
}}
className={styles.formInput}
onChange={(e)=>setItemName(e.target.value)}
onFocus={(e) => e.target.style.borderColor = '#60d37e'}
onBlur={(e) => e.target.style.borderColor = '#ccc'}
placeholder="Masukkan nama item"
/>
</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.formRow}>
<div className={styles.formGroup}>
<label className={styles.formLabel}>Harga</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',
}}
className={styles.formInput}
onChange={(e)=>setItemPrice(e.target.value)}
onFocus={(e) => e.target.style.borderColor = '#60d37e'}
onBlur={(e) => e.target.style.borderColor = '#ccc'}
placeholder="Rp 0"
/>
</div>
<div style={{ width: '48%' }}>
<p style={{ marginBottom: '5px', fontWeight: '500', marginTop: 0 }}>Harga promo</p>
<div className={styles.formGroup}>
<label className={styles.formLabel}>Harga Promo</label>
<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',
}}
className={styles.formInput}
onChange={(e)=>setItemPromoPrice(e.target.value)}
onFocus={(e) => e.target.style.borderColor = '#60d37e'}
onBlur={(e) => e.target.style.borderColor = '#ccc'}
placeholder="Opsional"
/>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', color: 'black' }}>
<p style={{ marginBottom: '5px', fontWeight: '500', marginTop: 0 }}>Deskripsi</p>
<div className={styles.formGroup}>
<label className={styles.formLabel}>Deskripsi</label>
<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..."
className={styles.formTextarea}
placeholder="Tambahkan deskripsi item..."
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>
<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>,
document.body
);
};

View File

@@ -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 */
}

View File

@@ -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) {
@@ -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
<ArrowUp size={20} />
</button>
<button
className={styles.iconBtn}
onClick={() => index === indexTotal - 1 ? null : moveItemTypeDown(itemTypeId)}
disabled={index === indexTotal - 1}
aria-label="Turunkan kategori"
>
<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>
<ArrowDown size={20} />
</button>
<button
className={styles.iconBtn}
onClick={toggleEditTypeItem}
aria-label="Edit kategori"
>
<Pencil size={20} />
</button>
</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
)}
{isEditMode && isEdit && (
<div className={styles.titleActions}>
<button
className={styles.iconBtn}
onClick={handleSaveType}
aria-label="Simpan"
>
<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
<Save size={20} />
</button>
<button
className={styles.iconBtn}
onClick={resetItems}
aria-label="Batal"
>
<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>
<X size={20} />
</button>
</div>
<div
style={{
width: '32px',
height: '32px', // Add a height to the div
display: 'flex', // Use flexbox
justifyContent: 'center', // Center horizontally
alignItems: 'center', // Center vertically
cursor: 'pointer'
}}
onClick={toggleEditTypeItem} // Move onClick here for the whole div
>
<svg
fill="#000000"
viewBox="0 0 32 32"
style={{ fillRule: 'evenodd', clipRule: 'evenodd', strokeLinejoin: 'round', strokeMiterlimit: 2 }}
version="1.1"
xmlSpace="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlnsSerif="http://www.serif.com/"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<path d="M12.965,5.462c0,-0 -2.584,0.004 -4.979,0.008c-3.034,0.006 -5.49,2.467 -5.49,5.5l0,13.03c0,1.459 0.579,2.858 1.611,3.889c1.031,1.032 2.43,1.611 3.889,1.611l13.003,0c3.038,-0 5.5,-2.462 5.5,-5.5c0,-2.405 0,-5.004 0,-5.004c0,-0.828 -0.672,-1.5 -1.5,-1.5c-0.827,-0 -1.5,0.672 -1.5,1.5l0,5.004c0,1.381 -1.119,2.5 -2.5,2.5l-13.003,0c-0.663,-0 -1.299,-0.263 -1.768,-0.732c-0.469,-0.469 -0.732,-1.105 -0.732,-1.768l0,-13.03c0,-1.379 1.117,-2.497 2.496,-2.5c2.394,-0.004 4.979,-0.008 4.979,-0.008c0.828,-0.002 1.498,-0.675 1.497,-1.503c-0.001,-0.828 -0.675,-1.499 -1.503,-1.497Z"></path>
<path d="M20.046,6.411l-6.845,6.846c-0.137,0.137 -0.232,0.311 -0.271,0.501l-1.081,5.152c-0.069,0.329 0.032,0.671 0.268,0.909c0.237,0.239 0.577,0.343 0.907,0.277l5.194,-1.038c0.193,-0.039 0.371,-0.134 0.511,-0.274l6.845,-6.845l-5.528,-5.528Zm1.415,-1.414l5.527,5.528l1.112,-1.111c1.526,-1.527 1.526,-4.001 -0,-5.527c-0.001,-0 -0.001,-0.001 -0.001,-0.001c-1.527,-1.526 -4.001,-1.526 -5.527,-0l-1.111,1.111Z"></path>
</g>
</svg>
</div>
</>
)}
</div>
}
@@ -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,18 +858,43 @@ 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.user_id == shopOwnerId || user.cafeId == shopId) &&
isEditMode && (
@@ -873,13 +903,8 @@ const ItemLister = ({
<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 && (
<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>
&nbsp;{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 && (
<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>
&nbsp;{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) =>
@@ -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>
)}
</>

View File

@@ -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-weight: 600;
font-style: normal;
font-size: 20px;
font-size: 22px;
color: rgba(88, 55, 50, 1);
text-align: left;
width: calc(70% - 10px);
padding-left: 10px;
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;
}
.itemWrapper:hover { /* remove hover effect */ }
.editModeLayout {
border-radius: 4px;
/* 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 {
@@ -225,3 +430,24 @@
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;
}

View File

@@ -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[
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={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["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';
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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,53 +48,31 @@ 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 [selectedImage, setSelectedImage] = useState(null);
const [previewUrl, setPreviewUrl] = useState("");
const [imageUrl, setImaguUrl] = useState("");
const canManage = user && (user.user_id == shopOwnerId || user.cafeId == shopId);
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' : 'auto' }}>
<div className="compact-centered-list">
<div ref={newItemDivRef} className="compact-item-type-list" style={{ display: 'inline-flex' }}>
{isEditMode && !isAddingNewItem && canManage && (
<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.user_id == shopOwnerId || user.cafeId == shopId) && (
<ItemType
className="compact-item-type compact-add-item"
onClick={toggleAddNewItem}
name={"buat baru"}
imageUrl={getImageUrl("uploads/assets/addnew.png")}
/>
>
Buat baru
</div>
)}
{user &&(
user.user_id == shopOwnerId || user.cafeId == shopId) &&
isAddingNewItem && (
<>
{canManage && isAddingNewItem && (
<ItemLister
shopId={shopId}
shopOwnerId={shopOwnerId}
@@ -128,29 +87,27 @@ const ItemTypeLister = ({
alwaysEdit={true}
handleUnEdit={toggleAddNewItem}
/>
</>
)}
{itemTypes && itemTypes.length > 0 && (
<ItemType
name={"semua"}
<div
className={`compact-item-type ${filterId === 0 ? 'selected' : ''}`}
onClick={() => onFilterChange(0)}
imageUrl={"uploads/assets/All.png"}
/>
>
Semua
</div>
)}
{itemTypes &&
itemTypes.map(
(itemType) =>
(
itemType.itemList.length > 0 || (user && (user.user_id == shopOwnerId || user.cafeId == shopId))) && (
<ItemType
{itemTypes && itemTypes.map((itemType) => (
<div
key={itemType.itemTypeId}
name={itemType.name}
imageUrl={getImageUrl(itemType.image)}
className={`compact-item-type ${filterId === itemType.itemTypeId ? 'selected' : ''}`}
onClick={() => onFilterChange(itemType.itemTypeId)}
selected={filterId === itemType.itemTypeId}
/>
)
)}
>
{formatName(itemType.name)}
</div>
))}
</div>
</div>
</div>
);

View File

@@ -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,7 +87,7 @@ 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} setModal={setModal}/>
)}
@@ -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
);
};

View File

@@ -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;
}
}

View File

@@ -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 */
}

View File

@@ -1,18 +1,13 @@
import React, { useState, useRef, useEffect } from "react";
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);
@@ -26,6 +21,9 @@ const SetPaymentQr = ({ shopId,
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 () => {
@@ -104,6 +102,8 @@ const SetPaymentQr = ({ shopId,
// Save cafe details
const handleSave = async () => {
setSaving(true);
setSaveStatus(null);
let qrPaymentFileCache;
console.log(qrPaymentFile)
if(qrPaymentFile != null)
@@ -120,107 +120,101 @@ 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 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>
<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>}
<button className={styles.button} onClick={() => qrPaymentInputRef.current.click()}>{qrPayment ? 'Ganti' : 'Unggah'}</button>
</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
{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 className={styles.section}>
<div className={styles.sectionHeader}>
<div>
<div className={styles.sectionTitle}>Open bill</div>
<div className={styles.sectionDesc}>Izinkan pelanggan menambah pesanan dalam satu sesi dan bayar di akhir.</div>
</div>
<div 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
checked={isOpenBillAvailable === 1}
offColor="#888"
onColor="#4CAF50"
uncheckedIcon={false}
@@ -229,17 +223,17 @@ const SetPaymentQr = ({ shopId,
width={50}
/>
</div>
</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>
<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} // Convert to boolean
checked={isNeedConfirmationState === 1}
offColor="#888"
onColor="#4CAF50"
uncheckedIcon={false}
@@ -248,112 +242,19 @@ const SetPaymentQr = ({ shopId,
width={50}
/>
</div>
</div>
<div style={styles.buttonContainer}>
<button onClick={handleSave} style={styles.saveButton}>
Simpan
<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;

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
.watermark {
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;

View File

@@ -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;
}
}

View File

@@ -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,6 +77,40 @@ 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);
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);
@@ -245,6 +280,8 @@ function CafePage({
removeConnectedGuestSides={removeConnectedGuestSides}
setIsEditMode={(e) => setIsEditMode(e)}
isEditMode={isEditMode}
zIndexLevel={9000}
rectZIndex={9000}
/>
<MusicPlayer
socket={socket}
@@ -318,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)"
@@ -388,3 +429,4 @@ function CafePage({
}
export default CafePage;

View File

@@ -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";
@@ -384,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' }}>
@@ -443,20 +450,20 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
</div>
)}
<div className={styles.NoteContainer}>
<span>Catatan :</span>
<span></span>
</div>
}
<div className={styles.NoteContainer}>
{getItemsByCafeId(shopId).length > 0 && (
<div className={`${styles.RoundedRectangle} ${cartStyles.sectionCard}`}>
<div className={cartStyles.sectionTitle}>Catatan Untuk Kasir</div>
<div className={cartStyles.divider}></div>
<textarea
ref={textareaRef}
className={styles.NoteInput}
placeholder="Tambahkan catatan..."
placeholder="Contoh: tanpa gula, ekstra es, dsb."
/>
</div>
</div>
}
)}
{transactionData &&
<div className={styles.RoundedRectangle} style={{ backgroundColor: '#c3c3c3', fontSize: '15px', display: 'flex', justifyContent: 'space-between' }}>
@@ -576,5 +583,6 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
)
}
</div>
</div>
);
}

View File

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

View File

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

View File

@@ -1,119 +1,153 @@
import React, { useState } from 'react';
import { 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}>
<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>
)}
</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="Username"
placeholder="kasir_baru"
value={username}
onChange={(e) => setUsername(e.target.value)}
style={styles.input}
/>
</div>
<div className={styles.field}>
<label className={styles.label}>Password</label>
<div className={styles.pwdRow}>
<input
type="password"
placeholder="Password"
className={styles.input}
type={showPassword ? 'text' : 'password'}
placeholder="••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
style={styles.input}
/>
<button type="submit" style={styles.button} disabled={loading}>
{loading ? 'Creating...' : 'Create Clerk'}
<button type="button" className={styles.button} onClick={()=>setShowPassword(!showPassword)}>
{showPassword ? 'Sembunyikan' : 'Tampilkan'}
</button>
{message && (
<p style={{ ...styles.message, color: message.includes('success') ? 'green' : 'red' }}>
{message}
</p>
)}
<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;

View File

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

View File

@@ -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;

View File

@@ -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)
setError(null);
if (data.length > 0) {
setSelectedMaterialIndex(0);
}
} catch (error) {
console.error("Error fetching materials:", error);
setError("Failed to fetch materials.");
}
};
const [materialsData, mutationsData] = await Promise.all([
getMaterials(cafeId),
getMaterialMutations(cafeId)
]);
const fetchMutations = async () => {
try {
const data = await getMaterialMutations(cafeId);
setMutations(data);
setMaterials(materialsData);
setMutations(mutationsData);
setError(null);
} 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 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}>
<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.formActions}>
<button
type="button"
className={styles.cancelButton}
onClick={() => {
setShowAddForm(false);
setNewMaterialName("");
setNewMaterialUnit("kilogram");
}}
>
Batal
</button>
<button
type="submit"
className={styles.saveButton}
disabled={!newMaterialName.trim()}
>
Tambah Bahan Baku
</button>
</div>
</form>
</div>
)}
{/* Materials List */}
{materials.length === 0 ? (
<div className={styles.emptyState}>
<div className={styles.emptyStateIcon}>📭</div>
<div className={styles.emptyStateText}>Belum ada bahan baku yang terdaftar</div>
<button
className={styles.addButton}
onClick={() => setShowAddForm(true)}
>
+ Tambah Bahan Baku Pertama
</button>
</div>
) : (
<div className={styles.materialsList}>
{materials.map((material) => (
<div key={material.materialId} className={styles.materialCard}>
{/* Material Summary (Always Visible) */}
<div
className={styles.materialSummary}
onClick={() => toggleExpand(material.materialId)}
>
<h3 className={styles.materialName}>{material.name}</h3>
<div className={styles.materialStock}>
{material.currentStock} {material.unit}
</div>
<div className={`${styles.expandIcon} ${expandedMaterials[material.materialId] ? styles.expanded : ''}`}>
</div>
</div>
{/* Material Detail (Expandable) */}
{expandedMaterials[material.materialId] && (
<div className={styles.materialDetail}>
<div className={styles.detailGrid}>
<div className={styles.detailItem}>
<span className={styles.detailLabel}>Stok Saat Ini</span>
<span className={styles.detailValue}>{material.currentStock} {material.unit}</span>
</div>
<div className={styles.detailItem}>
<span className={styles.detailLabel}>Harga per {material.unit}</span>
<span className={styles.detailValue}>Rp {formatCurrency(material.currentPrice || 0)}</span>
</div>
</div>
<div className={styles.detailActions}>
<button
className={styles.detailButton}
onClick={() => {
setEditingMaterialId(material.materialId);
}}
>
Update Stok
</button>
<button
className={styles.detailButton}
onClick={() => handleDeleteMaterial(material.materialId)}
>
🗑 Hapus
</button>
</div>
{/* Stock Update Form */}
{editingMaterialId === material.materialId && (
<div className={styles.stockUpdateForm}>
<h4 className={styles.formTitle}>Update Stok</h4>
<div className={styles.quantityControls}>
<button
className={styles.quantityButton}
onClick={() => handleQuantityChange(-1)}
disabled={newQuantity <= 0}
>
-
</button>
<p>{currentQuantity + quantityChange}</p>
<button onClick={() => handleQuantityChange(1)} className={styles.stockButton}>
<div className={styles.quantityDisplay}>
{newQuantity > 0 ? `+${newQuantity}` : newQuantity}
</div>
<button
className={styles.quantityButton}
onClick={() => handleQuantityChange(1)}
>
+
</button>
</div>
<div className={styles.uploadMessage}>
<p>harga per {materials && materials[selectedMaterialIndex]?.unit} sekarang</p>
</div>
<div className={styles.resultMessage}>
<div className={styles.formGroup}>
<label className={styles.formLabel}>Harga per {material.unit}</label>
<input
className={styles.resultMessageInput} // Replace inline style with CSS module class
disabled={!isEditCurrentPrice || quantityChange < 1}
type="text"
className={styles.formInput}
value={newPrice}
onChange={handleChange}
placeholder="Enter amount"
onChange={handlePriceChange}
placeholder="Masukkan harga"
/>
<div onClick={() => quantityChange < 1 ? null : setIsEditCurrentPrice(!isEditCurrentPrice)} className={quantityChange < 1 ? styles.changeButtonDisabled : styles.changeButtonEnabled}>
{isEditCurrentPrice ? 'Terapkan' : 'Ganti'}
</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
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 className={styles.historyTab}>
<h3 onClick={() => setIsViewingHistory(!isViewingHistory)}> {isViewingHistory ? '˅' : '˃'} Riwayat stok</h3>
{selectedMaterialIndex !== -1 && isViewingHistory && !loading && (
</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.sorter} onClick={() => setSortOrder(sortOrder == 'asc' ? 'desc' : 'asc')}>
Urutkan: {sortOrder === 'asc' ? "terlama" : "terbaru"} <div style={{ transform: 'rotate(90deg)' }}>&lt;&gt;</div>
<div className={styles.sortControls}>
<button
className={styles.sortButton}
onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}
>
Urutkan: {sortOrder === 'asc' ? 'Terlama' : 'Terbaru'}
</button>
</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 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 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>
)}
<div className={styles.emptyState} style={{ padding: '20px' }}>
<div>Belum ada riwayat perubahan stok</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>
</div>
);
};
export default SetPaymentQr;
export default MaterialList;

View File

@@ -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;
}
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 */
}
.title {
margin-bottom: 20px;
font-weight: bold;
}
.container:hover {
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
.uploadMessage {
font-weight: 600;
text-align: left;
}
.changeButtonEnabled {
padding-right: 10px;
background-color: green;
border-radius: 30px;
color: white;
font-weight: 700;
height: 36px;
line-height: 36px;
padding-left: 10px;
padding-height: 10px;
}
.changeButtonDisabled {
padding-right: 10px;
background-color: #a1a1a1;
border-radius: 30px;
color: white;
font-weight: 700;
height: 36px;
line-height: 36px;
padding-left: 10px;
padding-height: 10px;
}
.resultMessage {
margin-top: -13px;
text-align: left;
.header {
padding: 24px;
border-bottom: 1px solid #e6e6e6;
display: flex;
justify-content: space-between;
}
.resultMessage input {
padding-left: 8px;
width: 180px;
}
.stokContainer {
display: flex;
justify-content: space-evenly;
align-items: center;
margin-top: -20px;
margin-bottom: -15px;
text-align: left;
}
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);
}
.buttonContainer {
margin-top: 11px;
text-align: left;
}
.title {
margin: 0;
font-weight: 700;
font-size: 24px;
color: #333;
}
.stockButton {
padding: 10px 20px;
font-size: 3.5vw;
.addButton {
background-color: #28a745;
color: #fff;
color: white;
border: none;
border-radius: 30px;
border-radius: 12px;
padding: 12px 24px;
font-size: 16px;
font-weight: 600;
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 {
transition: all 0.2s ease;
display: flex;
margin: 10px 0;
align-items: center;
gap: 8px;
box-shadow: 0 2px 6px rgba(40, 167, 69, 0.2);
position: relative;
z-index: 202; /* Memastikan tombol berada di atas header */
}
.addButton:hover {
background-color: #218838;
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(40, 167, 69, 0.3);
}
.addButton:disabled {
background-color: #a1a1a1;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.content {
padding: 24px;
position: relative;
z-index: 200;
}
/* Materials List Styles */
.materialsList {
display: flex;
flex-direction: column;
gap: 12px;
}
.materialCard {
background: #ffffff;
border-radius: 12px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
border: 1px solid #eee;
position: relative;
z-index: 200;
}
.materialCard:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: #28a745;
}
.materialSummary {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
cursor: pointer;
position: relative;
z-index: 201;
}
.materialName {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0;
flex: 1;
}
.materialStock {
font-size: 14px;
color: #666;
}
margin: 0 16px 0 0;
}
.unit {
margin-top: 11px;
width: 100%;
height: 31px;
}
.expandIcon {
font-size: 18px;
color: #28a745;
transition: transform 0.2s ease;
}
.sorter {
border: 1px solid #c3c3c3;
padding: 5px;
.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;
justify-content: space-between;
margin-top: -10px;
margin-bottom: 10px;
}
gap: 12px;
margin-top: 16px;
}
.mutationCard {
.detailButton {
flex: 1;
padding: 12px 16px;
border-radius: 10px;
border: none;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
margin-bottom: 7px;
margin-top: 7px;
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;
}
.mutationTitle {
margin-left: 5px;
}
.mutationTitle h4 {
margin: 0;
.content {
padding: 16px;
}
.mutationTitle p {
margin: 0;
.detailGrid {
grid-template-columns: 1fr;
gap: 12px;
}
.formGrid {
grid-template-columns: 1fr;
gap: 16px;
}
.historyItems {
max-height: 200px;
}
.materialSummary {
padding: 14px 16px;
}
.materialName {
font-size: 15px;
}
.materialStock {
font-size: 13px;
margin-right: 12px;
}
.container {
max-height: 75vh; /* Menaikkan tinggi maksimum pada mobile */
}
}

View File

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

View File

@@ -30,6 +30,15 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
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]);
@@ -154,7 +163,7 @@ console.log(aggregatedItems.values())
return (
<div className={styles.Transactions}>
<h2 className={styles["Transactions-title"]}>
Transaksi selesai Rp {calculateAllTransactionsTotal(transactions)}
Transaksi selesai {formatRp(calculateAllTransactionsTotal(transactions))}
</h2>
<input
@@ -162,13 +171,13 @@ console.log(aggregatedItems.values())
placeholder="Cari nama item..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{ border: '0px', height: '42px', borderRadius: '15px', margin: '7px auto 10px', width: '88%', paddingLeft: '8px' }}
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} style={{ padding: '0 8px' }}>
<div className={styles.TransactionListContainer}>
{matchedItems.length > 0 && matchedItems.map(item => (
<div
@@ -183,7 +192,7 @@ console.log(aggregatedItems.values())
</ul>
<div className={styles.TotalContainer}>
<span>Total:</span>
<span>Rp {item.totalPrice}</span>
<span>{formatRp(item.totalPrice)}</span>
</div>
</div>
))}
@@ -191,68 +200,34 @@ console.log(aggregatedItems.values())
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.is_paid || transaction.confirmed === -2 && !transaction.is_paid ? (
<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.is_paid ? (
<ColorRing className={styles['receipt-logo']} />
) : transaction.confirmed === 3 || transaction.is_paid ? (
<div>
<svg
height="60px"
width="60px"
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
viewBox="0 0 506.4 506.4"
xmlSpace="preserve"
fill="#000000"
style={{ marginTop: '12px', marginBottom: '12px' }}
>
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<circle style={{ fill: '#54B265' }} cx="253.2" cy="253.2" r="249.2" />
<path
style={{ fill: '#F4EFEF' }}
d="M372.8,200.4l-11.2-11.2c-4.4-4.4-12-4.4-16.4,0L232,302.4l-69.6-69.6c-4.4-4.4-12-4.4-16.4,0 L134.4,244c-4.4,4.4-4.4,12,0,16.4l89.2,89.2c4.4,4.4,12,4.4,16.4,0l0,0l0,0l10.4-10.4l0.8-0.8l121.6-121.6 C377.2,212.4,377.2,205.2,372.8,200.4z"
></path>
<path d="M253.2,506.4C113.6,506.4,0,392.8,0,253.2S113.6,0,253.2,0s253.2,113.6,253.2,253.2S392.8,506.4,253.2,506.4z M253.2,8 C118,8,8,118,8,253.2s110,245.2,245.2,245.2s245.2-110,245.2-245.2S388.4,8,253.2,8z"></path>
<path d="M231.6,357.2c-4,0-8-1.6-11.2-4.4l-89.2-89.2c-6-6-6-16,0-22l11.6-11.6c6-6,16.4-6,22,0l66.8,66.8L342,186.4 c2.8-2.8,6.8-4.4,11.2-4.4c4,0,8,1.6,11.2,4.4l11.2,11.2l0,0c6,6,6,16,0,22L242.8,352.4C239.6,355.6,235.6,357.2,231.6,357.2z M154,233.6c-2,0-4,0.8-5.6,2.4l-11.6,11.6c-2.8,2.8-2.8,8,0,10.8l89.2,89.2c2.8,2.8,8,2.8,10.8,0l132.8-132.8c2.8-2.8,2.8-8,0-10.8 l-11.2-11.2c-2.8-2.8-8-2.8-10.8,0L234.4,306c-1.6,1.6-4,1.6-5.6,0l-69.6-69.6C158,234.4,156,233.6,154,233.6z"></path>
</g>
<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 && !transaction.is_paid ? (
<h3 className={getStatusClass(transaction)}>{transaction.confirmed === 1 && !transaction.is_paid ? (
"Silahkan Cek Pembayaran"
) : transaction.confirmed === -1 && !transaction.is_paid ? (
"Dibatalkan Oleh Kasir"
@@ -266,7 +241,7 @@ console.log(aggregatedItems.values())
"Silahkan Cek Ketersediaan"
)}</h3>
:
<h3>{transaction.confirmed === 1 ? (
<h3 className={getStatusClass(transaction)}>{transaction.confirmed === 1 ? (
(transaction.payment_type == 'cash' ? 'Silahkan Bayar Ke Kasir' : "Silahkan Bayar Dengan QRIS")
) : transaction.confirmed === -1 ? (
"Dibatalkan Oleh Kasir"
@@ -304,12 +279,11 @@ console.log(aggregatedItems.values())
<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 &&
{!transaction.is_paid && transaction.confirmed > -1 && (
<div
onClick={() => {
localStorage.setItem('lastTransaction', JSON.stringify(transaction));
@@ -322,12 +296,11 @@ console.log(aggregatedItems.values())
>
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 && (
@@ -350,12 +323,13 @@ console.log(aggregatedItems.values())
<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.is_paid && (transaction.confirmed == 0 || transaction.confirmed == 1 || transaction.confirmed == 2)) &&
{(deviceType == 'clerk' && !transaction.is_paid && (transaction.confirmed == 0 || transaction.confirmed == 1 || transaction.confirmed == 2)) && (
<div className={styles.ActionRow}>
<button
className={styles.PayButton}
onClick={() => handleConfirm(transaction.transactionId)}
@@ -363,25 +337,30 @@ console.log(aggregatedItems.values())
>
{
isPaymentLoading ? (
<ColorRing height="50" width="50" color="white" />
<ColorRing height="28" width="28" color="white" />
) : transaction.confirmed === 1 ? (
"Konfirmasi Telah Bayar"
"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>
)}
{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)}
@@ -404,40 +383,30 @@ console.log(aggregatedItems.values())
}
</div>
{deviceType == 'guestDevice' && transaction.confirmed >= 0 && transaction.confirmed < 2 && transaction.payment_type == 'cash' ?
{deviceType == 'guestDevice' && (
transaction.confirmed >= 0 && transaction.confirmed < 2 && transaction.payment_type == 'cash' ? (
<button
className={styles.PayButton}
className={styles.DeclineButton}
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)
)}
{isPaymentLoading ? '...' : 'Batalkan'}
</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)
}
) : (
(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"}
</h5>
{isPaymentOpen ? 'Kembali' : 'Batalkan'}
</button>
)
}
)
)}
</div>
))}
</div>

View File

@@ -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 */
}

View File

@@ -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;
}
}

View File

@@ -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"}
<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,7 +55,7 @@ const WelcomePage = ({
{welcomingText}
</h1>
<button className="get-started-button" onClick={onGetStarted}>
Get Started
Mulai
</button>
</div>
);

View File

@@ -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;
}
}

View File

@@ -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" }}
<div className="welcome-page-editor">
<div className="editor-header">
<h2 className="editor-title">Konfigurasi Halaman Selamat Datang</h2>
</div>
<div className="editor-content">
{/* Configuration Panel */}
<div className="config-panel">
{/* Top Preview Toggle */}
<div className="top-actions">
<button
type="button"
className="preview-button"
onClick={() => setShowPreview(!showPreview)}
>
<h2>Edit Welcome Page</h2>
<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"}
<EyeIcon /> {showPreview ? 'Tutup Pratinjau' : 'Preview'}
</button>
</div>
<div
style={{ width: "100%", height: "100%", position: "relative", flex: 1, borderRadius: '15px' }}
>
{showPreview && (
<div className="inline-preview">
<div className="inline-preview-content">
<WelcomePage
image={image}
welcomingText={welcomingText}
backgroundColor={backgroundColor}
textColor={textColor}
onGetStarted={() => setIsFullscreen(false)}
isFullscreen={isFullscreen}
onGetStarted={() => setShowPreview(false)}
isFullscreen={false}
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>
</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
>
&lt;&gt;
</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>