Compare commits
8 Commits
67cf759b31
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2add1c5090 | ||
|
|
3fbe0bb870 | ||
|
|
dae7fb9221 | ||
|
|
6ed982d6ef | ||
|
|
df203447a9 | ||
|
|
f58b40c70d | ||
|
|
6127415e37 | ||
|
|
b28c6ed0fe |
58
note/detailUI.md
Normal file
58
note/detailUI.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
**Overlay/Status**
|
||||||
|
- Loader Overlay: layar penuh dengan spinner `ThreeDots` dan pesan `screenMessag
|
||||||
|
e`; muncul saat `loading` true.
|
||||||
|
- Grayscale State: seluruh konten `div.Cafe` menjadi abu-abu saat `isExceededDea
|
||||||
|
dline` true (kelas `grayscale`).
|
||||||
|
- Modal Pesan: “Kafe sedang tidak tersedia” dapat muncul via `setModal("message"
|
||||||
|
, ...)` ketika `isExceededDeadline` dari socket; modalnya dikelola global (di lu
|
||||||
|
ar file ini).
|
||||||
|
|
||||||
|
**Header**
|
||||||
|
- Bar Atas “Menu”: judul “Menu”, info toko (nama, gambar), profil user, opsi log
|
||||||
|
out, akses pegawai, table code.
|
||||||
|
- Edit Mode Toggle: kontrol untuk mengaktifkan/menonaktifkan mode edit kategori/
|
||||||
|
item (prop `isEditMode` + `setIsEditMode`).
|
||||||
|
|
||||||
|
**Branding/Watermark**
|
||||||
|
- Dev Watermark (atas): `div.Watermark` kecil hanya tampil jika `API_BASE_URL` b
|
||||||
|
ukan domain resmi prod/dev (indikator environment).
|
||||||
|
- Footer Watermark: komponen `<Watermark />` di bagian paling bawah halaman.
|
||||||
|
|
||||||
|
**Music**
|
||||||
|
- Music Player: widget pemutar/antrian lagu dengan dukungan Spotify; menampilkan
|
||||||
|
status login Spotify (`isSpotifyNeedLogin`) dan antrian (`queue`), interaksi vi
|
||||||
|
a `socket`.
|
||||||
|
|
||||||
|
**Kategori (ItemTypeLister)**
|
||||||
|
- Daftar Kategori: list tipe menu (nama + gambar tipe, visibilitas).
|
||||||
|
- Filter Kategori: memilih 1 kategori untuk menyaring tampilan item (`filterId`)
|
||||||
|
.
|
||||||
|
- Edit Kategori: saat `isEditMode` aktif, bisa memilih tipe yang sedang diedit (
|
||||||
|
`beingEditedType`) dan mengubah urutan tipe (via kontrol di ItemLister).
|
||||||
|
|
||||||
|
**Daftar Item per Kategori (ItemLister x N)**
|
||||||
|
- Section per Kategori: render berulang untuk setiap tipe yang lolos filter. Men
|
||||||
|
ampilkan:
|
||||||
|
- Judul/nama kategori dan gambar (bila ada).
|
||||||
|
- Daftar item dalam kategori (nama, harga, promo, deskripsi, gambar, visibilit
|
||||||
|
as).
|
||||||
|
- Aksi Item:
|
||||||
|
- Tambah item: tombol/form untuk membuat item (owner/akses yang berwenang).
|
||||||
|
- Ubah item: edit nama, harga, promo, deskripsi, gambar.
|
||||||
|
- Reorder Kategori: panah/aksi “naik/turun” pada section untuk memindahkan posis
|
||||||
|
i kategori (memanggil `moveItemType*`).
|
||||||
|
- Raw Mode: jika tidak edit dan filter spesifik aktif, `raw` true untuk gaya tam
|
||||||
|
pilan ringkas.
|
||||||
|
|
||||||
|
**Sticky Bar (Keranjang & Transaksi)**
|
||||||
|
- Tombol Cart (kiri, utama): muncul jika bukan edit mode dan (user login atau ke
|
||||||
|
ranjang > 0).
|
||||||
|
- Menampilkan jumlah item (dengan “+” jika ada transaksi terakhir), total harg
|
||||||
|
a “Rp{totalPrice}” atau teks “Open bill” (jika `lastTransaction.payment_type ==
|
||||||
|
'paylater'`).
|
||||||
|
- Posisi: sticky di bawah (offset bottom ~40px), lebar responsif.
|
||||||
|
- Tombol Transactions (kanan, kecil): hanya muncul jika user login; navigasi ke
|
||||||
|
riwayat transaksi.
|
||||||
|
|
||||||
|
|
||||||
|
material list, material
|
||||||
219
package-lock.json
generated
219
package-lock.json
generated
@@ -14,12 +14,14 @@
|
|||||||
"@testing-library/jest-dom": "^5.17.0",
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"apexcharts": "^5.3.4",
|
||||||
"caniuse-lite": "^1.0.30001690",
|
"caniuse-lite": "^1.0.30001690",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"jsqr": "^1.4.0",
|
"jsqr": "^1.4.0",
|
||||||
|
"lucide-react": "^0.541.0",
|
||||||
"qr-scanner": "^1.4.2",
|
"qr-scanner": "^1.4.2",
|
||||||
"qrcode.react": "^3.1.0",
|
"qrcode.react": "^3.1.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
@@ -3905,171 +3907,6 @@
|
|||||||
"integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
|
"integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@mui/types": {
|
||||||
"version": "7.4.5",
|
"version": "7.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.5.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.6.tgz",
|
||||||
"integrity": "sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==",
|
"integrity": "sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@svgdotjs/svg.js": "^3.2.4"
|
"@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",
|
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.filter.js/-/svg.filter.js-3.0.9.tgz",
|
||||||
"integrity": "sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw==",
|
"integrity": "sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@svgdotjs/svg.js": "^3.2.4"
|
"@svgdotjs/svg.js": "^3.2.4"
|
||||||
},
|
},
|
||||||
@@ -4661,7 +4496,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.4.tgz",
|
||||||
"integrity": "sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==",
|
"integrity": "sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/Fuzzyma"
|
"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",
|
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.resize.js/-/svg.resize.js-2.0.5.tgz",
|
||||||
"integrity": "sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==",
|
"integrity": "sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14.18"
|
"node": ">= 14.18"
|
||||||
},
|
},
|
||||||
@@ -4686,7 +4519,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.select.js/-/svg.select.js-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.select.js/-/svg.select.js-4.0.3.tgz",
|
||||||
"integrity": "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==",
|
"integrity": "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14.18"
|
"node": ">= 14.18"
|
||||||
},
|
},
|
||||||
@@ -4924,26 +4756,6 @@
|
|||||||
"tslib": "^2.8.0"
|
"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": {
|
"node_modules/@testing-library/jest-dom": {
|
||||||
"version": "5.17.0",
|
"version": "5.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz",
|
||||||
@@ -5976,8 +5788,7 @@
|
|||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz",
|
||||||
"integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==",
|
"integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@zxing/browser": {
|
"node_modules/@zxing/browser": {
|
||||||
"version": "0.0.7",
|
"version": "0.0.7",
|
||||||
@@ -6315,7 +6126,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-5.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-5.3.4.tgz",
|
||||||
"integrity": "sha512-N0gNh8uLu/BN8N+BCphNK+gZAoSoUtDDn1jFGB+3+EMcv8s6vajuP3W0g4dMLTRp6chFkjMmQK3uD8pz4ISmLA==",
|
"integrity": "sha512-N0gNh8uLu/BN8N+BCphNK+gZAoSoUtDDn1jFGB+3+EMcv8s6vajuP3W0g4dMLTRp6chFkjMmQK3uD8pz4ISmLA==",
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@svgdotjs/svg.draggable.js": "^3.0.4",
|
"@svgdotjs/svg.draggable.js": "^3.0.4",
|
||||||
"@svgdotjs/svg.filter.js": "^3.0.8",
|
"@svgdotjs/svg.filter.js": "^3.0.8",
|
||||||
@@ -15199,6 +15009,15 @@
|
|||||||
"yallist": "^3.0.2"
|
"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": {
|
"node_modules/lz-string": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||||
@@ -20825,20 +20644,6 @@
|
|||||||
"is-typedarray": "^1.0.0"
|
"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": {
|
"node_modules/unbox-primitive": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
|
||||||
|
|||||||
@@ -10,12 +10,14 @@
|
|||||||
"@testing-library/jest-dom": "^5.17.0",
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"apexcharts": "^5.3.4",
|
||||||
"caniuse-lite": "^1.0.30001690",
|
"caniuse-lite": "^1.0.30001690",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"jsqr": "^1.4.0",
|
"jsqr": "^1.4.0",
|
||||||
|
"lucide-react": "^0.541.0",
|
||||||
"qr-scanner": "^1.4.2",
|
"qr-scanner": "^1.4.2",
|
||||||
"qrcode.react": "^3.1.0",
|
"qrcode.react": "^3.1.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
|||||||
149
src/App.css
149
src/App.css
@@ -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=Roboto:wght@400;500;700&display=swap");
|
||||||
@import url("https://fonts.googleapis.com/css2?family=Aboreto&family=Rubik+Doodle+Shadow&display=swap");
|
@import url("https://fonts.googleapis.com/css2?family=Aboreto&family=Rubik+Doodle+Shadow&display=swap");
|
||||||
@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@200;300;400;500;600;700;800&ital,wght@0,200..800;1,200..800&display=swap");
|
@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@200;300;400;500;600;700;800&display");
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--brand-primary: #73a585; /* general brand (e. g., music player) */
|
||||||
|
--brand-sage: #6B8F71; /* sage green for active category */
|
||||||
|
--brand-sage-50: #F0F6F2; /* very light hover bg */
|
||||||
|
--brand-sage-100: #E9F3ED; /* light hover bg */
|
||||||
|
--brand-sage-hover: #7FAE7D; /* hover for filled buttons */
|
||||||
|
--brand-sage-muted: #CFD8D3; /* disabled button */
|
||||||
|
}
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none; /* Firefox */
|
||||||
@@ -27,7 +36,7 @@ body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-image: url(https://i.ibb.co.com/F4FMw1jz/testuseonly.png);
|
background-image: url(https://i.ibb.co.com/F4FMw1jz/testuseonly.png);
|
||||||
z-index: 1000;
|
z-index: 0; /* align with item lister */
|
||||||
filter: opacity(0.04);
|
filter: opacity(0.04);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@@ -54,6 +63,142 @@ body {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* removed two-column layout; reverted to single column */
|
||||||
|
|
||||||
|
/* Ensure sticky cart bar stays above item overlays */
|
||||||
|
.StickyCartBar {
|
||||||
|
z-index: 120 !important; /* above content, below modal */
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 24px;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
pointer-events: none; /* only children capture clicks */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Intro slide/fade when bar first appears */
|
||||||
|
.StickyCartBar.intro { animation: bar-in 450ms ease-out; }
|
||||||
|
@keyframes bar-in {
|
||||||
|
0% { opacity: 0; transform: translateY(12px); }
|
||||||
|
100% { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Floating cart buttons inside sticky bar */
|
||||||
|
.StickyCartBar .cartBtn {
|
||||||
|
pointer-events: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 44px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: none;
|
||||||
|
background: rgba(115,165,133,0.96);
|
||||||
|
color: #fff;
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
box-shadow: 0 12px 28px rgba(115,165,133,0.35);
|
||||||
|
width: clamp(200px, 60vw, 420px);
|
||||||
|
transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.StickyCartBar .cartBtn:hover { box-shadow: 0 14px 32px rgba(115,165,133,0.4); transform: translateY(-1px); }
|
||||||
|
.StickyCartBar .cartBtn:active { transform: translateY(0); }
|
||||||
|
.StickyCartBar .cartBtn:focus-visible { outline: 3px solid rgba(115,165,133,0.4); outline-offset: 2px; }
|
||||||
|
|
||||||
|
/* Bump + ping animation when items are added */
|
||||||
|
.StickyCartBar .cartBtn.bump { animation: cart-bump 300ms ease; }
|
||||||
|
.StickyCartBar .cartBtn.bump::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
box-shadow: 0 0 0 0 rgba(115,165,133,0.35);
|
||||||
|
animation: cart-ping 600ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cart-bump {
|
||||||
|
0% { transform: scale(1); }
|
||||||
|
10% { transform: scale(0.98); }
|
||||||
|
30% { transform: scale(1.04); }
|
||||||
|
50% { transform: scale(1.02); }
|
||||||
|
100% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
@keyframes cart-ping {
|
||||||
|
0% { box-shadow: 0 0 0 0 rgba(115,165,133,0.35); }
|
||||||
|
100% { box-shadow: 0 0 0 14px rgba(115,165,133,0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.StickyCartBar .historyBtn {
|
||||||
|
pointer-events: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 48px;
|
||||||
|
height: 44px;
|
||||||
|
margin-left: 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: none;
|
||||||
|
background: rgba(115,165,133,0.96);
|
||||||
|
color: #fff;
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
box-shadow: 0 12px 28px rgba(115,165,133,0.35);
|
||||||
|
transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.2s ease;
|
||||||
|
}
|
||||||
|
.StickyCartBar .historyBtn:hover { box-shadow: 0 14px 32px rgba(115,165,133,0.4); transform: translateY(-1px); }
|
||||||
|
.StickyCartBar .historyBtn:active { transform: translateY(0); }
|
||||||
|
.StickyCartBar .historyBtn:focus-visible { outline: 3px solid rgba(115,165,133,0.4); outline-offset: 2px; }
|
||||||
|
|
||||||
|
/* Subtle pulse to draw attention on new transaction */
|
||||||
|
.StickyCartBar .historyBtn.pulse { animation: btn-pulse 900ms ease-out; }
|
||||||
|
@keyframes btn-pulse {
|
||||||
|
0% { box-shadow: 0 12px 28px rgba(115,165,133,0.35); }
|
||||||
|
60% { box-shadow: 0 18px 38px rgba(115,165,133,0.5); }
|
||||||
|
100% { box-shadow: 0 12px 28px rgba(115,165,133,0.35); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.StickyCartBar .summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.StickyCartBar .value { white-space: nowrap; font-weight: 800; }
|
||||||
|
.StickyCartBar .icon { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; margin-left: 8px; }
|
||||||
|
.StickyCartBar .icon { position: relative; }
|
||||||
|
.StickyCartBar .icon .badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
right: -6px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ff6b6b;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 18px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0;
|
||||||
|
box-shadow: 0 2px 6px rgba(255,107,107,0.35);
|
||||||
|
}
|
||||||
|
.StickyCartBar .icon .badge.pop { animation: badge-pop 280ms ease-out; }
|
||||||
|
@keyframes badge-pop {
|
||||||
|
0% { transform: scale(0.8); }
|
||||||
|
50% { transform: scale(1.2); }
|
||||||
|
100% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 420px) {
|
||||||
|
.StickyCartBar { bottom: 16px; }
|
||||||
|
.StickyCartBar .cartBtn { height: 46px; }
|
||||||
|
.StickyCartBar .historyBtn { height: 46px; width: 48px; }
|
||||||
|
}
|
||||||
|
|
||||||
.App-link {
|
.App-link {
|
||||||
color: #61dafb;
|
color: #61dafb;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import styles from "./Carousel.module.css"; // Import CSS module
|
|
||||||
|
|
||||||
const Carousel = ({ selectedIndex, items, onSelect }) => {
|
|
||||||
|
|
||||||
const moveToNext = () => {
|
|
||||||
console.log('aa')
|
|
||||||
if (selectedIndex < items.length - 1) {
|
|
||||||
console.log('bb')
|
|
||||||
onSelect(selectedIndex + 1); // Send the next index to the parent
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const moveToPrev = () => {
|
|
||||||
if (selectedIndex > -1) {
|
|
||||||
onSelect(selectedIndex - 1); // Send the previous index to the parent
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.carouselContainer}>
|
|
||||||
<div className={styles.carousel}>
|
|
||||||
{/* Previous Item */}
|
|
||||||
<div
|
|
||||||
className={`${styles.carouselItem} ${styles.prev}`}
|
|
||||||
onClick={moveToPrev}
|
|
||||||
style={{ visibility: selectedIndex === -1 && items.length > 0 ? "hidden" : "visible" , backgroundColor: items.length < 1 ? '#919191':'#007bff'}}
|
|
||||||
>
|
|
||||||
{selectedIndex === -1 ? (items.length > 0 ? "+" : "") : items[selectedIndex - 1]?.name || "+"}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Current Item */}
|
|
||||||
<div className={`${styles.carouselItem} ${styles.center}`}>
|
|
||||||
{selectedIndex >= 0 ? items[selectedIndex]?.name : "+"}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Next Item */}
|
|
||||||
<div
|
|
||||||
className={`${styles.carouselItem} ${styles.next}`}
|
|
||||||
onClick={moveToNext}
|
|
||||||
style={{
|
|
||||||
visibility: selectedIndex === items.length -1 && items.length > 0 ? "hidden" : "visible", backgroundColor: items.length < 1 ? '#919191':'#007bff'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{selectedIndex < items.length - 1 && items[selectedIndex + 1]?.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Carousel;
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
/* Carousel.module.css */
|
|
||||||
.carouselContainer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.carousel {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 600px;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.carouselItem {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
transition: transform 0.3s ease-in-out;
|
|
||||||
height: 200px;
|
|
||||||
background-color: #007bff;
|
|
||||||
color: white;
|
|
||||||
font-size: 24px;
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.carouselItem.prev,
|
|
||||||
.carouselItem.next {
|
|
||||||
transform: scale(0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.carouselItem.center {
|
|
||||||
transform: scale(1);
|
|
||||||
width: 134%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prevBtn,
|
|
||||||
.nextBtn {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-size: 16px;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prevBtn {
|
|
||||||
left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nextBtn {
|
|
||||||
right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.carouselItem {
|
|
||||||
font-size: 18px;
|
|
||||||
height: 110px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prevBtn,
|
|
||||||
.nextBtn {
|
|
||||||
padding: 8px 15px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ import { useLocation } from "react-router-dom";
|
|||||||
import { useNavigationHelpers } from "../helpers/navigationHelpers";
|
import { useNavigationHelpers } from "../helpers/navigationHelpers";
|
||||||
import Switch from "react-switch";
|
import Switch from "react-switch";
|
||||||
|
|
||||||
|
// Restore original gradient background for header container when shopName exists
|
||||||
const HeaderBarbackground = styled.div`
|
const HeaderBarbackground = styled.div`
|
||||||
${({ shopName }) =>
|
${({ shopName }) =>
|
||||||
shopName &&
|
shopName &&
|
||||||
@@ -19,10 +20,10 @@ const HeaderBar = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 20px 15px;
|
padding: 12px 14px;
|
||||||
color: black;
|
color: black;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
z-index: 200;
|
z-index: ${(props) => (props.zIndexLevel !== undefined ? props.zIndexLevel : 200)};
|
||||||
border: 1px solid #00000000;
|
border: 1px solid #00000000;
|
||||||
margin: 20px 12px;
|
margin: 20px 12px;
|
||||||
border-radius: 13px;
|
border-radius: 13px;
|
||||||
@@ -33,30 +34,15 @@ const HeaderBar = styled.div`
|
|||||||
const Title = styled.h2`
|
const Title = styled.h2`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-size:${(props) => (props.HeaderSize)};
|
font-size:${(props) => (props.HeaderSize)};
|
||||||
color: rgba(88, 55, 50, 1);
|
color: rgba(45, 45, 45, 1);
|
||||||
text-transform: uppercase;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ProfileName = styled.h2`
|
// SubTitle removed per redesign (no subtitle below cafe name)
|
||||||
position: absolute;
|
|
||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
// Deprecated the animated ProfileName in favor of a cleaner layout
|
||||||
font-weight: 500;
|
|
||||||
font-style: normal;
|
|
||||||
font-size: 30px;
|
|
||||||
z-index: 199;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
animation: ${(props) => {
|
|
||||||
if (props.animate === "grow") return gg;
|
|
||||||
if (props.animate === "shrink") return ss;
|
|
||||||
return nn;
|
|
||||||
}}
|
|
||||||
0.5s forwards;
|
|
||||||
text-align: left;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const nn = keyframes`
|
const nn = keyframes`
|
||||||
0% {
|
0% {
|
||||||
@@ -103,22 +89,17 @@ const ss = keyframes`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ProfileImage = styled.img`
|
const CafeAvatar = styled.img`
|
||||||
position: relative;
|
width: clamp(32px, 5vw, 56px);
|
||||||
width: 60px;
|
height: clamp(32px, 5vw, 56px);
|
||||||
height: 60px;
|
border-radius: 8px;
|
||||||
border-radius: 50%;
|
object-fit: cover;
|
||||||
object-fit: contain;
|
background: #f2f2f2;
|
||||||
cursor: pointer;
|
margin-left: 8px; /* extra left padding so it’s not too tight */
|
||||||
z-index: 199;
|
|
||||||
animation: ${(props) => {
|
|
||||||
if (props.animate === "grow") return g;
|
|
||||||
if (props.animate === "shrink") return s;
|
|
||||||
return "none";
|
|
||||||
}}
|
|
||||||
0.5s forwards;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// User initial avatar removed; only cafe image is shown on the left
|
||||||
|
|
||||||
const g = keyframes`
|
const g = keyframes`
|
||||||
0% {
|
0% {
|
||||||
top: 0px;
|
top: 0px;
|
||||||
@@ -149,62 +130,43 @@ const s = keyframes`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
/* Replace bubble-like animation with subtle fade/slide */
|
||||||
const grow = keyframes`
|
const grow = keyframes`
|
||||||
0% {
|
0% { opacity: 0; transform: translateY(-6px) scale(0.98); }
|
||||||
right: 12px;
|
100% { opacity: 1; transform: translateY(0) scale(1); }
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
border-top-left-radius: 50%;
|
|
||||||
border-bottom-left-radius: 50%;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
right: 28px;
|
|
||||||
width: 300px;
|
|
||||||
border-top-left-radius: 15px;
|
|
||||||
border-bottom-left-radius: 15px;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const shrink = keyframes`
|
const shrink = keyframes`
|
||||||
0% {
|
0% { opacity: 1; transform: translateY(0) scale(1); }
|
||||||
right: 12px;
|
100% { opacity: 0; transform: translateY(-6px) scale(0.98); }
|
||||||
width: 300px;
|
|
||||||
height: auto;
|
|
||||||
border-radius: 20px;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
right: 28px;
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
const Rectangle = styled.div`
|
const Rectangle = styled.div`
|
||||||
overflow-y: hidden;
|
overflow-y: auto;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 39px;
|
top: calc(100% + 8px);
|
||||||
right: 12px;
|
right: 0;
|
||||||
width: 200px;
|
width: 240px;
|
||||||
max-height: 87vh; /* or another appropriate value */
|
max-height: 75vh;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
z-index: 198;
|
z-index: ${(props) => (props.baseZIndex !== undefined ? props.baseZIndex : 198)};
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
animation: ${(props) => (props.animate === "grow" ? grow : shrink)} 0.5s
|
border: 1px solid #f0f0f0;
|
||||||
forwards;
|
border-radius: 12px;
|
||||||
padding: 10px;
|
animation: ${(props) => (props.animate === "grow" ? grow : shrink)} 0.2s forwards;
|
||||||
|
padding: 10px 12px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #393939;
|
color: #393939;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ChildContainer = styled.div`
|
const ChildContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: flex-end;
|
align-items: stretch;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
padding-top: 70px;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ChildWrapper = styled.div`
|
const ChildWrapper = styled.div`
|
||||||
@@ -213,25 +175,88 @@ const ChildWrapper = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
const Child = styled.div`
|
const Child = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 36px;
|
min-height: 36px;
|
||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
padding: ${(props) => (props.hasChildren ? '8px 0 4px' : '8px 4px')};
|
||||||
${(props) =>
|
${(props) =>
|
||||||
props.hasChildren
|
props.hasChildren
|
||||||
? `
|
? `
|
||||||
margin-top: 14px;
|
margin-top: 10px;
|
||||||
border-top: 0.5px solid #a5a5a5;
|
border-top: 0.5px solid #e9e9e9;
|
||||||
height: auto;
|
height: auto;
|
||||||
`
|
`
|
||||||
: `
|
: `
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const LeftGroup = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CenterGroup = styled.div`
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: none; /* so clicks pass through center when needed */
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CafeInfo = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RightGroup = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CategoryLabel = styled.div`
|
||||||
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #6B8F71;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
padding: 6px 2px;
|
||||||
|
pointer-events: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const HamburgerButton = styled.button`
|
||||||
|
width: clamp(32px, 4.5vw, 52px);
|
||||||
|
height: clamp(32px, 4.5vw, 52px);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
background: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
& > svg {
|
||||||
|
width: 60%;
|
||||||
|
height: 60%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const HamburgerIcon = () => (
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="3" y="6" width="18" height="2" rx="1" fill="#2d2d2d"/>
|
||||||
|
<rect x="3" y="11" width="18" height="2" rx="1" fill="#2d2d2d"/>
|
||||||
|
<rect x="3" y="16" width="18" height="2" rx="1" fill="#2d2d2d"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
const Header = ({
|
const Header = ({
|
||||||
HeaderText,
|
HeaderText,
|
||||||
@@ -251,7 +276,9 @@ const Header = ({
|
|||||||
removeConnectedGuestSides,
|
removeConnectedGuestSides,
|
||||||
setIsEditMode,
|
setIsEditMode,
|
||||||
isEditMode,
|
isEditMode,
|
||||||
HeaderMargin = '25px'
|
HeaderMargin = '25px',
|
||||||
|
zIndexLevel,
|
||||||
|
rectZIndex
|
||||||
}) => {
|
}) => {
|
||||||
const { goToLogin, goToGuestSideLogin, goToAdminCafes } =
|
const { goToLogin, goToGuestSideLogin, goToAdminCafes } =
|
||||||
useNavigationHelpers(shopId, tableCode);
|
useNavigationHelpers(shopId, tableCode);
|
||||||
@@ -261,10 +288,10 @@ const Header = ({
|
|||||||
const [guestSideOf, setGuestSideOf] = useState(null);
|
const [guestSideOf, setGuestSideOf] = useState(null);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const handleImageClick = () => {
|
const toggleMenu = () => {
|
||||||
if (showRectangle) {
|
if (showRectangle) {
|
||||||
setAnimate("shrink");
|
setAnimate("shrink");
|
||||||
setTimeout(() => setShowRectangle(false), 500);
|
setTimeout(() => setShowRectangle(false), 200);
|
||||||
} else {
|
} else {
|
||||||
setAnimate("grow");
|
setAnimate("grow");
|
||||||
setShowRectangle(true);
|
setShowRectangle(true);
|
||||||
@@ -274,15 +301,14 @@ const Header = ({
|
|||||||
const handleClickOutside = (event) => {
|
const handleClickOutside = (event) => {
|
||||||
if (rectangleRef.current && !rectangleRef.current.contains(event.target)) {
|
if (rectangleRef.current && !rectangleRef.current.contains(event.target)) {
|
||||||
setAnimate("shrink");
|
setAnimate("shrink");
|
||||||
setTimeout(() => setShowRectangle(false), 500);
|
setTimeout(() => setShowRectangle(false), 200);
|
||||||
rectangleRef.current.style.overflow = "hidden";
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (showRectangle) {
|
if (showRectangle) {
|
||||||
setAnimate("shrink");
|
setAnimate("shrink");
|
||||||
setTimeout(() => setShowRectangle(false), 500);
|
setTimeout(() => setShowRectangle(false), 200);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -321,38 +347,48 @@ const Header = ({
|
|||||||
// Otherwise, use the possessive function
|
// Otherwise, use the possessive function
|
||||||
return `${cafeName}'s menu`;
|
return `${cafeName}'s menu`;
|
||||||
};
|
};
|
||||||
return (
|
|
||||||
|
const formatCafeName = (name) => {
|
||||||
|
if (!name) return name;
|
||||||
|
return name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
|
};
|
||||||
|
return (
|
||||||
<HeaderBarbackground shopName={shopName}>
|
<HeaderBarbackground shopName={shopName}>
|
||||||
<HeaderBar HeaderMargin={HeaderMargin} shopName={shopName}>
|
<HeaderBar HeaderMargin={HeaderMargin} shopName={shopName} zIndexLevel={zIndexLevel}>
|
||||||
<Title HeaderSize={HeaderSize}>
|
<LeftGroup>
|
||||||
{shopName == null
|
<CafeAvatar
|
||||||
? HeaderText == null
|
src={shopImage && !shopImage.includes('undefined') ? shopImage : "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS-DjX_bGBax4NL14ULvkAdU4FP3FKoWXWu5w&s"}
|
||||||
? "kedaimaster"
|
alt="Cafe"
|
||||||
: HeaderText
|
/>
|
||||||
: shopName}
|
</LeftGroup>
|
||||||
</Title>
|
<CenterGroup>
|
||||||
<div style={{ visibility: showProfile ? "visible" : "hidden" }}>
|
<Title HeaderSize={HeaderSize}>
|
||||||
<ProfileImage
|
{formatCafeName(
|
||||||
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"}
|
shopName == null
|
||||||
alt="Profile"
|
? HeaderText == null
|
||||||
onClick={user.username !== undefined ? handleImageClick : null}
|
? "kedaimaster"
|
||||||
animate={showRectangle && animate}
|
: HeaderText
|
||||||
/>
|
: shopName
|
||||||
<ProfileName animate={showRectangle && animate}>
|
)}
|
||||||
{showProfile && user.username !== undefined ? user.username : "guest"}
|
</Title>
|
||||||
</ProfileName>
|
</CenterGroup>
|
||||||
{showRectangle && (
|
|
||||||
<Rectangle ref={rectangleRef} animate={animate}>
|
<RightGroup style={{ visibility: showProfile ? "visible" : "hidden", position: 'relative' }}>
|
||||||
<ChildContainer>
|
<HamburgerButton onClick={toggleMenu} aria-label="Open menu">
|
||||||
|
<HamburgerIcon />
|
||||||
|
</HamburgerButton>
|
||||||
|
{showRectangle && (
|
||||||
|
<Rectangle ref={rectangleRef} animate={animate} baseZIndex={rectZIndex !== undefined ? rectZIndex : zIndexLevel}>
|
||||||
|
<ChildContainer>
|
||||||
{guestSideOfClerk && guestSideOfClerk.clerkUsername && (
|
{guestSideOfClerk && guestSideOfClerk.clerkUsername && (
|
||||||
<Child hasChildren>
|
<Child hasChildren>
|
||||||
this is the guest side of {guestSideOfClerk.clerkUsername}
|
this is the guest side of {guestSideOfClerk.clerkUsername}
|
||||||
</Child>
|
</Child>
|
||||||
)}
|
)}
|
||||||
{user.username !== undefined && (
|
{user.username !== undefined && (
|
||||||
<Child onClick={() => setModal("edit_account")}>
|
<CategoryLabel>Kelola akun</CategoryLabel>
|
||||||
Kelola akun
|
|
||||||
</Child>
|
|
||||||
)}
|
)}
|
||||||
{user.roleId == 0 && (
|
{user.roleId == 0 && (
|
||||||
<Child onClick={()=>setModal('create_coupon', {})}>Buat Voucher</Child>)}
|
<Child onClick={()=>setModal('create_coupon', {})}>Buat Voucher</Child>)}
|
||||||
@@ -364,9 +400,9 @@ const Header = ({
|
|||||||
user.roleId === 1 && (
|
user.roleId === 1 && (
|
||||||
<>
|
<>
|
||||||
<Child hasChildren>
|
<Child hasChildren>
|
||||||
<Child>
|
<CategoryLabel>
|
||||||
{shopName}
|
{formatCafeName(shopName)}
|
||||||
</Child>
|
</CategoryLabel>
|
||||||
<Child>
|
<Child>
|
||||||
Mode pengembangan
|
Mode pengembangan
|
||||||
<Switch
|
<Switch
|
||||||
@@ -381,7 +417,7 @@ const Header = ({
|
|||||||
</Child>
|
</Child>
|
||||||
|
|
||||||
<Child hasChildren>
|
<Child hasChildren>
|
||||||
<Child>Konfigurasi</Child>
|
<CategoryLabel>Konfigurasi</CategoryLabel>
|
||||||
<Child onClick={() => setModal("welcome_config")}>
|
<Child onClick={() => setModal("welcome_config")}>
|
||||||
Desain kafe
|
Desain kafe
|
||||||
</Child>
|
</Child>
|
||||||
@@ -393,7 +429,7 @@ const Header = ({
|
|||||||
</Child>
|
</Child>
|
||||||
</Child>
|
</Child>
|
||||||
<Child hasChildren>
|
<Child hasChildren>
|
||||||
<Child>Kasir</Child>
|
<CategoryLabel>Kasir</CategoryLabel>
|
||||||
<Child onClick={() => setModal("create_clerk")}>
|
<Child onClick={() => setModal("create_clerk")}>
|
||||||
+ Tambah
|
+ Tambah
|
||||||
</Child>
|
</Child>
|
||||||
@@ -420,7 +456,7 @@ const Header = ({
|
|||||||
user.cafeId == shopId &&
|
user.cafeId == shopId &&
|
||||||
user.roleId === 2 && (
|
user.roleId === 2 && (
|
||||||
<Child hasChildren>
|
<Child hasChildren>
|
||||||
<Child>{shopName}</Child>
|
<CategoryLabel>{formatCafeName(shopName)}</CategoryLabel>
|
||||||
|
|
||||||
<Child>
|
<Child>
|
||||||
Mode pengembangan
|
Mode pengembangan
|
||||||
@@ -435,7 +471,7 @@ const Header = ({
|
|||||||
</Child>
|
</Child>
|
||||||
|
|
||||||
<Child hasChildren>
|
<Child hasChildren>
|
||||||
<Child>Konfigurasi</Child>
|
<CategoryLabel>Konfigurasi</CategoryLabel>
|
||||||
<Child onClick={() => setModal("welcome_config")}>
|
<Child onClick={() => setModal("welcome_config")}>
|
||||||
Desain kafe
|
Desain kafe
|
||||||
</Child>
|
</Child>
|
||||||
@@ -478,11 +514,12 @@ const Header = ({
|
|||||||
{user.username !== undefined && (
|
{user.username !== undefined && (
|
||||||
<Child hasChildren ><Child onClick={isLogout}>Logout</Child></Child>
|
<Child hasChildren ><Child onClick={isLogout}>Logout</Child></Child>
|
||||||
)}
|
)}
|
||||||
</ChildContainer>
|
</ChildContainer>
|
||||||
</Rectangle>
|
</Rectangle>
|
||||||
)}
|
)}
|
||||||
</div>
|
</RightGroup>
|
||||||
</HeaderBar></HeaderBarbackground>
|
</HeaderBar>
|
||||||
|
</HeaderBarbackground>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
348
src/components/IdentifyCafeModal.js
Normal file
348
src/components/IdentifyCafeModal.js
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import styles from "./IdentifyCafeModal.module.css";
|
||||||
|
import API_BASE_URL from "../config.js";
|
||||||
|
import { getTables, createTable } from "../helpers/tableHelper";
|
||||||
|
import { saveCafeDetails } from "../helpers/cafeHelpers";
|
||||||
|
import { getImageUrl } from "../helpers/itemHelper";
|
||||||
|
import { toPng } from "html-to-image";
|
||||||
|
|
||||||
|
export default function IdentifyCafeModal({ shop }) {
|
||||||
|
const [cafeIdentifyName, setCafeIdentifyName] = useState(shop.cafeIdentifyName || "");
|
||||||
|
const [availability, setAvailability] = useState(null); // 200 ok, 409 taken
|
||||||
|
const [checking, setChecking] = useState(false);
|
||||||
|
|
||||||
|
const [tables, setTables] = useState([]);
|
||||||
|
const [selectedTable, setSelectedTable] = useState(null);
|
||||||
|
|
||||||
|
const [qrSize, setQrSize] = useState(Number(shop.scale) || 1);
|
||||||
|
const [qrX, setQrX] = useState(Number(shop.xposition) || 50);
|
||||||
|
const [qrY, setQrY] = useState(Number(shop.yposition) || 50);
|
||||||
|
const [fontSize, setFontSize] = useState(Number(shop.fontsize) || 16);
|
||||||
|
const [fontColor, setFontColor] = useState(shop.fontcolor || "#FFFFFF");
|
||||||
|
const [fontX, setFontX] = useState(Number(shop.fontxposition) || 50);
|
||||||
|
const [fontY, setFontY] = useState(Number(shop.fontyposition) || 85);
|
||||||
|
const [bgImageUrl, setBgImageUrl] = useState(getImageUrl(shop.qrBackground));
|
||||||
|
const bgFileRef = useRef(null);
|
||||||
|
|
||||||
|
const [newTableNo, setNewTableNo] = useState("");
|
||||||
|
const previewRef = useRef(null);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [currentStep, setCurrentStep] = useState(1); // 1=Alamat, 2=Desain QR, 3=Meja
|
||||||
|
const [saveStatus, setSaveStatus] = useState(null); // 'success' | 'error'
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
const initialDesignRef = useRef({
|
||||||
|
qrSize: Number(shop.scale) || 1,
|
||||||
|
qrX: Number(shop.xposition) || 50,
|
||||||
|
qrY: Number(shop.yposition) || 50,
|
||||||
|
fontSize: Number(shop.fontsize) || 16,
|
||||||
|
fontColor: shop.fontcolor || "#FFFFFF",
|
||||||
|
fontX: Number(shop.fontxposition) || 50,
|
||||||
|
fontY: Number(shop.fontyposition) || 85,
|
||||||
|
bgImageUrl: getImageUrl(shop.qrBackground),
|
||||||
|
});
|
||||||
|
|
||||||
|
const shopHost = useMemo(() => window.location.hostname, []);
|
||||||
|
const fullLink = useMemo(() => `${shopHost}/${cafeIdentifyName}`, [shopHost, cafeIdentifyName]);
|
||||||
|
|
||||||
|
// Debounced availability check
|
||||||
|
const debounceRef = useRef(null);
|
||||||
|
const handleIdentifyChange = (e) => {
|
||||||
|
const val = e.target.value
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s+/g, "_")
|
||||||
|
.replace(/[^a-z0-9_]/g, "");
|
||||||
|
setCafeIdentifyName(val);
|
||||||
|
setChecking(true);
|
||||||
|
setAvailability(null);
|
||||||
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||||
|
debounceRef.current = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/cafe/check-identifyName/${val}`);
|
||||||
|
setAvailability(res.ok ? 200 : 409);
|
||||||
|
} catch (_) {
|
||||||
|
setAvailability(409);
|
||||||
|
} finally {
|
||||||
|
setChecking(false);
|
||||||
|
}
|
||||||
|
}, 600);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load tables
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const fetched = await getTables(shop.cafeId);
|
||||||
|
setTables(fetched || []);
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [shop.cafeId]);
|
||||||
|
|
||||||
|
const handleUploadBg = (e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
setBgImageUrl(url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateTable = async () => {
|
||||||
|
if (!newTableNo) return;
|
||||||
|
try {
|
||||||
|
const created = await createTable(shop.cafeId, { tableNo: newTableNo });
|
||||||
|
setTables((t) => [...t, created]);
|
||||||
|
setNewTableNo("");
|
||||||
|
} catch (e) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setSaving(true);
|
||||||
|
setSaveStatus(null);
|
||||||
|
const qrBackgroundFile = bgFileRef.current?.files?.[0];
|
||||||
|
const details = {
|
||||||
|
qrSize,
|
||||||
|
qrPosition: { left: qrX, top: qrY },
|
||||||
|
qrBackgroundFile,
|
||||||
|
fontsize: fontSize,
|
||||||
|
fontcolor: fontColor,
|
||||||
|
fontPosition: { left: fontX, top: fontY },
|
||||||
|
cafeIdentifyName: shop.cafeIdentifyName !== cafeIdentifyName ? cafeIdentifyName : null,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await saveCafeDetails(shop.cafeId, details);
|
||||||
|
setSaveStatus('success');
|
||||||
|
} catch (e) {
|
||||||
|
setSaveStatus('error');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadPreview = async () => {
|
||||||
|
if (!previewRef.current) return;
|
||||||
|
const node = previewRef.current;
|
||||||
|
const originalBg = node.style.backgroundColor;
|
||||||
|
node.style.backgroundColor = "transparent";
|
||||||
|
try {
|
||||||
|
const dataUrl = await toPng(node, { pixelRatio: 2 });
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = dataUrl;
|
||||||
|
link.download = selectedTable ? `QR Meja (${selectedTable.tableNo}).png` : `QR ${shop.name}.png`;
|
||||||
|
link.click();
|
||||||
|
} catch (e) {
|
||||||
|
// noop
|
||||||
|
} finally {
|
||||||
|
node.style.backgroundColor = originalBg;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyLink = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(fullLink);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(()=>setCopied(false), 1400);
|
||||||
|
} catch (_) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyPreset = (preset) => {
|
||||||
|
if (preset === 'center') {
|
||||||
|
setQrX(50); setQrY(50); setQrSize(1);
|
||||||
|
setFontX(50); setFontY(85);
|
||||||
|
} else if (preset === 'topLeft') {
|
||||||
|
setQrX(25); setQrY(30); setQrSize(1);
|
||||||
|
setFontX(50); setFontY(85);
|
||||||
|
} else if (preset === 'bottomRight') {
|
||||||
|
setQrX(75); setQrY(70); setQrSize(1);
|
||||||
|
setFontX(50); setFontY(15);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetDesign = () => {
|
||||||
|
const d = initialDesignRef.current;
|
||||||
|
setQrSize(d.qrSize);
|
||||||
|
setQrX(d.qrX);
|
||||||
|
setQrY(d.qrY);
|
||||||
|
setFontSize(d.fontSize);
|
||||||
|
setFontColor(d.fontColor);
|
||||||
|
setFontX(d.fontX);
|
||||||
|
setFontY(d.fontY);
|
||||||
|
setBgImageUrl(d.bgImageUrl);
|
||||||
|
if (bgFileRef.current) bgFileRef.current.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Positioning helpers
|
||||||
|
const qrStyle = {
|
||||||
|
left: `${qrX}%`,
|
||||||
|
top: `${qrY}%`,
|
||||||
|
transform: `translate(-50%, -50%) scale(${qrSize})`,
|
||||||
|
};
|
||||||
|
const fontStyle = {
|
||||||
|
left: `${fontX}%`,
|
||||||
|
top: `${fontY}%`,
|
||||||
|
transform: `translate(-50%, -50%)`,
|
||||||
|
color: fontColor,
|
||||||
|
fontSize: `${fontSize}px`,
|
||||||
|
position: "absolute",
|
||||||
|
fontWeight: 700,
|
||||||
|
};
|
||||||
|
|
||||||
|
const qrData = selectedTable ? `${fullLink}/${selectedTable.tableNo}` : fullLink;
|
||||||
|
|
||||||
|
const canProceedFromStep1 = () => {
|
||||||
|
if (!cafeIdentifyName) return false;
|
||||||
|
if (cafeIdentifyName === (shop.cafeIdentifyName || '')) return true;
|
||||||
|
return availability === 200 && !checking;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.title}>Identifikasi kedai</div>
|
||||||
|
<div className={styles.steps}>
|
||||||
|
<div className={`${styles.step} ${currentStep === 1 ? styles.stepActive : ''}`}>
|
||||||
|
<span className={styles.stepNumber}>1</span> Alamat
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.step} ${currentStep === 2 ? styles.stepActive : ''}`}>
|
||||||
|
<span className={styles.stepNumber}>2</span> Desain QR
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.step} ${currentStep === 3 ? styles.stepActive : ''}`}>
|
||||||
|
<span className={styles.stepNumber}>3</span> Meja
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.scrollArea}>
|
||||||
|
{currentStep === 1 && (
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.sectionTitle}>Alamat kedai</div>
|
||||||
|
<div className={styles.row}>
|
||||||
|
<div className={styles.domainPrefix}>{shopHost}/</div>
|
||||||
|
<input
|
||||||
|
value={cafeIdentifyName}
|
||||||
|
onChange={handleIdentifyChange}
|
||||||
|
className={styles.input}
|
||||||
|
placeholder="alamat_kedai"
|
||||||
|
/>
|
||||||
|
<div className={`${styles.status} ${availability === 200 ? styles.statusOk : availability === 409 ? styles.statusBad : ''}`}>
|
||||||
|
{checking ? 'Memeriksa…' : availability === 200 ? 'Tersedia' : availability === 409 ? 'Terpakai' : 'Menunggu input'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.helpText}>Gunakan huruf kecil, angka, dan garis bawah (_). Contoh: kopikenangan_malam</div>
|
||||||
|
<div className={styles.copyRow}>
|
||||||
|
<input className={styles.linkField} readOnly value={fullLink} />
|
||||||
|
<button className={styles.button} onClick={copyLink}>{copied ? 'Disalin' : 'Salin'}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentStep === 2 && (
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.sectionTitle}>Desain QR</div>
|
||||||
|
<div className={styles.presets}>
|
||||||
|
<button className={styles.presetButton} onClick={()=>applyPreset('center')}>Tengah</button>
|
||||||
|
<button className={styles.presetButton} onClick={()=>applyPreset('topLeft')}>Atas-Kiri</button>
|
||||||
|
<button className={styles.presetButton} onClick={()=>applyPreset('bottomRight')}>Bawah-Kanan</button>
|
||||||
|
<button className={styles.presetButton} onClick={resetDesign}>Reset desain</button>
|
||||||
|
</div>
|
||||||
|
<div className={styles.grid}>
|
||||||
|
<div>
|
||||||
|
<div ref={previewRef} id="qr-code-container" className={styles.previewBox} style={{ backgroundColor: '#fff' }}>
|
||||||
|
{bgImageUrl && <img src={bgImageUrl} alt="Background" className={styles.bgPreview} />}
|
||||||
|
<img
|
||||||
|
className={styles.qrLayer}
|
||||||
|
style={qrStyle}
|
||||||
|
alt="QR"
|
||||||
|
src={`https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=${encodeURIComponent(qrData)}`}
|
||||||
|
/>
|
||||||
|
<div style={fontStyle}>{selectedTable ? selectedTable.tableNo : 'Kedai'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.controls}>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label}>Gambar latar</label>
|
||||||
|
<input ref={bgFileRef} type="file" accept="image/*" onChange={handleUploadBg} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label}>Skala QR ({qrSize}x)</label>
|
||||||
|
<input className={styles.range} type="range" min="0.5" max="3" step="0.1" value={qrSize} onChange={(e)=>setQrSize(parseFloat(e.target.value))} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label}>Posisi QR - horizontal ({qrX}%)</label>
|
||||||
|
<input className={styles.range} type="range" min="0" max="100" step="1" value={qrX} onChange={(e)=>setQrX(parseInt(e.target.value))} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label}>Posisi QR - vertikal ({qrY}%)</label>
|
||||||
|
<input className={styles.range} type="range" min="0" max="100" step="1" value={qrY} onChange={(e)=>setQrY(parseInt(e.target.value))} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label}>Ukuran teks ({fontSize}px)</label>
|
||||||
|
<input className={styles.range} type="range" min="8" max="48" step="1" value={fontSize} onChange={(e)=>setFontSize(parseInt(e.target.value))} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label}>Warna teks</label>
|
||||||
|
<input className={styles.colorInput} type="color" value={fontColor} onChange={(e)=>setFontColor(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label}>Posisi teks - horizontal ({fontX}%)</label>
|
||||||
|
<input className={styles.range} type="range" min="0" max="100" step="1" value={fontX} onChange={(e)=>setFontX(parseInt(e.target.value))} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label}>Posisi teks - vertikal ({fontY}%)</label>
|
||||||
|
<input className={styles.range} type="range" min="0" max="100" step="1" value={fontY} onChange={(e)=>setFontY(parseInt(e.target.value))} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.helpText}>Tip: Gunakan latar yang kontras agar QR mudah dipindai.</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentStep === 3 && (
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.sectionTitle}>Daftar meja</div>
|
||||||
|
<div className={styles.tables}>
|
||||||
|
<input className={styles.input} placeholder="Meja A1" value={newTableNo} onChange={(e)=>setNewTableNo(e.target.value)} />
|
||||||
|
<button className={styles.button} onClick={handleCreateTable}>Buat meja</button>
|
||||||
|
</div>
|
||||||
|
<div className={styles.tableList}>
|
||||||
|
{tables && tables
|
||||||
|
.filter((t)=>t.tableNo !== 0)
|
||||||
|
.map((t)=>{
|
||||||
|
const active = selectedTable && selectedTable.tableId === t.tableId;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={t.tableId}
|
||||||
|
className={`${styles.tableItem} ${active ? styles.tableItemActive : ''}`}
|
||||||
|
onClick={()=>setSelectedTable(active ? null : t)}
|
||||||
|
>
|
||||||
|
{t.tableNo}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className={styles.helpText}>Pilih meja untuk membuat QR khusus meja (opsional).</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<button className={`${styles.button} ${styles.secondary}`} disabled={currentStep === 1} onClick={()=>setCurrentStep((s)=>Math.max(1, s-1))}>Kembali</button>
|
||||||
|
{currentStep < 3 ? (
|
||||||
|
<button className={`${styles.button} ${styles.primary}`} disabled={currentStep === 1 && !canProceedFromStep1()} onClick={()=>setCurrentStep((s)=>Math.min(3, s+1))}>Lanjut</button>
|
||||||
|
) : (
|
||||||
|
<button className={`${styles.button} ${styles.primary}`} onClick={handleSave} disabled={saving}>{saving ? 'Menyimpan…' : 'Simpan'}</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||||
|
{saveStatus === 'success' && <div className={`${styles.banner} ${styles.bannerSuccess}`}>Simpan berhasil</div>}
|
||||||
|
{saveStatus === 'error' && <div className={`${styles.banner} ${styles.bannerError}`}>Gagal menyimpan</div>}
|
||||||
|
<button className={`${styles.button} ${styles.muted}`} onClick={downloadPreview}>Download QR {selectedTable ? 'meja' : 'kedai'}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
299
src/components/IdentifyCafeModal.module.css
Normal file
299
src/components/IdentifyCafeModal.module.css
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
/* IdentifyCafeModal.module.css */
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #fff;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
background: #fafafa;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepNumber {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #e9ecef;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepActive {
|
||||||
|
border-color: #cdebd8;
|
||||||
|
background: #e9f7ef;
|
||||||
|
color: #245c3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpText {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollArea {
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
background: #fafafa;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.domainPrefix {
|
||||||
|
background: #f0f0f0;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 10px 0 0 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 0 10px 10px 0;
|
||||||
|
outline: none;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusOk {
|
||||||
|
color: #155724;
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBad {
|
||||||
|
color: #721c24;
|
||||||
|
background: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyRow {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkField {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.3fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewBox {
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
height: 280px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bgPreview {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrLayer {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presets {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 8px 0 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presetButton {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
background: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorInput {
|
||||||
|
height: 38px;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tables {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableList {
|
||||||
|
max-height: 180px;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableItem {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableItemActive {
|
||||||
|
background: #e9f7ef;
|
||||||
|
border: 1px solid #cdebd8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
background: #28a745;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
background: #f6f6f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bannerSuccess {
|
||||||
|
color: #155724;
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bannerError {
|
||||||
|
color: #721c24;
|
||||||
|
background: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
.grid { grid-template-columns: 1fr; }
|
||||||
|
.controls { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ const Item = ({
|
|||||||
blank,
|
blank,
|
||||||
forCart,
|
forCart,
|
||||||
forInvoice,
|
forInvoice,
|
||||||
|
portrait,
|
||||||
|
hideDetails,
|
||||||
name: initialName,
|
name: initialName,
|
||||||
description: initialDescription,
|
description: initialDescription,
|
||||||
price: initialPrice,
|
price: initialPrice,
|
||||||
@@ -53,10 +55,10 @@ const Item = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
handleCreateItem(itemName, itemPrice, selectedImage, previewUrl);
|
handleCreateItem(itemName, itemPrice, selectedImage, previewUrl, itemDescription, promoPrice);
|
||||||
};
|
};
|
||||||
const handleUpdate = () => {
|
const handleUpdate = () => {
|
||||||
handleUpdateItem(itemName, itemPrice, selectedImage, previewUrl);
|
handleUpdateItem(itemName, itemPrice, selectedImage, itemDescription, promoPrice);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveClick = () => {
|
const handleRemoveClick = () => {
|
||||||
@@ -74,6 +76,11 @@ const Item = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatCurrency = (value) => {
|
||||||
|
const num = Number(value) || 0;
|
||||||
|
return num.toLocaleString('id-ID');
|
||||||
|
};
|
||||||
|
|
||||||
const handlePriceChange = (event) => {
|
const handlePriceChange = (event) => {
|
||||||
setItemPrice(event.target.value);
|
setItemPrice(event.target.value);
|
||||||
};
|
};
|
||||||
@@ -89,245 +96,73 @@ const Item = ({
|
|||||||
setItemName(event.target.value);
|
setItemName(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toTitleCase = (str) => {
|
||||||
|
if (!str) return str;
|
||||||
|
return String(str)
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/(^|[\s\-/'])([a-zA-Z\u00C0-\u024F])/g, (m, p1, p2) => p1 + p2.toUpperCase());
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayName = toTitleCase(itemName);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${!last && !forInvoice ? styles.notLast : ""}`}>
|
<div className={`${(!portrait && !last && !forInvoice) ? styles.notLast : ""}`}>
|
||||||
<div className={`${styles.item} ${forInvoice ? styles.itemInvoice : ""} `}>
|
<div className={`${styles.item} ${forInvoice ? styles.itemInvoice : ""} ${portrait ? styles.itemPortrait : ""} `}>
|
||||||
{!forInvoice && (
|
{!forInvoice && (
|
||||||
// <div className={styles.imageContainer}>
|
<div className={styles.imageWrap}>
|
||||||
<img
|
<img
|
||||||
src={
|
src={previewUrl}
|
||||||
previewUrl
|
onError={({ currentTarget }) => {
|
||||||
}
|
currentTarget.onerror = null;
|
||||||
onError={({ currentTarget }) => {
|
currentTarget.src =
|
||||||
currentTarget.onerror = null; // prevents looping
|
"https://png.pngtree.com/png-vector/20221125/ourmid/pngtree-no-image-available-icon-flatvector-illustration-pic-design-profile-vector-png-image_40966566.jpg";
|
||||||
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" }}
|
||||||
alt={itemName}
|
className={styles.imageContainer}
|
||||||
style={{
|
/>
|
||||||
filter: !isAvailable ? "grayscale(100%)" : "none",
|
{promoPrice && promoPrice != 0 && promoPrice != '' && (
|
||||||
}}
|
<div className={styles.promoPill}>Promo</div>
|
||||||
className={styles.imageContainer}
|
)}
|
||||||
/>
|
{portrait && (
|
||||||
// </div>
|
<>
|
||||||
|
<div className={styles.overlayName}>{displayName}</div>
|
||||||
|
<div className={styles.bottomOverlay}>
|
||||||
|
<div className={styles.overlayPriceCol}>
|
||||||
|
{(promoPrice && promoPrice != 0 && promoPrice != '') ? (
|
||||||
|
<>
|
||||||
|
<span className={styles.overlayOriginal}>Rp {formatCurrency(initialPrice)}</span>
|
||||||
|
<span className={styles.overlayPromo}>Rp {formatCurrency(promoPrice)}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className={styles.overlayPromo}>Rp {formatCurrency(initialPrice)}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.qtyGroup}>
|
||||||
|
<button className={styles.qtyBtn} onClick={handleNegativeClick} aria-label="Kurangi">-</button>
|
||||||
|
<span className={styles.qtyVal}>{itemQty}</span>
|
||||||
|
<button className={styles.qtyBtn} onClick={handlePlusClick} aria-label="Tambah">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{forInvoice && (
|
||||||
|
<div className={styles.itemDetails}>
|
||||||
|
<div className={styles.plusNegative2} onClick={onRemoveClick}>⌧</div>
|
||||||
|
<p className={styles.multiplySymbol}>x</p>
|
||||||
|
<p className={styles.qtyInvoice}>{itemQty}</p>
|
||||||
|
<p className={styles.itemPriceInvoice}>Rp {formatCurrency(itemQty * (promoPrice > 0 ? promoPrice : itemPrice))}</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={styles.itemDetails}>
|
|
||||||
{forInvoice &&
|
|
||||||
<svg
|
|
||||||
className={styles.plusNegative2}
|
|
||||||
onClick={onRemoveClick}
|
|
||||||
clipRule="evenodd"
|
|
||||||
fillRule="evenodd"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeMiterlimit="2"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="m12.002 2c5.518 0 9.998 4.48 9.998 9.998 0 5.517-4.48 9.997-9.998 9.997-5.517 0-9.997-4.48-9.997-9.997 0-5.518 4.48-9.998 9.997-9.998zm0 1.5c-4.69 0-8.497 3.808-8.497 8.498s3.807 8.497 8.497 8.497 8.498-3.807 8.498-8.497-3.808-8.498-8.498-8.498zm-.747 7.75h-3.5c-.414 0-.75.336-.75.75s.336.75.75.75h3.5v3.5c0 .414.336.75.75.75s.75-.336.75-.75v-3.5h3.5c.414 0 .75-.336.75-.75s-.336-.75-.75-.75h-3.5v-3.5c0-.414-.336-.75-.75-.75s-.75.336-.75.75z"
|
|
||||||
fillRule="nonzero"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
}
|
|
||||||
{/* <input
|
|
||||||
className={`${
|
|
||||||
forInvoice ? styles.itemInvoiceName : styles.itemName
|
|
||||||
} ${isBeingEdit || blank ? styles.blank : styles.notblank} ${
|
|
||||||
!isAvailable ? styles.disabled : ""
|
|
||||||
}`}
|
|
||||||
value={itemName}
|
|
||||||
placeholder="Nama item"
|
|
||||||
onChange={handleNameChange}
|
|
||||||
disabled={!blank && !isBeingEdit}
|
|
||||||
/> */}
|
|
||||||
<h3 style={{
|
|
||||||
textTransform: 'capitalize',
|
|
||||||
margin: `${forInvoice ? '13px 0px 10px 10px' : '5px 0px 10px 10px'}`,
|
|
||||||
fontSize: '16px',
|
|
||||||
display: '-webkit-box',
|
|
||||||
WebkitBoxOrient: 'vertical',
|
|
||||||
overflow: 'hidden',
|
|
||||||
WebkitLineClamp: 2,
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
width: `${forInvoice? '160px' : 'unset'}`
|
|
||||||
}}>
|
|
||||||
{itemName}
|
|
||||||
</h3>
|
|
||||||
{forInvoice && (
|
|
||||||
<>
|
|
||||||
<p className={styles.multiplySymbol}>x</p>
|
|
||||||
<p className={styles.qtyInvoice}>{itemQty}</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!forInvoice && (
|
|
||||||
// <input
|
|
||||||
// className={`${styles.itemPrice} ${
|
|
||||||
// isBeingEdit || blank ? styles.blank : styles.notblank
|
|
||||||
// } ${!isAvailable ? styles.disabled : ""}`}
|
|
||||||
// value={itemPrice}
|
|
||||||
// placeholder="Harga"
|
|
||||||
// onChange={handlePriceChange}
|
|
||||||
// disabled={!blank && !isBeingEdit}
|
|
||||||
// />
|
|
||||||
|
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
alignItems: 'center',
|
|
||||||
color: 'rgb(28, 29, 29)',
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
fontWeight: 600,
|
|
||||||
lineHeight: '1rem',
|
|
||||||
marginLeft: 10
|
|
||||||
}}>
|
|
||||||
{promoPrice && promoPrice != 0 && promoPrice != '' ?
|
|
||||||
<>
|
|
||||||
<div style={{
|
|
||||||
position: 'relative',
|
|
||||||
marginTop: '0.125rem',
|
|
||||||
display: 'flex',
|
|
||||||
width: '87px',
|
|
||||||
alignItems: 'center',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
borderRadius: '9999px',
|
|
||||||
backgroundColor: !isAvailable ? 'gray' : 'unset',
|
|
||||||
backgroundImage: isAvailable && 'linear-gradient(to right, #e52535, #fe6d78)',
|
|
||||||
padding: '0.25rem 0rem',
|
|
||||||
color: 'rgb(255, 255, 255)',
|
|
||||||
fontSize: '0.75rem',
|
|
||||||
fontWeight: 600,
|
|
||||||
lineHeight: '1rem',
|
|
||||||
justifyContent: 'center'
|
|
||||||
}}>
|
|
||||||
Promo {(((initialPrice - promoPrice) / initialPrice) * 100).toFixed(0)}%
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ display: 'flex' }}>
|
|
||||||
<span style={{
|
|
||||||
marginLeft: '1rem',
|
|
||||||
marginRight: '0.5rem',
|
|
||||||
marginTop: '0.125rem'
|
|
||||||
}}>{promoPrice}</span>
|
|
||||||
<span style={{
|
|
||||||
marginTop: '0.125rem',
|
|
||||||
color: 'rgb(114, 114, 114)',
|
|
||||||
textDecoration: 'line-through'
|
|
||||||
}}>{initialPrice}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
:
|
|
||||||
<>
|
|
||||||
|
|
||||||
<div style={{ display: 'flex' }}>
|
|
||||||
<span style={{
|
|
||||||
marginRight: '0.5rem',
|
|
||||||
marginTop: '0.125rem'
|
|
||||||
}}>{initialPrice}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!forInvoice &&
|
|
||||||
(!isBeingEdit && itemQty != 0 ? (
|
|
||||||
<div className={styles.itemQty}>
|
|
||||||
<svg
|
|
||||||
className={styles.plusNegative}
|
|
||||||
onClick={handleNegativeClick}
|
|
||||||
clipRule="evenodd"
|
|
||||||
fillRule="evenodd"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeMiterlimit="2"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="m12.002 2.005c5.518 0 9.998 4.48 9.998 9.997 0 5.518-4.48 9.998-9.998 9.998-5.517 0-9.997-4.48-9.997-9.998 0-5.517 4.48-9.997 9.997-9.997zm0 1.5c-4.69 0-8.497 3.807-8.497 8.497s3.807 8.498 8.497 8.498 8.498-3.808 8.498-8.498-3.808-8.497-8.498-8.497zm4.253 7.75h-8.5c-.414 0-.75.336-.75.75s.336.75.75.75h8.5c.414 0 .75-.336.75-.75s-.336-.75-.75-.75z"
|
|
||||||
fillRule="nonzero"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{!blank && !isBeingEdit ? (
|
|
||||||
<p className={styles.itemQtyValue}>{itemQty}</p>
|
|
||||||
) : (
|
|
||||||
<input
|
|
||||||
className={styles.itemQtyInput}
|
|
||||||
value={itemQty}
|
|
||||||
onChange={handleQtyChange}
|
|
||||||
disabled={!blank && !isBeingEdit}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<svg
|
|
||||||
className={styles.plusNegative}
|
|
||||||
onClick={handlePlusClick}
|
|
||||||
clipRule="evenodd"
|
|
||||||
fillRule="evenodd"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeMiterlimit="2"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="m12.002 2c5.518 0 9.998 4.48 9.998 9.998 0 5.517-4.48 9.997-9.998 9.997-5.517 0-9.997-4.48-9.997-9.997 0-5.518 4.48-9.998 9.997-9.998zm0 1.5c-4.69 0-8.497 3.808-8.497 8.498s3.807 8.497 8.497 8.497 8.498-3.807 8.498-8.497-3.808-8.498-8.498-8.498zm-.747 7.75h-3.5c-.414 0-.75.336-.75.75s.336.75.75.75h3.5v3.5c0 .414.336.75.75.75s.75-.336.75-.75v-3.5h3.5c.414 0 .75-.336.75-.75s-.336-.75-.75-.75h-3.5v-3.5c0-.414-.336-.75-.75-.75s-.75.336-.75.75z"
|
|
||||||
fillRule="nonzero"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
) : !blank && !isBeingEdit ? (
|
|
||||||
<div className={styles.itemQty}>
|
|
||||||
<button
|
|
||||||
className={styles.addButton}
|
|
||||||
style={{ backgroundColor: !isAvailable ? "" : "inherit", border: `2px solid ${isAvailable ? 'inherit' : 'gray'}`, color: `${isAvailable ? '#a8c7a9' : 'gray'}` }}
|
|
||||||
onClick={handlePlusClick}
|
|
||||||
disabled={!isAvailable} // Optionally disable the button if not available
|
|
||||||
>
|
|
||||||
Pesan
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className={styles.itemQty}>
|
|
||||||
<button
|
|
||||||
className={styles.addButton}
|
|
||||||
style={{
|
|
||||||
backgroundColor: "white",
|
|
||||||
width: "150px",
|
|
||||||
color: '#a8c7a9'
|
|
||||||
}}
|
|
||||||
onClick={isBeingEdit ? handleUpdate : handleCreate}
|
|
||||||
>
|
|
||||||
{isBeingEdit ? "Simpan" : "Buat"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{forInvoice && (
|
|
||||||
<p className={styles.itemPriceInvoice}>Rp {itemQty * (promoPrice > 0? promoPrice : itemPrice)}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{forCart && (
|
{forCart && (
|
||||||
<div className={styles.remove} onClick={handleRemoveClick}>
|
<div className={styles.remove} onClick={handleRemoveClick}>
|
||||||
ⓧ
|
ⓧ
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* {blank && (
|
|
||||||
<button className={styles.createItem} onClick={handleCreate}>
|
|
||||||
Create Item
|
|
||||||
</button>
|
|
||||||
)} */}
|
|
||||||
</div>
|
</div>
|
||||||
{itemDescription && itemDescription != 'undefined' && itemDescription?.length &&
|
{null}
|
||||||
<div>
|
|
||||||
<p style={{
|
|
||||||
maxHeight: '34px',
|
|
||||||
display: '-webkit-box',
|
|
||||||
WebkitBoxOrient: 'vertical',
|
|
||||||
overflow: 'hidden',
|
|
||||||
WebkitLineClamp: 2,
|
|
||||||
textOverflow: 'ellipsis', color: '#5f5f5f', fontSize: '14px', padding: '5px', margin: 0
|
|
||||||
}}>{itemDescription}</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,32 +1,100 @@
|
|||||||
.itemContainer {
|
.itemContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
/* gap: 10px; */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding-left: 5px;
|
width: 100%;
|
||||||
margin-top: 5px;
|
gap: 12px;
|
||||||
margin-bottom: 5px;
|
padding: 16px;
|
||||||
color: rgba(88, 55, 50, 1);
|
margin: 0;
|
||||||
font-size: 32px;
|
border: none;
|
||||||
box-sizing: border-box; /* Include padding and border in the element's total width */
|
border-radius: 12px;
|
||||||
width: 100%; /* Ensure the item does not exceed the parent's width */
|
background: #ffffff;
|
||||||
overflow: hidden; /* Prevent internal overflow */
|
box-shadow: 0 1px 5px rgba(0,0,0,0.08);
|
||||||
padding-top: 10px;
|
box-sizing: border-box;
|
||||||
margin-bottom: 5px;
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item:not(.itemInvoice) {
|
.item:hover { /* remove hover effect on list items */ }
|
||||||
/* border-top: 2px solid #00000017; */
|
|
||||||
|
/* Portrait variant for cafe page grid */
|
||||||
|
.itemPortrait {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notLast{
|
.imageWrap {
|
||||||
padding-bottom: 10px;
|
position: relative;
|
||||||
border-bottom: 2px solid #00000017;
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemPortrait .imageWrap {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageContainer {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemPortrait .imageContainer {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemDetails {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemPortrait .itemDetails {
|
||||||
|
align-items: flex-start;
|
||||||
|
text-align: left;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemPortrait .itemQty {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemPortrait .title {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemPortrait .priceNow {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notLast {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.itemInvoice {
|
.itemInvoice {
|
||||||
@@ -37,10 +105,14 @@
|
|||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 1px 5px rgba(0,0,0,0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.itemInvoice:last-child {
|
.itemInvoice:last-child {
|
||||||
margin-bottom: 0; /* Remove margin-bottom for the last child */
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.itemImage {
|
.itemImage {
|
||||||
@@ -48,29 +120,30 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.imageContainer {
|
.item:not(.itemPortrait) .imageContainer {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 26vw;
|
width: 80px;
|
||||||
height: 26vw;
|
height: 80px;
|
||||||
border-radius: 12px;
|
border-radius: 10px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay {
|
.overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 15px;
|
top: 16px;
|
||||||
left: 8px;
|
left: 12px;
|
||||||
right: 8px;
|
right: 12px;
|
||||||
bottom: 15px;
|
bottom: 16px;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
color: white;
|
color: white;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 20px;
|
border-radius: 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
font-size: 3.3vw;
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay:hover {
|
.overlay:hover {
|
||||||
@@ -81,20 +154,18 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.itemDetails {
|
.infoRow {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: baseline;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-left: 10px;
|
gap: 12px;
|
||||||
margin-right: 10px;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.itemInvoiceDetails {
|
.itemInvoiceDetails {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-left: 10px;
|
margin-left: 12px;
|
||||||
margin-top: -15px;
|
margin-top: -15px;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
@@ -102,50 +173,47 @@
|
|||||||
.itemName {
|
.itemName {
|
||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
width: calc(100% - 15px); /* Adjust the width to prevent overflow */
|
width: calc(100% - 20px);
|
||||||
font-size: 5vw;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin: 0 5px;
|
margin: 0 6px;
|
||||||
color: rgba(88, 55, 50, 1);
|
color: #333;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.itemInvoiceName {
|
.itemInvoiceName {
|
||||||
width: calc(260% - 15px);
|
width: calc(260% - 20px);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
font-size: 1.3rem;
|
font-size: 1.2rem;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.multiplySymbol {
|
.multiplySymbol {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qtyInvoice {
|
.qtyInvoice {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
.itemPrice {
|
.itemPrice {
|
||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
display: none;
|
||||||
font-style: normal;
|
|
||||||
font-weight: 600;
|
|
||||||
width: calc(100% - 15px); /* Adjust the width to prevent overflow */
|
|
||||||
font-size: 3.3vw;
|
|
||||||
/* margin-bottom: 35px; */
|
|
||||||
margin-left: 5px;
|
|
||||||
color: #3a3a3a;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.itemPriceInvoice {
|
.itemPriceInvoice {
|
||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
font-size: 0.9rem;
|
font-size: 1rem;
|
||||||
margin-left: 5px;
|
margin-left: 6px;
|
||||||
color: #d9c61c;
|
color: #6B8F71;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
margin-top: 22px;
|
margin-top: 22px;
|
||||||
}
|
}
|
||||||
@@ -153,75 +221,404 @@
|
|||||||
.itemQty {
|
.itemQty {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 0.9rem;
|
justify-content: flex-end;
|
||||||
margin-left: 5px;
|
gap: 12px;
|
||||||
color: #a8c7a9;
|
min-height: 40px;
|
||||||
fill: #a8c7a9;
|
|
||||||
height: 40px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.itemQtyValue {
|
.itemQtyValue {
|
||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
margin-top: 19px;
|
margin-top: 22px;
|
||||||
margin-left: 1px;
|
margin-left: 2px;
|
||||||
margin-right: 1px;
|
margin-right: 2px;
|
||||||
width: 25px;
|
width: 32px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.itemQtyInput {
|
.itemQtyInput {
|
||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
width: 30px; /* Adjust the width to prevent overflow */
|
width: 40px;
|
||||||
font-size: 0.9rem;
|
font-size: 1rem;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 12px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.addButton {
|
.addButton {
|
||||||
background-color: #ffffff;
|
background-color: var(--brand-sage, #6B8F71);
|
||||||
border: 2px solid #a8c7a9;
|
border: 1px solid var(--brand-sage, #6B8F71);
|
||||||
/* border: none; */
|
color: #ffffff;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 87px;
|
min-width: 90px;
|
||||||
height: 32px;
|
height: 40px;
|
||||||
margin-left: 5px;
|
padding: 0 16px;
|
||||||
margin-top: 5px;
|
border-radius: 10px;
|
||||||
border-radius: 20px;
|
box-shadow: 0 2px 6px rgba(107, 143, 113, 0.2);
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.addButton:hover {
|
||||||
|
background-color: var(--brand-sage-hover, #5a7a60);
|
||||||
|
border-color: var(--brand-sage-hover, #5a7a60);
|
||||||
|
box-shadow: 0 4px 10px rgba(107, 143, 113, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.addButton:disabled {
|
||||||
|
background-color: var(--brand-sage-muted, #CFD8D3);
|
||||||
|
border-color: var(--brand-sage-muted, #CFD8D3);
|
||||||
|
cursor: default;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
.grayscale {
|
.grayscale {
|
||||||
filter: grayscale(100%);
|
filter: grayscale(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled {
|
.disabled {
|
||||||
color: gray;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plusNegative {
|
.plusNegative {
|
||||||
width: 35px;
|
width: 40px;
|
||||||
height: 35px;
|
height: 40px;
|
||||||
margin: 2.5px 0 -0.5px 0px;
|
border-radius: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: #f8f9fa;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plusNegative:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
border-color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plusNegative2 {
|
.plusNegative2 {
|
||||||
width: 84px;
|
width: 40px;
|
||||||
height: 21px;
|
height: 40px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transform: rotate(45deg);
|
right: 16px;
|
||||||
left: -33px;
|
top: 50%;
|
||||||
top: 21px;
|
transform: translateY(-50%);
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: #f8f9fa;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plusNegative2:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
border-color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
.remove {
|
.remove {
|
||||||
width: 25px;
|
width: 32px;
|
||||||
height: 25px;
|
height: 32px;
|
||||||
margin-top: -10px;
|
position: absolute;
|
||||||
margin-right: 10px;
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ff4d4d;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 6px rgba(255, 77, 77, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove:hover {
|
||||||
|
background: #ff1a1a;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* New elements for clean cafe item card */
|
||||||
|
.title {
|
||||||
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 17px;
|
||||||
|
color: #333;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
text-transform: capitalize;
|
||||||
|
text-align: left;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive type scale for title and price */
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.priceNow {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.title {
|
||||||
|
font-size: 19px;
|
||||||
|
}
|
||||||
|
.priceNow {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.priceRow {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promoBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px 10px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(to right, #e52535, #fe6d78);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.priceNow {
|
||||||
|
color: #1c1d1d;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 15px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.priceOld {
|
||||||
|
color: #727272;
|
||||||
|
font-size: 13px;
|
||||||
|
text-decoration: line-through;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qtyGroup {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
height: 36px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qtyBtn {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #333;
|
||||||
|
border: none;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qtyBtn:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qtyVal {
|
||||||
|
min-width: 30px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Promo pill overlay on image */
|
||||||
|
.promoPill {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 2;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 6px 12px;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #e53935;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 2px 6px rgba(229, 57, 53, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Info overlay on image (name + price) */
|
||||||
|
.overlayName {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
left: 12px;
|
||||||
|
z-index: 2;
|
||||||
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: #fff;
|
||||||
|
mix-blend-mode: difference;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 70%;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
pointer-events: none;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
.overlayName {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlayPrice {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlayNow {
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlayOld {
|
||||||
|
color: rgba(255,255,255,0.8);
|
||||||
|
font-size: 12px;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bottom overlay bar: price left, qty right */
|
||||||
|
.bottomOverlay {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 0 0 12px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* New stacked price for clarity */
|
||||||
|
.overlayPriceCol {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlayOriginal {
|
||||||
|
color: rgba(255,255,255,0.85);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 12px;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
.overlayOriginal {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlayPromo {
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
.overlayOriginal {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.overlayPromo {
|
||||||
|
font-size: 19px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Title text below image for portrait mode */
|
||||||
|
.title {
|
||||||
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
margin: 10px 4px 6px 4px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
.title {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.itemInvoice .itemDetails {
|
.itemInvoice .itemDetails {
|
||||||
@@ -233,7 +630,7 @@
|
|||||||
.itemInvoice .itemName,
|
.itemInvoice .itemName,
|
||||||
.itemInvoice .itemPrice,
|
.itemInvoice .itemPrice,
|
||||||
.itemInvoice .itemQty .qtyInvoice .multiplySymbol {
|
.itemInvoice .itemQty .qtyInvoice .multiplySymbol {
|
||||||
font-size: 0.9rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blank {
|
.blank {
|
||||||
@@ -246,6 +643,98 @@
|
|||||||
|
|
||||||
.createItem {
|
.createItem {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 15px;
|
left: 20px;
|
||||||
right: 15px;
|
right: 20px;
|
||||||
|
bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Item list container */
|
||||||
|
.item-list-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section header */
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 0;
|
||||||
|
border-bottom: 1px solid #e6e6e6;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty state */
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
text-align: center;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
color: #ccc;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state-title {
|
||||||
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state-description {
|
||||||
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card variant */
|
||||||
|
.item-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||||
|
padding: 16px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card:hover { /* no hover */ }
|
||||||
|
|
||||||
|
/* List variant */
|
||||||
|
.item-list-item {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-list-item:hover { /* no hover */ }
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
import styles from "./Modal.module.css";
|
import styles from "./Modal.module.css";
|
||||||
import { getImageUrl } from "../helpers/itemHelper.js";
|
import { getImageUrl } from "../helpers/itemHelper.js";
|
||||||
|
|
||||||
@@ -23,9 +24,10 @@ const ItemConfig = ({
|
|||||||
const [itemDescription, setItemDescription] = useState(initialDescription);
|
const [itemDescription, setItemDescription] = useState(initialDescription);
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
const textareaRef = useRef(null);
|
const textareaRef = useRef(null);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [saveStatus, setSaveStatus] = useState(null); // 'success' | 'error'
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
// Prevent scrolling when modal is open
|
// Prevent scrolling when modal is open
|
||||||
document.body.style.overflow = "hidden";
|
document.body.style.overflow = "hidden";
|
||||||
if(selectedImage){
|
if(selectedImage){
|
||||||
@@ -80,139 +82,135 @@ const ItemConfig = ({
|
|||||||
}
|
}
|
||||||
}, [textareaRef.current]);
|
}, [textareaRef.current]);
|
||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = async () => {
|
||||||
console.log(itemPromoPrice)
|
setSaving(true);
|
||||||
handleCreateItem(itemName, itemPrice, selectedImage, previewUrl, itemDescription, itemPromoPrice);
|
setSaveStatus(null);
|
||||||
document.body.style.overflow = "auto";
|
try {
|
||||||
|
await Promise.resolve(handleCreateItem(itemName, itemPrice, selectedImage, previewUrl, itemDescription, itemPromoPrice));
|
||||||
|
setSaveStatus('success');
|
||||||
|
} catch (e) {
|
||||||
|
setSaveStatus('error');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const handleUpdate = () => {
|
const handleUpdate = async () => {
|
||||||
console.log(itemName, itemPrice, selectedImage, itemDescription, itemPromoPrice)
|
setSaving(true);
|
||||||
handleUpdateItem(itemName, itemPrice, selectedImage, itemDescription, itemPromoPrice);
|
setSaveStatus(null);
|
||||||
document.body.style.overflow = "auto";
|
try {
|
||||||
|
await Promise.resolve(handleUpdateItem(itemName, itemPrice, selectedImage, itemDescription, itemPromoPrice));
|
||||||
|
setSaveStatus('success');
|
||||||
|
} catch (e) {
|
||||||
|
setSaveStatus('error');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return createPortal(
|
||||||
<div onClick={handleOverlayClick} style={{ position: 'fixed', width: '100vw', height: '100vh', left: 0, bottom: 0, display: 'flex', flexDirection: 'column-reverse', zIndex: 301, backgroundColor: '#00000061' }}>
|
<div onClick={handleOverlayClick} className={styles.modalOverlay}>
|
||||||
<div onClick={handleContentClick} style={{ display: 'flex', flexDirection: 'column', padding: '15px', backgroundColor: 'white', borderRadius: '20px 20px 0 0', overflowY: 'auto' }}>
|
<div onClick={handleContentClick} className={styles.modalContent}>
|
||||||
<div style={{ display: 'flex' }}>
|
<div className={styles.imageSection}>
|
||||||
<div style={{ width: '26vw', height: '26vw', marginRight: '10px' }}>
|
<div className={styles.imagePreview}>
|
||||||
<img style={{ width: '100%', height: '100%', borderRadius: '10px', objectFit: 'cover' }} src={previewUrl} />
|
<img src={previewUrl} alt="Preview" className={styles.previewImage} />
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
ref={fileInputRef}
|
|
||||||
className={styles.fileInput}
|
|
||||||
onChange={handleFileChange}
|
|
||||||
accept="image/*"
|
|
||||||
style={{ display: "none" }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{ width: '72%', height: '26vw', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
<div className={styles.imageActions}>
|
||||||
<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' }}>
|
<button
|
||||||
{isBeingEdit ? 'Ganti gambar' : 'Tambah gambar'}
|
onClick={handleChangeImage}
|
||||||
</div>
|
className={styles.actionButton}
|
||||||
<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
|
{isBeingEdit ? 'Ganti Gambar' : 'Tambah Gambar'}
|
||||||
</div>
|
</button>
|
||||||
|
{isBeingEdit && (
|
||||||
|
<button
|
||||||
|
onClick={handleDelete}
|
||||||
|
className={`${styles.actionButton} ${styles.deleteButton}`}
|
||||||
|
>
|
||||||
|
Hapus
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', color: 'black', marginTop: '10px' }}>
|
|
||||||
<p style={{ marginBottom: '5px', fontWeight: '500' }}>Nama item</p>
|
|
||||||
<input
|
<input
|
||||||
value={itemName}
|
type="file"
|
||||||
style={{
|
ref={fileInputRef}
|
||||||
padding: '10px',
|
className={styles.fileInput}
|
||||||
borderRadius: '8px',
|
onChange={handleFileChange}
|
||||||
border: '1px solid #ccc',
|
accept="image/*"
|
||||||
fontSize: '14px',
|
style={{ display: "none" }}
|
||||||
width: '100%',
|
|
||||||
marginBottom: '15px',
|
|
||||||
outline: 'none',
|
|
||||||
transition: 'all 0.3s ease',
|
|
||||||
boxSizing: 'border-box', // Make sure the padding doesn't cause overflow
|
|
||||||
}}
|
|
||||||
onChange={(e)=>setItemName(e.target.value)}
|
|
||||||
onFocus={(e) => e.target.style.borderColor = '#60d37e'}
|
|
||||||
onBlur={(e) => e.target.style.borderColor = '#ccc'}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', color: 'black', justifyContent: 'space-between' }}>
|
<div className={styles.formSection}>
|
||||||
<div style={{ width: '48%' }}>
|
<div className={styles.bannerRow}>
|
||||||
<p style={{ marginBottom: '5px', fontWeight: '500', marginTop: 0 }}>Harga</p>
|
{saveStatus === 'success' && (
|
||||||
|
<span className={`${styles.banner} ${styles.bannerSuccess}`}>Perubahan disimpan</span>
|
||||||
|
)}
|
||||||
|
{saveStatus === 'error' && (
|
||||||
|
<span className={`${styles.banner} ${styles.bannerError}`}>Gagal menyimpan perubahan</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.formLabel}>Nama Item</label>
|
||||||
<input
|
<input
|
||||||
value={itemPrice}
|
value={itemName}
|
||||||
style={{
|
className={styles.formInput}
|
||||||
padding: '10px',
|
onChange={(e)=>setItemName(e.target.value)}
|
||||||
borderRadius: '8px',
|
placeholder="Masukkan nama item"
|
||||||
border: '1px solid #ccc',
|
|
||||||
fontSize: '14px',
|
|
||||||
width: '100%',
|
|
||||||
marginBottom: '15px',
|
|
||||||
outline: 'none',
|
|
||||||
transition: 'all 0.3s ease',
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
}}
|
|
||||||
onChange={(e)=>setItemPrice(e.target.value)}
|
|
||||||
onFocus={(e) => e.target.style.borderColor = '#60d37e'}
|
|
||||||
onBlur={(e) => e.target.style.borderColor = '#ccc'}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ width: '48%' }}>
|
|
||||||
<p style={{ marginBottom: '5px', fontWeight: '500', marginTop: 0 }}>Harga promo</p>
|
<div className={styles.formRow}>
|
||||||
<input
|
<div className={styles.formGroup}>
|
||||||
value={itemPromoPrice}
|
<label className={styles.formLabel}>Harga</label>
|
||||||
placeholder="Opsional"
|
<input
|
||||||
style={{
|
value={itemPrice}
|
||||||
padding: '10px',
|
className={styles.formInput}
|
||||||
borderRadius: '8px',
|
onChange={(e)=>setItemPrice(e.target.value)}
|
||||||
border: '1px solid #ccc',
|
placeholder="Rp 0"
|
||||||
fontSize: '14px',
|
/>
|
||||||
width: '100%',
|
</div>
|
||||||
marginBottom: '15px',
|
<div className={styles.formGroup}>
|
||||||
outline: 'none',
|
<label className={styles.formLabel}>Harga Promo</label>
|
||||||
transition: 'all 0.3s ease',
|
<input
|
||||||
boxSizing: 'border-box',
|
value={itemPromoPrice}
|
||||||
}}
|
className={styles.formInput}
|
||||||
onChange={(e)=>setItemPromoPrice(e.target.value)}
|
onChange={(e)=>setItemPromoPrice(e.target.value)}
|
||||||
onFocus={(e) => e.target.style.borderColor = '#60d37e'}
|
placeholder="Opsional"
|
||||||
onBlur={(e) => e.target.style.borderColor = '#ccc'}
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.formLabel}>Deskripsi</label>
|
||||||
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
className={styles.formTextarea}
|
||||||
|
placeholder="Tambahkan deskripsi item..."
|
||||||
|
value={itemDescription}
|
||||||
|
onChange={(e)=>setItemDescription(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', color: 'black' }}>
|
<div className={styles.formActions}>
|
||||||
<p style={{ marginBottom: '5px', fontWeight: '500', marginTop: 0 }}>Deskripsi</p>
|
<button
|
||||||
|
onClick={cancelEdit}
|
||||||
<textarea
|
className={`${styles.formButton} ${styles.cancelButton}`}
|
||||||
ref={textareaRef}
|
disabled={saving}
|
||||||
style={{
|
>
|
||||||
padding: '10px',
|
Batal
|
||||||
borderRadius: '8px',
|
</button>
|
||||||
border: '1px solid #ccc',
|
<button
|
||||||
fontSize: '14px',
|
onClick={() => { isBeingEdit ? handleUpdate() : handleCreate() }}
|
||||||
width: '100%',
|
className={`${styles.formButton} ${styles.saveButton}`}
|
||||||
marginBottom: '15px',
|
disabled={saving}
|
||||||
outline: 'none',
|
>
|
||||||
transition: 'all 0.3s ease',
|
{saving ? 'Menyimpan…' : (isBeingEdit ? 'Simpan Perubahan' : 'Buat Item')}
|
||||||
boxSizing: 'border-box',
|
</button>
|
||||||
resize: 'none', // Prevent manual resize that could cause overflow
|
|
||||||
}}
|
|
||||||
placeholder="Tambah deskripsi..."
|
|
||||||
value={itemDescription}
|
|
||||||
onChange={(e)=>setItemDescription(e.target.value)}
|
|
||||||
onFocus={(e) => e.target.style.borderColor = '#60d37e'}
|
|
||||||
onBlur={(e) => e.target.style.borderColor = '#ccc'}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ width: '100%', height: '35px', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
|
||||||
<div onClick={() => {isBeingEdit ? handleUpdate() : handleCreate()} } style={{ width: '100%', height: '40px', alignContent: 'center', textAlign: 'center', borderRadius: '10px', border: '1px solid #60d37e', color: '#60d37e', backgroundColor: 'white', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
|
||||||
{isBeingEdit? 'Simpan' : 'Buat Item'}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>,
|
||||||
|
document.body
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
z-index: 200;
|
z-index: 2147483600 !important; /* above watermark and page layers */
|
||||||
background-color: rgba(0, 0, 0, 0.38); /* #00000061 */
|
background-color: rgba(0, 0, 0, 0.38); /* #00000061 */
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,4 +98,4 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
import ItemType from "./ItemType.js";
|
import ItemType from "./ItemType.js";
|
||||||
import { createItemType, updateItemDeletionStatus } from "../helpers/itemHelper.js";
|
import { createItemType, updateItemDeletionStatus } from "../helpers/itemHelper.js";
|
||||||
import ItemConfig from "./ItemConfig.js"
|
import ItemConfig from "./ItemConfig.js"
|
||||||
|
import { ArrowUp, ArrowDown, Pencil, Save, X } from 'lucide-react';
|
||||||
|
|
||||||
const ItemLister = ({
|
const ItemLister = ({
|
||||||
index,
|
index,
|
||||||
@@ -73,6 +74,22 @@ const ItemLister = ({
|
|||||||
|
|
||||||
const [randomKey, setRandomKey] = useState(0);
|
const [randomKey, setRandomKey] = useState(0);
|
||||||
|
|
||||||
|
// Dummy items for style preview on cafe page (non-invoice, non-edit)
|
||||||
|
const showGrid = !isEdit && !forInvoice;
|
||||||
|
const dummyCount = showGrid ? Math.max(0, 6 - items.length) : 0;
|
||||||
|
const dummyItems = Array.from({ length: dummyCount }, (_, i) => ({
|
||||||
|
itemId: `dummy-${itemTypeId}-${i}`,
|
||||||
|
name: `Sample ${i + 1}`,
|
||||||
|
price: 25000 + i * 5000,
|
||||||
|
promoPrice: i % 2 === 1 ? 20000 + i * 3000 : 0,
|
||||||
|
qty: 0,
|
||||||
|
description: 'Contoh deskripsi singkat item.',
|
||||||
|
image: getImageUrl(`uploads/samples/sample (${(i % 16) + 1}).png`),
|
||||||
|
availability: true,
|
||||||
|
selectedImage: null,
|
||||||
|
}));
|
||||||
|
const displayItems = items; // no dummy items on cafe page
|
||||||
|
|
||||||
const handlePlusClick = (itemId) => {
|
const handlePlusClick = (itemId) => {
|
||||||
const updatedItems = items.map((item) => {
|
const updatedItems = items.map((item) => {
|
||||||
if (item.itemId === itemId) {
|
if (item.itemId === itemId) {
|
||||||
@@ -608,7 +625,15 @@ const ItemLister = ({
|
|||||||
|
|
||||||
{(isEdit && isFirstStep || !isEdit) &&
|
{(isEdit && isFirstStep || !isEdit) &&
|
||||||
<div className={styles["title-container"]}>
|
<div className={styles["title-container"]}>
|
||||||
{isEdit && <ItemType blank={true} imageUrl={previewUrl} />}
|
{isEdit && (
|
||||||
|
<div className={styles["image-preview"]}>
|
||||||
|
<img
|
||||||
|
src={previewUrl}
|
||||||
|
alt="Category Preview"
|
||||||
|
className={styles["preview-image"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<input
|
<input
|
||||||
className={`${styles.title} ${isEdit ? styles.border : styles.noborder
|
className={`${styles.title} ${isEdit ? styles.border : styles.noborder
|
||||||
}`}
|
}`}
|
||||||
@@ -618,87 +643,49 @@ const ItemLister = ({
|
|||||||
disabled={!isEdit}
|
disabled={!isEdit}
|
||||||
/>
|
/>
|
||||||
{isEditMode && !isEdit && (
|
{isEditMode && !isEdit && (
|
||||||
<>
|
<div className={styles.titleActions}>
|
||||||
<div
|
<button
|
||||||
style={{
|
className={styles.iconBtn}
|
||||||
width: '32px',
|
onClick={() => index === 0 ? null : moveItemTypeUp(itemTypeId)}
|
||||||
height: '32px', // Add a height to the div
|
disabled={index === 0}
|
||||||
display: 'flex', // Use flexbox
|
aria-label="Naikkan kategori"
|
||||||
justifyContent: 'center', // Center horizontally
|
|
||||||
alignItems: 'center', // Center vertically
|
|
||||||
cursor: 'pointer'
|
|
||||||
}}
|
|
||||||
onClick={index == 0 ? null : () => moveItemTypeUp(itemTypeId)} // Move onClick here for the whole div
|
|
||||||
>
|
>
|
||||||
<svg
|
<ArrowUp size={20} />
|
||||||
viewBox="0 0 16 16"
|
</button>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<button
|
||||||
fill="#000000"
|
className={styles.iconBtn}
|
||||||
style={{ width: '100%', height: '100%' }} // Ensure SVG fits the div
|
onClick={() => index === indexTotal - 1 ? null : moveItemTypeDown(itemTypeId)}
|
||||||
>
|
disabled={index === indexTotal - 1}
|
||||||
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
|
aria-label="Turunkan kategori"
|
||||||
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
|
|
||||||
<g id="SVGRepo_iconCarrier">
|
|
||||||
<path d="m 1 11 c 0 -0.265625 0.105469 -0.519531 0.292969 -0.707031 l 6 -6 c 0.390625 -0.390625 1.023437 -0.390625 1.414062 0 l 6 6 c 0.1875 0.1875 0.292969 0.441406 0.292969 0.707031 s -0.105469 0.519531 -0.292969 0.707031 c -0.390625 0.390625 -1.023437 0.390625 -1.414062 0 l -5.292969 -5.292969 l -5.292969 5.292969 c -0.390625 0.390625 -1.023437 0.390625 -1.414062 0 c -0.1875 -0.1875 -0.292969 -0.441406 -0.292969 -0.707031 z m 0 0" fill={index === 0 ? "gray" : "#2e3436"}></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: '32px',
|
|
||||||
height: '32px', // Add a height to the div
|
|
||||||
display: 'flex', // Use flexbox
|
|
||||||
justifyContent: 'center', // Center horizontally
|
|
||||||
alignItems: 'center', // Center vertically
|
|
||||||
cursor: 'pointer'
|
|
||||||
}}
|
|
||||||
onClick={index == indexTotal - 1 ? null : () => moveItemTypeDown(itemTypeId)} // Move onClick here for the whole div
|
|
||||||
>
|
>
|
||||||
<svg
|
<ArrowDown size={20} />
|
||||||
viewBox="0 0 16 16"
|
</button>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<button
|
||||||
fill="#000000"
|
className={styles.iconBtn}
|
||||||
style={{ width: '100%', height: '100%' }} // Ensure SVG fits the div
|
onClick={toggleEditTypeItem}
|
||||||
>
|
aria-label="Edit 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 5 c 0 -0.265625 0.105469 -0.519531 0.292969 -0.707031 c 0.390625 -0.390625 1.023437 -0.390625 1.414062 0 l 5.292969 5.292969 l 5.292969 -5.292969 c 0.390625 -0.390625 1.023437 -0.390625 1.414062 0 c 0.1875 0.1875 0.292969 0.441406 0.292969 0.707031 s -0.105469 0.519531 -0.292969 0.707031 l -6 6 c -0.390625 0.390625 -1.023437 0.390625 -1.414062 0 l -6 -6 c -0.1875 -0.1875 -0.292969 -0.441406 -0.292969 -0.707031 z m 0 0" fill={index === indexTotal - 1 ? "gray" : "#2e3436"}></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: '32px',
|
|
||||||
height: '32px', // Add a height to the div
|
|
||||||
display: 'flex', // Use flexbox
|
|
||||||
justifyContent: 'center', // Center horizontally
|
|
||||||
alignItems: 'center', // Center vertically
|
|
||||||
cursor: 'pointer'
|
|
||||||
}}
|
|
||||||
onClick={toggleEditTypeItem} // Move onClick here for the whole div
|
|
||||||
>
|
>
|
||||||
<svg
|
<Pencil size={20} />
|
||||||
fill="#000000"
|
</button>
|
||||||
viewBox="0 0 32 32"
|
</div>
|
||||||
style={{ fillRule: 'evenodd', clipRule: 'evenodd', strokeLinejoin: 'round', strokeMiterlimit: 2 }}
|
)}
|
||||||
version="1.1"
|
{isEditMode && isEdit && (
|
||||||
xmlSpace="preserve"
|
<div className={styles.titleActions}>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<button
|
||||||
xmlnsSerif="http://www.serif.com/"
|
className={styles.iconBtn}
|
||||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
onClick={handleSaveType}
|
||||||
>
|
aria-label="Simpan"
|
||||||
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
|
>
|
||||||
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
|
<Save size={20} />
|
||||||
<g id="SVGRepo_iconCarrier">
|
</button>
|
||||||
<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>
|
<button
|
||||||
<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>
|
className={styles.iconBtn}
|
||||||
</g>
|
onClick={resetItems}
|
||||||
</svg>
|
aria-label="Batal"
|
||||||
</div>
|
>
|
||||||
|
<X size={20} />
|
||||||
</>
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -712,6 +699,7 @@ const ItemLister = ({
|
|||||||
handleImageChange(previewUrl, selectedImage)
|
handleImageChange(previewUrl, selectedImage)
|
||||||
}
|
}
|
||||||
imageUrl={getImageUrl("uploads/assets/addnew.png")}
|
imageUrl={getImageUrl("uploads/assets/addnew.png")}
|
||||||
|
compact={false}
|
||||||
/>
|
/>
|
||||||
{/* {typeImage != null && !previewUrl.includes(typeImage) && (
|
{/* {typeImage != null && !previewUrl.includes(typeImage) && (
|
||||||
<ItemType
|
<ItemType
|
||||||
@@ -729,6 +717,7 @@ const ItemLister = ({
|
|||||||
handleImageChange(previewUrl, selectedImage)
|
handleImageChange(previewUrl, selectedImage)
|
||||||
}
|
}
|
||||||
imageUrl={getImageUrl("uploads/assets/beverage4.jpg")}
|
imageUrl={getImageUrl("uploads/assets/beverage4.jpg")}
|
||||||
|
compact={false}
|
||||||
/>
|
/>
|
||||||
<ItemType
|
<ItemType
|
||||||
rectangular={true}
|
rectangular={true}
|
||||||
@@ -736,6 +725,7 @@ const ItemLister = ({
|
|||||||
handleImageChange(previewUrl, selectedImage)
|
handleImageChange(previewUrl, selectedImage)
|
||||||
}
|
}
|
||||||
imageUrl={getImageUrl("uploads/assets/beverage1.png")}
|
imageUrl={getImageUrl("uploads/assets/beverage1.png")}
|
||||||
|
compact={false}
|
||||||
/>
|
/>
|
||||||
<ItemType
|
<ItemType
|
||||||
rectangular={true}
|
rectangular={true}
|
||||||
@@ -743,6 +733,7 @@ const ItemLister = ({
|
|||||||
handleImageChange(previewUrl, selectedImage)
|
handleImageChange(previewUrl, selectedImage)
|
||||||
}
|
}
|
||||||
imageUrl={getImageUrl("uploads/assets/beverage2.png")}
|
imageUrl={getImageUrl("uploads/assets/beverage2.png")}
|
||||||
|
compact={false}
|
||||||
/>
|
/>
|
||||||
<ItemType
|
<ItemType
|
||||||
rectangular={true}
|
rectangular={true}
|
||||||
@@ -750,6 +741,7 @@ const ItemLister = ({
|
|||||||
handleImageChange(previewUrl, selectedImage)
|
handleImageChange(previewUrl, selectedImage)
|
||||||
}
|
}
|
||||||
imageUrl={getImageUrl("uploads/assets/beverage3.png")}
|
imageUrl={getImageUrl("uploads/assets/beverage3.png")}
|
||||||
|
compact={false}
|
||||||
/>
|
/>
|
||||||
<ItemType
|
<ItemType
|
||||||
rectangular={true}
|
rectangular={true}
|
||||||
@@ -757,6 +749,7 @@ const ItemLister = ({
|
|||||||
handleImageChange(previewUrl, selectedImage)
|
handleImageChange(previewUrl, selectedImage)
|
||||||
}
|
}
|
||||||
imageUrl={getImageUrl("uploads/assets/snack5.png")}
|
imageUrl={getImageUrl("uploads/assets/snack5.png")}
|
||||||
|
compact={false}
|
||||||
/>
|
/>
|
||||||
<ItemType
|
<ItemType
|
||||||
rectangular={true}
|
rectangular={true}
|
||||||
@@ -764,6 +757,7 @@ const ItemLister = ({
|
|||||||
handleImageChange(previewUrl, selectedImage)
|
handleImageChange(previewUrl, selectedImage)
|
||||||
}
|
}
|
||||||
imageUrl={getImageUrl("uploads/assets/dessert1.png")}
|
imageUrl={getImageUrl("uploads/assets/dessert1.png")}
|
||||||
|
compact={false}
|
||||||
/>
|
/>
|
||||||
<ItemType
|
<ItemType
|
||||||
rectangular={true}
|
rectangular={true}
|
||||||
@@ -771,6 +765,7 @@ const ItemLister = ({
|
|||||||
handleImageChange(previewUrl, selectedImage)
|
handleImageChange(previewUrl, selectedImage)
|
||||||
}
|
}
|
||||||
imageUrl={getImageUrl("uploads/assets/dessert2.jpg")}
|
imageUrl={getImageUrl("uploads/assets/dessert2.jpg")}
|
||||||
|
compact={false}
|
||||||
/>
|
/>
|
||||||
<ItemType
|
<ItemType
|
||||||
rectangular={true}
|
rectangular={true}
|
||||||
@@ -778,6 +773,7 @@ const ItemLister = ({
|
|||||||
handleImageChange(previewUrl, selectedImage)
|
handleImageChange(previewUrl, selectedImage)
|
||||||
}
|
}
|
||||||
imageUrl={getImageUrl("uploads/assets/food4.jpg")}
|
imageUrl={getImageUrl("uploads/assets/food4.jpg")}
|
||||||
|
compact={false}
|
||||||
/>
|
/>
|
||||||
<ItemType
|
<ItemType
|
||||||
rectangular={true}
|
rectangular={true}
|
||||||
@@ -785,6 +781,7 @@ const ItemLister = ({
|
|||||||
handleImageChange(previewUrl, selectedImage)
|
handleImageChange(previewUrl, selectedImage)
|
||||||
}
|
}
|
||||||
imageUrl={getImageUrl("uploads/assets/food1.png")}
|
imageUrl={getImageUrl("uploads/assets/food1.png")}
|
||||||
|
compact={false}
|
||||||
/>
|
/>
|
||||||
<ItemType
|
<ItemType
|
||||||
rectangular={true}
|
rectangular={true}
|
||||||
@@ -792,6 +789,7 @@ const ItemLister = ({
|
|||||||
handleImageChange(previewUrl, selectedImage)
|
handleImageChange(previewUrl, selectedImage)
|
||||||
}
|
}
|
||||||
imageUrl={getImageUrl("uploads/assets/food2.jpg")}
|
imageUrl={getImageUrl("uploads/assets/food2.jpg")}
|
||||||
|
compact={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ItemType
|
<ItemType
|
||||||
@@ -800,6 +798,7 @@ const ItemLister = ({
|
|||||||
handleImageChange(previewUrl, selectedImage)
|
handleImageChange(previewUrl, selectedImage)
|
||||||
}
|
}
|
||||||
imageUrl={getImageUrl("uploads/assets/food3.png")}
|
imageUrl={getImageUrl("uploads/assets/food3.png")}
|
||||||
|
compact={false}
|
||||||
/>
|
/>
|
||||||
<ItemType
|
<ItemType
|
||||||
rectangular={true}
|
rectangular={true}
|
||||||
@@ -807,6 +806,7 @@ const ItemLister = ({
|
|||||||
handleImageChange(previewUrl, selectedImage)
|
handleImageChange(previewUrl, selectedImage)
|
||||||
}
|
}
|
||||||
imageUrl={getImageUrl("uploads/assets/packet1.png")}
|
imageUrl={getImageUrl("uploads/assets/packet1.png")}
|
||||||
|
compact={false}
|
||||||
/>
|
/>
|
||||||
<ItemType
|
<ItemType
|
||||||
rectangular={true}
|
rectangular={true}
|
||||||
@@ -814,6 +814,7 @@ const ItemLister = ({
|
|||||||
handleImageChange(previewUrl, selectedImage)
|
handleImageChange(previewUrl, selectedImage)
|
||||||
}
|
}
|
||||||
imageUrl={getImageUrl("uploads/assets/packet2.png")}
|
imageUrl={getImageUrl("uploads/assets/packet2.png")}
|
||||||
|
compact={false}
|
||||||
/>
|
/>
|
||||||
<ItemType
|
<ItemType
|
||||||
rectangular={true}
|
rectangular={true}
|
||||||
@@ -821,6 +822,7 @@ const ItemLister = ({
|
|||||||
handleImageChange(previewUrl, selectedImage)
|
handleImageChange(previewUrl, selectedImage)
|
||||||
}
|
}
|
||||||
imageUrl={getImageUrl("uploads/assets/snack1.png")}
|
imageUrl={getImageUrl("uploads/assets/snack1.png")}
|
||||||
|
compact={false}
|
||||||
/>
|
/>
|
||||||
<ItemType
|
<ItemType
|
||||||
rectangular={true}
|
rectangular={true}
|
||||||
@@ -828,6 +830,7 @@ const ItemLister = ({
|
|||||||
handleImageChange(previewUrl, selectedImage)
|
handleImageChange(previewUrl, selectedImage)
|
||||||
}
|
}
|
||||||
imageUrl={getImageUrl("uploads/assets/snack2.png")}
|
imageUrl={getImageUrl("uploads/assets/snack2.png")}
|
||||||
|
compact={false}
|
||||||
/>
|
/>
|
||||||
<ItemType
|
<ItemType
|
||||||
rectangular={true}
|
rectangular={true}
|
||||||
@@ -835,6 +838,7 @@ const ItemLister = ({
|
|||||||
handleImageChange(previewUrl, selectedImage)
|
handleImageChange(previewUrl, selectedImage)
|
||||||
}
|
}
|
||||||
imageUrl={getImageUrl("uploads/assets/snack3.png")}
|
imageUrl={getImageUrl("uploads/assets/snack3.png")}
|
||||||
|
compact={false}
|
||||||
/>
|
/>
|
||||||
<ItemType
|
<ItemType
|
||||||
rectangular={true}
|
rectangular={true}
|
||||||
@@ -842,6 +846,7 @@ const ItemLister = ({
|
|||||||
handleImageChange(previewUrl, selectedImage)
|
handleImageChange(previewUrl, selectedImage)
|
||||||
}
|
}
|
||||||
imageUrl={getImageUrl("uploads/assets/snack4.png")}
|
imageUrl={getImageUrl("uploads/assets/snack4.png")}
|
||||||
|
compact={false}
|
||||||
/>
|
/>
|
||||||
{Array.from({ length: 16 }, (_, index) => {
|
{Array.from({ length: 16 }, (_, index) => {
|
||||||
const sampleNumber = index + 1; // To get numbers from 1 to 16
|
const sampleNumber = index + 1; // To get numbers from 1 to 16
|
||||||
@@ -853,18 +858,43 @@ const ItemLister = ({
|
|||||||
handleImageChange(previewUrl, selectedImage)
|
handleImageChange(previewUrl, selectedImage)
|
||||||
}
|
}
|
||||||
imageUrl={getImageUrl(`uploads/samples/sample (${sampleNumber}).png`)}
|
imageUrl={getImageUrl(`uploads/samples/sample (${sampleNumber}).png`)}
|
||||||
|
compact={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => setIsFirstStep(false)} style={{ width: '100%', height: '40px', borderRadius: '20px' }}>selanjutnya</button>
|
<button onClick={() => setIsFirstStep(false)} className={styles["add-item-button"]}>
|
||||||
|
Selanjutnya
|
||||||
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{(isEdit && !isFirstStep || !isEdit) &&
|
{(isEdit && !isFirstStep || !isEdit) &&
|
||||||
<div key={randomKey}>
|
<div key={randomKey}>
|
||||||
{isEdit && <div style={{ display: 'flex', justifyContent: 'flex-start' }}><div style={{ marginTop: '49px', marginRight: '10px', marginLeft: '10px' }} onClick={() => setIsFirstStep(true)}><svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 512 512"><path d="M48,256c0,114.87,93.13,208,208,208s208-93.13,208-208S370.87,48,256,48,48,141.13,48,256Zm212.65-91.36a16,16,0,0,1,.09,22.63L208.42,240H342a16,16,0,0,1,0,32H208.42l52.32,52.73A16,16,0,1,1,238,347.27l-79.39-80a16,16,0,0,1,0-22.54l79.39-80A16,16,0,0,1,260.65,164.64Z" /></svg></div>
|
{isEdit && (
|
||||||
<h2 className={styles["item-list-title"]}>{items && items.length < 1 ? 'Buat item' : 'Daftar item'}</h2></div>}
|
<div className={styles["settings-section"]}>
|
||||||
<div className={styles["item-list"]}>
|
<h3 className={styles["settings-title"]}>Pengaturan Kategori</h3>
|
||||||
|
<div className={styles["setting-row"]}>
|
||||||
|
<span className={styles["setting-label"]}>Visibilitas Kategori</span>
|
||||||
|
<div className={styles["switch-container"]}>
|
||||||
|
<span className={styles["switch-label"]}>
|
||||||
|
{isVisible ? "Tampil" : "Tersembunyi"}
|
||||||
|
</span>
|
||||||
|
<Switch
|
||||||
|
onChange={() => setIsVisible(!isVisible)}
|
||||||
|
checked={isVisible}
|
||||||
|
offColor="#cccccc"
|
||||||
|
onColor="#6B8F71"
|
||||||
|
uncheckedIcon={false}
|
||||||
|
checkedIcon={false}
|
||||||
|
height={24}
|
||||||
|
width={48}
|
||||||
|
handleDiameter={20}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={`${styles["item-list"]} ${(!isEdit && !forInvoice) ? styles["item-grid"] : ""}`}>
|
||||||
{user && (
|
{user && (
|
||||||
user.user_id == shopOwnerId || user.cafeId == shopId) &&
|
user.user_id == shopOwnerId || user.cafeId == shopId) &&
|
||||||
isEditMode && (
|
isEditMode && (
|
||||||
@@ -873,13 +903,8 @@ const ItemLister = ({
|
|||||||
<button
|
<button
|
||||||
className={styles["add-item-button"]}
|
className={styles["add-item-button"]}
|
||||||
onClick={toggleAddNewItem}
|
onClick={toggleAddNewItem}
|
||||||
style={{
|
|
||||||
display: "inline-block",
|
|
||||||
height: "120px",
|
|
||||||
fontSize: "20px",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Tambah item +
|
+ Tambah Item Baru
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{isAddingNewItem && (
|
{isAddingNewItem && (
|
||||||
@@ -888,7 +913,7 @@ const ItemLister = ({
|
|||||||
cancelEdit={() => toggleAddNewItem()}
|
cancelEdit={() => toggleAddNewItem()}
|
||||||
handleCreateItem={onCreateItem}
|
handleCreateItem={onCreateItem}
|
||||||
/>
|
/>
|
||||||
<Item blank={true} handleCreateItem={onCreateItem} />
|
<Item blank={true} handleCreateItem={onCreateItem} hideDetails={!showGrid} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -915,48 +940,29 @@ const ItemLister = ({
|
|||||||
{isEditMode && isEditItem != item.itemId && (
|
{isEditMode && isEditItem != item.itemId && (
|
||||||
|
|
||||||
<div className={styles["editModeLayout"]}>
|
<div className={styles["editModeLayout"]}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', height: '40px', marginLeft: '7.5vw' }}>
|
<div>
|
||||||
{isEditMode && (
|
{isEditMode && (
|
||||||
<Switch
|
<div className={styles["switch-container"]}>
|
||||||
onChange={() => handleChange(item.itemId)}
|
<span className={styles["switch-label"]}>
|
||||||
checked={item.availability}
|
{item.availability ? "Tersedia" : "Tidak Tersedia"}
|
||||||
/>
|
</span>
|
||||||
|
<Switch
|
||||||
|
onChange={() => handleChange(item.itemId)}
|
||||||
|
checked={item.availability}
|
||||||
|
offColor="#cccccc"
|
||||||
|
onColor="#6B8F71"
|
||||||
|
uncheckedIcon={false}
|
||||||
|
checkedIcon={false}
|
||||||
|
height={20}
|
||||||
|
width={40}
|
||||||
|
handleDiameter={16}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<h3>
|
|
||||||
{item.availability ? "Tersedia" : "Tidak tersedia"}
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div onClick={() => editItem(item.itemId)} style={{ display: 'flex', alignItems: 'center', height: '40px', marginRight: '7.5vw' }}>
|
<div onClick={() => editItem(item.itemId)}>
|
||||||
<div
|
<Pencil size={18} />
|
||||||
style={{
|
|
||||||
width: '32px',
|
|
||||||
height: '32px', // Add a height to the div
|
|
||||||
display: 'flex', // Use flexbox
|
|
||||||
justifyContent: 'center', // Center horizontally
|
|
||||||
alignItems: 'center', // Center vertically
|
|
||||||
cursor: 'pointer'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
fill="white"
|
|
||||||
viewBox="0 0 32 32"
|
|
||||||
style={{ fillRule: 'evenodd', clipRule: 'evenodd', strokeLinejoin: 'round', strokeMiterlimit: 2 }}
|
|
||||||
version="1.1"
|
|
||||||
xmlSpace="preserve"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlnsSerif="http://www.serif.com/"
|
|
||||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
|
||||||
>
|
|
||||||
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
|
|
||||||
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
|
|
||||||
<g id="SVGRepo_iconCarrier">
|
|
||||||
<path d="M12.965,5.462c0,-0 -2.584,0.004 -4.979,0.008c-3.034,0.006 -5.49,2.467 -5.49,5.5l0,13.03c0,1.459 0.579,2.858 1.611,3.889c1.031,1.032 2.43,1.611 3.889,1.611l13.003,0c3.038,-0 5.5,-2.462 5.5,-5.5c0,-2.405 0,-5.004 0,-5.004c0,-0.828 -0.672,-1.5 -1.5,-1.5c-0.827,-0 -1.5,0.672 -1.5,1.5l0,5.004c0,1.381 -1.119,2.5 -2.5,2.5l-13.003,0c-0.663,-0 -1.299,-0.263 -1.768,-0.732c-0.469,-0.469 -0.732,-1.105 -0.732,-1.768l0,-13.03c0,-1.379 1.117,-2.497 2.496,-2.5c2.394,-0.004 4.979,-0.008 4.979,-0.008c0.828,-0.002 1.498,-0.675 1.497,-1.503c-0.001,-0.828 -0.675,-1.499 -1.503,-1.497Z"></path>
|
|
||||||
<path d="M20.046,6.411l-6.845,6.846c-0.137,0.137 -0.232,0.311 -0.271,0.501l-1.081,5.152c-0.069,0.329 0.032,0.671 0.268,0.909c0.237,0.239 0.577,0.343 0.907,0.277l5.194,-1.038c0.193,-0.039 0.371,-0.134 0.511,-0.274l6.845,-6.845l-5.528,-5.528Zm1.415,-1.414l5.527,5.528l1.112,-1.111c1.526,-1.527 1.526,-4.001 -0,-5.527c-0.001,-0 -0.001,-0.001 -0.001,-0.001c-1.527,-1.526 -4.001,-1.526 -5.527,-0l-1.111,1.111Z"></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h3>Edit item</h3>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -971,6 +977,7 @@ const ItemLister = ({
|
|||||||
qty={item.qty}
|
qty={item.qty}
|
||||||
imageUrl={item.image}
|
imageUrl={item.image}
|
||||||
imageFile={item.selectedImage}
|
imageFile={item.selectedImage}
|
||||||
|
hideDetails={!showGrid}
|
||||||
onPlusClick={() => handlePlusClick(item.itemId)}
|
onPlusClick={() => handlePlusClick(item.itemId)}
|
||||||
onNegativeClick={() => handleNegativeClick(item.itemId)}
|
onNegativeClick={() => handleNegativeClick(item.itemId)}
|
||||||
onRemoveClick={() => handleRemoveClick(item.itemId)}
|
onRemoveClick={() => handleRemoveClick(item.itemId)}
|
||||||
@@ -990,13 +997,6 @@ const ItemLister = ({
|
|||||||
<div
|
<div
|
||||||
key={item.itemId}>
|
key={item.itemId}>
|
||||||
{isEditItem == item.itemId && (
|
{isEditItem == item.itemId && (
|
||||||
// <button
|
|
||||||
// className={styles["add-item-button"]}
|
|
||||||
// onClick={() => editItem(0)}
|
|
||||||
// style={{ display: "inline-block" }}
|
|
||||||
// >
|
|
||||||
// batal
|
|
||||||
// </button>
|
|
||||||
<ItemConfig
|
<ItemConfig
|
||||||
isBeingEdit={true}
|
isBeingEdit={true}
|
||||||
name={item.name}
|
name={item.name}
|
||||||
@@ -1022,23 +1022,30 @@ const ItemLister = ({
|
|||||||
<div className={styles["itemWrapper"]}>
|
<div className={styles["itemWrapper"]}>
|
||||||
{(isEditMode && isEditItem != item.itemId || item.willBeDeleted) && (
|
{(isEditMode && isEditItem != item.itemId || item.willBeDeleted) && (
|
||||||
<div className={styles["editModeLayout"]}>
|
<div className={styles["editModeLayout"]}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', height: '40px', marginLeft: '7.5vw' }}>
|
<div>
|
||||||
{!item.willBeDeleted && isEditMode && (
|
{!item.willBeDeleted && isEditMode && (
|
||||||
<Switch
|
<div className={styles["switch-container"]}>
|
||||||
onChange={() => handleChange(item.itemId)}
|
<span className={styles["switch-label"]}>
|
||||||
checked={item.availability}
|
{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 ?
|
{item.willBeDeleted && (
|
||||||
|
<span style={{ backgroundColor: 'black', padding: '6px 12px', borderRadius: '20px' }}>
|
||||||
<h3 style={{ backgroundColor: 'black', padding: '13px 26px' }}>
|
|
||||||
Ditandai untuk dihapus
|
Ditandai untuk dihapus
|
||||||
</h3>
|
</span>
|
||||||
:
|
)}
|
||||||
<h3>
|
|
||||||
{item.availability ? "Tersedia" : "Tidak tersedia"}
|
|
||||||
</h3>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div onClick={() => {
|
<div onClick={() => {
|
||||||
@@ -1047,38 +1054,8 @@ const ItemLister = ({
|
|||||||
} else {
|
} else {
|
||||||
handleItemDeletionToggle(item.itemId, false);
|
handleItemDeletionToggle(item.itemId, false);
|
||||||
}
|
}
|
||||||
}}
|
}}>
|
||||||
style={{ display: 'flex', alignItems: 'center', height: '40px', marginRight: '7.5vw' }}>
|
<Pencil size={18} />
|
||||||
{!item.willBeDeleted && <div
|
|
||||||
style={{
|
|
||||||
width: '32px',
|
|
||||||
height: '32px', // Add a height to the div
|
|
||||||
display: 'flex', // Use flexbox
|
|
||||||
justifyContent: 'center', // Center horizontally
|
|
||||||
alignItems: 'center', // Center vertically
|
|
||||||
cursor: 'pointer'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
fill="white"
|
|
||||||
viewBox="0 0 32 32"
|
|
||||||
style={{ fillRule: 'evenodd', clipRule: 'evenodd', strokeLinejoin: 'round', strokeMiterlimit: 2 }}
|
|
||||||
version="1.1"
|
|
||||||
xmlSpace="preserve"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlnsSerif="http://www.serif.com/"
|
|
||||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
|
||||||
>
|
|
||||||
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
|
|
||||||
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
|
|
||||||
<g id="SVGRepo_iconCarrier">
|
|
||||||
<path d="M12.965,5.462c0,-0 -2.584,0.004 -4.979,0.008c-3.034,0.006 -5.49,2.467 -5.49,5.5l0,13.03c0,1.459 0.579,2.858 1.611,3.889c1.031,1.032 2.43,1.611 3.889,1.611l13.003,0c3.038,-0 5.5,-2.462 5.5,-5.5c0,-2.405 0,-5.004 0,-5.004c0,-0.828 -0.672,-1.5 -1.5,-1.5c-0.827,-0 -1.5,0.672 -1.5,1.5l0,5.004c0,1.381 -1.119,2.5 -2.5,2.5l-13.003,0c-0.663,-0 -1.299,-0.263 -1.768,-0.732c-0.469,-0.469 -0.732,-1.105 -0.732,-1.768l0,-13.03c0,-1.379 1.117,-2.497 2.496,-2.5c2.394,-0.004 4.979,-0.008 4.979,-0.008c0.828,-0.002 1.498,-0.675 1.497,-1.503c-0.001,-0.828 -0.675,-1.499 -1.503,-1.497Z"></path>
|
|
||||||
<path d="M20.046,6.411l-6.845,6.846c-0.137,0.137 -0.232,0.311 -0.271,0.501l-1.081,5.152c-0.069,0.329 0.032,0.671 0.268,0.909c0.237,0.239 0.577,0.343 0.907,0.277l5.194,-1.038c0.193,-0.039 0.371,-0.134 0.511,-0.274l6.845,-6.845l-5.528,-5.528Zm1.415,-1.414l5.527,5.528l1.112,-1.111c1.526,-1.527 1.526,-4.001 -0,-5.527c-0.001,-0 -0.001,-0.001 -0.001,-0.001c-1.527,-1.526 -4.001,-1.526 -5.527,-0l-1.111,1.111Z"></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<h3>{item.willBeDeleted ? 'Batalkan' : 'Edit item'}</h3>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1088,6 +1065,8 @@ const ItemLister = ({
|
|||||||
last={index === indexTotal - 1 && indexx === items.length - 1}
|
last={index === indexTotal - 1 && indexx === items.length - 1}
|
||||||
forCart={forCart}
|
forCart={forCart}
|
||||||
forInvoice={forInvoice}
|
forInvoice={forInvoice}
|
||||||
|
portrait={showGrid}
|
||||||
|
hideDetails={!showGrid}
|
||||||
name={item.name}
|
name={item.name}
|
||||||
price={item.price}
|
price={item.price}
|
||||||
promoPrice={item.promoPrice}
|
promoPrice={item.promoPrice}
|
||||||
@@ -1097,9 +1076,9 @@ const ItemLister = ({
|
|||||||
itemTypeId ? getImageUrl(item.image) : item.image
|
itemTypeId ? getImageUrl(item.image) : item.image
|
||||||
}
|
}
|
||||||
imageFile={item.selectedImage}
|
imageFile={item.selectedImage}
|
||||||
onPlusClick={() => handlePlusClick(item.itemId)}
|
onPlusClick={() => (String(item.itemId).startsWith('dummy-') ? undefined : handlePlusClick(item.itemId))}
|
||||||
onNegativeClick={() => handleNegativeClick(item.itemId)}
|
onNegativeClick={() => (String(item.itemId).startsWith('dummy-') ? undefined : handleNegativeClick(item.itemId))}
|
||||||
onRemoveClick={() => handleRemoveClick(item.itemId)}
|
onRemoveClick={() => (String(item.itemId).startsWith('dummy-') ? undefined : handleRemoveClick(item.itemId))}
|
||||||
isBeingEdit={isEditItem == item.itemId}
|
isBeingEdit={isEditItem == item.itemId}
|
||||||
isAvailable={item.availability}
|
isAvailable={item.availability}
|
||||||
handleUpdateItem={(name, price, image, description, promoPrice) =>
|
handleUpdateItem={(name, price, image, description, promoPrice) =>
|
||||||
@@ -1128,33 +1107,6 @@ const ItemLister = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{isEdit && (
|
|
||||||
<div className={styles.PaymentOption}>
|
|
||||||
<div className={styles.TotalContainer}>
|
|
||||||
<span>Pengaturan</span>
|
|
||||||
<span></span>
|
|
||||||
</div>
|
|
||||||
<div className={styles.OptionContainer}>
|
|
||||||
<span>sembunyikan semua</span>
|
|
||||||
<span>
|
|
||||||
<Switch
|
|
||||||
onChange={() => setIsVisible(!isVisible)}
|
|
||||||
checked={!isVisible}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<button onClick={handleSaveType} className={styles.PayButton}>
|
|
||||||
{false ? (
|
|
||||||
<ColorRing height="50" width="50" color="white" />
|
|
||||||
) : (
|
|
||||||
"Simpan"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<div className={styles.Pay2Button} onClick={resetItems}>
|
|
||||||
Kembali
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -2,80 +2,88 @@
|
|||||||
|
|
||||||
.item-lister {
|
.item-lister {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px; /* Adjust padding as needed */
|
padding: 16px;
|
||||||
box-sizing: border-box; /* Ensure padding doesn't affect width */
|
box-sizing: border-box;
|
||||||
white-space: break-spaces;
|
white-space: break-spaces;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 0 !important; /* ensure behind modal overlay */
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullscreen {
|
.fullscreen {
|
||||||
position: fixed; /* Keep the container fixed */
|
position: fixed;
|
||||||
top: 0; /* Adjust the top position as needed */
|
top: 0;
|
||||||
bottom: 0; /* Occupy the full height of the viewport */
|
bottom: 0;
|
||||||
left: 0; /* Align to the left */
|
left: 0;
|
||||||
right: 0; /* Align to the right */
|
right: 0;
|
||||||
background-color: white; /* Background color */
|
background-color: white;
|
||||||
z-index: 1000; /* Layering */
|
z-index: 100; /* keep above page, below modal overlays (>=200) */
|
||||||
overflow-y: auto; /* Allow vertical scrolling */
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-container {
|
.grid-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(4, 1fr);
|
||||||
gap: 10px;
|
gap: 16px;
|
||||||
/* padding: 10px; */
|
height: calc(52vw - 20px);
|
||||||
/* max-height: calc(3 * (25vw - 20px) + 20px); */
|
padding: 16px;
|
||||||
overflow-y: auto;
|
background-color: #f8f9fa;
|
||||||
height: calc(49vw - 20px);
|
border-radius: 12px;
|
||||||
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-height: 0px) {
|
@media (min-height: 0px) {
|
||||||
.grid-container {
|
.grid-container {
|
||||||
height: 27vh;
|
height: 30vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-height: 630px) {
|
@media (min-height: 630px) {
|
||||||
.grid-container {
|
.grid-container {
|
||||||
height: 27vh;
|
height: 30vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-height: 636px) {
|
@media (min-height: 636px) {
|
||||||
.grid-container {
|
.grid-container {
|
||||||
height: 29vh;
|
height: 32vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-height: 650px) {
|
@media (min-height: 650px) {
|
||||||
.grid-container {
|
.grid-container {
|
||||||
height: 34vh;
|
height: 38vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-height: 705px) {
|
@media (min-height: 705px) {
|
||||||
.grid-container {
|
.grid-container {
|
||||||
height: 37vh;
|
height: 41vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-height: 735px) {
|
@media (min-height: 735px) {
|
||||||
.grid-container {
|
.grid-container {
|
||||||
height: 38vh;
|
height: 42vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-height: 759px) {
|
@media (min-height: 759px) {
|
||||||
.grid-container {
|
.grid-container {
|
||||||
height: 40vh;
|
height: 44vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-height: 819px) {
|
@media (min-height: 819px) {
|
||||||
.grid-container {
|
.grid-container {
|
||||||
height: 44vh;
|
height: 48vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-height: 830px) {
|
@media (min-height: 830px) {
|
||||||
.grid-container {
|
.grid-container {
|
||||||
height: 47vh;
|
height: 51vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-height: 892px) {
|
@media (min-height: 892px) {
|
||||||
.grid-container {
|
.grid-container {
|
||||||
height: 49vh;
|
height: 53vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,46 +91,166 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||||
|
position: relative;
|
||||||
|
z-index: 0 !important; /* ensure behind modal overlay */
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleActions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconBtn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #2d2d2d;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||||
|
position: relative;
|
||||||
|
z-index: 0 !important; /* ensure behind modal overlay */
|
||||||
|
}
|
||||||
|
.iconBtn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.iconBtn:hover:not(:disabled) {
|
||||||
|
background: var(--brand-sage-50, #F0F6F2);
|
||||||
|
border-color: var(--brand-sage, #6B8F71);
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-size: 20px;
|
font-size: 22px;
|
||||||
color: rgba(88, 55, 50, 1);
|
color: rgba(88, 55, 50, 1);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
width: calc(70% - 10px);
|
width: calc(70% - 12px);
|
||||||
padding-left: 10px;
|
padding-left: 12px;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
position: relative;
|
||||||
|
z-index: 151;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-typeItem-button {
|
.edit-typeItem-button {
|
||||||
margin-left: auto; /* Push the button to the right */
|
margin-left: auto;
|
||||||
padding: 8px 16px; /* Adjust padding as needed */
|
padding: 10px 16px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
background-color: #007bff;
|
background-color: #007bff;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
position: relative;
|
||||||
|
z-index: 152;
|
||||||
|
}
|
||||||
|
.edit-typeItem-button:hover {
|
||||||
|
background-color: #0069d9;
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-item-button {
|
.add-item-button {
|
||||||
margin-top: 10px;
|
margin: 0; /* follow parent gap for top/bottom spacing */
|
||||||
padding: 8px 16px; /* Adjust padding as needed */
|
display: inline-block;
|
||||||
font-size: 14px;
|
width: 275px; /* requested size */
|
||||||
background-color: #359d42d1;
|
height: 275px; /* requested size */
|
||||||
color: #fff;
|
padding: 0; /* match image padding (none) */
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
background: transparent; /* card drawn via ::before */
|
||||||
|
color: transparent; /* no visible text */
|
||||||
|
border-radius: 12px; /* match portrait image radius */
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: none;
|
||||||
|
box-shadow: none;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1; /* behind modal overlay */
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-item-button::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0; /* fill container */
|
||||||
|
border-radius: 12px; /* match portrait image radius */
|
||||||
|
background:
|
||||||
|
/* vertical bar */
|
||||||
|
linear-gradient(var(--brand-sage, #6B8F71), var(--brand-sage, #6B8F71)) center/5px 20% no-repeat,
|
||||||
|
/* horizontal bar */
|
||||||
|
linear-gradient(var(--brand-sage, #6B8F71), var(--brand-sage, #6B8F71)) center/20% 5px no-repeat,
|
||||||
|
/* base */
|
||||||
|
var(--brand-sage-50, #F0F6F2);
|
||||||
|
border: 2px dashed var(--brand-sage, #6B8F71);
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.10);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-item-button:focus-visible {
|
||||||
|
outline: 2px solid var(--brand-sage, #6B8F71);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overlay text inside the left tile area */
|
||||||
|
.add-item-button::after { content: none; }
|
||||||
|
|
||||||
|
.add-item-button:hover {
|
||||||
|
background-color: #ffffff;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-list {
|
.item-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column; /* Display items in a column */
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 0 !important; /* ensure behind modal overlay */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generic switch row styling reused in multiple spots */
|
||||||
|
.switch-container {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid layout for portrait cards on cafe page */
|
||||||
|
.item-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
.item-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.item-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.border {
|
.border {
|
||||||
@@ -132,40 +260,91 @@
|
|||||||
.noborder {
|
.noborder {
|
||||||
border: 1px solid #ffffff00;
|
border: 1px solid #ffffff00;
|
||||||
}
|
}
|
||||||
|
|
||||||
.itemWrapper {
|
.itemWrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 1px 5px rgba(0,0,0,0.08);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
z-index: 150;
|
||||||
}
|
}
|
||||||
.editModeLayout {
|
.itemWrapper:hover { /* remove hover effect */ }
|
||||||
border-radius: 4px;
|
|
||||||
|
.editModeLayout {
|
||||||
|
/* Turn full-width bar into subtle corner controls over image */
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 100;
|
inset: 0;
|
||||||
background-color: #0000008c;
|
z-index: 155; /* above itemWrapper */
|
||||||
width: 100%;
|
pointer-events: none; /* only children are interactive */
|
||||||
top: 7px;
|
}
|
||||||
bottom: -4px;
|
|
||||||
display: flex;
|
/* Left badge: availability switch + label */
|
||||||
flex-direction: row;
|
.editModeLayout > div:first-child {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
left: 16px;
|
||||||
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
gap: 8px;
|
||||||
font-size: 14px;
|
background: rgba(0, 0, 0, 0.55);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
pointer-events: auto;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Right badge: edit/unmark button */
|
||||||
|
.editModeLayout > div:last-child {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: rgba(0, 0, 0, 0.55);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 999px;
|
||||||
|
pointer-events: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease, transform 0.2s ease;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editModeLayout > div:last-child:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When label exists, keep it compact and readable */
|
||||||
|
.editModeLayout .switch-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PaymentOption {
|
.PaymentOption {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
background-color: #e9e9e9;
|
background-color: #ffffff;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: calc(10px + 2vmin);
|
font-size: calc(12px + 2vmin);
|
||||||
color: rgba(88, 55, 50, 1);
|
color: rgba(88, 55, 50, 1);
|
||||||
border-radius: 15px 15px 0 0;
|
border-radius: 20px 20px 0 0;
|
||||||
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
z-index: 300; /* Menurunkan z-index agar tidak menutupi material list */
|
||||||
z-index: 300;
|
box-shadow: 0 -2px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.TotalContainer {
|
.TotalContainer {
|
||||||
@@ -176,20 +355,28 @@
|
|||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-size: 1.5em;
|
font-size: 1.8em;
|
||||||
padding: 10px 0;
|
padding: 15px 0;
|
||||||
|
border-bottom: 1px solid #e6e6e6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.OptionContainer {
|
.OptionContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
width: 80vw;
|
width: 80vw;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-size: 0.9em;
|
font-size: 1.1em;
|
||||||
padding: 10px 0;
|
padding: 15px 0;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.OptionContainer span:first-child {
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
.PayButton {
|
.PayButton {
|
||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -197,7 +384,7 @@
|
|||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
|
|
||||||
width: 80vw;
|
width: 80vw;
|
||||||
height: 70px;
|
height: 56px;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
background-color: rgba(88, 55, 50, 1);
|
background-color: rgba(88, 55, 50, 1);
|
||||||
color: white;
|
color: white;
|
||||||
@@ -205,14 +392,32 @@
|
|||||||
margin: 0px auto;
|
margin: 0px auto;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-bottom: 23px;
|
margin-bottom: 23px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 4px 12px rgba(88, 55, 50, 0.2);
|
||||||
|
position: relative;
|
||||||
|
z-index: 301;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PayButton:hover {
|
||||||
|
background-color: rgba(70, 45, 40, 1);
|
||||||
|
box-shadow: 0 6px 16px rgba(88, 55, 50, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Pay2Button {
|
.Pay2Button {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: rgba(88, 55, 50, 1);
|
color: rgba(88, 55, 50, 1);
|
||||||
font-size: 1em;
|
font-size: 1.2em;
|
||||||
margin-bottom: 25px;
|
font-weight: 500;
|
||||||
|
margin-bottom: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
z-index: 301;
|
||||||
|
}
|
||||||
|
.Pay2Button:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-list-title {
|
.item-list-title {
|
||||||
@@ -224,4 +429,25 @@
|
|||||||
font-size: 6vw;
|
font-size: 6vw;
|
||||||
color: black;
|
color: black;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Harga dalam item list - dikurangi ukurannya agar proporsional */
|
||||||
|
.itemPriceList {
|
||||||
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #1c1d1d;
|
||||||
|
margin: 2px 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemPriceList.promo {
|
||||||
|
color: #e53935;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemPriceList.original {
|
||||||
|
color: #727272;
|
||||||
|
font-size: 12px;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useRef, useEffect, useState } from "react";
|
import React, { useRef, useEffect, useState } from "react";
|
||||||
import styles from "./ItemType.module.css";
|
import styles from "./ItemType.module.css";
|
||||||
|
import { Coffee, CupSoda, CakeSlice, Utensils, Grid2X2, Plus } from 'lucide-react';
|
||||||
|
|
||||||
export default function ItemType({
|
export default function ItemType({
|
||||||
onClick,
|
onClick,
|
||||||
@@ -9,6 +10,8 @@ export default function ItemType({
|
|||||||
imageUrl,
|
imageUrl,
|
||||||
selected,
|
selected,
|
||||||
rectangular,
|
rectangular,
|
||||||
|
compact,
|
||||||
|
noIcon, // New prop to remove icons
|
||||||
}) {
|
}) {
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
const [namee, setName] = useState(name);
|
const [namee, setName] = useState(name);
|
||||||
@@ -57,42 +60,64 @@ export default function ItemType({
|
|||||||
onCreate(namee, selectedImage);
|
onCreate(namee, selectedImage);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatName = (val) => {
|
||||||
|
if (!val || typeof val !== 'string') return val;
|
||||||
|
return val
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconImageUrl = imageUrl === 'uploads/assets/All.png' ? 'icon:all' : imageUrl;
|
||||||
|
|
||||||
|
// If noIcon is true, we render a button-like element without icons
|
||||||
|
if (noIcon) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles["item-type"]} ${compact ? styles["item-type-compact"] : ""}`}
|
||||||
|
style={{ zIndex: blank ? 301 : "inherit" }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={onClick}
|
||||||
|
className={`${styles["item-type-rect"]} ${compact ? styles["item-type-rect-compact"] : ""} ${selected ? styles["selected"] : ""}`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: selected ? 'var(--brand-sage, #6B8F71)' : '#ffffff',
|
||||||
|
borderColor: selected ? 'var(--brand-sage, #6B8F71)' : '#e6e6e6',
|
||||||
|
color: selected ? '#ffffff' : '#2d2d2d',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{formatName(namee)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={`${styles["item-type"]} ${compact ? styles["item-type-compact"] : ""}`}
|
||||||
styles[
|
|
||||||
namee
|
|
||||||
? "item-type"
|
|
||||||
: rectangular
|
|
||||||
? "item-type-rectangular"
|
|
||||||
: "item-type-nomargin"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
style={{ zIndex: blank ? 301 : "inherit" }}
|
style={{ zIndex: blank ? 301 : "inherit" }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
onClick={
|
onClick={
|
||||||
rectangular ? (blank ? null : () => onClick(imageUrl)) : onClick
|
rectangular ? (blank ? null : () => onClick(iconImageUrl)) : onClick
|
||||||
}
|
}
|
||||||
className={styles["item-type-rect"]}
|
className={`${styles["item-type-rect"]} ${compact ? styles["item-type-rect-compact"] : ""} ${selected ? styles["selected"] : ""}`}
|
||||||
style={{
|
style={{
|
||||||
top: selected ? "-10px" : "initial",
|
backgroundColor: selected ? 'var(--brand-sage, #6B8F71)' : '#ffffff',
|
||||||
|
borderColor: selected ? 'var(--brand-sage, #6B8F71)' : '#e6e6e6',
|
||||||
|
color: selected ? '#ffffff' : '#2d2d2d',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{imageUrl != 'uploads/assets/All.png' ?
|
{iconImageUrl === 'uploads/assets/All.png' ? (
|
||||||
<img
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
src={previewUrl}
|
|
||||||
alt={namee}
|
|
||||||
className={styles["item-type-image"]}
|
|
||||||
/>
|
|
||||||
:<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="100%" height="100%" viewBox="0 0 800.000000 800.000000"
|
width="100%" height="100%" viewBox="0 0 800.000000 800.000000"
|
||||||
preserveAspectRatio="xMidYMid meet">
|
preserveAspectRatio="xMidYMid meet">
|
||||||
<metadata>
|
<metadata>
|
||||||
Created by potrace 1.16, written by Peter Selinger 2001-2019
|
Created by potrace 1.16, written by Peter Selinger 2001-2019
|
||||||
</metadata>
|
</metadata>
|
||||||
<g transform="translate(0.000000,800.000000) scale(0.100000,-0.100000)"
|
<g transform="translate(0.000000,800.000000) scale(0.100000,-0.100000)"
|
||||||
fill="#000000" stroke="none">
|
fill="currentColor" stroke="none">
|
||||||
<path d="M3708 7165 c-3 -4 -44 -10 -90 -15 -266 -28 -530 -91 -753 -180 -11
|
<path d="M3708 7165 c-3 -4 -44 -10 -90 -15 -266 -28 -530 -91 -753 -180 -11
|
||||||
-4 -42 -16 -70 -26 -27 -9 -129 -57 -225 -106 -186 -94 -188 -95 -262 -145
|
-4 -42 -16 -70 -26 -27 -9 -129 -57 -225 -106 -186 -94 -188 -95 -262 -145
|
||||||
-26 -18 -52 -33 -58 -33 -5 0 -24 -13 -42 -28 -18 -16 -53 -43 -78 -61 -124
|
-26 -18 -52 -33 -58 -33 -5 0 -24 -13 -42 -28 -18 -16 -53 -43 -78 -61 -124
|
||||||
@@ -138,7 +163,17 @@ c261 0 329 -3 352 -14z m1237 -2 c52 -35 54 -49 54 -379 0 -348 -2 -360 -69
|
|||||||
58 40 59 387 60 178 0 328 -4 342 -10z"/>
|
58 40 59 387 60 178 0 328 -4 342 -10z"/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
}
|
) : (iconImageUrl && typeof iconImageUrl === 'string' && iconImageUrl.startsWith('icon:')) ? (
|
||||||
|
<div style={{width:'100%',height:'100%',display:'flex',alignItems:'center',justifyContent:'center'}}>
|
||||||
|
<LucideCategoryIcon name={namee} iconKey={(iconImageUrl || '').split(':')[1]} compact={compact} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
src={previewUrl}
|
||||||
|
alt={namee}
|
||||||
|
className={styles["item-type-image"]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{blank && rectangular && (
|
{blank && rectangular && (
|
||||||
<div className={styles["item-type-image-container"]}>
|
<div className={styles["item-type-image-container"]}>
|
||||||
<input
|
<input
|
||||||
@@ -152,18 +187,42 @@ c261 0 329 -3 352 -14z m1237 -2 c52 -35 54 -49 54 -379 0 -348 -2 -360 -69
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!rectangular && !blank && (
|
{!rectangular && !blank && (
|
||||||
<input
|
<div className={`${styles["item-type-name"]} ${compact ? styles["item-type-name-compact"] : ""}`} style={{ color: selected ? '#2d2d2d' : '#333' }}>
|
||||||
ref={inputRef}
|
{formatName(namee)}
|
||||||
className={`${styles["item-type-name"]} ${styles.noborder}`}
|
</div>
|
||||||
value={namee}
|
|
||||||
onChange={handleNameChange}
|
|
||||||
disabled={true}
|
|
||||||
style={{
|
|
||||||
top: selected ? "-5px" : "initial",
|
|
||||||
borderBottom: selected ? "1px solid #000" : "none",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function LucideCategoryIcon({ name, iconKey, compact }) {
|
||||||
|
const key = pickIconKey(name, iconKey);
|
||||||
|
const size = compact ? '65%' : '56%';
|
||||||
|
switch (key) {
|
||||||
|
case 'coffee':
|
||||||
|
return <Coffee color={'currentColor'} size={size} strokeWidth={2} />;
|
||||||
|
case 'drink':
|
||||||
|
return <CupSoda color={'currentColor'} size={size} strokeWidth={2} />;
|
||||||
|
case 'dessert':
|
||||||
|
return <CakeSlice color={'currentColor'} size={size} strokeWidth={2} />;
|
||||||
|
case 'food':
|
||||||
|
return <Utensils color={'currentColor'} size={size} strokeWidth={2} />;
|
||||||
|
case 'all':
|
||||||
|
return <Grid2X2 color={'currentColor'} size={size} strokeWidth={2} />;
|
||||||
|
case 'plus':
|
||||||
|
return <Plus color={'currentColor'} size={size} strokeWidth={2} />;
|
||||||
|
default:
|
||||||
|
return <Utensils color={'currentColor'} size={size} strokeWidth={2} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickIconKey(name, iconKey) {
|
||||||
|
const n = (name || '').toLowerCase();
|
||||||
|
if (iconKey === 'plus') return 'plus';
|
||||||
|
if (iconKey === 'all') return 'all';
|
||||||
|
if (/(kopi|coffee|espresso|latte|americano|kapal|brew)/.test(n)) return 'coffee';
|
||||||
|
if (/(teh|tea|drink|minum|soda|juice|jus|milk|susu|lemon)/.test(n)) return 'drink';
|
||||||
|
if (/(dessert|cake|kue|manis|ice|es krim|ice-cream)/.test(n)) return 'dessert';
|
||||||
|
if (/(food|makan|snack|cemilan|nasi|mie|noodle|soup|sup|ayam|daging|ikan|roti|sandwich|burger|pizza)/.test(n)) return 'food';
|
||||||
|
return 'food';
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
.item-type {
|
.item-type {
|
||||||
width: calc(25vw - 20px);
|
width: auto;
|
||||||
height: calc(30vw - 20px);
|
height: auto;
|
||||||
margin: 1px 10px 0px;
|
margin: 0 6px;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -10,55 +10,107 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.item-type-rectangular {
|
|
||||||
width: calc(25vw - 20px);
|
/* Compact version of item-type */
|
||||||
height: calc(25vw - 20px);
|
.item-type-compact {
|
||||||
overflow: visible;
|
margin: 0 4px;
|
||||||
text-align: center;
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: start;
|
|
||||||
position: relative; /* Ensure absolute positioning inside works */
|
|
||||||
}
|
}
|
||||||
.item-type-nomargin {
|
|
||||||
width: calc(25vw - 20px);
|
.item-type-rectangular {
|
||||||
height: calc(39vw - 20px);
|
width: calc(30vw - 24px);
|
||||||
|
height: calc(30vw - 24px);
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
position: relative; /* Ensure absolute positioning inside works */
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item-type-nomargin {
|
||||||
|
width: calc(30vw - 24px);
|
||||||
|
height: calc(45vw - 24px);
|
||||||
|
overflow: visible;
|
||||||
|
text-align: center;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.item-type-rect {
|
.item-type-rect {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 13vw;
|
height: 48px; /* Fixed height for better touch targets */
|
||||||
width: 13vw;
|
min-width: 100px; /* Minimum width */
|
||||||
|
padding: 0 20px; /* Horizontal padding */
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-radius: 15px;
|
border-radius: 12px; /* Square rounded corners */
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #2d2d2d;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compact version of item-type-rect */
|
||||||
|
.item-type-rect-compact {
|
||||||
|
height: 42px; /* Slightly smaller for compact version */
|
||||||
|
min-width: 90px;
|
||||||
|
padding: 0 18px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-type-rect:hover {
|
||||||
|
background-color: var(--brand-sage-100, #E9F3ED);
|
||||||
|
border-color: var(--brand-sage, #6B8F71);
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-type-rect.selected {
|
||||||
|
background-color: var(--brand-sage, #6B8F71);
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: var(--brand-sage, #6B8F71);
|
||||||
|
box-shadow: 0 2px 6px rgba(107, 143, 113, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-type-name {
|
.item-type-name {
|
||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-size: 14px;
|
font-size: 16px;
|
||||||
color: #333;
|
color: #2d2d2d;
|
||||||
width: calc(25vw - 30px);
|
width: auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
position: relative; /* Needed for positioning the button */
|
position: relative;
|
||||||
|
margin-top: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compact version of item-type-name */
|
||||||
|
.item-type-name-compact {
|
||||||
|
font-size: 14px;
|
||||||
|
max-width: 140px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-type-image {
|
.item-type-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-radius: 15px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-type-image-container {
|
.item-type-image-container {
|
||||||
@@ -68,6 +120,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-type-image-input {
|
.item-type-image-input {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -79,12 +132,12 @@
|
|||||||
|
|
||||||
.item-type-create {
|
.item-type-create {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 76%; /* Position below the input */
|
top: 76%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
margin-top: 10px; /* Space between input and button */
|
margin-top: 12px;
|
||||||
width: 20vw;
|
width: 24vw;
|
||||||
text-align: center; /* Center button text */
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.border {
|
.border {
|
||||||
@@ -94,3 +147,17 @@
|
|||||||
.noborder {
|
.noborder {
|
||||||
border: 1px solid #ffffff00;
|
border: 1px solid #ffffff00;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* No icon styles */
|
||||||
|
.no-icon {
|
||||||
|
height: 48px;
|
||||||
|
min-width: 100px;
|
||||||
|
padding: 0 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
}
|
||||||
@@ -1,23 +1,95 @@
|
|||||||
|
/* ItemTypeLister.css */
|
||||||
|
|
||||||
|
/* New clean, intuitive category bar */
|
||||||
.item-type-lister {
|
.item-type-lister {
|
||||||
width: 100vw;
|
width: 100%;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding: 3px 0px;
|
padding: 8px 0; /* Reduced padding for more compact design */
|
||||||
margin-bottom: -5px;
|
margin-bottom: 8px; /* Reduced margin for more compact design */
|
||||||
|
scrollbar-width: thin;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item-type-lister::-webkit-scrollbar {
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-type-lister::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #c5c5c5;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px; /* Reduced gap for more compact design */
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 6px 10px; /* Reduced padding */
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.category-bar::-webkit-scrollbar { display: none; }
|
||||||
|
|
||||||
|
/* Legacy horizontal tile list container (used for tile UI) */
|
||||||
.item-type-list {
|
.item-type-list {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
-ms-overflow-style: none; /* IE and Edge */
|
-ms-overflow-style: none;
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
gap: 6px; /* Added gap for consistent spacing */
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-type {
|
.category-chip {
|
||||||
display: inline-block;
|
flex: 0 0 auto;
|
||||||
margin-right: 20px;
|
display: inline-flex;
|
||||||
/* Space between items */
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
height: 32px; /* Reduced height for more compact design */
|
||||||
|
padding: 0 14px; /* Reduced padding for more compact design */
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #2d2d2d;
|
||||||
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 13px; /* Slightly smaller font */
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
.category-chip:hover {
|
||||||
|
border-color: #d0d0d0;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
}
|
||||||
|
.category-chip.selected {
|
||||||
|
background: #73a585;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #73a585;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-chip .chip-icon {
|
||||||
|
width: 16px; /* Reduced icon size */
|
||||||
|
height: 16px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-item-chip {
|
||||||
|
background: #f4f7f5;
|
||||||
|
border-color: #dfe7e2;
|
||||||
|
color: #4a6b5a;
|
||||||
|
}
|
||||||
|
.add-item-chip:hover { background: #eaf1ed; }
|
||||||
.rect-creator {
|
.rect-creator {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
@@ -25,7 +97,7 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
z-index: 300;
|
z-index: 0; /* align with item lister */
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@@ -55,13 +127,123 @@
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
align-self: center; /* Center the button horizontally */
|
align-self: center; /* Center the button horizontally */
|
||||||
}
|
}
|
||||||
.item-type-name {
|
|
||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
/* Legacy styles kept for ItemType grid if needed elsewhere */
|
||||||
font-style: normal;
|
|
||||||
height: 20vw;
|
/* Compact centered item type list without icon tiles */
|
||||||
font-size: 1.5rem;
|
.compact-centered-list {
|
||||||
font-weight: 500;
|
display: flex;
|
||||||
color: black;
|
justify-content: center;
|
||||||
text-transform: capitalize;
|
align-items: center;
|
||||||
z-index: 301;
|
width: 100%;
|
||||||
|
padding: 8px 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-item-type {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #2d2d2d;
|
||||||
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
margin: 0 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.compact-item-type:hover {
|
||||||
|
border-color: #d0d0d0;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
}
|
||||||
|
.compact-item-type.selected {
|
||||||
|
background: #73a585;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #73a585;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-add-item {
|
||||||
|
background: #f4f7f5;
|
||||||
|
border-color: #dfe7e2;
|
||||||
|
color: #4a6b5a;
|
||||||
|
}
|
||||||
|
.compact-add-item:hover {
|
||||||
|
background: #eaf1ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.item-type-lister {
|
||||||
|
padding: 6px 0;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-bar {
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-type-list {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-chip {
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-chip .chip-icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-item-type {
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 0 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.item-type-lister {
|
||||||
|
padding: 4px 0;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-bar {
|
||||||
|
gap: 3px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-type-list {
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-chip {
|
||||||
|
height: 26px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-chip .chip-icon {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-item-type {
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import React, { useState, useRef, useEffect } from "react";
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
import smoothScroll from "smooth-scroll-into-view-if-needed";
|
import smoothScroll from "smooth-scroll-into-view-if-needed";
|
||||||
import "./ItemTypeLister.css";
|
import "./ItemTypeLister.css";
|
||||||
import ItemType from "./ItemType";
|
import { createItem } from "../helpers/itemHelper.js";
|
||||||
import { createItem, createItemType } from "../helpers/itemHelper.js";
|
|
||||||
import { getImageUrl } from "../helpers/itemHelper";
|
import { getImageUrl } from "../helpers/itemHelper";
|
||||||
import ItemLister from "./ItemLister";
|
import ItemLister from "./ItemLister";
|
||||||
|
|
||||||
@@ -22,31 +21,13 @@ const ItemTypeLister = ({
|
|||||||
const newItemDivRef = useRef(null);
|
const newItemDivRef = useRef(null);
|
||||||
const [items, setItems] = useState([]);
|
const [items, setItems] = useState([]);
|
||||||
|
|
||||||
const [itemTypeName, setItemTypeName] = useState("");
|
|
||||||
const handleCreateItem = (name, price, selectedImage, previewUrl, description, promoPrice) => {
|
|
||||||
console.log(previewUrl);
|
|
||||||
const newItem = {
|
|
||||||
itemId: items.length + 1,
|
|
||||||
name,
|
|
||||||
price,
|
|
||||||
selectedImage,
|
|
||||||
image: previewUrl,
|
|
||||||
availability: true,
|
|
||||||
description,
|
|
||||||
promoPrice
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update the items state with the new item
|
|
||||||
setItems((prevItems) => [...prevItems, newItem]);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Effect to handle changes to isAddingNewItem
|
// Effect to handle changes to isAddingNewItem
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAddingNewItem && newItemDivRef.current) {
|
if (isAddingNewItem && newItemDivRef.current) {
|
||||||
// Use smooth-scroll-into-view-if-needed to scroll to the target div
|
// Use smooth-scroll-into-view-if-needed to scroll to the target div
|
||||||
smoothScroll(newItemDivRef.current, {
|
smoothScroll(newItemDivRef.current, {
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
block: "start", // Adjust this based on your needs
|
block: "start",
|
||||||
inline: "nearest",
|
inline: "nearest",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -54,11 +35,11 @@ const ItemTypeLister = ({
|
|||||||
|
|
||||||
smoothScroll(node, {
|
smoothScroll(node, {
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
block: "start", // Adjust this based on your needs
|
block: "start",
|
||||||
inline: "nearest",
|
inline: "nearest",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isAddingNewItem]); // Dependency array includes isAddingNewItem
|
}, [isAddingNewItem]);
|
||||||
|
|
||||||
const toggleAddNewItem = () => {
|
const toggleAddNewItem = () => {
|
||||||
setIsAddingNewItem((prev) => !prev);
|
setIsAddingNewItem((prev) => !prev);
|
||||||
@@ -67,93 +48,69 @@ const ItemTypeLister = ({
|
|||||||
document.body.style.overflow = !isAddingNewItem ? "hidden" : "auto";
|
document.body.style.overflow = !isAddingNewItem ? "hidden" : "auto";
|
||||||
};
|
};
|
||||||
|
|
||||||
async function handleCreate(name, selectedImage) {
|
// Removed legacy image upload logic used by the old tile view
|
||||||
createItemType(shopId, name, selectedImage);
|
|
||||||
}
|
const canManage = user && (user.user_id == shopOwnerId || user.cafeId == shopId);
|
||||||
|
|
||||||
const [selectedImage, setSelectedImage] = useState(null);
|
const formatName = (name) => {
|
||||||
const [previewUrl, setPreviewUrl] = useState("");
|
if (!name) return name;
|
||||||
const [imageUrl, setImaguUrl] = useState("");
|
return name
|
||||||
|
.toLowerCase()
|
||||||
useEffect(() => {
|
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
// 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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="item-type-lister" style={{ overflowX: isAddingNewItem ? 'hidden' : 'auto' }}>
|
||||||
className="item-type-lister"
|
<div className="compact-centered-list">
|
||||||
style={{ overflowX: isAddingNewItem ? "hidden" : "" }}
|
<div ref={newItemDivRef} className="compact-item-type-list" style={{ display: 'inline-flex' }}>
|
||||||
>
|
{isEditMode && !isAddingNewItem && canManage && (
|
||||||
<div
|
<div
|
||||||
ref={newItemDivRef}
|
className="compact-item-type compact-add-item"
|
||||||
className="item-type-list"
|
|
||||||
style={{ display: isAddingNewItem ? "inline-flex" : "inline-flex" }}
|
|
||||||
>
|
|
||||||
{isEditMode &&
|
|
||||||
!isAddingNewItem &&
|
|
||||||
user && (
|
|
||||||
user.user_id == shopOwnerId || user.cafeId == shopId) && (
|
|
||||||
<ItemType
|
|
||||||
onClick={toggleAddNewItem}
|
onClick={toggleAddNewItem}
|
||||||
name={"buat baru"}
|
>
|
||||||
imageUrl={getImageUrl("uploads/assets/addnew.png")}
|
Buat baru
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{canManage && isAddingNewItem && (
|
||||||
|
<ItemLister
|
||||||
|
shopId={shopId}
|
||||||
|
shopOwnerId={shopOwnerId}
|
||||||
|
user={user}
|
||||||
|
typeName={""}
|
||||||
|
setShopItems={setShopItems}
|
||||||
|
itemList={items}
|
||||||
|
isEditMode={true}
|
||||||
|
handleCreateItem={(itemTypeId, name, price, selectedImage, description, promoPrice) => createItem(shopId, name, price, selectedImage, itemTypeId, description, promoPrice)}
|
||||||
|
beingEditedType={beingEditedType}
|
||||||
|
setBeingEditedType={setBeingEditedType}
|
||||||
|
alwaysEdit={true}
|
||||||
|
handleUnEdit={toggleAddNewItem}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{user &&(
|
|
||||||
user.user_id == shopOwnerId || user.cafeId == shopId) &&
|
{itemTypes && itemTypes.length > 0 && (
|
||||||
isAddingNewItem && (
|
<div
|
||||||
<>
|
className={`compact-item-type ${filterId === 0 ? 'selected' : ''}`}
|
||||||
<ItemLister
|
onClick={() => onFilterChange(0)}
|
||||||
shopId={shopId}
|
>
|
||||||
shopOwnerId={shopOwnerId}
|
Semua
|
||||||
user={user}
|
</div>
|
||||||
typeName={""}
|
|
||||||
setShopItems={setShopItems}
|
|
||||||
itemList={items}
|
|
||||||
isEditMode={true}
|
|
||||||
handleCreateItem={(itemTypeId, name, price, selectedImage, description, promoPrice) => createItem(shopId, name, price, selectedImage, itemTypeId, description, promoPrice)}
|
|
||||||
beingEditedType={beingEditedType}
|
|
||||||
setBeingEditedType={setBeingEditedType}
|
|
||||||
alwaysEdit={true}
|
|
||||||
handleUnEdit={toggleAddNewItem}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{itemTypes && itemTypes.length > 0 && (
|
|
||||||
<ItemType
|
|
||||||
name={"semua"}
|
|
||||||
onClick={() => onFilterChange(0)}
|
|
||||||
imageUrl={"uploads/assets/All.png"}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{itemTypes &&
|
|
||||||
itemTypes.map(
|
|
||||||
(itemType) =>
|
|
||||||
(
|
|
||||||
itemType.itemList.length > 0 || (user && (user.user_id == shopOwnerId || user.cafeId == shopId))) && (
|
|
||||||
<ItemType
|
|
||||||
key={itemType.itemTypeId}
|
|
||||||
name={itemType.name}
|
|
||||||
imageUrl={getImageUrl(itemType.image)}
|
|
||||||
onClick={() => onFilterChange(itemType.itemTypeId)}
|
|
||||||
selected={filterId === itemType.itemTypeId}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{itemTypes && itemTypes.map((itemType) => (
|
||||||
|
<div
|
||||||
|
key={itemType.itemTypeId}
|
||||||
|
className={`compact-item-type ${filterId === itemType.itemTypeId ? 'selected' : ''}`}
|
||||||
|
onClick={() => onFilterChange(itemType.itemTypeId)}
|
||||||
|
>
|
||||||
|
{formatName(itemType.name)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ItemTypeLister;
|
export default ItemTypeLister;
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import React, {useState, useEffect} from "react";
|
import React, {useState, useEffect} from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
import styles from "./Modal.module.css";
|
import styles from "./Modal.module.css";
|
||||||
|
|
||||||
import AccountUpdatePage from "../components/AccountUpdatePage.js";
|
import AccountUpdatePage from "../components/AccountUpdatePage.js";
|
||||||
import CreateClerk from "../pages/CreateClerk"
|
import CreateClerk from "../pages/CreateClerk"
|
||||||
import CreateCafe from "../pages/CreateCafe"
|
import CreateCafe from "../pages/CreateCafe"
|
||||||
import CreateTenant from "../pages/CreateTenant"
|
import CreateTenant from "../pages/CreateTenant"
|
||||||
import TablesPage from "./TablesPage.js";
|
import IdentifyCafeModal from "./IdentifyCafeModal.js";
|
||||||
import PaymentOptions from "./PaymentOptions.js";
|
import PaymentOptions from "./PaymentOptions.js";
|
||||||
import Transaction from "../pages/Transaction";
|
import Transaction from "../pages/Transaction";
|
||||||
import Transaction_item from "../pages/Transaction_item";
|
import Transaction_item from "../pages/Transaction_item";
|
||||||
@@ -75,9 +76,9 @@ const Modal = ({ user, shop, isOpen, onClose, modalContent, deviceType, setModal
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
};
|
};
|
||||||
if(modalContent == '') handleOverlayClick();
|
if(modalContent == '') handleOverlayClick();
|
||||||
return (
|
return createPortal(
|
||||||
<div key={updateKey} onClick={handleOverlayClick} className={styles.modalOverlay}>
|
<div key={updateKey} onClick={handleOverlayClick} className={styles.modalOverlay}>
|
||||||
<div className={styles.modalContent} onClick={handleContentClick}>
|
<div className={`${styles.modalContent} ${(modalContent === 'edit_tables' || modalContent === 'payment_option' || modalContent === 'create_clerk') ? styles.modalContentWide : ''}`} onClick={handleContentClick}>
|
||||||
|
|
||||||
{modalContent === "edit_account" && <AccountUpdatePage user={user} />}
|
{modalContent === "edit_account" && <AccountUpdatePage user={user} />}
|
||||||
{modalContent === "reset-password" && <ResetPassword />}
|
{modalContent === "reset-password" && <ResetPassword />}
|
||||||
@@ -86,7 +87,7 @@ const Modal = ({ user, shop, isOpen, onClose, modalContent, deviceType, setModal
|
|||||||
{modalContent === "create_clerk" && <CreateClerk shopId={shop.cafeId} />}
|
{modalContent === "create_clerk" && <CreateClerk shopId={shop.cafeId} />}
|
||||||
{modalContent === "create_kedai" && <CreateCafe shopId={shop.cafeId} />}
|
{modalContent === "create_kedai" && <CreateCafe shopId={shop.cafeId} />}
|
||||||
{modalContent === "create_tenant" && <CreateTenant shopId={shop.cafeId} />}
|
{modalContent === "create_tenant" && <CreateTenant shopId={shop.cafeId} />}
|
||||||
{modalContent === "edit_tables" && <TablesPage shop={shop} />}
|
{modalContent === "edit_tables" && <IdentifyCafeModal shop={shop} />}
|
||||||
{modalContent === "new_transaction" && (
|
{modalContent === "new_transaction" && (
|
||||||
<Transaction propsShopId={shop.cafeId} handleMoveToTransaction={handleMoveToTransaction} depth={depth} shopImg={shopImg} setModal={setModal}/>
|
<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 === "message" && <Message handleYes={onModalYesFunction} handleNo={handleNo}/>}
|
||||||
{modalContent === "player-prompt" && <PlayerPrompt cafeId={shop.cafeId} setModal={setModal} handleClose={handleOverlayClick} welcomePageConfig={shop.welcomePageConfig}/>}
|
{modalContent === "player-prompt" && <PlayerPrompt cafeId={shop.cafeId} setModal={setModal} handleClose={handleOverlayClick} welcomePageConfig={shop.welcomePageConfig}/>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>,
|
||||||
|
document.body
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,37 +2,295 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: -1px;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: rgba(0, 0, 0, 0.5);
|
background: rgba(0, 0, 0, 0.7);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 1000;
|
z-index: 2147483647 !important; /* ensure above any app layers */
|
||||||
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modalContent {
|
.modalContent {
|
||||||
width: 80vw;
|
width: 100%;
|
||||||
max-height: 80vh;
|
max-width: 500px;
|
||||||
|
max-height: 90vh;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||||
|
animation: modalAppear 0.3s ease-out;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: visible; /* Add this line to enable scrolling */
|
z-index: 10000; /* ensure above any page overlays */
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalContentWide {
|
||||||
|
max-width: 920px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalAppear {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageSection {
|
||||||
|
padding: 24px 24px 16px 24px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-bottom: 1px solid #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imagePreview {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
background: #e9ecef;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.previewImage {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageActions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionButton {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: none;
|
||||||
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionButton:not(.deleteButton) {
|
||||||
|
background-color: var(--brand-sage, #6B8F71);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 6px rgba(107, 143, 113, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionButton:not(.deleteButton):hover {
|
||||||
|
background-color: #5a7a60;
|
||||||
|
box-shadow: 0 4px 10px rgba(107, 143, 113, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteButton {
|
||||||
|
background-color: #ff4d4d;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 6px rgba(255, 77, 77, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteButton:hover {
|
||||||
|
background-color: #ff1a1a;
|
||||||
|
box-shadow: 0 4px 10px rgba(255, 77, 77, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formSection {
|
||||||
|
padding: 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroup {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formRow {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formRow .formGroup {
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formLabel {
|
||||||
|
display: block;
|
||||||
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formInput {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: #ffffff;
|
||||||
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #333;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formInput:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--brand-sage, #6B8F71);
|
||||||
|
box-shadow: 0 0 0 3px rgba(107, 143, 113, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formTextarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: #ffffff;
|
||||||
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #333;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 100px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formTextarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--brand-sage, #6B8F71);
|
||||||
|
box-shadow: 0 0 0 3px rgba(107, 143, 113, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formActions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bannerRow {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bannerSuccess {
|
||||||
|
color: #155724;
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bannerError {
|
||||||
|
color: #721c24;
|
||||||
|
background: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formButton {
|
||||||
|
flex: 1;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: none;
|
||||||
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancelButton {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
color: #333;
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancelButton:hover {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.saveButton {
|
||||||
|
background-color: var(--brand-sage, #6B8F71);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 6px rgba(107, 143, 113, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.saveButton:hover {
|
||||||
|
background-color: #5a7a60;
|
||||||
|
box-shadow: 0 4px 10px rgba(107, 143, 113, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
.closeButton {
|
.closeButton {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 16px;
|
||||||
right: 10px;
|
right: 16px;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #333;
|
color: #999;
|
||||||
padding: 0;
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
z-index: 1001; /* Menambah z-index agar selalu di atas modal */
|
||||||
}
|
}
|
||||||
|
|
||||||
.closeButton:hover {
|
.closeButton:hover {
|
||||||
color: #f44336; /* Change color on hover for better UX */
|
background: #f0f0f0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.modalContent {
|
||||||
|
margin: 10px;
|
||||||
|
max-height: 95vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageSection {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formSection {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formRow {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formActions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,14 +31,14 @@
|
|||||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
|
||||||
|
|
||||||
border-radius: 0 0 15px 15px;
|
border-radius: 0 0 15px 15px;
|
||||||
z-index: 1;
|
z-index: 0; /* align with item lister */
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-name {
|
.current-name {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 1;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
margin: 35px 30px;
|
margin: 35px 30px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
.current-artist {
|
.current-artist {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 1;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
margin: -32px 30px;
|
margin: -32px 30px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
.progress-container {
|
.progress-container {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 1;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
margin: 12px 30px;
|
margin: 12px 30px;
|
||||||
}
|
}
|
||||||
@@ -318,4 +318,3 @@
|
|||||||
.search-button.clicked {
|
.search-button.clicked {
|
||||||
background-color: #d0c7b3; /* The color when clicked */
|
background-color: #d0c7b3; /* The color when clicked */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
import React, { useState, useRef, useEffect } from "react";
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
import QrScanner from "qr-scanner"; // Import qr-scanner
|
import QrScanner from "qr-scanner"; // Import qr-scanner
|
||||||
import { getImageUrl } from "../helpers/itemHelper";
|
import { getImageUrl } from "../helpers/itemHelper";
|
||||||
import {
|
import { getCafe, saveCafeDetails } from "../helpers/cafeHelpers";
|
||||||
getCafe,
|
|
||||||
saveCafeDetails,
|
|
||||||
setConfirmationStatus,
|
|
||||||
setOpenBillAvailability
|
|
||||||
} from "../helpers/cafeHelpers";
|
|
||||||
import Switch from "react-switch"; // Import the Switch component
|
import Switch from "react-switch"; // Import the Switch component
|
||||||
|
import styles from "./PaymentOptions.module.css";
|
||||||
|
|
||||||
const SetPaymentQr = ({ shopId,
|
const SetPaymentQr = ({ shopId, qrCodeUrl }) => {
|
||||||
qrCodeUrl }) => {
|
const [qrPosition, setQrPosition] = useState([50, 50]); // legacy kept for API compatibility
|
||||||
const [qrPosition, setQrPosition] = useState([50, 50]);
|
const [qrSize, setQrSize] = useState(50); // legacy kept for API compatibility
|
||||||
const [qrSize, setQrSize] = useState(50);
|
|
||||||
const [qrPayment, setQrPayment] = useState();
|
const [qrPayment, setQrPayment] = useState();
|
||||||
const [qrPaymentFile, setQrPaymentFile] = useState();
|
const [qrPaymentFile, setQrPaymentFile] = useState();
|
||||||
const [qrCodeDetected, setQrCodeDetected] = useState(false);
|
const [qrCodeDetected, setQrCodeDetected] = useState(false);
|
||||||
@@ -26,6 +21,9 @@ const SetPaymentQr = ({ shopId,
|
|||||||
|
|
||||||
const [isConfigQRIS, setIsConfigQRIS] = useState(false);
|
const [isConfigQRIS, setIsConfigQRIS] = useState(false);
|
||||||
const [isOpenBillAvailable, setIsOpenBillAvailable] = useState(false);
|
const [isOpenBillAvailable, setIsOpenBillAvailable] = useState(false);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [saveStatus, setSaveStatus] = useState(null); // 'success' | 'error'
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchCafe = async () => {
|
const fetchCafe = async () => {
|
||||||
@@ -104,6 +102,8 @@ const SetPaymentQr = ({ shopId,
|
|||||||
|
|
||||||
// Save cafe details
|
// Save cafe details
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
|
setSaving(true);
|
||||||
|
setSaveStatus(null);
|
||||||
let qrPaymentFileCache;
|
let qrPaymentFileCache;
|
||||||
console.log(qrPaymentFile)
|
console.log(qrPaymentFile)
|
||||||
if(qrPaymentFile != null)
|
if(qrPaymentFile != null)
|
||||||
@@ -120,240 +120,141 @@ const SetPaymentQr = ({ shopId,
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await saveCafeDetails(cafe.cafeId, details);
|
const response = await saveCafeDetails(cafe.cafeId, details);
|
||||||
|
setIsNeedConfirmationState(response.needsConfirmation ? 1 : 0);
|
||||||
setIsNeedConfirmationState(response.needsConfirmation ? 1 : 0); // Update state after saving
|
setIsQRISavailable(response.isQRISavailable ? 1 : 0);
|
||||||
setIsQRISavailable(response.isQRISavailable ? 1 : 0); // Update state after saving
|
setIsOpenBillAvailable(response.isOpenBillAvailable ? 1 : 0);
|
||||||
setIsOpenBillAvailable(response.isOpenBillAvailable ? 1 : 0); // Update state after saving
|
setSaveStatus('success');
|
||||||
|
|
||||||
console.log("Cafe details saved:", response);
|
console.log("Cafe details saved:", response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving cafe details:", error);
|
console.error("Error saving cafe details:", error);
|
||||||
|
setSaveStatus('error');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const copyQrData = async () => {
|
||||||
|
if (!qrCodeData) return;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(qrCodeData);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(()=>setCopied(false), 1200);
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={styles.container}>
|
<div className={styles.container}>
|
||||||
<h3 style={styles.title}>Konfigurasi pembayaran</h3>
|
<h3 className={styles.title}>Konfigurasi pembayaran</h3>
|
||||||
|
|
||||||
<div style={styles.switchContainer}>
|
<div className={styles.section}>
|
||||||
<p style={styles.uploadMessage}>
|
<div className={styles.sectionHeader}>
|
||||||
Pembayaran QRIS.
|
<div>
|
||||||
</p>
|
<div className={styles.sectionTitle}>Pembayaran QRIS</div>
|
||||||
|
<div className={styles.sectionDesc}>Aktifkan agar pelanggan dapat membayar via QRIS. Kasir tetap perlu verifikasi rekening.</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
onChange={(checked) => setIsQRISavailable(checked ? 1 : 0)}
|
||||||
|
checked={isQRISavailable === 1}
|
||||||
|
offColor="#888"
|
||||||
|
onColor="#4CAF50"
|
||||||
|
uncheckedIcon={false}
|
||||||
|
checkedIcon={false}
|
||||||
|
height={25}
|
||||||
|
width={50}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{isConfigQRIS ?
|
<div className={styles.row}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`${styles.button} ${styles.configButton}`}
|
||||||
|
onClick={() => isQRISavailable === 1 && setIsConfigQRIS(true)}
|
||||||
|
disabled={isQRISavailable !== 1}
|
||||||
|
>
|
||||||
|
Konfigurasi QRIS
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isConfigQRIS && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
id="qr-code-container"
|
id="qr-code-container"
|
||||||
ref={qrCodeContainerRef}
|
ref={qrCodeContainerRef}
|
||||||
|
className={styles.imageBox}
|
||||||
onClick={() => qrPaymentInputRef.current.click()}
|
onClick={() => qrPaymentInputRef.current.click()}
|
||||||
style={{
|
style={{ backgroundImage: `url(${qrPayment})` }}
|
||||||
...styles.qrCodeContainer,
|
|
||||||
backgroundImage: `url(${qrPayment})`,
|
|
||||||
backgroundPosition: "center",
|
|
||||||
backgroundRepeat: "no-repeat",
|
|
||||||
backgroundSize: "contain",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<input
|
<input type="file" accept="image/*" ref={qrPaymentInputRef} style={{ display: 'none' }} onChange={handleFileChange} />
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
ref={qrPaymentInputRef}
|
|
||||||
style={{ display: "none" }}
|
|
||||||
onChange={handleFileChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.uploadMessage}>
|
<div className={styles.smallNote}>Klik area untuk unggah/ganti gambar QR</div>
|
||||||
<p>Klik untuk ganti background</p>
|
<div className={styles.detectRow}>
|
||||||
</div>
|
<div className={`${styles.tag} ${qrCodeDetected ? styles.tagOk : styles.tagBad}`}>
|
||||||
<div style={styles.resultMessage}>
|
{qrCodeDetected && qrPayment !== 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPsNr0TPq8dHT3nBwDQ6OHQQTrqzVFoeBOmuWfgyErrLbJi6f6CnnYhpNHEvkJ_2X-kyI&usqp=CAU' ? 'QR terdeteksi' : 'Tidak ada QR terdeteksi'}
|
||||||
{qrCodeDetected && qrPayment !== 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPsNr0TPq8dHT3nBwDQ6OHQQTrqzVFoeBOmuWfgyErrLbJi6f6CnnYhpNHEvkJ_2X-kyI&usqp=CAU' ? <p>QR terdeteksi</p> : <p>Tidak ada qr terdeteksi</p>}
|
|
||||||
{qrCodeDetected && qrPayment !== 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPsNr0TPq8dHT3nBwDQ6OHQQTrqzVFoeBOmuWfgyErrLbJi6f6CnnYhpNHEvkJ_2X-kyI&usqp=CAU' ? <div
|
|
||||||
onClick={() => qrPaymentInputRef.current.click()} style={styles.uploadButton}>Ganti</div> : <div
|
|
||||||
onClick={() => qrPaymentInputRef.current.click()} style={styles.uploadButton}>Unggah</div>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div onClick={() => setIsConfigQRIS(false)}
|
|
||||||
|
|
||||||
style={{
|
|
||||||
...styles.qrisConfigButton,
|
|
||||||
width: '100%',
|
|
||||||
marginLeft: "0",
|
|
||||||
}}
|
|
||||||
>Terapkan</div>
|
|
||||||
</>
|
|
||||||
:
|
|
||||||
<>
|
|
||||||
<p style={styles.description}>
|
|
||||||
Aktifkan fitur agar pelanggan dapat menggunakan opsi pembayaran QRIS, namun kasir anda perlu memeriksa rekening untuk memastikan pembayaran.
|
|
||||||
</p>
|
|
||||||
<div style={{ display: 'flex' }}>
|
|
||||||
<Switch
|
|
||||||
onChange={(checked) => setIsQRISavailable(checked ? 1 : 0)}
|
|
||||||
checked={isQRISavailable === 1} // Convert to boolean
|
|
||||||
offColor="#888"
|
|
||||||
onColor="#4CAF50"
|
|
||||||
uncheckedIcon={false}
|
|
||||||
checkedIcon={false}
|
|
||||||
height={25}
|
|
||||||
width={50}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
onClick={() => setIsConfigQRIS(true)}
|
|
||||||
style={{
|
|
||||||
...styles.qrisConfigButton,
|
|
||||||
backgroundColor: isQRISavailable == 1 ? styles.qrisConfigButton.backgroundColor : 'gray',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Konfigurasi QRIS
|
|
||||||
</div>
|
</div>
|
||||||
|
<button className={styles.button} onClick={() => qrPaymentInputRef.current.click()}>{qrPayment ? 'Ganti' : 'Unggah'}</button>
|
||||||
|
</div>
|
||||||
|
{qrCodeDetected && (
|
||||||
|
<div className={styles.copyRow}>
|
||||||
|
<input className={styles.linkField} readOnly value={qrCodeData || ''} />
|
||||||
|
<button className={styles.button} onClick={copyQrData}>{copied ? 'Disalin' : 'Salin'}</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.actionsRight}>
|
||||||
|
<button className={`${styles.button} ${styles.primary}`} onClick={() => setIsConfigQRIS(false)}>Terapkan</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
</div>
|
|
||||||
<div style={styles.switchContainer}>
|
|
||||||
<p style={styles.uploadMessage}>
|
|
||||||
Open bill
|
|
||||||
</p>
|
|
||||||
<p style={styles.description}>
|
|
||||||
Aktifkan fitur agar pelanggan dapat menambahkan pesanan selama sesi berlangsung tanpa perlu melakukan transaksi baru dan hanya membayar di akhir.
|
|
||||||
</p>
|
|
||||||
<Switch
|
|
||||||
onChange={(checked) => setIsOpenBillAvailable(checked ? 1 : 0)}
|
|
||||||
checked={isOpenBillAvailable === 1} // Convert to boolean
|
|
||||||
offColor="#888"
|
|
||||||
onColor="#4CAF50"
|
|
||||||
uncheckedIcon={false}
|
|
||||||
checkedIcon={false}
|
|
||||||
height={25}
|
|
||||||
width={50}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={styles.switchContainer}>
|
<div className={styles.section}>
|
||||||
<p style={styles.uploadMessage}>
|
<div className={styles.sectionHeader}>
|
||||||
Pengecekan ganda
|
<div>
|
||||||
</p>
|
<div className={styles.sectionTitle}>Open bill</div>
|
||||||
<p style={styles.description}>
|
<div className={styles.sectionDesc}>Izinkan pelanggan menambah pesanan dalam satu sesi dan bayar di akhir.</div>
|
||||||
Nyalakan agar kasir memeriksa kembali ketersediaan produk sebelum pelanggan membayar.
|
</div>
|
||||||
</p>
|
<Switch
|
||||||
<Switch
|
onChange={(checked) => setIsOpenBillAvailable(checked ? 1 : 0)}
|
||||||
onChange={(checked) => setIsNeedConfirmationState(checked ? 1 : 0)}
|
checked={isOpenBillAvailable === 1}
|
||||||
checked={isNeedConfirmationState === 1} // Convert to boolean
|
offColor="#888"
|
||||||
offColor="#888"
|
onColor="#4CAF50"
|
||||||
onColor="#4CAF50"
|
uncheckedIcon={false}
|
||||||
uncheckedIcon={false}
|
checkedIcon={false}
|
||||||
checkedIcon={false}
|
height={25}
|
||||||
height={25}
|
width={50}
|
||||||
width={50}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={styles.buttonContainer}>
|
<div className={styles.section}>
|
||||||
<button onClick={handleSave} style={styles.saveButton}>
|
<div className={styles.sectionHeader}>
|
||||||
Simpan
|
<div>
|
||||||
|
<div className={styles.sectionTitle}>Pengecekan ganda</div>
|
||||||
|
<div className={styles.sectionDesc}>Kasir memeriksa kembali ketersediaan item sebelum pembayaran.</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
onChange={(checked) => setIsNeedConfirmationState(checked ? 1 : 0)}
|
||||||
|
checked={isNeedConfirmationState === 1}
|
||||||
|
offColor="#888"
|
||||||
|
onColor="#4CAF50"
|
||||||
|
uncheckedIcon={false}
|
||||||
|
checkedIcon={false}
|
||||||
|
height={25}
|
||||||
|
width={50}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<div>
|
||||||
|
{saveStatus === 'success' && <span className={`${styles.banner} ${styles.bannerSuccess}`}>Simpan berhasil</span>}
|
||||||
|
{saveStatus === 'error' && <span className={`${styles.banner} ${styles.bannerError}`}>Gagal menyimpan</span>}
|
||||||
|
</div>
|
||||||
|
<button className={`${styles.button} ${styles.primary}`} onClick={handleSave} disabled={saving}>
|
||||||
|
{saving ? 'Menyimpan…' : 'Simpan'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Styles
|
|
||||||
const styles = {
|
|
||||||
container: {
|
|
||||||
position: 'relative',
|
|
||||||
overflowY: 'auto',
|
|
||||||
overflowX: 'hidden',
|
|
||||||
maxHeight: '80vh',
|
|
||||||
width: '100%',
|
|
||||||
backgroundColor: "white",
|
|
||||||
padding: "20px",
|
|
||||||
borderRadius: "8px",
|
|
||||||
boxShadow: "0 2px 10px rgba(0, 0, 0, 0.1)",
|
|
||||||
textAlign: "center", // Center text and children
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
marginBottom: "20px",
|
|
||||||
fontWeight: "bold",
|
|
||||||
},
|
|
||||||
qrCodeContainer: {
|
|
||||||
backgroundColor: '#999999',
|
|
||||||
borderRadius: '20px',
|
|
||||||
position: "relative",
|
|
||||||
width: "100%",
|
|
||||||
height: "200px",
|
|
||||||
backgroundSize: "contain",
|
|
||||||
overflow: "hidden",
|
|
||||||
margin: "0 auto", // Center the QR code container
|
|
||||||
marginTop: '10px'
|
|
||||||
},
|
|
||||||
uploadMessage: {
|
|
||||||
fontWeight: 600,
|
|
||||||
textAlign: "left",
|
|
||||||
},
|
|
||||||
qrisConfigButton: {
|
|
||||||
borderRadius: '15px',
|
|
||||||
backgroundColor: '#28a745',
|
|
||||||
width: '144px',
|
|
||||||
textAlign: 'center',
|
|
||||||
color: 'white',
|
|
||||||
lineHeight: '24px',
|
|
||||||
marginLeft: '14px',
|
|
||||||
},
|
|
||||||
uploadButton: {
|
|
||||||
paddingRight: '10px',
|
|
||||||
backgroundColor: '#28a745',
|
|
||||||
borderRadius: '30px',
|
|
||||||
color: 'white',
|
|
||||||
fontWeight: 700,
|
|
||||||
height: '36px',
|
|
||||||
lineHeight: '36px',
|
|
||||||
paddingLeft: '10px',
|
|
||||||
paddingHeight: '10px',
|
|
||||||
},
|
|
||||||
resultMessage: {
|
|
||||||
marginTop: "-24px",
|
|
||||||
textAlign: "left",
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between'
|
|
||||||
},
|
|
||||||
buttonContainer: {
|
|
||||||
marginTop: "20px",
|
|
||||||
textAlign: "left",
|
|
||||||
},
|
|
||||||
saveButton: {
|
|
||||||
padding: "10px 20px",
|
|
||||||
fontSize: "16px",
|
|
||||||
backgroundColor: "#28a745",
|
|
||||||
color: "#fff",
|
|
||||||
border: "none",
|
|
||||||
borderRadius: "30px",
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "background-color 0.3s",
|
|
||||||
},
|
|
||||||
switchContainer: {
|
|
||||||
textAlign: "left",
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
margin: "10px 0",
|
|
||||||
fontSize: "14px",
|
|
||||||
color: "#666",
|
|
||||||
},
|
|
||||||
sliderContainer: {
|
|
||||||
marginBottom: "20px",
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
display: "block",
|
|
||||||
marginBottom: "10px",
|
|
||||||
},
|
|
||||||
sliderWrapper: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
flex: "1",
|
|
||||||
margin: "0 10px",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SetPaymentQr;
|
export default SetPaymentQr;
|
||||||
|
|||||||
171
src/components/PaymentOptions.module.css
Normal file
171
src/components/PaymentOptions.module.css
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
/* PaymentOptions.module.css */
|
||||||
|
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
max-height: 80vh;
|
||||||
|
width: 100%;
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
background: #fafafa;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionDesc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
background: #28a745;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.configButton {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageBox {
|
||||||
|
background-color: #999999;
|
||||||
|
border-radius: 12px;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 220px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-size: contain;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 10px 0 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smallNote {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detectRow {
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagOk {
|
||||||
|
color: #155724;
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagBad {
|
||||||
|
color: #721c24;
|
||||||
|
background: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyRow {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkField {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionsRight {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bannerSuccess {
|
||||||
|
color: #155724;
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bannerError {
|
||||||
|
color: #721c24;
|
||||||
|
background: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.container { padding: 16px; }
|
||||||
|
}
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
.watermark {
|
.watermark {
|
||||||
z-index: 5;
|
z-index: 0; /* align with item lister */
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
background-color: rgb(222, 237, 100);
|
background-color: rgb(222, 237, 100);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
/* Media query for desktop */
|
/* Media query for desktop */
|
||||||
@media (min-width: 768px) { /* Adjust the min-width as needed */
|
@media (min-width: 768px) { /* Adjust the min-width as needed */
|
||||||
.watermark {
|
.watermark {
|
||||||
z-index: 5;
|
z-index: 0; /* align with item lister */
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
background-color: rgb(222, 237, 100);
|
background-color: rgb(222, 237, 100);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -52,4 +52,4 @@
|
|||||||
|
|
||||||
.watermarkFooter {
|
.watermarkFooter {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,95 @@ body {
|
|||||||
sans-serif;
|
sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
overflow-x: hidden;
|
||||||
|
background-color: #e9e9e9;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
monospace;
|
monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ensure proper scrolling behavior */
|
||||||
|
html, body {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure proper z-index stacking */
|
||||||
|
* {
|
||||||
|
z-index: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-height: 75vh; /* Menaikkan tinggi maksimum pada mobile */
|
||||||
|
}
|
||||||
|
|
||||||
|
.materialCard {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.materialSummary {
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.materialName {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.materialStock {
|
||||||
|
font-size: 13px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailGrid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGrid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Memastikan MaterialList tetap dapat di-scroll */
|
||||||
|
.MaterialList-module__container {
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// src/CafePage.js
|
// src/CafePage.js
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
useParams,
|
useParams,
|
||||||
useSearchParams,
|
useSearchParams,
|
||||||
@@ -55,6 +55,7 @@ function CafePage({
|
|||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const token = searchParams.get("token");
|
const token = searchParams.get("token");
|
||||||
const { shopIdentifier, tableCode } = useParams();
|
const { shopIdentifier, tableCode } = useParams();
|
||||||
|
// Send params to parent immediately (original behavior)
|
||||||
sendParam({ shopIdentifier, tableCode });
|
sendParam({ shopIdentifier, tableCode });
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -76,6 +77,40 @@ function CafePage({
|
|||||||
const [config, setConfig] = useState({});
|
const [config, setConfig] = useState({});
|
||||||
|
|
||||||
const [beingEditedType, setBeingEditedType] = useState(0);
|
const [beingEditedType, setBeingEditedType] = useState(0);
|
||||||
|
// Sticky floating cart bar animations and visibility
|
||||||
|
const [cartBump, setCartBump] = useState(false);
|
||||||
|
const [barIntro, setBarIntro] = useState(false);
|
||||||
|
const [historyPulse, setHistoryPulse] = useState(false);
|
||||||
|
const prevShowRef = useRef(false);
|
||||||
|
const prevTxnRef = useRef(null);
|
||||||
|
const showBar = !isEditMode && (user.username || cartItemsLength > 0);
|
||||||
|
|
||||||
|
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 checkWelcomePageConfig = () => {
|
||||||
// const parsedConfig = JSON.parse(welcomePageConfig);
|
// const parsedConfig = JSON.parse(welcomePageConfig);
|
||||||
@@ -245,6 +280,8 @@ function CafePage({
|
|||||||
removeConnectedGuestSides={removeConnectedGuestSides}
|
removeConnectedGuestSides={removeConnectedGuestSides}
|
||||||
setIsEditMode={(e) => setIsEditMode(e)}
|
setIsEditMode={(e) => setIsEditMode(e)}
|
||||||
isEditMode={isEditMode}
|
isEditMode={isEditMode}
|
||||||
|
zIndexLevel={9000}
|
||||||
|
rectZIndex={9000}
|
||||||
/>
|
/>
|
||||||
<MusicPlayer
|
<MusicPlayer
|
||||||
socket={socket}
|
socket={socket}
|
||||||
@@ -318,29 +355,33 @@ function CafePage({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{!isEditMode && (user.username || cartItemsLength > 0) &&
|
{showBar &&
|
||||||
<div style={{ marginTop: '10px', height: '40px', position: 'sticky', bottom: '40px', display: 'flex', justifyContent: 'center', alignItems: 'center', textAlign: 'center' }}>
|
<div className={`StickyCartBar${barIntro ? ' intro' : ''}`}>
|
||||||
{(lastTransaction != null || cartItemsLength > 0) &&
|
{(lastTransaction != null || cartItemsLength > 0) &&
|
||||||
<div onClick={goToCart} style={{ backgroundColor: '#73a585', width: user.username ? '55vw' : '70vw', height: '40px', borderRadius: '30px', display: 'flex', justifyContent: 'space-between', padding: '0 20px' }}>
|
<div role="button" tabIndex={0} aria-label="Buka keranjang" onKeyDown={(e)=>{ if(e.key==='Enter'||e.key===' ') { e.preventDefault(); goToCart(); } }} onClick={goToCart} className={`cartBtn${cartBump ? ' bump' : ''}`}>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', alignContent: 'center' }}>{lastTransaction != null && '+'}{cartItemsLength} item</div>
|
<div className="summary">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', width: '130px' }}>
|
{(lastTransaction != null) && <span>+</span>}
|
||||||
|
<span>{cartItemsLength} item</span>
|
||||||
|
</div>
|
||||||
|
<div className="summary" style={{ gap: 6 }}>
|
||||||
{((lastTransaction == null || lastTransaction?.payment_type != 'paylater')) ?
|
{((lastTransaction == null || lastTransaction?.payment_type != 'paylater')) ?
|
||||||
<span style={{ whiteSpace: 'nowrap' }}>Rp{totalPrice}</span>
|
<span className="value">{`Rp ${Number(totalPrice || 0).toLocaleString('id-ID')}`}</span>
|
||||||
:
|
:
|
||||||
<span style={{ whiteSpace: 'nowrap' }}>Open bill</span>
|
<span className="value">Open bill</span>
|
||||||
}
|
}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', marginLeft: '5px', width: '20px' }}>
|
<div className="icon">
|
||||||
<svg viewBox="0 0 34 34" style={{ fill: 'white', marginTop: '4px' }}>
|
<svg viewBox="0 0 34 34" style={{ fill: 'white', marginTop: '4px' }}>
|
||||||
<path d="M9.79175 24.75C8.09591 24.75 6.72383 26.1375 6.72383 27.8333C6.72383 29.5292 8.09591 30.9167 9.79175 30.9167C11.4876 30.9167 12.8751 29.5292 12.8751 27.8333C12.8751 26.1375 11.4876 24.75 9.79175 24.75ZM0.541748 0.0833435V3.16668H3.62508L9.17508 14.8679L7.09383 18.645C6.84717 19.0767 6.70842 19.5854 6.70842 20.125C6.70842 21.8208 8.09591 23.2083 9.79175 23.2083H28.2917V20.125H10.4392C10.2234 20.125 10.0538 19.9554 10.0538 19.7396L10.1001 19.5546L11.4876 17.0417H22.973C24.1292 17.0417 25.1467 16.4096 25.6709 15.4538L31.1901 5.44834C31.3134 5.23251 31.3751 4.97043 31.3751 4.70834C31.3751 3.86043 30.6813 3.16668 29.8334 3.16668H7.03217L5.583 0.0833435H0.541748ZM25.2084 24.75C23.5126 24.75 22.1405 26.1375 22.1405 27.8333C22.1405 29.5292 23.5126 30.9167 25.2084 30.9167C26.9042 30.9167 28.2917 29.5292 28.2917 27.8333C28.2917 26.1375 26.9042 24.75 25.2084 24.75Z"></path>
|
<path d="M9.79175 24.75C8.09591 24.75 6.72383 26.1375 6.72383 27.8333C6.72383 29.5292 8.09591 30.9167 9.79175 30.9167C11.4876 30.9167 12.8751 29.5292 12.8751 27.8333C12.8751 26.1375 11.4876 24.75 9.79175 24.75ZM0.541748 0.0833435V3.16668H3.62508L9.17508 14.8679L7.09383 18.645C6.84717 19.0767 6.70842 19.5854 6.70842 20.125C6.70842 21.8208 8.09591 23.2083 9.79175 23.2083H28.2917V20.125H10.4392C10.2234 20.125 10.0538 19.9554 10.0538 19.7396L10.1001 19.5546L11.4876 17.0417H22.973C24.1292 17.0417 25.1467 16.4096 25.6709 15.4538L31.1901 5.44834C31.3134 5.23251 31.3751 4.97043 31.3751 4.70834C31.3751 3.86043 30.6813 3.16668 29.8334 3.16668H7.03217L5.583 0.0833435H0.541748ZM25.2084 24.75C23.5126 24.75 22.1405 26.1375 22.1405 27.8333C22.1405 29.5292 23.5126 30.9167 25.2084 30.9167C26.9042 30.9167 28.2917 29.5292 28.2917 27.8333C28.2917 26.1375 26.9042 24.75 25.2084 24.75Z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
{cartItemsLength > 0 && <span className={`badge${cartBump ? ' pop' : ''}`}>{cartItemsLength > 9 ? '9+' : cartItemsLength}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{user.username &&
|
{user.username &&
|
||||||
<div onClick={goToTransactions} style={{ backgroundColor: '#73a585', width: '15vw', height: '40px', borderRadius: '30px', display: 'flex', justifyContent: 'center', marginLeft: lastTransaction != null || cartItemsLength > 0 ? '6px' : '0px' }}>
|
<div role="button" tabIndex={0} aria-label="Lihat riwayat" onKeyDown={(e)=>{ if(e.key==='Enter'||e.key===' ') { e.preventDefault(); goToTransactions(); } }} onClick={goToTransactions} className={`historyBtn${historyPulse ? ' pulse' : ''}`}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '38px', marginRight: '5px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 38, marginRight: 6 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', marginLeft: '5px', width: '20px' }}>
|
<div className="icon">
|
||||||
<svg viewBox="0 0 512 512">
|
<svg viewBox="0 0 512 512">
|
||||||
<g
|
<g
|
||||||
transform="translate(0 460) scale(0.09 -0.09)"
|
transform="translate(0 460) scale(0.09 -0.09)"
|
||||||
@@ -388,3 +429,4 @@ function CafePage({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default CafePage;
|
export default CafePage;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useRef, useEffect, useState } from "react";
|
import React, { useRef, useEffect, useState } from "react";
|
||||||
import styles from "./Invoice.module.css";
|
import styles from "./Invoice.module.css";
|
||||||
|
import cartStyles from "./CartPage.module.css";
|
||||||
import { useParams } from "react-router-dom"; // Changed from useSearchParams to useLocation
|
import { useParams } from "react-router-dom"; // Changed from useSearchParams to useLocation
|
||||||
import { ThreeDots, ColorRing } from "react-loader-spinner";
|
import { ThreeDots, ColorRing } from "react-loader-spinner";
|
||||||
|
|
||||||
@@ -384,7 +385,13 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
|
|||||||
return (
|
return (
|
||||||
<div className={styles.Invoice} style={{ height: (getItemsByCafeId(shopId).length > 0 ? '' : '100vh'), minHeight: (getItemsByCafeId(shopId).length > 0 ? '100vh' : '') }}>
|
<div className={styles.Invoice} style={{ height: (getItemsByCafeId(shopId).length > 0 ? '' : '100vh'), minHeight: (getItemsByCafeId(shopId).length > 0 ? '100vh' : '') }}>
|
||||||
|
|
||||||
<div onClick={goToShop} style={{ marginLeft: '22px', marginTop: '49px', marginRight: '10px', display: 'flex', flexWrap: 'nowrap', alignItems: 'center', fontSize: '25px' }} ><svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 512 512"><path d="M48,256c0,114.87,93.13,208,208,208s208-93.13,208-208S370.87,48,256,48,48,141.13,48,256Zm212.65-91.36a16,16,0,0,1,.09,22.63L208.42,240H342a16,16,0,0,1,0,32H208.42l52.32,52.73A16,16,0,1,1,238,347.27l-79.39-80a16,16,0,0,1,0-22.54l79.39-80A16,16,0,0,1,260.65,164.64Z" /></svg>Keranjang</div>
|
<div className={cartStyles.header}>
|
||||||
|
<div className={cartStyles.backBtn} onClick={goToShop} aria-label="Kembali">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 512 512"><path fill="#fff" d="M48,256c0,114.87,93.13,208,208,208s208-93.13,208-208S370.87,48,256,48,48,141.13,48,256Zm212.65-91.36a16,16,0,0,1,.09,22.63L208.42,240H342a16,16,0,0,1,0,32H208.42l52.32,52.73A16,16,0,1,1,238,347.27l-79.39-80a16,16,0,0,1,0-22.54l79.39-80A16,16,0,0,1,260.65,164.64Z" /></svg>
|
||||||
|
</div>
|
||||||
|
<div className={cartStyles.title}>Keranjang</div>
|
||||||
|
</div>
|
||||||
|
<div className={cartStyles.container}>
|
||||||
|
|
||||||
{(transactionData == null && getItemsByCafeId(shopId).length < 1) ?
|
{(transactionData == null && getItemsByCafeId(shopId).length < 1) ?
|
||||||
<div style={{ height: '75vh', display: 'flex', justifyContent: 'center', flexDirection: 'column', alignContent: 'center', alignItems: 'center' }}>
|
<div style={{ height: '75vh', display: 'flex', justifyContent: 'center', flexDirection: 'column', alignContent: 'center', alignItems: 'center' }}>
|
||||||
@@ -443,21 +450,21 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={styles.NoteContainer}>
|
|
||||||
<span>Catatan :</span>
|
|
||||||
<span></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.NoteContainer}>
|
|
||||||
<textarea
|
|
||||||
ref={textareaRef}
|
|
||||||
className={styles.NoteInput}
|
|
||||||
placeholder="Tambahkan catatan..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{getItemsByCafeId(shopId).length > 0 && (
|
||||||
|
<div className={`${styles.RoundedRectangle} ${cartStyles.sectionCard}`}>
|
||||||
|
<div className={cartStyles.sectionTitle}>Catatan Untuk Kasir</div>
|
||||||
|
<div className={cartStyles.divider}></div>
|
||||||
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
className={styles.NoteInput}
|
||||||
|
placeholder="Contoh: tanpa gula, ekstra es, dsb."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{transactionData &&
|
{transactionData &&
|
||||||
<div className={styles.RoundedRectangle} style={{ backgroundColor: '#c3c3c3', fontSize: '15px', display: 'flex', justifyContent: 'space-between' }}>
|
<div className={styles.RoundedRectangle} style={{ backgroundColor: '#c3c3c3', fontSize: '15px', display: 'flex', justifyContent: 'space-between' }}>
|
||||||
{transactionData.payment_type != 'paylater' ?
|
{transactionData.payment_type != 'paylater' ?
|
||||||
@@ -575,6 +582,7 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
.CheckoutContainer {
|
.CheckoutContainer {
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
z-index: 100; /* Menurunkan z-index agar tidak menutupi material list */
|
||||||
}
|
}
|
||||||
|
|
||||||
.EmailContainer {
|
.EmailContainer {
|
||||||
@@ -111,4 +112,4 @@
|
|||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
margin-bottom: 25px;
|
margin-bottom: 25px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
68
src/pages/CartPage.module.css
Normal file
68
src/pages/CartPage.module.css
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
.header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 5;
|
||||||
|
background: var(--brand-sage, #6B8F71);
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backBtn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
border: 1px solid rgba(255,255,255,0.25);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionCard {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||||||
|
padding: 12px;
|
||||||
|
margin: 12px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #2d2d2d;
|
||||||
|
margin: 4px 0 10px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rowBetween {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #eee;
|
||||||
|
margin: 8px 0 12px 0;
|
||||||
|
}
|
||||||
@@ -1,119 +1,153 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { createClerks } from '../helpers/userHelpers'; // Adjust the import path as needed
|
import { createClerks, getClerks } from '../helpers/userHelpers';
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
import styles from './CreateClerk.module.css';
|
||||||
|
|
||||||
const CreateClerk = ({ shopId }) => {
|
const CreateClerk = ({ shopId }) => {
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [message, setMessage] = useState('');
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [banner, setBanner] = useState(null); // { type: 'success'|'error', text: string }
|
||||||
|
const [clerks, setClerks] = useState([]);
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const queryParams = new URLSearchParams(location.search);
|
const queryParams = new URLSearchParams(location.search);
|
||||||
const cafeIdParam = queryParams.get("cafeId");
|
const cafeIdParam = queryParams.get("cafeId");
|
||||||
|
const effectiveShopId = useMemo(()=> shopId || cafeIdParam, [shopId, cafeIdParam]);
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
const load = async ()=>{
|
||||||
|
if (!effectiveShopId) return;
|
||||||
|
try {
|
||||||
|
const data = await getClerks(effectiveShopId);
|
||||||
|
if (data && Array.isArray(data)) setClerks(data);
|
||||||
|
} catch (e) {}
|
||||||
|
};
|
||||||
|
load();
|
||||||
|
}, [effectiveShopId]);
|
||||||
|
|
||||||
|
const generatePassword = () => {
|
||||||
|
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%';
|
||||||
|
let pwd = '';
|
||||||
|
for (let i = 0; i < 12; i++) pwd += chars[Math.floor(Math.random()*chars.length)];
|
||||||
|
setPassword(pwd);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async (event) => {
|
const handleSubmit = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setLoading(true);
|
setSaving(true);
|
||||||
setMessage('');
|
setBanner(null);
|
||||||
|
|
||||||
// Basic validation
|
// Basic validation
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
setMessage('Username and password are required');
|
setBanner({ type: 'error', text: 'Username dan password wajib diisi' });
|
||||||
setLoading(false);
|
setSaving(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (username.length < 3) {
|
||||||
|
setBanner({ type: 'error', text: 'Username minimal 3 karakter' });
|
||||||
|
setSaving(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (password.length < 6) {
|
||||||
|
setBanner({ type: 'error', text: 'Password minimal 6 karakter' });
|
||||||
|
setSaving(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const create = await createClerks(shopId || cafeIdParam, username, password);
|
const create = await createClerks(effectiveShopId, username, password);
|
||||||
|
if (create) {
|
||||||
if (create) setMessage('Clerk created successfully');
|
setBanner({ type: 'success', text: 'Kasir berhasil ditambahkan' });
|
||||||
else setMessage('Failed to create clerk');
|
// Refresh list
|
||||||
|
try {
|
||||||
|
const data = await getClerks(effectiveShopId);
|
||||||
|
if (data && Array.isArray(data)) setClerks(data);
|
||||||
|
} catch {}
|
||||||
|
// Clear form
|
||||||
|
setUsername('');
|
||||||
|
setPassword('');
|
||||||
|
} else {
|
||||||
|
setBanner({ type: 'error', text: 'Gagal menambahkan kasir' });
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setMessage('Error creating clerk');
|
setBanner({ type: 'error', text: 'Terjadi kesalahan saat menambahkan kasir' });
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={styles.container}>
|
<div className={styles.container}>
|
||||||
<h2 style={styles.header}>Tambah Kasir</h2>
|
<div className={styles.header}>
|
||||||
<form onSubmit={handleSubmit} style={styles.form}>
|
<h2 className={styles.title}>Tambah Kasir</h2>
|
||||||
<input
|
{banner && (
|
||||||
type="text"
|
<div className={`${styles.banner} ${banner.type === 'success' ? styles.bannerSuccess : styles.bannerError}`}>
|
||||||
placeholder="Username"
|
{banner.text}
|
||||||
value={username}
|
</div>
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
|
||||||
style={styles.input}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
placeholder="Password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
style={styles.input}
|
|
||||||
/>
|
|
||||||
<button type="submit" style={styles.button} disabled={loading}>
|
|
||||||
{loading ? 'Creating...' : 'Create Clerk'}
|
|
||||||
</button>
|
|
||||||
{message && (
|
|
||||||
<p style={{ ...styles.message, color: message.includes('success') ? 'green' : 'red' }}>
|
|
||||||
{message}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</form>
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.sectionTitle}>Form kasir baru</div>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className={styles.formRow}>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label}>Username</label>
|
||||||
|
<input
|
||||||
|
className={styles.input}
|
||||||
|
type="text"
|
||||||
|
placeholder="kasir_baru"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label}>Password</label>
|
||||||
|
<div className={styles.pwdRow}>
|
||||||
|
<input
|
||||||
|
className={styles.input}
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
placeholder="••••••"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button type="button" className={styles.button} onClick={()=>setShowPassword(!showPassword)}>
|
||||||
|
{showPassword ? 'Sembunyikan' : 'Tampilkan'}
|
||||||
|
</button>
|
||||||
|
<button type="button" className={styles.button} onClick={generatePassword}>
|
||||||
|
Generate
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<button className={`${styles.button} ${styles.primary}`} type="submit" disabled={saving}>
|
||||||
|
{saving ? 'Menambahkan…' : 'Tambah Kasir'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.sectionTitle}>Daftar kasir</div>
|
||||||
|
<div className={styles.list}>
|
||||||
|
{clerks && clerks.length > 0 ? (
|
||||||
|
clerks.map((c) => (
|
||||||
|
<div key={c.user_id || c.username} className={`${styles.listItem} ${styles.muted}`}>
|
||||||
|
<span>@{c.username}</span>
|
||||||
|
{/* Tempatkan tombol hapus jika API tersedia */}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className={styles.listItem}>Belum ada kasir</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Basic styling to make it mobile-friendly with a white background
|
|
||||||
const styles = {
|
|
||||||
container: {
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
width: '100%',
|
|
||||||
maxWidth: '350px',
|
|
||||||
margin: '0 auto',
|
|
||||||
padding: '20px',
|
|
||||||
boxShadow: '0 4px 10px rgba(0, 0, 0, 0.1)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
textAlign: 'center',
|
|
||||||
marginBottom: '20px',
|
|
||||||
fontSize: '20px',
|
|
||||||
color: '#333',
|
|
||||||
},
|
|
||||||
form: {
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: '15px',
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
padding: '12px',
|
|
||||||
fontSize: '16px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid #ccc',
|
|
||||||
width: '100%',
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
backgroundColor: '#f9f9f9',
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
padding: '12px',
|
|
||||||
fontSize: '16px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: 'none',
|
|
||||||
backgroundColor: '#28a745',
|
|
||||||
color: 'white',
|
|
||||||
cursor: 'pointer',
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
message: {
|
|
||||||
textAlign: 'center',
|
|
||||||
marginTop: '10px',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CreateClerk;
|
export default CreateClerk;
|
||||||
|
|||||||
137
src/pages/CreateClerk.module.css
Normal file
137
src/pages/CreateClerk.module.css
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
/* CreateClerk.module.css */
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: #fff;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
background: #fafafa;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formRow {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwdRow {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
background: #28a745;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bannerSuccess {
|
||||||
|
color: #155724;
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bannerError {
|
||||||
|
color: #721c24;
|
||||||
|
background: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
max-height: 220px;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.formRow { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -7,6 +7,8 @@
|
|||||||
font-size: calc(10px + 2vmin);
|
font-size: calc(10px + 2vmin);
|
||||||
color: rgba(88, 55, 50, 1);
|
color: rgba(88, 55, 50, 1);
|
||||||
background-color: #e9e9e9;
|
background-color: #e9e9e9;
|
||||||
|
position: relative;
|
||||||
|
z-index: 50; /* Memastikan berada di bawah material list */
|
||||||
}
|
}
|
||||||
|
|
||||||
.Invoice-title {
|
.Invoice-title {
|
||||||
@@ -45,6 +47,8 @@
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
z-index: 100; /* Menurunkan z-index lebih jauh agar tidak menutupi material list */
|
||||||
|
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.PaymentOptionMargin {
|
.PaymentOptionMargin {
|
||||||
@@ -159,6 +163,7 @@
|
|||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
margin-bottom: 7px;
|
margin-bottom: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.OrderTypeContainer {
|
.OrderTypeContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
import React, { useState, useRef, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import jsQR from "jsqr";
|
|
||||||
import { getImageUrl } from "../helpers/itemHelper";
|
|
||||||
import {
|
|
||||||
getCafe,
|
|
||||||
saveCafeDetails,
|
|
||||||
setConfirmationStatus,
|
|
||||||
} from "../helpers/cafeHelpers";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getMaterials,
|
getMaterials,
|
||||||
createMaterial,
|
createMaterial,
|
||||||
@@ -17,347 +9,497 @@ import {
|
|||||||
getMaterialMutations,
|
getMaterialMutations,
|
||||||
} from "../helpers/materialMutationHelpers";
|
} from "../helpers/materialMutationHelpers";
|
||||||
|
|
||||||
import Switch from "react-switch"; // Import the Switch component
|
import styles from './MaterialList.module.css';
|
||||||
import Carousel from '../components/Carousel'
|
|
||||||
import styles from './MaterialList.module.css'; // Import the CSS Module
|
|
||||||
|
|
||||||
const SetPaymentQr = ({ cafeId }) => {
|
const MaterialList = ({ cafeId }) => {
|
||||||
// All your state and logic goes here (unchanged)
|
// State declarations
|
||||||
const [materials, setMaterials] = useState([]);
|
const [materials, setMaterials] = useState([]);
|
||||||
const [mutations, setMutations] = useState([]);
|
const [mutations, setMutations] = useState([]);
|
||||||
const [newMaterialName, setNewMaterialName] = useState("");
|
|
||||||
const [newMaterialUnit, setNewMaterialUnit] = useState("kilogram");
|
|
||||||
const [newMaterialImage, setNewMaterialImage] = useState(null);
|
|
||||||
const [deleting, setDeleting] = useState(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [showForm, setShowForm] = useState(false);
|
|
||||||
const [selectedMaterialIndex, setSelectedMaterialIndex] = useState(-1);
|
// Add material form state
|
||||||
const [latestMutation, setLatestMutation] = useState([]);
|
const [showAddForm, setShowAddForm] = useState(false);
|
||||||
const [currentQuantity, setCurrentQuantity] = useState(-1);
|
const [newMaterialName, setNewMaterialName] = useState("");
|
||||||
const [currentPrice, setCurrentPrice] = useState(0);
|
const [newMaterialUnit, setNewMaterialUnit] = useState("kilogram");
|
||||||
const [newPrice, setNewPrice] = useState(0);
|
|
||||||
const [quantityChange, setQuantityChange] = useState(0);
|
// Edit material state
|
||||||
|
const [editingMaterialId, setEditingMaterialId] = useState(null);
|
||||||
|
const [newQuantity, setNewQuantity] = useState(0);
|
||||||
|
const [newPrice, setNewPrice] = useState("");
|
||||||
|
|
||||||
|
// Expand state for each material
|
||||||
|
const [expandedMaterials, setExpandedMaterials] = useState({});
|
||||||
|
|
||||||
|
// View state
|
||||||
const [sortOrder, setSortOrder] = useState("desc");
|
const [sortOrder, setSortOrder] = useState("desc");
|
||||||
const [isEditCurrentPrice, setIsEditCurrentPrice] = useState(false);
|
const [isViewingHistory, setIsViewingHistory] = useState({});
|
||||||
const [isViewingHistory, setIsViewingHistory] = useState(false);
|
|
||||||
|
// Format currency helper
|
||||||
|
const formatCurrency = (value) => {
|
||||||
|
if (!value) return "0";
|
||||||
|
return parseInt(value.toString().replace(/\./g, "")).toLocaleString('id-ID');
|
||||||
|
};
|
||||||
|
|
||||||
const convertToInteger = (formattedValue) => {
|
const convertToInteger = (formattedValue) => {
|
||||||
// Remove dots and convert to integer
|
return parseInt(formattedValue.replace(/\./g, ""), 10) || 0;
|
||||||
return parseInt(formattedValue.replace(/\./g, ""), 10);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatCurrency = (value) => {
|
// Handle price input change
|
||||||
if (!value) return "";
|
const handlePriceChange = (e) => {
|
||||||
// Remove existing formatting (dots) and format again
|
|
||||||
const numericValue = value.toString().replace(/\D/g, ""); // Keep only digits
|
|
||||||
return numericValue.replace(/\B(?=(\d{3})+(?!\d))/g, "."); // Add dot as thousands separator
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = (e) => {
|
|
||||||
const formattedValue = formatCurrency(e.target.value);
|
const formattedValue = formatCurrency(e.target.value);
|
||||||
setNewPrice(formattedValue);
|
setNewPrice(formattedValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fetch materials and mutations
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchMaterials = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await getMaterials(cafeId);
|
const [materialsData, mutationsData] = await Promise.all([
|
||||||
setMaterials(data);
|
getMaterials(cafeId),
|
||||||
console.log(data)
|
getMaterialMutations(cafeId)
|
||||||
|
]);
|
||||||
|
|
||||||
|
setMaterials(materialsData);
|
||||||
|
setMutations(mutationsData);
|
||||||
setError(null);
|
setError(null);
|
||||||
if (data.length > 0) {
|
|
||||||
setSelectedMaterialIndex(0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching materials:", error);
|
|
||||||
setError("Failed to fetch materials.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchMutations = async () => {
|
|
||||||
try {
|
|
||||||
const data = await getMaterialMutations(cafeId);
|
|
||||||
setMutations(data);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message);
|
console.error("Error fetching data:", err);
|
||||||
|
setError("Gagal memuat data bahan baku.");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchMaterials();
|
if (cafeId) {
|
||||||
fetchMutations();
|
fetchData();
|
||||||
|
}
|
||||||
}, [cafeId]);
|
}, [cafeId]);
|
||||||
|
|
||||||
const filteredMutations = mutations.filter((mutation) => mutation.materialId === materials[selectedMaterialIndex]?.materialId) || [];
|
// Handle create material
|
||||||
|
const handleCreateMaterial = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!newMaterialName.trim()) return;
|
||||||
|
|
||||||
const sortedMutations = filteredMutations
|
setLoading(true);
|
||||||
.filter((mutation) => mutation.materialId === materials[selectedMaterialIndex].materialId)
|
try {
|
||||||
.sort((a, b) => {
|
const formData = new FormData();
|
||||||
|
formData.append("name", newMaterialName);
|
||||||
|
formData.append("unit", newMaterialUnit);
|
||||||
|
|
||||||
|
await createMaterial(cafeId, formData);
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
setNewMaterialName("");
|
||||||
|
setNewMaterialUnit("kilogram");
|
||||||
|
setShowAddForm(false);
|
||||||
|
|
||||||
|
// Refresh materials list
|
||||||
|
const data = await getMaterials(cafeId);
|
||||||
|
setMaterials(data);
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error creating material:", err);
|
||||||
|
setError("Gagal menambah bahan baku.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle delete material
|
||||||
|
const handleDeleteMaterial = async (materialId) => {
|
||||||
|
if (!window.confirm("Apakah Anda yakin ingin menghapus bahan baku ini?")) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await deleteMaterial(materialId);
|
||||||
|
|
||||||
|
// Refresh materials list
|
||||||
|
const data = await getMaterials(cafeId);
|
||||||
|
setMaterials(data);
|
||||||
|
|
||||||
|
// Close expanded view if deleted material was expanded
|
||||||
|
setExpandedMaterials(prevState => {
|
||||||
|
const newState = { ...prevState };
|
||||||
|
delete newState[materialId];
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error deleting material:", err);
|
||||||
|
setError("Gagal menghapus bahan baku.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle update stock
|
||||||
|
const handleUpdateStock = async (materialId) => {
|
||||||
|
if (newQuantity === 0 && !convertToInteger(newPrice)) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const material = materials.find(m => m.materialId === materialId);
|
||||||
|
const currentQuantity = material ? material.currentStock : 0;
|
||||||
|
const finalQuantity = currentQuantity + newQuantity;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("newStock", finalQuantity);
|
||||||
|
formData.append("priceAtp", convertToInteger(newPrice));
|
||||||
|
formData.append("reason", `Update stok: ${newQuantity > 0 ? '+' : ''}${newQuantity}`);
|
||||||
|
|
||||||
|
await createMaterialMutation(materialId, formData);
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
const [materialsData, mutationsData] = await Promise.all([
|
||||||
|
getMaterials(cafeId),
|
||||||
|
getMaterialMutations(cafeId)
|
||||||
|
]);
|
||||||
|
|
||||||
|
setMaterials(materialsData);
|
||||||
|
setMutations(mutationsData);
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
setNewQuantity(0);
|
||||||
|
setNewPrice("");
|
||||||
|
setEditingMaterialId(null);
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error updating stock:", err);
|
||||||
|
setError("Gagal memperbarui stok.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get filtered and sorted mutations for a material
|
||||||
|
const getFilteredMutations = (materialId) => {
|
||||||
|
const filtered = mutations.filter(m => m.materialId === materialId);
|
||||||
|
return filtered.sort((a, b) => {
|
||||||
if (sortOrder === "asc") {
|
if (sortOrder === "asc") {
|
||||||
return new Date(a.createdAt) - new Date(b.createdAt);
|
return new Date(a.createdAt) - new Date(b.createdAt);
|
||||||
} else {
|
} else {
|
||||||
return new Date(b.createdAt) - new Date(a.createdAt);
|
return new Date(b.createdAt) - new Date(a.createdAt);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleCreateMaterial = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("name", newMaterialName);
|
|
||||||
formData.append("unit", newMaterialUnit);
|
|
||||||
if (newMaterialImage) {
|
|
||||||
formData.append("image", newMaterialImage);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await createMaterial(cafeId, formData);
|
|
||||||
setNewMaterialName("");
|
|
||||||
setNewMaterialUnit("kilogram");
|
|
||||||
setNewMaterialImage(null);
|
|
||||||
setShowForm(false);
|
|
||||||
const data = await getMaterials(cafeId);
|
|
||||||
setMaterials(data);
|
|
||||||
setError(null);
|
|
||||||
if (data.length > 0) {
|
|
||||||
setSelectedMaterialIndex(0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating material:", error);
|
|
||||||
setError("Failed to create material.");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteMaterial = async (materialId) => {
|
// Format date for display
|
||||||
setDeleting(materialId);
|
|
||||||
try {
|
|
||||||
await deleteMaterial(materialId);
|
|
||||||
const updatedMaterials = materials.filter(
|
|
||||||
(material) => material.materialId !== materialId
|
|
||||||
);
|
|
||||||
setMaterials(updatedMaterials);
|
|
||||||
setError(null);
|
|
||||||
if (selectedMaterialIndex === materialId) {
|
|
||||||
setSelectedMaterialIndex(
|
|
||||||
updatedMaterials.length > 0 ? updatedMaterials[0].materialId : null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error deleting material:", error);
|
|
||||||
setError("Failed to delete material.");
|
|
||||||
} finally {
|
|
||||||
setDeleting(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleQuantityChange = (change) => {
|
|
||||||
setQuantityChange((prev) => prev + change);
|
|
||||||
if (quantityChange + change < 1) setNewPrice(currentPrice);
|
|
||||||
|
|
||||||
setIsEditCurrentPrice(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setQuantityChange(0);
|
|
||||||
if (materials.length > 0 || selectedMaterialIndex > -1) {
|
|
||||||
const materialMutations = mutations.filter(
|
|
||||||
(mutation) => mutation.materialId === materials[selectedMaterialIndex]?.materialId
|
|
||||||
);
|
|
||||||
console.log(materialMutations)
|
|
||||||
if (materialMutations.length > 0) {
|
|
||||||
const latestMutation = materialMutations.reduce(
|
|
||||||
(latest, current) =>
|
|
||||||
new Date(current.createdAt) > new Date(latest.createdAt)
|
|
||||||
? current
|
|
||||||
: latest,
|
|
||||||
materialMutations[0]
|
|
||||||
);
|
|
||||||
setLatestMutation(latestMutation);
|
|
||||||
setCurrentQuantity(latestMutation.newStock);
|
|
||||||
setCurrentPrice(formatCurrency(latestMutation.priceAtp));
|
|
||||||
setNewPrice(formatCurrency(latestMutation.priceAtp));
|
|
||||||
} else {
|
|
||||||
setCurrentQuantity(0); // Default value if no mutations exist
|
|
||||||
setLatestMutation({ newStock: 0 });
|
|
||||||
setCurrentPrice(0);
|
|
||||||
setNewPrice(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
setIsViewingHistory(false);
|
|
||||||
}, [materials, mutations, selectedMaterialIndex]);
|
|
||||||
|
|
||||||
const handleUpdateStock = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const newprice = convertToInteger(newPrice)
|
|
||||||
const newStock = currentQuantity + quantityChange;
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("newStock", newStock);
|
|
||||||
formData.append("priceAtp", newprice);
|
|
||||||
formData.append("reason", "Stock update");
|
|
||||||
|
|
||||||
await createMaterialMutation(materials[selectedMaterialIndex].materialId, formData);
|
|
||||||
setQuantityChange(0);
|
|
||||||
const updatedMutations = await getMaterialMutations(cafeId);
|
|
||||||
setMutations(updatedMutations);
|
|
||||||
setCurrentQuantity(newStock);
|
|
||||||
setError(null);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error updating stock:", error);
|
|
||||||
setError("Failed to update stock.");
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentMaterial = materials.find(
|
|
||||||
(material) => material.materialId === selectedMaterialIndex
|
|
||||||
);
|
|
||||||
const formatDate = (timestamp) => {
|
const formatDate = (timestamp) => {
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
return date.toLocaleString();
|
return date.toLocaleString('id-ID', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Toggle material expansion
|
||||||
|
const toggleExpand = (materialId) => {
|
||||||
|
setExpandedMaterials(prevState => {
|
||||||
|
const newState = {
|
||||||
|
...prevState,
|
||||||
|
[materialId]: !prevState[materialId]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset form when expanding/collapsing
|
||||||
|
if (!prevState[materialId]) {
|
||||||
|
setNewQuantity(0);
|
||||||
|
setNewPrice("");
|
||||||
|
setEditingMaterialId(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle history view for a material
|
||||||
|
const toggleHistoryView = (materialId) => {
|
||||||
|
setIsViewingHistory(prevState => ({
|
||||||
|
...prevState,
|
||||||
|
[materialId]: !prevState[materialId]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle quantity change
|
||||||
|
const handleQuantityChange = (change) => {
|
||||||
|
setNewQuantity(prev => Math.max(0, prev + change));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading && materials.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.loading}>
|
||||||
|
<div className={styles.loadingSpinner}>↻</div>
|
||||||
|
<div>Memuat data bahan baku...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
{loading ? (
|
<div className={styles.header}>
|
||||||
<></>
|
<h2 className={styles.title}>Manajemen Bahan Baku</h2>
|
||||||
) : (
|
<button
|
||||||
<>
|
className={styles.addButton}
|
||||||
<h3 className={styles.title}>Bahan baku</h3>
|
onClick={() => setShowAddForm(!showAddForm)}
|
||||||
<Carousel items={materials} onSelect={(e) => setSelectedMaterialIndex(e)} selectedIndex={selectedMaterialIndex} />
|
>
|
||||||
{selectedMaterialIndex !== -1 ? (
|
{showAddForm ? 'Batal' : '+ Tambah Bahan Baku'}
|
||||||
<>
|
</button>
|
||||||
<div className={styles.switchContainer}>
|
</div>
|
||||||
<h3>Stok sekarang {currentQuantity}</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.stokContainer}>
|
<div className={styles.content}>
|
||||||
<button onClick={() => handleQuantityChange(currentQuantity + quantityChange > 0 ? -1 : 0)} className={styles.stockButton}>
|
{error && (
|
||||||
-
|
<div className={styles.error} style={{
|
||||||
</button>
|
padding: '12px',
|
||||||
<p>{currentQuantity + quantityChange}</p>
|
backgroundColor: '#fff0f0',
|
||||||
<button onClick={() => handleQuantityChange(1)} className={styles.stockButton}>
|
color: '#dc3545',
|
||||||
+
|
borderRadius: '8px',
|
||||||
</button>
|
marginBottom: '20px'
|
||||||
</div>
|
}}>
|
||||||
<div className={styles.uploadMessage}>
|
{error}
|
||||||
<p>harga per {materials && materials[selectedMaterialIndex]?.unit} sekarang</p>
|
</div>
|
||||||
</div>
|
)}
|
||||||
<div className={styles.resultMessage}>
|
|
||||||
<input
|
{/* Add Material Form */}
|
||||||
className={styles.resultMessageInput} // Replace inline style with CSS module class
|
{showAddForm && (
|
||||||
disabled={!isEditCurrentPrice || quantityChange < 1}
|
<div className={styles.addMaterialForm}>
|
||||||
value={newPrice}
|
<h3 className={styles.sectionTitle}>Tambah Bahan Baku Baru</h3>
|
||||||
onChange={handleChange}
|
<form onSubmit={handleCreateMaterial}>
|
||||||
placeholder="Enter amount"
|
<div className={styles.formGrid}>
|
||||||
/>
|
<div className={styles.formGroup}>
|
||||||
<div onClick={() => quantityChange < 1 ? null : setIsEditCurrentPrice(!isEditCurrentPrice)} className={quantityChange < 1 ? styles.changeButtonDisabled : styles.changeButtonEnabled}>
|
<label className={styles.formLabel}>Nama Bahan Baku</label>
|
||||||
{isEditCurrentPrice ? 'Terapkan' : 'Ganti'}
|
<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>
|
</div>
|
||||||
<div className={styles.buttonContainer}>
|
|
||||||
<button onClick={handleUpdateStock} className={styles.saveButton}>
|
<div className={styles.formActions}>
|
||||||
Laporkan {quantityChange > 0 ? 'penambahan' : 'stok sekarang'} {quantityChange < 1 ? currentQuantity + quantityChange : quantityChange} {materials[selectedMaterialIndex]?.unit}
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.historyTab}>
|
</form>
|
||||||
<h3 onClick={() => setIsViewingHistory(!isViewingHistory)}> {isViewingHistory ? '˅' : '˃'} Riwayat stok</h3>
|
</div>
|
||||||
{selectedMaterialIndex !== -1 && isViewingHistory && !loading && (
|
)}
|
||||||
<>
|
|
||||||
<div className={styles.sorter} onClick={() => setSortOrder(sortOrder == 'asc' ? 'desc' : 'asc')}>
|
{/* Materials List */}
|
||||||
Urutkan: {sortOrder === 'asc' ? "terlama" : "terbaru"} <div style={{ transform: 'rotate(90deg)' }}><></div>
|
{materials.length === 0 ? (
|
||||||
</div>
|
<div className={styles.emptyState}>
|
||||||
<div className={styles.historyContainer}>
|
<div className={styles.emptyStateIcon}>📭</div>
|
||||||
<div className={styles.mutationContainer}>
|
<div className={styles.emptyStateText}>Belum ada bahan baku yang terdaftar</div>
|
||||||
{sortedMutations.length > 0 ? (
|
<button
|
||||||
sortedMutations.map((mutation) => (
|
className={styles.addButton}
|
||||||
<div key={mutation.id} className={styles.mutationCard}>
|
onClick={() => setShowAddForm(true)}
|
||||||
<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">
|
+ Tambah Bahan Baku Pertama
|
||||||
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
</button>
|
||||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
</div>
|
||||||
<g id="SVGRepo_iconCarrier">
|
) : (
|
||||||
<g id="Interface / Book">
|
<div className={styles.materialsList}>
|
||||||
<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>
|
{materials.map((material) => (
|
||||||
</g>
|
<div key={material.materialId} className={styles.materialCard}>
|
||||||
</g>
|
{/* Material Summary (Always Visible) */}
|
||||||
</svg>
|
<div
|
||||||
</div>
|
className={styles.materialSummary}
|
||||||
<div className={styles.mutationTitle}>
|
onClick={() => toggleExpand(material.materialId)}
|
||||||
<h4>{formatDate(mutation.createdAt)}</h4>
|
>
|
||||||
<p>Total stok: {mutation.newStock} || +{mutation.newStock - mutation.oldStock} || {formatCurrency((mutation.newStock - mutation.oldStock) * mutation.priceAtp)}</p>
|
<h3 className={styles.materialName}>{material.name}</h3>
|
||||||
</div>
|
<div className={styles.materialStock}>
|
||||||
</div>
|
{material.currentStock} {material.unit}
|
||||||
))
|
</div>
|
||||||
) : (
|
<div className={`${styles.expandIcon} ${expandedMaterials[material.materialId] ? styles.expanded : ''}`}>
|
||||||
<p>Tidak ada laporan perubahan stok.</p>
|
▼
|
||||||
)}
|
</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>
|
</div>
|
||||||
</>
|
|
||||||
|
<div className={styles.detailActions}>
|
||||||
|
<button
|
||||||
|
className={styles.detailButton}
|
||||||
|
onClick={() => {
|
||||||
|
setEditingMaterialId(material.materialId);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✏️ Update Stok
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.detailButton}
|
||||||
|
onClick={() => handleDeleteMaterial(material.materialId)}
|
||||||
|
>
|
||||||
|
🗑️ Hapus
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stock Update Form */}
|
||||||
|
{editingMaterialId === material.materialId && (
|
||||||
|
<div className={styles.stockUpdateForm}>
|
||||||
|
<h4 className={styles.formTitle}>Update Stok</h4>
|
||||||
|
|
||||||
|
<div className={styles.quantityControls}>
|
||||||
|
<button
|
||||||
|
className={styles.quantityButton}
|
||||||
|
onClick={() => handleQuantityChange(-1)}
|
||||||
|
disabled={newQuantity <= 0}
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
<div className={styles.quantityDisplay}>
|
||||||
|
{newQuantity > 0 ? `+${newQuantity}` : newQuantity}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className={styles.quantityButton}
|
||||||
|
onClick={() => handleQuantityChange(1)}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.formGroup}>
|
||||||
|
<label className={styles.formLabel}>Harga per {material.unit}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.formInput}
|
||||||
|
value={newPrice}
|
||||||
|
onChange={handlePriceChange}
|
||||||
|
placeholder="Masukkan harga"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.formActions}>
|
||||||
|
<button
|
||||||
|
className={styles.cancelButton}
|
||||||
|
onClick={() => {
|
||||||
|
setEditingMaterialId(null);
|
||||||
|
setNewQuantity(0);
|
||||||
|
setNewPrice("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.saveButton}
|
||||||
|
onClick={() => handleUpdateStock(material.materialId)}
|
||||||
|
disabled={newQuantity === 0 && !convertToInteger(newPrice)}
|
||||||
|
>
|
||||||
|
Simpan Perubahan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* History Section */}
|
||||||
|
<div className={styles.historySection}>
|
||||||
|
<h4
|
||||||
|
className={styles.historyTitle}
|
||||||
|
onClick={() => toggleHistoryView(material.materialId)}
|
||||||
|
>
|
||||||
|
{isViewingHistory[material.materialId] ? '▲' : '▼'} Riwayat Stok
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{isViewingHistory[material.materialId] && (
|
||||||
|
<>
|
||||||
|
<div className={styles.sortControls}>
|
||||||
|
<button
|
||||||
|
className={styles.sortButton}
|
||||||
|
onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}
|
||||||
|
>
|
||||||
|
Urutkan: {sortOrder === 'asc' ? 'Terlama' : 'Terbaru'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.historyItems}>
|
||||||
|
{getFilteredMutations(material.materialId).length > 0 ? (
|
||||||
|
getFilteredMutations(material.materialId).map((mutation) => (
|
||||||
|
<div key={mutation.id} className={styles.historyItem}>
|
||||||
|
<div className={styles.historyIcon}>📊</div>
|
||||||
|
<div className={styles.historyContent}>
|
||||||
|
<div className={styles.historyDate}>{formatDate(mutation.createdAt)}</div>
|
||||||
|
<div className={styles.historyDetails}>
|
||||||
|
Stok: {mutation.newStock} {material.unit} |
|
||||||
|
Perubahan: {mutation.newStock - mutation.oldStock > 0 ? '+' : ''}{mutation.newStock - mutation.oldStock} |
|
||||||
|
Harga: Rp {formatCurrency(mutation.priceAtp)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className={styles.emptyState} style={{ padding: '20px' }}>
|
||||||
|
<div>Belum ada riwayat perubahan stok</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
))}
|
||||||
) : (
|
</div>
|
||||||
<>
|
)}
|
||||||
<div className={styles.description}>
|
</div>
|
||||||
<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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SetPaymentQr;
|
export default MaterialList;
|
||||||
@@ -1,149 +1,556 @@
|
|||||||
/* SetPaymentQr.module.css */
|
/* MaterialList.module.css */
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 47vh;
|
background-color: #ffffff;
|
||||||
background-color: white;
|
border-radius: 16px;
|
||||||
padding: 20px;
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
border-radius: 8px;
|
padding: 0;
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
margin-bottom: 24px;
|
||||||
text-align: center;
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
z-index: 200; /* Menaikkan z-index agar berada di atas cartbar */
|
||||||
|
max-height: 70vh; /* Membatasi tinggi maksimum */
|
||||||
|
overflow-y: auto; /* Memungkinkan scrolling vertikal */
|
||||||
|
}
|
||||||
|
|
||||||
|
.container:hover {
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 24px;
|
||||||
|
border-bottom: 1px solid #e6e6e6;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background-color: #ffffff;
|
||||||
|
z-index: 201; /* Memastikan header berada di atas container */
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addButton {
|
||||||
|
background-color: #28a745;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
box-shadow: 0 2px 6px rgba(40, 167, 69, 0.2);
|
||||||
|
position: relative;
|
||||||
|
z-index: 202; /* Memastikan tombol berada di atas header */
|
||||||
|
}
|
||||||
|
|
||||||
|
.addButton:hover {
|
||||||
|
background-color: #218838;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 10px rgba(40, 167, 69, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.addButton:disabled {
|
||||||
|
background-color: #a1a1a1;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 24px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Materials List Styles */
|
||||||
|
.materialsList {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.materialCard {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
position: relative;
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.materialCard:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
border-color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.materialSummary {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
z-index: 201;
|
||||||
|
}
|
||||||
|
|
||||||
|
.materialName {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.materialStock {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0 16px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandIcon {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #28a745;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandIcon.expanded {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Material Detail Styles */
|
||||||
|
.materialDetail {
|
||||||
|
padding: 0 20px 20px 20px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 0 0 12px 12px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailItem {
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailLabel {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailValue {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailActions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailButton {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: none;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.updateButton {
|
||||||
|
background-color: #28a745;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 6px rgba(40, 167, 69, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.updateButton:hover {
|
||||||
|
background-color: #218838;
|
||||||
|
box-shadow: 0 4px 10px rgba(40, 167, 69, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteButton {
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 6px rgba(220, 53, 69, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteButton:hover {
|
||||||
|
background-color: #c82333;
|
||||||
|
box-shadow: 0 4px 10px rgba(220, 53, 69, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stock Update Form */
|
||||||
|
.stockUpdateForm {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
position: relative;
|
||||||
|
z-index: 201;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formTitle {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantityControls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantityButton {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: #ffffff;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantityButton:hover {
|
||||||
|
border-color: #28a745;
|
||||||
|
background: #f0f8f1;
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantityDisplay {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
min-width: 80px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroup {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formLabel {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formInput {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formInput:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #28a745;
|
||||||
|
box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formActions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formButton {
|
||||||
|
flex: 1;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: none;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saveButton {
|
||||||
|
background-color: #28a745;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 6px rgba(40, 167, 69, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.saveButton:hover {
|
||||||
|
background-color: #218838;
|
||||||
|
box-shadow: 0 4px 10px rgba(40, 167, 69, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancelButton {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
color: #333;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancelButton:hover {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add Material Form */
|
||||||
|
.addMaterialForm {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
margin-top: 24px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 201;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formSection {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullWidth {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.emptyState {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyStateIcon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyStateText {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* History Section */
|
||||||
|
.historySection {
|
||||||
|
margin-top: 24px;
|
||||||
|
border-top: 1px solid #e6e6e6;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyTitle {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyTitle:hover {
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyItems {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
max-height: 250px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyItems::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyItems::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyItems::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyIcon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
background: #b9b9b9;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyContent {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyDate {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0 0 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyDetails {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortControls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortButton {
|
||||||
|
background: #f0f0f0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #555;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortButton:hover {
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading State */
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadingSpinner {
|
||||||
|
font-size: 32px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header {
|
||||||
|
padding: 16px;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.content {
|
||||||
margin-bottom: 20px;
|
padding: 16px;
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.uploadMessage {
|
.detailGrid {
|
||||||
font-weight: 600;
|
grid-template-columns: 1fr;
|
||||||
text-align: left;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.changeButtonEnabled {
|
.formGrid {
|
||||||
padding-right: 10px;
|
grid-template-columns: 1fr;
|
||||||
background-color: green;
|
gap: 16px;
|
||||||
border-radius: 30px;
|
|
||||||
color: white;
|
|
||||||
font-weight: 700;
|
|
||||||
height: 36px;
|
|
||||||
line-height: 36px;
|
|
||||||
padding-left: 10px;
|
|
||||||
padding-height: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.changeButtonDisabled {
|
.historyItems {
|
||||||
padding-right: 10px;
|
max-height: 200px;
|
||||||
background-color: #a1a1a1;
|
|
||||||
border-radius: 30px;
|
|
||||||
color: white;
|
|
||||||
font-weight: 700;
|
|
||||||
height: 36px;
|
|
||||||
line-height: 36px;
|
|
||||||
padding-left: 10px;
|
|
||||||
padding-height: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.resultMessage {
|
.materialSummary {
|
||||||
margin-top: -13px;
|
padding: 14px 16px;
|
||||||
text-align: left;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.resultMessage input {
|
.materialName {
|
||||||
padding-left: 8px;
|
font-size: 15px;
|
||||||
width: 180px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stokContainer {
|
.materialStock {
|
||||||
display: flex;
|
font-size: 13px;
|
||||||
justify-content: space-evenly;
|
margin-right: 12px;
|
||||||
align-items: center;
|
|
||||||
margin-top: -20px;
|
|
||||||
margin-bottom: -15px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonContainer {
|
.container {
|
||||||
margin-top: 11px;
|
max-height: 75vh; /* Menaikkan tinggi maksimum pada mobile */
|
||||||
text-align: left;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.stockButton {
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-size: 3.5vw;
|
|
||||||
background-color: #28a745;
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 30px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.saveButton {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-size: 3.5vw;
|
|
||||||
background-color: #28a745;
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 30px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switchContainer {
|
|
||||||
margin-top: 20px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.historyTab {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.historyContainer {
|
|
||||||
text-align: left;
|
|
||||||
max-height: 15vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
display: flex;
|
|
||||||
margin: 10px 0;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unit {
|
|
||||||
margin-top: 11px;
|
|
||||||
width: 100%;
|
|
||||||
height: 31px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sorter {
|
|
||||||
border: 1px solid #c3c3c3;
|
|
||||||
padding: 5px;
|
|
||||||
border-radius: 10px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: -10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mutationCard {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 7px;
|
|
||||||
margin-top: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mutationTitle {
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
.mutationTitle h4 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mutationTitle p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
76
src/pages/StickyCartBar.module.css
Normal file
76
src/pages/StickyCartBar.module.css
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/* StickyCartBar.module.css */
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 40px;
|
||||||
|
z-index: 120; /* above items, below modal */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
pointer-events: none; /* allow buttons to define interaction */
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 980px;
|
||||||
|
padding: 0 12px;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainBtn {
|
||||||
|
flex: 1;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--brand-primary, #73a585);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 6px 18px rgba(115,165,133,0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cartIcon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyBtn {
|
||||||
|
width: 48px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--brand-primary, #73a585);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 6px 18px rgba(115,165,133,0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.mainBtn { height: 42px; }
|
||||||
|
.historyBtn { height: 42px; width: 46px; }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -30,6 +30,15 @@ export default function Transactions({ shop, shopId, propsShopId, sendParam, dev
|
|||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
setMatchedItems(searchAndAggregateItems(transactions, searchTerm));
|
setMatchedItems(searchAndAggregateItems(transactions, searchTerm));
|
||||||
}, [searchTerm, transactions]);
|
}, [searchTerm, transactions]);
|
||||||
@@ -154,7 +163,7 @@ console.log(aggregatedItems.values())
|
|||||||
return (
|
return (
|
||||||
<div className={styles.Transactions}>
|
<div className={styles.Transactions}>
|
||||||
<h2 className={styles["Transactions-title"]}>
|
<h2 className={styles["Transactions-title"]}>
|
||||||
Transaksi selesai Rp {calculateAllTransactionsTotal(transactions)}
|
Transaksi selesai {formatRp(calculateAllTransactionsTotal(transactions))}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
@@ -162,13 +171,13 @@ console.log(aggregatedItems.values())
|
|||||||
placeholder="Cari nama item..."
|
placeholder="Cari nama item..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
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) */}
|
{/* 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 => (
|
{matchedItems.length > 0 && matchedItems.map(item => (
|
||||||
<div
|
<div
|
||||||
@@ -183,7 +192,7 @@ console.log(aggregatedItems.values())
|
|||||||
</ul>
|
</ul>
|
||||||
<div className={styles.TotalContainer}>
|
<div className={styles.TotalContainer}>
|
||||||
<span>Total:</span>
|
<span>Total:</span>
|
||||||
<span>Rp {item.totalPrice}</span>
|
<span>{formatRp(item.totalPrice)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -191,94 +200,60 @@ console.log(aggregatedItems.values())
|
|||||||
transactions.map((transaction) => (
|
transactions.map((transaction) => (
|
||||||
<div
|
<div
|
||||||
key={transaction.transactionId}
|
key={transaction.transactionId}
|
||||||
className={styles.RoundedRectangle}
|
className={`${styles.RoundedRectangle} ${!transaction.is_paid ? styles.unpaid : ''}`}
|
||||||
style={{ overflow: 'hidden' }}
|
|
||||||
>
|
>
|
||||||
|
|
||||||
<div className={styles['receipt-header']}>
|
<div className={styles['receipt-header']}>
|
||||||
{transaction.confirmed === 1 ? (
|
{transaction.confirmed === 1 ? (
|
||||||
<ColorRing className={styles['receipt-logo']} />
|
<ColorRing className={styles['receipt-logo']} />
|
||||||
) : transaction.confirmed === -1 && !transaction.is_paid || transaction.confirmed === -2 && !transaction.is_paid ? (
|
) : (transaction.confirmed === -1 && !transaction.is_paid) || (transaction.confirmed === -2 && !transaction.is_paid) ? (
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', margin: '16px 0px' }}>
|
<div style={{ display: 'flex', justifyContent: 'center', margin: '12px 0' }}>
|
||||||
<svg
|
<svg width="60" height="60" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
style={{ width: '60px', transform: 'Rotate(45deg)' }}
|
<path d="M18.3 5.7a1 1 0 0 0-1.4 0L12 10.6 7.1 5.7A1 1 0 0 0 5.7 7.1L10.6 12l-4.9 4.9a1 1 0 1 0 1.4 1.4L12 13.4l4.9 4.9a1 1 0 0 0 1.4-1.4L13.4 12l4.9-4.9a1 1 0 0 0 0-1.4z" fill="#E45454"/>
|
||||||
clipRule="evenodd"
|
|
||||||
fillRule="evenodd"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeMiterlimit="2"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="m12.002 2c5.518 0 9.998 4.48 9.998 9.998 0 5.517-4.48 9.997-9.998 9.997-5.517 0-9.997-4.48-9.997-9.997 0-5.518 4.48-9.998 9.997-9.998zm0 1.5c-4.69 0-8.497 3.808-8.497 8.498s3.807 8.497 8.497 8.497 8.498-3.807 8.498-8.497-3.808-8.498-8.498-8.498zm-.747 7.75h-3.5c-.414 0-.75.336-.75.75s.336.75.75.75h3.5v3.5c0 .414.336.75.75.75s.75-.336.75-.75v-3.5h3.5c.414 0 .75-.336.75-.75s-.336-.75-.75-.75h-3.5v-3.5c0-.414-.336-.75-.75-.75s-.75.336-.75.75z"
|
|
||||||
fillRule="nonzero"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
) : transaction.confirmed === 2 && !transaction.is_paid ? (
|
) : transaction.confirmed === 2 && !transaction.is_paid ? (
|
||||||
<ColorRing className={styles['receipt-logo']} />
|
<ColorRing className={styles['receipt-logo']} />
|
||||||
) : transaction.confirmed === 3 || transaction.is_paid ? (
|
) : transaction.confirmed === 3 || transaction.is_paid ? (
|
||||||
<div>
|
<div style={{ display: 'flex', justifyContent: 'center', margin: '12px 0' }}>
|
||||||
<svg
|
<svg width="60" height="60" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
height="60px"
|
<path d="M9 16.2 4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4z" fill="#54B265"/>
|
||||||
width="60px"
|
|
||||||
version="1.1"
|
|
||||||
id="Layer_1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
|
||||||
viewBox="0 0 506.4 506.4"
|
|
||||||
xmlSpace="preserve"
|
|
||||||
fill="#000000"
|
|
||||||
style={{ marginTop: '12px', marginBottom: '12px' }}
|
|
||||||
>
|
|
||||||
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
|
|
||||||
<g id="SVGRepo_tracerCarrier" strokeLinecap="round" strokeLinejoin="round"></g>
|
|
||||||
<g id="SVGRepo_iconCarrier">
|
|
||||||
<circle style={{ fill: '#54B265' }} cx="253.2" cy="253.2" r="249.2" />
|
|
||||||
<path
|
|
||||||
style={{ fill: '#F4EFEF' }}
|
|
||||||
d="M372.8,200.4l-11.2-11.2c-4.4-4.4-12-4.4-16.4,0L232,302.4l-69.6-69.6c-4.4-4.4-12-4.4-16.4,0 L134.4,244c-4.4,4.4-4.4,12,0,16.4l89.2,89.2c4.4,4.4,12,4.4,16.4,0l0,0l0,0l10.4-10.4l0.8-0.8l121.6-121.6 C377.2,212.4,377.2,205.2,372.8,200.4z"
|
|
||||||
></path>
|
|
||||||
<path d="M253.2,506.4C113.6,506.4,0,392.8,0,253.2S113.6,0,253.2,0s253.2,113.6,253.2,253.2S392.8,506.4,253.2,506.4z M253.2,8 C118,8,8,118,8,253.2s110,245.2,245.2,245.2s245.2-110,245.2-245.2S388.4,8,253.2,8z"></path>
|
|
||||||
<path d="M231.6,357.2c-4,0-8-1.6-11.2-4.4l-89.2-89.2c-6-6-6-16,0-22l11.6-11.6c6-6,16.4-6,22,0l66.8,66.8L342,186.4 c2.8-2.8,6.8-4.4,11.2-4.4c4,0,8,1.6,11.2,4.4l11.2,11.2l0,0c6,6,6,16,0,22L242.8,352.4C239.6,355.6,235.6,357.2,231.6,357.2z M154,233.6c-2,0-4,0.8-5.6,2.4l-11.6,11.6c-2.8,2.8-2.8,8,0,10.8l89.2,89.2c2.8,2.8,8,2.8,10.8,0l132.8-132.8c2.8-2.8,2.8-8,0-10.8 l-11.2-11.2c-2.8-2.8-8-2.8-10.8,0L234.4,306c-1.6,1.6-4,1.6-5.6,0l-69.6-69.6C158,234.4,156,233.6,154,233.6z"></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
||||||
<ColorRing className={styles['receipt-logo']} />
|
<ColorRing className={styles['receipt-logo']} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
<div className={styles['receipt-info']}>
|
<div className={styles['receipt-info']}>
|
||||||
{deviceType == 'clerk' ?
|
{deviceType == 'clerk' ?
|
||||||
<h3>{transaction.confirmed === 1 && !transaction.is_paid ? (
|
<h3 className={getStatusClass(transaction)}>{transaction.confirmed === 1 && !transaction.is_paid ? (
|
||||||
"Silahkan Cek Pembayaran"
|
"Silahkan Cek Pembayaran"
|
||||||
) : transaction.confirmed === -1 && !transaction.is_paid ? (
|
) : transaction.confirmed === -1 && !transaction.is_paid ? (
|
||||||
"Dibatalkan Oleh Kasir"
|
"Dibatalkan Oleh Kasir"
|
||||||
) : transaction.confirmed === -2 && !transaction.is_paid ? (
|
) : transaction.confirmed === -2 && !transaction.is_paid ? (
|
||||||
"Dibatalkan Oleh Pelanggan"
|
"Dibatalkan Oleh Pelanggan"
|
||||||
) : transaction.confirmed === 2 && !transaction.is_paid ? (
|
) : transaction.confirmed === 2 && !transaction.is_paid ? (
|
||||||
"Sedang Diproses"
|
"Sedang Diproses"
|
||||||
) : transaction.confirmed === 3 || transaction.is_paid ? (
|
) : transaction.confirmed === 3 || transaction.is_paid ? (
|
||||||
"Transaksi Sukses"
|
"Transaksi Sukses"
|
||||||
) : (
|
) : (
|
||||||
"Silahkan Cek Ketersediaan"
|
"Silahkan Cek Ketersediaan"
|
||||||
)}</h3>
|
)}</h3>
|
||||||
:
|
:
|
||||||
<h3>{transaction.confirmed === 1 ? (
|
<h3 className={getStatusClass(transaction)}>{transaction.confirmed === 1 ? (
|
||||||
(transaction.payment_type == 'cash' ? 'Silahkan Bayar Ke Kasir' : "Silahkan Bayar Dengan QRIS")
|
(transaction.payment_type == 'cash' ? 'Silahkan Bayar Ke Kasir' : "Silahkan Bayar Dengan QRIS")
|
||||||
) : transaction.confirmed === -1 ? (
|
) : transaction.confirmed === -1 ? (
|
||||||
"Dibatalkan Oleh Kasir"
|
"Dibatalkan Oleh Kasir"
|
||||||
) : transaction.confirmed === -2 ? (
|
) : transaction.confirmed === -2 ? (
|
||||||
"Dibatalkan Oleh Pelanggan"
|
"Dibatalkan Oleh Pelanggan"
|
||||||
) : transaction.confirmed === 2 ? (
|
) : transaction.confirmed === 2 ? (
|
||||||
"Sedang diproses"
|
"Sedang diproses"
|
||||||
) : transaction.confirmed === 3 ? (
|
) : transaction.confirmed === 3 ? (
|
||||||
"Transaksi Sukses"
|
"Transaksi Sukses"
|
||||||
) : (
|
) : (
|
||||||
"Memeriksa Ketersediaan "
|
"Memeriksa Ketersediaan "
|
||||||
)}</h3>}
|
)}</h3>}
|
||||||
|
|
||||||
<p>Transaction ID: {transaction.transactionId}</p>
|
<p>Transaction ID: {transaction.transactionId}</p>
|
||||||
<p>Payment Type: {transaction.payment_type}</p>
|
<p>Payment Type: {transaction.payment_type}</p>
|
||||||
@@ -304,12 +279,11 @@ console.log(aggregatedItems.values())
|
|||||||
<ul>
|
<ul>
|
||||||
{transaction.DetailedTransactions.map((detail) => (
|
{transaction.DetailedTransactions.map((detail) => (
|
||||||
<li key={detail.detailedTransactionId}>
|
<li key={detail.detailedTransactionId}>
|
||||||
<span>{detail.Item.name}</span> - {detail.qty < 1 ? 'tidak tersedia' : `${detail.qty} x Rp
|
<span>{detail.Item.name}</span> - {detail.qty < 1 ? 'tidak tersedia' : `${detail.qty} x ${formatRp(detail.promoPrice ? detail.promoPrice : detail.price)}`}
|
||||||
${detail.promoPrice ? detail.promoPrice : detail.price}`}
|
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
{!transaction.is_paid && transaction.confirmed > -1 &&
|
{!transaction.is_paid && transaction.confirmed > -1 && (
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
localStorage.setItem('lastTransaction', JSON.stringify(transaction));
|
localStorage.setItem('lastTransaction', JSON.stringify(transaction));
|
||||||
@@ -322,12 +296,11 @@ console.log(aggregatedItems.values())
|
|||||||
>
|
>
|
||||||
Tambah pesanan
|
Tambah pesanan
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
<h2 className={styles["Transactions-detail"]}>
|
<h2 className={styles["Transactions-detail"]}>
|
||||||
{transaction.serving_type === "pickup"
|
{transaction.serving_type === "pickup"
|
||||||
? "Self pickup"
|
? "Self pickup"
|
||||||
: `Serve to ${transaction.Table ? transaction.Table.tableNo : "N/A"
|
: `Serve to ${transaction.Table ? transaction.Table.tableNo : "N/A"}`}
|
||||||
}`}
|
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{transaction.notes && (
|
{transaction.notes && (
|
||||||
@@ -350,38 +323,44 @@ console.log(aggregatedItems.values())
|
|||||||
<div className={styles.TotalContainer}>
|
<div className={styles.TotalContainer}>
|
||||||
<span>Total:</span>
|
<span>Total:</span>
|
||||||
<span>
|
<span>
|
||||||
Rp {calculateTotalPrice(transaction.DetailedTransactions)}
|
{formatRp(calculateTotalPrice(transaction.DetailedTransactions))}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.TotalContainer}>
|
<div className={styles.TotalContainer}>
|
||||||
{(deviceType == 'clerk' && !transaction.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)) && (
|
||||||
<button
|
<div className={styles.ActionRow}>
|
||||||
className={styles.PayButton}
|
<button
|
||||||
onClick={() => handleConfirm(transaction.transactionId)}
|
className={styles.PayButton}
|
||||||
disabled={isPaymentLoading}
|
onClick={() => handleConfirm(transaction.transactionId)}
|
||||||
>
|
disabled={isPaymentLoading}
|
||||||
{
|
>
|
||||||
isPaymentLoading ? (
|
{
|
||||||
<ColorRing height="50" width="50" color="white" />
|
isPaymentLoading ? (
|
||||||
) : transaction.confirmed === 1 ? (
|
<ColorRing height="28" width="28" color="white" />
|
||||||
"Konfirmasi Telah Bayar"
|
) : transaction.confirmed === 1 ? (
|
||||||
) : transaction.confirmed === 2 ? (
|
"Konfirmasi"
|
||||||
"Confirm item is ready"
|
) : transaction.confirmed === 2 ? (
|
||||||
) : (
|
"Confirm item is ready"
|
||||||
"Confirm availability"
|
) : (
|
||||||
)
|
"Confirm availability"
|
||||||
}
|
)
|
||||||
|
}
|
||||||
</button>
|
</button>
|
||||||
}
|
<button
|
||||||
|
className={styles.DeclineButton}
|
||||||
|
onClick={() => handleDecline(transaction.transactionId)}
|
||||||
|
disabled={isPaymentLoading}
|
||||||
|
>
|
||||||
|
{isPaymentLoading ? '...' : 'Batalkan'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{deviceType == 'guestDevice' && transaction.confirmed < 2 && transaction.payment_type != 'cash' && transaction.payment_type != 'paylater/cash' &&
|
{deviceType == 'guestDevice' && transaction.confirmed < 2 && transaction.payment_type != 'cash' && transaction.payment_type != 'paylater/cash' &&
|
||||||
<ButtonWithReplica
|
<ButtonWithReplica
|
||||||
paymentUrl={paymentUrl}
|
paymentUrl={paymentUrl}
|
||||||
price={
|
price={formatRp(calculateTotalPrice(transaction.DetailedTransactions))}
|
||||||
"Rp" + calculateTotalPrice(transaction.DetailedTransactions)
|
|
||||||
}
|
|
||||||
disabled={isPaymentLoading}
|
disabled={isPaymentLoading}
|
||||||
isPaymentLoading={isPaymentLoading}
|
isPaymentLoading={isPaymentLoading}
|
||||||
handleClick={() => handleConfirm(transaction.transactionId)}
|
handleClick={() => handleConfirm(transaction.transactionId)}
|
||||||
@@ -404,40 +383,30 @@ console.log(aggregatedItems.values())
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{deviceType == 'guestDevice' && transaction.confirmed >= 0 && transaction.confirmed < 2 && transaction.payment_type == 'cash' ?
|
{deviceType == 'guestDevice' && (
|
||||||
<button
|
transaction.confirmed >= 0 && transaction.confirmed < 2 && transaction.payment_type == 'cash' ? (
|
||||||
className={styles.PayButton}
|
<button
|
||||||
onClick={() => handleDecline(transaction.transactionId)}
|
className={styles.DeclineButton}
|
||||||
disabled={
|
onClick={() => handleDecline(transaction.transactionId)}
|
||||||
transaction.confirmed === -1 ||
|
disabled={
|
||||||
transaction.confirmed === 3 ||
|
transaction.confirmed === -1 ||
|
||||||
isPaymentLoading
|
transaction.confirmed === 3 ||
|
||||||
} // Disable button if confirmed (1) or declined (-1) or
|
isPaymentLoading
|
||||||
>
|
|
||||||
{isPaymentLoading ? (
|
|
||||||
<ColorRing height="50" width="50" color="white" />
|
|
||||||
) : transaction.confirmed === -1 ? (
|
|
||||||
"Ditolak" // Display "Declined" if the transaction is declined (-1)
|
|
||||||
) : transaction.confirmed === -2 ? (
|
|
||||||
"Dibatalkan" // Display "Declined" if the transaction is declined (-1)
|
|
||||||
) : (
|
|
||||||
"Batalkan" // Display "Confirm availability" if the transaction is not confirmed (0)
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
:
|
|
||||||
((transaction.confirmed >= 0 && transaction.confirmed < 2 && transaction.payment_type != 'paylater/cash' && transaction.payment_type != 'paylater/cashless' || isPaymentOpen) &&
|
|
||||||
<h5
|
|
||||||
className={`${styles.DeclineButton}`}
|
|
||||||
onClick={() =>
|
|
||||||
isPaymentOpen
|
|
||||||
? setIsPaymentOpen(false)
|
|
||||||
: handleDecline(transaction.transactionId)
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isPaymentOpen ? "kembali" : "batalkan"}
|
{isPaymentLoading ? '...' : 'Batalkan'}
|
||||||
</h5>
|
</button>
|
||||||
|
) : (
|
||||||
|
(transaction.confirmed >= 0 && transaction.confirmed < 2 && (transaction.payment_type != 'paylater/cash' && transaction.payment_type != 'paylater/cashless' || isPaymentOpen)) && (
|
||||||
|
<button
|
||||||
|
className={styles.DeclineButton}
|
||||||
|
onClick={() => isPaymentOpen ? setIsPaymentOpen(false) : handleDecline(transaction.transactionId)}
|
||||||
|
>
|
||||||
|
{isPaymentOpen ? 'Kembali' : 'Batalkan'}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,41 +13,43 @@
|
|||||||
}
|
}
|
||||||
.Transactions {
|
.Transactions {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
background-color: white;
|
background-color: #f5f7f6;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
font-size: calc(10px + 2vmin);
|
font-size: calc(10px + 2vmin);
|
||||||
color: rgba(88, 55, 50, 1);
|
color: rgba(40, 40, 40, 1);
|
||||||
background-color: #e9e9e9;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.Transactions-title {
|
.Transactions-title {
|
||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
font-weight: 500;
|
font-weight: 700;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-size: 6vw;
|
font-size: clamp(18px, 3.6vw, 24px);
|
||||||
color: black;
|
color: var(--brand-sage, #6B8F71);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
margin-left: 20px;
|
margin: 16px 16px 8px 16px;
|
||||||
margin-top: 57px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.Transactions-detail {
|
.Transactions-detail {
|
||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-size: 15px;
|
font-size: 13px;
|
||||||
color: rgba(88, 55, 50, 1);
|
color: #555;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
margin-left: 20px;
|
margin-left: 16px;
|
||||||
margin-top: 17px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.TransactionListContainer {
|
.TransactionListContainer {
|
||||||
overflow-y: auto; /* Enables vertical scrolling */
|
overflow-y: auto; /* Enables vertical scrolling */
|
||||||
background-color: #dbdbdb;
|
background-color: transparent;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
padding: 8px 12px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.TotalContainer {
|
.TotalContainer {
|
||||||
@@ -57,14 +59,14 @@
|
|||||||
/* width: 100%; */
|
/* width: 100%; */
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-size: 15px;
|
font-size: 13px;
|
||||||
/* padding: 10px; */
|
/* padding: 10px; */
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin-bottom: 17px;
|
margin-bottom: 12px;
|
||||||
margin-left: 20px;
|
margin-left: 16px;
|
||||||
margin-right: 20px;
|
margin-right: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.PaymentContainer {
|
.PaymentContainer {
|
||||||
@@ -83,61 +85,66 @@
|
|||||||
|
|
||||||
.PayButton {
|
.PayButton {
|
||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-size: 12px; /* Adjusted for better readability */
|
font-size: 12px;
|
||||||
padding: 12px 16px; /* Added padding for a better look */
|
padding: 10px 14px;
|
||||||
border-radius: 50px;
|
border-radius: 10px; /* square rounded */
|
||||||
background-color: rgba(88, 55, 50, 1);
|
background-color: var(--brand-primary, #73a585);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
margin: 0 auto;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: block; /* Centering the button */
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.DeclineButton {
|
.DeclineButton {
|
||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
z-index: 201;
|
font-weight: 600;
|
||||||
position: relative;
|
|
||||||
font-weight: 500;
|
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-size: 15px;
|
font-size: 12px;
|
||||||
padding: 12px 24px; /* Add some padding for spacing */
|
padding: 10px 14px;
|
||||||
color: rgba(88, 55, 50, 1);
|
color: #444;
|
||||||
background-color: transparent; /* No background */
|
background-color: #f0f0f0;
|
||||||
border: none; /* No border */
|
border: 1px solid #e0e0e0;
|
||||||
margin: 0 auto; /* Center horizontally */
|
border-radius: 10px; /* square rounded */
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: block; /* Center the text horizontally */
|
display: inline-flex;
|
||||||
text-align: center; /* Center the text within the button */
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.DeclineButton.active {
|
.DeclineButton.active { opacity: 0.9; }
|
||||||
position: relative;
|
|
||||||
z-index: 201;
|
/* Row for primary/secondary action buttons */
|
||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
.ActionRow {
|
||||||
font-weight: 500;
|
display: flex;
|
||||||
font-style: normal;
|
gap: 8px;
|
||||||
font-size: 20px;
|
width: 100%;
|
||||||
padding: 12px 24px; /* Add some padding for spacing */
|
justify-content: center;
|
||||||
color: rgba(88, 55, 50, 1);
|
|
||||||
background-color: transparent; /* No background */
|
|
||||||
border: none; /* No border */
|
|
||||||
margin: 0 auto; /* Center horizontally */
|
|
||||||
cursor: pointer;
|
|
||||||
display: block; /* Center the text horizontally */
|
|
||||||
text-align: center; /* Center the text within the button */
|
|
||||||
margin-bottom: 23px; /* Space at the bottom to match the PayButton */
|
|
||||||
}
|
}
|
||||||
.RoundedRectangle {
|
.RoundedRectangle {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 20px;
|
border-radius: 16px;
|
||||||
padding: 15px; /* Adjusted for better spacing */
|
padding: 12px 12px 10px;
|
||||||
margin: 12px;
|
margin: 0;
|
||||||
background-color: #f9f9f9;
|
background-color: #ffffff;
|
||||||
|
border: 1px solid var(--brand-sage-100, #E9F3ED);
|
||||||
|
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.06);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 280px; /* base height, grows with content */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Unpaid tickets: taller to keep content inside ticket design */
|
||||||
|
.unpaid {
|
||||||
|
min-height: 380px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Self pickup accent: subtle brand accent */
|
||||||
|
/* removed pickup accent and badge per request */
|
||||||
|
|
||||||
.expression {
|
.expression {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -201,20 +208,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.receipt-logo {
|
.receipt-logo {
|
||||||
width: 80px;
|
width: 60px;
|
||||||
height: 80px;
|
height: 60px;
|
||||||
border-radius: 50%; /* Circular logo */
|
border-radius: 50%; /* Circular logo */
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.receipt-info h3 {
|
.receipt-info h3 {
|
||||||
font-size: 16px;
|
font-size: 13px;
|
||||||
margin: 5px 0;
|
font-weight: 700;
|
||||||
|
color: var(--brand-sage, #6B8F71);
|
||||||
|
margin: 4px 0 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Status-colors override */
|
||||||
|
.receipt-info h3.statusNeutral { color: var(--brand-sage, #6B8F71); }
|
||||||
|
.receipt-info h3.statusSuccess { color: #54B265; }
|
||||||
|
.receipt-info h3.statusCancelled { color: #E45454; }
|
||||||
|
|
||||||
.receipt-info p {
|
.receipt-info p {
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
margin: 2px 0;
|
margin: 2px 0;
|
||||||
}
|
}
|
||||||
/* Dotted line with circular cutouts */
|
/* Dotted line with circular cutouts */
|
||||||
@@ -222,39 +237,61 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 15px 0;
|
margin: 12px 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dotted-line .line {
|
.dotted-line .line {
|
||||||
border-top: 13px dotted #dbdbdb;
|
border-top: 10px dotted #e9e9e9;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 18px;
|
margin: 0 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dotted-line .circle-left {
|
.dotted-line .circle-left {
|
||||||
left: -25px;
|
left: -18px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 50px;
|
width: 36px;
|
||||||
height: 50px;
|
height: 36px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: #dbdbdb;
|
background-color: #e9e9e9;
|
||||||
display: flex; /* Use flexbox to center the inner circle */
|
display: flex; /* Use flexbox to center the inner circle */
|
||||||
justify-content: center; /* Center horizontally */
|
justify-content: center; /* Center horizontally */
|
||||||
align-items: center; /* Center vertically */
|
align-items: center; /* Center vertically */
|
||||||
}
|
}
|
||||||
|
|
||||||
.dotted-line .circle-right {
|
.dotted-line .circle-right {
|
||||||
right: -25px;
|
right: -18px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 50px;
|
width: 36px;
|
||||||
height: 50px;
|
height: 36px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: #dbdbdb;
|
background-color: #e9e9e9;
|
||||||
display: flex; /* Use flexbox to center the inner circle */
|
display: flex; /* Use flexbox to center the inner circle */
|
||||||
justify-content: center; /* Center horizontally */
|
justify-content: center; /* Center horizontally */
|
||||||
align-items: center; /* Center vertically */
|
align-items: center; /* Center vertically */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* No scroll body for items per request */
|
||||||
|
|
||||||
|
/* Compact list text */
|
||||||
|
.RoundedRectangle ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0 12px 6px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.RoundedRectangle ul li {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
padding: 3px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid density tweaks */
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.TransactionListContainer { gap: 14px; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
|
||||||
|
}
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.TransactionListContainer { gap: 16px; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
|
||||||
|
}
|
||||||
|
|
||||||
.inner-circle {
|
.inner-circle {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
height: 80%;
|
height: 80%;
|
||||||
@@ -389,10 +426,11 @@
|
|||||||
|
|
||||||
.addNewItem{
|
.addNewItem{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 27px;
|
height: 28px;
|
||||||
background-color: rgb(115, 165, 133);
|
background-color: var(--brand-primary, rgb(115, 165, 133));
|
||||||
border-radius: 11px;
|
border-radius: 10px; /* square rounded */
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: white;
|
color: white;
|
||||||
line-height: 27px;
|
line-height: 28px;
|
||||||
}
|
font-size: 12px; /* compact */
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-container {
|
.image-container {
|
||||||
@@ -15,6 +17,11 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fileInput {
|
.fileInput {
|
||||||
@@ -24,38 +31,134 @@
|
|||||||
.circular-image {
|
.circular-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcoming-text {
|
.welcoming-text {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.get-started-button {
|
.get-started-button {
|
||||||
padding: 10px 20px;
|
padding: 12px 24px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 25px;
|
border-radius: 25px;
|
||||||
background-color: #007bff; /* Bootstrap primary color */
|
background-color: #28a745; /* Bootstrap primary color */
|
||||||
color: white;
|
color: white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
transition: background-color 0.3s;
|
transition: background-color 0.3s;
|
||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 2px 6px rgba(40, 167, 69, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.get-started-button:hover {
|
.get-started-button:hover {
|
||||||
background-color: #0056b3; /* Darker shade on hover */
|
background-color: #218838; /* Darker shade on hover */
|
||||||
|
box-shadow: 0 4px 10px rgba(40, 167, 69, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.get-started-button:active {
|
||||||
|
transform: translateY(1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fullscreen styles */
|
/* Fullscreen styles */
|
||||||
.fullscreen {
|
.fullscreen {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 50%;
|
top: 0;
|
||||||
left: 50%;
|
left: 0;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
transform: translate(-50%, -50%);
|
z-index: 1000;
|
||||||
z-index: 300;
|
background-color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fullscreen .image-container {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen .welcoming-text {
|
||||||
|
font-size: 32px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen .get-started-button {
|
||||||
|
padding: 16px 32px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.welcome-page {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcoming-text {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.get-started-button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen .image-container {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen .welcoming-text {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen .get-started-button {
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.image-container {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcoming-text {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.get-started-button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen .image-container {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen .welcoming-text {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen .get-started-button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// WelcomePage.js
|
// WelcomePage.js
|
||||||
import React,{useRef} from "react";
|
import React, { useRef } from "react";
|
||||||
import "./WelcomePage.css";
|
import "./WelcomePage.css";
|
||||||
|
|
||||||
const WelcomePage = ({
|
const WelcomePage = ({
|
||||||
@@ -15,6 +15,15 @@ const WelcomePage = ({
|
|||||||
const handleImageClick = () => {
|
const handleImageClick = () => {
|
||||||
fileInputRef.current.click();
|
fileInputRef.current.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// SVG Icon for camera
|
||||||
|
const CameraIcon = () => (
|
||||||
|
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M21 19H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2z" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
<circle cx="12" cy="13" r="4" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`welcome-page ${isFullscreen ? "fullscreen" : ""}`} // Corrected the className syntax
|
className={`welcome-page ${isFullscreen ? "fullscreen" : ""}`} // Corrected the className syntax
|
||||||
@@ -29,10 +38,13 @@ const WelcomePage = ({
|
|||||||
className="image-container"
|
className="image-container"
|
||||||
>
|
>
|
||||||
{!isFullscreen &&
|
{!isFullscreen &&
|
||||||
<div onClick={handleImageClick} style={{width: '100%', height: '100%', position:'absolute'}}>
|
<div onClick={handleImageClick} style={{width: '100%', height: '100%', position:'absolute', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer'}}>
|
||||||
<h1 style={{textAlign:'left'}}>
|
<div style={{textAlign: 'center', color: 'white'}}>
|
||||||
{image ? "Click To Change Image" : "Click To Add Image"}
|
<CameraIcon />
|
||||||
</h1>
|
<h1 style={{textAlign:'center', fontSize: '16px', margin: '10px 0 0 0'}}>
|
||||||
|
{image ? "Klik untuk mengganti gambar" : "Klik untuk menambahkan gambar"}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef} style={{display: 'none', width:'100%', height: '100%'}}type="file" accept="image/*" onChange={onImageChange} />
|
ref={fileInputRef} style={{display: 'none', width:'100%', height: '100%'}}type="file" accept="image/*" onChange={onImageChange} />
|
||||||
</div>
|
</div>
|
||||||
@@ -43,10 +55,10 @@ const WelcomePage = ({
|
|||||||
{welcomingText}
|
{welcomingText}
|
||||||
</h1>
|
</h1>
|
||||||
<button className="get-started-button" onClick={onGetStarted}>
|
<button className="get-started-button" onClick={onGetStarted}>
|
||||||
Get Started
|
Mulai
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default WelcomePage;
|
export default WelcomePage;
|
||||||
@@ -1,56 +1,395 @@
|
|||||||
/* WelcomePageEditor.css */
|
/* WelcomePageEditor.css */
|
||||||
|
|
||||||
.welcome-page-editor {
|
.welcome-page-editor {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
width: 100%;
|
||||||
justify-content: center;
|
height: 100vh;
|
||||||
padding: 20px;
|
background-color: #ffffff;
|
||||||
border: 1px solid #ddd;
|
border-radius: 16px;
|
||||||
border-radius: 8px;
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
background-color: rgb(207, 207, 207);
|
overflow: hidden; /* contain scroll inside editor-content */
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
.editor-header {
|
||||||
margin-bottom: 20px;
|
padding: 24px;
|
||||||
|
border-bottom: 1px solid #e6e6e6;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-title {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 700;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="file"] {
|
.editor-content {
|
||||||
margin-bottom: 20px;
|
display: flex;
|
||||||
}
|
flex: 1; /* take remaining height */
|
||||||
|
|
||||||
textarea {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100px; /* Adjust as needed */
|
overflow-y: auto; /* enable vertical scroll in editor-content */
|
||||||
padding: 10px;
|
overflow-x: hidden;
|
||||||
border: 1px solid #ccc;
|
justify-content: center;
|
||||||
border-radius: 4px;
|
align-items: flex-start;
|
||||||
resize: none;
|
padding: 16px;
|
||||||
margin-bottom: 20px;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea:focus {
|
.config-panel {
|
||||||
border-color: #007bff; /* Highlight border color on focus */
|
width: 100%;
|
||||||
|
max-width: 960px;
|
||||||
|
padding: 24px;
|
||||||
|
border-right: none;
|
||||||
|
background-color: #fafafa;
|
||||||
|
overflow-y: visible;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #333;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
border-color: #28a745;
|
||||||
|
box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
.color-input {
|
||||||
margin-bottom: 20px;
|
height: 45px;
|
||||||
|
padding: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-input {
|
||||||
|
min-height: 80px;
|
||||||
|
resize: vertical;
|
||||||
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-label {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: none;
|
||||||
|
background-color: #28a745;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 6px rgba(40, 167, 69, 0.2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-button:hover {
|
||||||
|
background-color: #218838;
|
||||||
|
box-shadow: 0 4px 10px rgba(40, 167, 69, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-button:disabled {
|
||||||
|
background-color: #a1a1a1;
|
||||||
|
cursor: not-allowed;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-button:hover {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid #ffffff;
|
||||||
|
border-top: 2px solid transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-header {
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-bottom: 1px solid #e6e6e6;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-title {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px;
|
||||||
|
overflow: auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-preview {
|
.welcome-preview {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
max-width: 400px;
|
||||||
border: 1px dashed #ccc; /* Preview border style */
|
border-radius: 16px;
|
||||||
border-radius: 8px;
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||||
padding: 20px;
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-toggle {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 24px;
|
||||||
|
right: 24px;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-toggle:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.9);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Top actions row */
|
||||||
|
.top-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.top-actions .preview-button {
|
||||||
|
width: auto;
|
||||||
|
padding: 10px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme & color redesign */
|
||||||
|
.hex-input {
|
||||||
|
max-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatches {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatch {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrast-card {
|
||||||
|
margin-top: 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrast-inner {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrast-title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrast-preview {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.theme-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-preview {
|
||||||
|
margin-top: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-preview-content {
|
||||||
|
padding: 16px;
|
||||||
|
min-height: 320px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: #ffffff; /* Background for preview area */
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.editor-content {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel {
|
||||||
|
width: 100%;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: none;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel {
|
||||||
|
height: 50vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.editor-header {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-preview-content {
|
||||||
|
min-height: 260px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const WelcomePageEditor = ({ cafeId, welcomePageConfig }) => {
|
|||||||
const [isWelcomePageActive, setIsWelcomePageActive] = useState(false); // State for the switch
|
const [isWelcomePageActive, setIsWelcomePageActive] = useState(false); // State for the switch
|
||||||
const [loading, setLoading] = useState(false); // Loading state
|
const [loading, setLoading] = useState(false); // Loading state
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
const [showPreview, setShowPreview] = useState(false);
|
||||||
|
|
||||||
// Load existing welcome page configuration when component mounts
|
// Load existing welcome page configuration when component mounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -51,6 +52,21 @@ const WelcomePageEditor = ({ cafeId, welcomePageConfig }) => {
|
|||||||
setTextColor(e.target.value);
|
setTextColor(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Extra handlers for HEX input fields
|
||||||
|
const handleBackgroundHexInput = (e) => {
|
||||||
|
let v = e.target.value;
|
||||||
|
if (!v.startsWith('#')) v = '#' + v.replace(/#/g, '');
|
||||||
|
if (v.length > 7) v = v.slice(0, 7);
|
||||||
|
setBackgroundColor(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTextHexInput = (e) => {
|
||||||
|
let v = e.target.value;
|
||||||
|
if (!v.startsWith('#')) v = '#' + v.replace(/#/g, '');
|
||||||
|
if (v.length > 7) v = v.slice(0, 7);
|
||||||
|
setTextColor(v);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
@@ -72,85 +88,236 @@ const WelcomePageEditor = ({ cafeId, welcomePageConfig }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// SVG Icons
|
||||||
|
const CoffeeIcon = () => (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M17 8h1.5a2.5 2.5 0 0 1 0 5H15m0-5H13m-1 0H6a3 3 0 0 0-3 3v1.5a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V11a3 3 0 0 0-3-3h-5.5m-1 0V5a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1V2m-5 0v1a1 1 0 0 0-1 1v1m0 0v1a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const PaletteIcon = () => (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14 3v4a1 1 0 0 0 1 1h4m-9 4a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm6 10v-2a3 3 0 0 0-3-3h-4a3 3 0 0 0-3 3v2h10ZM16 21v-2a3 3 0 0 0-3-3h-2a3 3 0 0 0-3 3v2h8Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const TypeIcon = () => (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4 7V4h16v3M9 20h6M12 4v16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const SaveIcon = () => (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
<path d="M17 21v-8H7v8M7 3v5h8M12 11h6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const EyeIcon = () => (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8Z" stroke="currentColor" strokeWidth="2"/>
|
||||||
|
<path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" stroke="currentColor" strokeWidth="2"/>
|
||||||
|
<path d="M22 12c-1.5-4-5-6-10-6S4 8 2 12" stroke="currentColor" strokeWidth="2"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="welcome-page-editor">
|
||||||
className="welcome-page-editor"
|
<div className="editor-header">
|
||||||
style={{ width: "80vw", height: "80vh" }}
|
<h2 className="editor-title">Konfigurasi Halaman Selamat Datang</h2>
|
||||||
>
|
|
||||||
<h2>Edit Welcome Page</h2>
|
|
||||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
|
||||||
<textarea
|
|
||||||
value={welcomingText}
|
|
||||||
onChange={handleTextChange}
|
|
||||||
placeholder="Enter welcoming text..."
|
|
||||||
style={{ height: "20px", resize: "none" }} // Reduced height
|
|
||||||
/>
|
|
||||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
|
||||||
<label>
|
|
||||||
Background Color:
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
value={backgroundColor}
|
|
||||||
onChange={handleColorChange}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
Text Color:
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
value={textColor}
|
|
||||||
onChange={handleTextColorChange}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
|
||||||
<label style={{ marginRight: "10px" }}>Is Welcome Page Active:</label>
|
|
||||||
<Switch
|
|
||||||
onChange={() => setIsWelcomePageActive(!isWelcomePageActive)}
|
|
||||||
checked={isWelcomePageActive}
|
|
||||||
offColor="#888"
|
|
||||||
onColor="#0a0"
|
|
||||||
uncheckedIcon={false}
|
|
||||||
checkedIcon={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button onClick={handleSave} disabled={loading}>
|
|
||||||
{loading ? "Saving..." : "Save Configuration"}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
style={{ width: "100%", height: "100%", position: "relative", flex: 1, borderRadius: '15px' }}
|
<div className="editor-content">
|
||||||
>
|
{/* Configuration Panel */}
|
||||||
<WelcomePage
|
<div className="config-panel">
|
||||||
image={image}
|
{/* Top Preview Toggle */}
|
||||||
welcomingText={welcomingText}
|
<div className="top-actions">
|
||||||
backgroundColor={backgroundColor}
|
<button
|
||||||
textColor={textColor}
|
type="button"
|
||||||
onGetStarted={() => setIsFullscreen(false)}
|
className="preview-button"
|
||||||
isFullscreen={isFullscreen}
|
onClick={() => setShowPreview(!showPreview)}
|
||||||
onImageChange={handleImageChange}
|
>
|
||||||
/>
|
<EyeIcon /> {showPreview ? 'Tutup Pratinjau' : 'Preview'}
|
||||||
<div style={{ position: "absolute", bottom: 0, right: 0 }}>
|
</button>
|
||||||
<svg
|
</div>
|
||||||
width="100" // Adjust size as needed
|
|
||||||
height="100" // Adjust size as needed
|
{showPreview && (
|
||||||
style={{ position: "absolute", bottom: 0, right: 0 }}
|
<div className="inline-preview">
|
||||||
onClick={() => setIsFullscreen(true)}
|
<div className="inline-preview-content">
|
||||||
|
<WelcomePage
|
||||||
|
image={image}
|
||||||
|
welcomingText={welcomingText}
|
||||||
|
backgroundColor={backgroundColor}
|
||||||
|
textColor={textColor}
|
||||||
|
onGetStarted={() => setShowPreview(false)}
|
||||||
|
isFullscreen={false}
|
||||||
|
onImageChange={handleImageChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="config-section">
|
||||||
|
<h3 className="section-title">
|
||||||
|
<CoffeeIcon />
|
||||||
|
Konten Utama
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="config-group">
|
||||||
|
<label className="config-label">Teks Selamat Datang</label>
|
||||||
|
<textarea
|
||||||
|
className="config-input textarea-input"
|
||||||
|
value={welcomingText}
|
||||||
|
onChange={handleTextChange}
|
||||||
|
placeholder="Masukkan teks selamat datang..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="config-group">
|
||||||
|
<label className="config-label">Gambar Latar Belakang</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleImageChange}
|
||||||
|
className="config-input"
|
||||||
|
/>
|
||||||
|
{image && (
|
||||||
|
<div style={{ marginTop: '12px', textAlign: 'center' }}>
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
alt="Preview"
|
||||||
|
style={{
|
||||||
|
maxWidth: '100%',
|
||||||
|
maxHeight: '150px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #ddd'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="config-section">
|
||||||
|
<h3 className="section-title">
|
||||||
|
<PaletteIcon />
|
||||||
|
Tema & Warna
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="theme-grid">
|
||||||
|
<div className="color-field">
|
||||||
|
<label className="config-label">Warna Latar Belakang</label>
|
||||||
|
<div className="color-row">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
className="config-input color-input"
|
||||||
|
value={backgroundColor}
|
||||||
|
onChange={handleColorChange}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="config-input hex-input"
|
||||||
|
value={backgroundColor}
|
||||||
|
onChange={handleBackgroundHexInput}
|
||||||
|
maxLength={7}
|
||||||
|
placeholder="#FFFFFF"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="swatches" aria-label="Background presets">
|
||||||
|
{['#ffffff','#f8f9fa','#fff3cd','#e8f5e9','#e3f2fd','#212529'].map((c) => (
|
||||||
|
<button
|
||||||
|
key={c}
|
||||||
|
className="swatch"
|
||||||
|
style={{ backgroundColor: c, borderColor: c.toLowerCase()==='#ffffff'?'#ddd':'transparent' }}
|
||||||
|
onClick={() => setBackgroundColor(c)}
|
||||||
|
title={c}
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="color-field">
|
||||||
|
<label className="config-label">Warna Teks</label>
|
||||||
|
<div className="color-row">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
className="config-input color-input"
|
||||||
|
value={textColor}
|
||||||
|
onChange={handleTextColorChange}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="config-input hex-input"
|
||||||
|
value={textColor}
|
||||||
|
onChange={handleTextHexInput}
|
||||||
|
maxLength={7}
|
||||||
|
placeholder="#000000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="swatches" aria-label="Text presets">
|
||||||
|
{['#000000','#212529','#343a40','#6c757d','#ffffff','#28a745'].map((c) => (
|
||||||
|
<button
|
||||||
|
key={c}
|
||||||
|
className="swatch"
|
||||||
|
style={{ backgroundColor: c, borderColor: c.toLowerCase()==='#ffffff'?'#ddd':'transparent' }}
|
||||||
|
onClick={() => setTextColor(c)}
|
||||||
|
title={c}
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="contrast-card" style={{ backgroundColor }}>
|
||||||
|
<div className="contrast-inner">
|
||||||
|
<div className="contrast-title">Pratinjau Kontrast</div>
|
||||||
|
<div className="contrast-preview" style={{ color: textColor }}>{welcomingText || 'Contoh Teks'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="config-section">
|
||||||
|
<h3 className="section-title">
|
||||||
|
<TypeIcon />
|
||||||
|
Pengaturan
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="switch-container">
|
||||||
|
<span className="switch-label">Aktifkan Halaman Selamat Datang</span>
|
||||||
|
<Switch
|
||||||
|
onChange={() => setIsWelcomePageActive(!isWelcomePageActive)}
|
||||||
|
checked={isWelcomePageActive}
|
||||||
|
offColor="#cccccc"
|
||||||
|
onColor="#28a745"
|
||||||
|
uncheckedIcon={false}
|
||||||
|
checkedIcon={false}
|
||||||
|
height={24}
|
||||||
|
width={48}
|
||||||
|
handleDiameter={20}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="save-button"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<g transform="rotate(45 50 50)">
|
{loading ? (
|
||||||
<circle cx="50" cy="50" r="40" fill="rgba(0, 0, 0, 0.5)" />
|
<>
|
||||||
<text
|
<div className="loading-spinner"></div>
|
||||||
x="50"
|
Menyimpan...
|
||||||
y="50"
|
</>
|
||||||
textAnchor="middle"
|
) : (
|
||||||
dominantBaseline="middle"
|
<>
|
||||||
fontSize="24"
|
<SaveIcon />
|
||||||
fill="white" // Adjust text color as needed
|
Simpan Konfigurasi
|
||||||
>
|
</>
|
||||||
<>
|
)}
|
||||||
</text>
|
</button>
|
||||||
</g>
|
{/* Pratinjau dipicu oleh tombol atas; section khusus dihapus */}
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user