diff --git a/src/components/IdentifyCafeModal.js b/src/components/IdentifyCafeModal.js new file mode 100644 index 0000000..7a2f56d --- /dev/null +++ b/src/components/IdentifyCafeModal.js @@ -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 ( +
+
+
Identifikasi kedai
+
+
+ 1 Alamat +
+
+ 2 Desain QR +
+
+ 3 Meja +
+
+
+ +
+ {currentStep === 1 && ( +
+
Alamat kedai
+
+
{shopHost}/
+ +
+ {checking ? 'Memeriksa…' : availability === 200 ? 'Tersedia' : availability === 409 ? 'Terpakai' : 'Menunggu input'} +
+
+
Gunakan huruf kecil, angka, dan garis bawah (_). Contoh: kopikenangan_malam
+
+ + +
+
+ )} + + {currentStep === 2 && ( +
+
Desain QR
+
+ + + + +
+
+
+
+ {bgImageUrl && Background} + QR +
{selectedTable ? selectedTable.tableNo : 'Kedai'}
+
+
+
+
+ + +
+
+ + setQrSize(parseFloat(e.target.value))} /> +
+
+ + setQrX(parseInt(e.target.value))} /> +
+
+ + setQrY(parseInt(e.target.value))} /> +
+
+ + setFontSize(parseInt(e.target.value))} /> +
+
+ + setFontColor(e.target.value)} /> +
+
+ + setFontX(parseInt(e.target.value))} /> +
+
+ + setFontY(parseInt(e.target.value))} /> +
+
+
+
Tip: Gunakan latar yang kontras agar QR mudah dipindai.
+
+ )} + + {currentStep === 3 && ( +
+
Daftar meja
+
+ setNewTableNo(e.target.value)} /> + +
+
+ {tables && tables + .filter((t)=>t.tableNo !== 0) + .map((t)=>{ + const active = selectedTable && selectedTable.tableId === t.tableId; + return ( +
setSelectedTable(active ? null : t)} + > + {t.tableNo} +
+ ); + })} +
+
Pilih meja untuk membuat QR khusus meja (opsional).
+
+ )} +
+ +
+
+ + {currentStep < 3 ? ( + + ) : ( + + )} +
+
+ {saveStatus === 'success' &&
Simpan berhasil
} + {saveStatus === 'error' &&
Gagal menyimpan
} + +
+
+
+ ); +} diff --git a/src/components/IdentifyCafeModal.module.css b/src/components/IdentifyCafeModal.module.css new file mode 100644 index 0000000..1e8a84c --- /dev/null +++ b/src/components/IdentifyCafeModal.module.css @@ -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; } +} diff --git a/src/components/ItemConfig.js b/src/components/ItemConfig.js index d528c18..3254c84 100644 --- a/src/components/ItemConfig.js +++ b/src/components/ItemConfig.js @@ -23,6 +23,8 @@ const ItemConfig = ({ const [itemDescription, setItemDescription] = useState(initialDescription); const fileInputRef = useRef(null); const textareaRef = useRef(null); + const [saving, setSaving] = useState(false); + const [saveStatus, setSaveStatus] = useState(null); // 'success' | 'error' useEffect(() => { // Prevent scrolling when modal is open @@ -79,15 +81,29 @@ const ItemConfig = ({ } }, [textareaRef.current]); - const handleCreate = () => { - console.log(itemPromoPrice) - handleCreateItem(itemName, itemPrice, selectedImage, previewUrl, itemDescription, itemPromoPrice); - document.body.style.overflow = "auto"; + const handleCreate = async () => { + setSaving(true); + setSaveStatus(null); + try { + await Promise.resolve(handleCreateItem(itemName, itemPrice, selectedImage, previewUrl, itemDescription, itemPromoPrice)); + setSaveStatus('success'); + } catch (e) { + setSaveStatus('error'); + } finally { + setSaving(false); + } }; - const handleUpdate = () => { - console.log(itemName, itemPrice, selectedImage, itemDescription, itemPromoPrice) - handleUpdateItem(itemName, itemPrice, selectedImage, itemDescription, itemPromoPrice); - document.body.style.overflow = "auto"; + const handleUpdate = async () => { + setSaving(true); + setSaveStatus(null); + try { + await Promise.resolve(handleUpdateItem(itemName, itemPrice, selectedImage, itemDescription, itemPromoPrice)); + setSaveStatus('success'); + } catch (e) { + setSaveStatus('error'); + } finally { + setSaving(false); + } }; return ( @@ -124,6 +140,14 @@ const ItemConfig = ({
+
+ {saveStatus === 'success' && ( + Perubahan disimpan + )} + {saveStatus === 'error' && ( + Gagal menyimpan perubahan + )} +
- -
@@ -186,4 +212,4 @@ const ItemConfig = ({ ); }; -export default ItemConfig; \ No newline at end of file +export default ItemConfig; diff --git a/src/components/ItemTypeLister.css b/src/components/ItemTypeLister.css index e1ce173..408db88 100644 --- a/src/components/ItemTypeLister.css +++ b/src/components/ItemTypeLister.css @@ -1,10 +1,12 @@ +/* ItemTypeLister.css */ + /* New clean, intuitive category bar */ .item-type-lister { width: 100%; overflow-x: auto; white-space: nowrap; - padding: 12px 0; - margin-bottom: 12px; + padding: 8px 0; /* Reduced padding for more compact design */ + margin-bottom: 8px; /* Reduced margin for more compact design */ scrollbar-width: thin; display: flex; justify-content: center; @@ -12,20 +14,20 @@ } .item-type-lister::-webkit-scrollbar { - height: 8px; + height: 6px; } .item-type-lister::-webkit-scrollbar-thumb { background-color: #c5c5c5; - border-radius: 4px; + border-radius: 3px; } .category-bar { display: flex; align-items: center; - gap: 10px; + gap: 6px; /* Reduced gap for more compact design */ overflow-x: auto; - padding: 10px 15px; + padding: 6px 10px; /* Reduced padding */ -ms-overflow-style: none; scrollbar-width: none; justify-content: center; @@ -39,7 +41,7 @@ -ms-overflow-style: none; scrollbar-width: none; overflow-y: hidden; - gap: 10px; + gap: 6px; /* Added gap for consistent spacing */ justify-content: center; align-items: center; width: 100%; @@ -49,37 +51,34 @@ flex: 0 0 auto; display: inline-flex; align-items: center; - gap: 10px; - height: 42px; - padding: 0 18px; - border-radius: 12px; /* Square rounded corners */ + 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: 16px; + font-size: 13px; /* Slightly smaller font */ cursor: pointer; user-select: none; transition: all 0.2s ease; justify-content: center; - box-shadow: 0 1px 3px rgba(0,0,0,0.05); } .category-chip:hover { border-color: #d0d0d0; background-color: #f8f8f8; - box-shadow: 0 2px 6px rgba(0,0,0,0.1); } .category-chip.selected { background: #73a585; color: #ffffff; border-color: #73a585; - box-shadow: 0 2px 6px rgba(115, 165, 133, 0.2); } .category-chip .chip-icon { - width: 22px; - height: 22px; + width: 16px; /* Reduced icon size */ + height: 16px; display: inline-flex; align-items: center; justify-content: center; @@ -105,40 +104,146 @@ .inline-container { display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 12px; - padding: 12px; - overflow-y: auto; + grid-template-columns: repeat(4, 1fr); /* Always 4 columns */ + gap: 10px; /* Spacing between grid items */ + padding: 10px; /* Padding inside grid */ + overflow-y: auto; /* Allow scrolling if items overflow */ } .grid-container { display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 12px; - padding: 12px; - max-height: calc(3 * (30vw - 24px) + 24px); - overflow-y: auto; - padding-top: 18px; - height: calc(48vw - 24px); + grid-template-columns: repeat(4, 1fr); /* Always 4 columns */ + gap: 10px; /* Spacing between grid items */ + padding: 10px; /* Padding inside grid */ + max-height: calc(3 * (25vw - 20px) + 20px); /* 3 items + gaps */ + overflow-y: auto; /* Allow scrolling if items overflow */ + padding-top: 15px; + height: calc(43vw - 20px); } .add-button { - margin: 12px; - padding: 12px 24px; - position: absolute; + margin: 10px; /* Margin around the button */ + padding: 10px 20px; /* Padding for the button */ + position: absolute; /* Optional, for styling */ bottom: 0; - align-self: center; + align-self: center; /* Center the button horizontally */ } -/* Centered container for item type list */ -.centered-item-type-list { +/* Legacy styles kept for ItemType grid if needed elsewhere */ + +/* Compact centered item type list without icon tiles */ +.compact-centered-list { display: flex; justify-content: center; align-items: center; width: 100%; - padding: 12px 0; + padding: 8px 0; + overflow-x: auto; } -/* No icon styles */ -.no-icon { - padding: 0 20px; /* Increased padding for better touch targets */ +.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; + } } \ No newline at end of file diff --git a/src/components/ItemTypeLister.js b/src/components/ItemTypeLister.js index 3705764..0248f0d 100644 --- a/src/components/ItemTypeLister.js +++ b/src/components/ItemTypeLister.js @@ -1,7 +1,6 @@ import React, { useState, useRef, useEffect } from "react"; import smoothScroll from "smooth-scroll-into-view-if-needed"; import "./ItemTypeLister.css"; -import ItemType from "./ItemType"; import { createItem } from "../helpers/itemHelper.js"; import { getImageUrl } from "../helpers/itemHelper"; import ItemLister from "./ItemLister"; @@ -62,15 +61,15 @@ const ItemTypeLister = ({ return (
-
-
+
+
{isEditMode && !isAddingNewItem && canManage && ( - + > + Buat baru +
)} {canManage && isAddingNewItem && ( @@ -91,24 +90,22 @@ const ItemTypeLister = ({ )} {itemTypes && itemTypes.length > 0 && ( - onFilterChange(0)} - selected={filterId === 0} - noIcon={true} - compact={false} // Make it larger for better touch targets - /> + > + Semua +
)} {itemTypes && itemTypes.map((itemType) => ( - onFilterChange(itemType.itemTypeId)} - selected={filterId === itemType.itemTypeId} - noIcon={true} - compact={false} // Make it larger for better touch targets - /> + > + {formatName(itemType.name)} +
))}
diff --git a/src/components/Modal.js b/src/components/Modal.js index e3ef0ae..1316c00 100644 --- a/src/components/Modal.js +++ b/src/components/Modal.js @@ -5,7 +5,7 @@ import AccountUpdatePage from "../components/AccountUpdatePage.js"; import CreateClerk from "../pages/CreateClerk" import CreateCafe from "../pages/CreateCafe" import CreateTenant from "../pages/CreateTenant" -import TablesPage from "./TablesPage.js"; +import IdentifyCafeModal from "./IdentifyCafeModal.js"; import PaymentOptions from "./PaymentOptions.js"; import Transaction from "../pages/Transaction"; import Transaction_item from "../pages/Transaction_item"; @@ -77,7 +77,7 @@ const Modal = ({ user, shop, isOpen, onClose, modalContent, deviceType, setModal if(modalContent == '') handleOverlayClick(); return (
-
+
{modalContent === "edit_account" && } {modalContent === "reset-password" && } @@ -86,7 +86,7 @@ const Modal = ({ user, shop, isOpen, onClose, modalContent, deviceType, setModal {modalContent === "create_clerk" && } {modalContent === "create_kedai" && } {modalContent === "create_tenant" && } - {modalContent === "edit_tables" && } + {modalContent === "edit_tables" && } {modalContent === "new_transaction" && ( )} diff --git a/src/components/Modal.module.css b/src/components/Modal.module.css index bd6ebe3..b1d694b 100644 --- a/src/components/Modal.module.css +++ b/src/components/Modal.module.css @@ -8,7 +8,7 @@ display: flex; justify-content: center; align-items: center; - z-index: 1000; + z-index: 9999; padding: 20px; } @@ -23,6 +23,12 @@ flex-direction: column; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); animation: modalAppear 0.3s ease-out; + position: relative; + z-index: 10000; /* ensure above any page overlays */ +} + +.modalContentWide { + max-width: 920px; } @keyframes modalAppear { @@ -178,6 +184,31 @@ 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; @@ -262,4 +293,4 @@ .formActions { flex-direction: column; } -} \ No newline at end of file +} diff --git a/src/components/PaymentOptions.js b/src/components/PaymentOptions.js index e324cb2..d229195 100644 --- a/src/components/PaymentOptions.js +++ b/src/components/PaymentOptions.js @@ -1,18 +1,13 @@ import React, { useState, useRef, useEffect } from "react"; import QrScanner from "qr-scanner"; // Import qr-scanner import { getImageUrl } from "../helpers/itemHelper"; -import { - getCafe, - saveCafeDetails, - setConfirmationStatus, - setOpenBillAvailability -} from "../helpers/cafeHelpers"; +import { getCafe, saveCafeDetails } from "../helpers/cafeHelpers"; import Switch from "react-switch"; // Import the Switch component +import styles from "./PaymentOptions.module.css"; -const SetPaymentQr = ({ shopId, - qrCodeUrl }) => { - const [qrPosition, setQrPosition] = useState([50, 50]); - const [qrSize, setQrSize] = useState(50); +const SetPaymentQr = ({ shopId, qrCodeUrl }) => { + const [qrPosition, setQrPosition] = useState([50, 50]); // legacy kept for API compatibility + const [qrSize, setQrSize] = useState(50); // legacy kept for API compatibility const [qrPayment, setQrPayment] = useState(); const [qrPaymentFile, setQrPaymentFile] = useState(); const [qrCodeDetected, setQrCodeDetected] = useState(false); @@ -26,6 +21,9 @@ const SetPaymentQr = ({ shopId, const [isConfigQRIS, setIsConfigQRIS] = useState(false); const [isOpenBillAvailable, setIsOpenBillAvailable] = useState(false); + const [saving, setSaving] = useState(false); + const [saveStatus, setSaveStatus] = useState(null); // 'success' | 'error' + const [copied, setCopied] = useState(false); useEffect(() => { const fetchCafe = async () => { @@ -104,6 +102,8 @@ const SetPaymentQr = ({ shopId, // Save cafe details const handleSave = async () => { + setSaving(true); + setSaveStatus(null); let qrPaymentFileCache; console.log(qrPaymentFile) if(qrPaymentFile != null) @@ -120,240 +120,141 @@ const SetPaymentQr = ({ shopId, try { const response = await saveCafeDetails(cafe.cafeId, details); - - setIsNeedConfirmationState(response.needsConfirmation ? 1 : 0); // Update state after saving - setIsQRISavailable(response.isQRISavailable ? 1 : 0); // Update state after saving - setIsOpenBillAvailable(response.isOpenBillAvailable ? 1 : 0); // Update state after saving - + setIsNeedConfirmationState(response.needsConfirmation ? 1 : 0); + setIsQRISavailable(response.isQRISavailable ? 1 : 0); + setIsOpenBillAvailable(response.isOpenBillAvailable ? 1 : 0); + setSaveStatus('success'); console.log("Cafe details saved:", response); } catch (error) { console.error("Error saving cafe details:", error); + setSaveStatus('error'); + } finally { + setSaving(false); } }; + const copyQrData = async () => { + if (!qrCodeData) return; + try { + await navigator.clipboard.writeText(qrCodeData); + setCopied(true); + setTimeout(()=>setCopied(false), 1200); + } catch {} + }; + return ( -
-

Konfigurasi pembayaran

+
+

Konfigurasi pembayaran

-
-

- Pembayaran QRIS. -

+
+
+
+
Pembayaran QRIS
+
Aktifkan agar pelanggan dapat membayar via QRIS. Kasir tetap perlu verifikasi rekening.
+
+ setIsQRISavailable(checked ? 1 : 0)} + checked={isQRISavailable === 1} + offColor="#888" + onColor="#4CAF50" + uncheckedIcon={false} + checkedIcon={false} + height={25} + width={50} + /> +
- {isConfigQRIS ? +
+ +
+ + {isConfigQRIS && ( <>
qrPaymentInputRef.current.click()} - style={{ - ...styles.qrCodeContainer, - backgroundImage: `url(${qrPayment})`, - backgroundPosition: "center", - backgroundRepeat: "no-repeat", - backgroundSize: "contain", - }} + style={{ backgroundImage: `url(${qrPayment})` }} > - +
-
-

Klik untuk ganti background

-
-
- {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' ?
qrPaymentInputRef.current.click()} style={styles.uploadButton}>Ganti
:
qrPaymentInputRef.current.click()} style={styles.uploadButton}>Unggah
} -
- -
setIsConfigQRIS(false)} - - style={{ - ...styles.qrisConfigButton, - width: '100%', - marginLeft: "0", - }} - >Terapkan
- - : - <> -

- Aktifkan fitur agar pelanggan dapat menggunakan opsi pembayaran QRIS, namun kasir anda perlu memeriksa rekening untuk memastikan pembayaran. -

-
- setIsQRISavailable(checked ? 1 : 0)} - checked={isQRISavailable === 1} // Convert to boolean - offColor="#888" - onColor="#4CAF50" - uncheckedIcon={false} - checkedIcon={false} - height={25} - width={50} - /> -
setIsConfigQRIS(true)} - style={{ - ...styles.qrisConfigButton, - backgroundColor: isQRISavailable == 1 ? styles.qrisConfigButton.backgroundColor : 'gray', - }} - > - Konfigurasi QRIS +
Klik area untuk unggah/ganti gambar QR
+
+
+ {qrCodeDetected && qrPayment !== 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPsNr0TPq8dHT3nBwDQ6OHQQTrqzVFoeBOmuWfgyErrLbJi6f6CnnYhpNHEvkJ_2X-kyI&usqp=CAU' ? 'QR terdeteksi' : 'Tidak ada QR terdeteksi'}
- + +
+ {qrCodeDetected && ( +
+ + +
+ )} +
+
- } -
-
-

- Open bill -

-

- Aktifkan fitur agar pelanggan dapat menambahkan pesanan selama sesi berlangsung tanpa perlu melakukan transaksi baru dan hanya membayar di akhir. -

- setIsOpenBillAvailable(checked ? 1 : 0)} - checked={isOpenBillAvailable === 1} // Convert to boolean - offColor="#888" - onColor="#4CAF50" - uncheckedIcon={false} - checkedIcon={false} - height={25} - width={50} - /> + )}
-
-

- Pengecekan ganda -

-

- Nyalakan agar kasir memeriksa kembali ketersediaan produk sebelum pelanggan membayar. -

- setIsNeedConfirmationState(checked ? 1 : 0)} - checked={isNeedConfirmationState === 1} // Convert to boolean - offColor="#888" - onColor="#4CAF50" - uncheckedIcon={false} - checkedIcon={false} - height={25} - width={50} - /> +
+
+
+
Open bill
+
Izinkan pelanggan menambah pesanan dalam satu sesi dan bayar di akhir.
+
+ setIsOpenBillAvailable(checked ? 1 : 0)} + checked={isOpenBillAvailable === 1} + offColor="#888" + onColor="#4CAF50" + uncheckedIcon={false} + checkedIcon={false} + height={25} + width={50} + /> +
-
-
); }; -// 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; diff --git a/src/components/PaymentOptions.module.css b/src/components/PaymentOptions.module.css new file mode 100644 index 0000000..6d91f6c --- /dev/null +++ b/src/components/PaymentOptions.module.css @@ -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; } +} + diff --git a/src/components/TablesPage.js b/src/components/TablesPage.js deleted file mode 100644 index 5270ed5..0000000 --- a/src/components/TablesPage.js +++ /dev/null @@ -1,1180 +0,0 @@ -import React, { useState, useRef, useEffect } from "react"; -import API_BASE_URL from "../config.js"; -import { getImageUrl } from "../helpers/itemHelper"; -import { getTables, updateTable, createTable } from "../helpers/tableHelper"; -import { - getCafe, - saveCafeDetails, - setConfirmationStatus, -} from "../helpers/cafeHelpers"; - -import { toPng } from 'html-to-image'; -import { ColorRing } from "react-loader-spinner"; - -const SetPaymentQr = ({ shop }) => { - const [initialPos, setInitialPos] = useState({ - left: shop.xposition, - top: shop.yposition, - }); - - const [isViewingQR, setIsViewingQR] = useState(false); - const [isConfigFont, setIsConfigFont] = useState(false); - const [fontsize, setfontsize] = useState(shop.fontsize); - const [fontcolor, setfontcolor] = useState(shop.fontcolor); - const [initialFontPos, setInitialFontPos] = useState({ - left: shop.fontxposition, - top: shop.fontyposition, - }); - - const [initialSize, setInitialSize] = useState(shop.scale); - const [bgImageUrl, setBgImageUrl] = useState(getImageUrl(shop.qrBackground)); - const qrBackgroundInputRef = useRef(null); - - const [cafeIdentifyNameUpdate, setCafeIdentifyNameUpdate] = useState(shop.cafeIdentifyName); - const shopUrl = window.location.hostname + "/" + cafeIdentifyNameUpdate; - - - const cafeIdentifyNameRef = useRef(null); - const [isconfigcafeidentityname, setIsConfigCafeIdentityName] = useState(false); - - - const generateQRCodeUrl = (tableCode) => { - if (tableCode != null) { - return `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent( - shopUrl + "/" + tableCode - )}`; - } else { - return `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent( - shopUrl - )}`; - } - }; - - const [isConfig, setIsConfig] = useState(false); - const [isConfigQR, setIsConfigQR] = useState(false); - const [isViewTables, setIsViewTables] = useState(false); - - const [tables, setTables] = useState([]); - - const [selectedTable, setSelectedTable] = useState(null); - - const [tableNo, setTableNo] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [identifyNameResponse, setIdentifyNameResponse] = useState('-----------------'); - - useEffect(() => { - const fetchData = async () => { - try { - console.log(shop); - const fetchedTables = await getTables(shop.cafeId); - setTables(fetchedTables); - } catch (error) { - console.error("Error fetching tables:", error); - } - }; - - fetchData(); - }, [shop.cafeId]); - - const handleSave = () => { - const qrBackgroundFile = qrBackgroundInputRef.current.files[0]; // Get the selected file for qrBackground - - // Prepare the details object - const details = { - qrSize: initialSize, - qrPosition: initialPos, - qrBackgroundFile, - fontsize, - fontcolor, - fontPosition: initialFontPos, - cafeIdentifyName: shop.cafeIdentifyName != cafeIdentifyNameUpdate ? cafeIdentifyNameUpdate : null, - name: shop.name != inputValue ? inputValue : null - }; - - // Call saveCafeDetails function with the updated details object - saveCafeDetails(shop.cafeId, details) - .then((response) => { - console.log("Cafe details saved:", response); - window.location.href = `/${cafeIdentifyNameUpdate}?modal=edit_tables`; - }) - .catch((error) => { - console.error("Error saving cafe details:", error); - }); - }; - - - const handlePositionChange = (e) => { - const { name, value } = e.target; - setInitialPos((prevPosition) => ({ - ...prevPosition, - [name]: parseFloat(value).toFixed(2), - })); - }; - - const handleFontPositionChange = (e) => { - const { name, value } = e.target; - setInitialFontPos((prevPosition) => ({ - ...prevPosition, - [name]: parseFloat(value).toFixed(2), - })); - }; - - const handleSizeChange = (e) => { - setInitialSize(parseFloat(e.target.value).toFixed(2)); - }; - - const handleFontSizeChange = (e) => { - setfontsize(parseFloat(e.target.value).toFixed(2)); - }; - - const handleFileChange = (e) => { - const file = e.target.files[0]; - if (file) { - const newBgImage = URL.createObjectURL(file); // Create a temporary URL for display - setBgImageUrl(newBgImage); - } - }; - - const handleCreate = async () => { - // if (newTable) { - try { - const createdTable = await createTable(shop.cafeId, { - // ...newTable, - tableNo, - }); - setTables([...tables, createdTable]); - setTableNo(""); - } catch (error) { - console.error("Error creating table:", error); - } - }; - - function downloadQrCodeContainer({ selectedTable, shop }) { - const node = document.getElementById('qr-code-container'); - - if (!node) return; - - // Save the original background color - const originalBackgroundColor = node.style.backgroundColor; - - // Temporarily remove the background color - node.style.backgroundColor = 'transparent'; - - const isTableSelected = selectedTable != null; - - toPng(node, { pixelRatio: 2 }) // Adjust pixel ratio for higher resolution - .then((dataUrl) => { - const link = document.createElement('a'); - link.href = dataUrl; - - // Set the file name based on whether selectedTable exists - link.download = isTableSelected - ? `QR Meja (${selectedTable.tableNo}).png` - : `QR ${shop.name}.png`; - - link.click(); - }) - .catch((err) => { - console.error('Could not download the image', err); - }) - .finally(() => { - // Restore the original background color after the download - node.style.backgroundColor = originalBackgroundColor; - }); - } - - // This will hold the timeout ID so we can clear it when needed - const typingTimeoutRef = useRef(null); - - const handleInputChange = (e) => { - setIsLoading(true) - const updatedValue = e.target.value - .toLowerCase() - .replace(/ /g, '_') - .replace(/[^a-z0-9_]/g, ''); - setCafeIdentifyNameUpdate(updatedValue); - - // Clear the existing timeout - if (typingTimeoutRef.current) { - clearTimeout(typingTimeoutRef.current); - } - - // Set a new timeout - typingTimeoutRef.current = setTimeout(() => { - // Call the function to check if the name is already used - checkIfNameIsUsed(updatedValue); - }, 1000); // 1 second delay - }; - - const checkIfNameIsUsed = async (newIdentifyName) => { - // Replace this with your actual API call - try { - const response = await fetch(API_BASE_URL + `/cafe/check-identifyName/${newIdentifyName}`); - console.log(response) - if (response.ok) { - setIsLoading(false); - setIdentifyNameResponse(200) - } - else { - setIsLoading(false); - setIdentifyNameResponse(409) - } - } catch (error) { - - } - }; - - const [inputValue, setInputValue] = useState(shop.name); - const inputRef = useRef(null); - - // Monospace font for accurate character width calculation - const monospaceFont = 'monospace'; - - // Minimum width for the input field - const minWidth = 100; // Minimum width in pixels (adjust as necessary) - - // Function to handle input change and adjust the width - const handleInputNameChange = (e) => { - setInputValue(e.target.value); - }; - - // Resize the input width based on the number of characters typed - useEffect(() => { - if (inputRef.current) { - // Get the width of a single character - const characterWidth = 10; // You can adjust this value based on the actual font size - - // Calculate the required width based on the number of characters - const newWidth = characterWidth * inputValue?.length + 30; - - // Set the input width, ensuring it doesn't go below the minimum width - inputRef.current.style.width = `${Math.max(newWidth, minWidth)}px`; - } - }, [inputValue]); // Trigger effect when the input value changes - - return ( -
-

Identifikasi kedai

-
-
-
-
-
-
- -
- -
-
-
-
- -
-
-
-
- {window.location.hostname}/ -
- { - setIsConfigCafeIdentityName(true); // Set the state to true when input is focused - }} - /> - -
-
-
- - - -
-
-
-
- - -
-
- - - -
-
-
-
-
-
-
-
-
-
-
-
-
- -
- - KedaiMaster - - - . - -
- -
- - -
-
-
-
-
- - - -
-
-
-
-
-
-
-
-
-
-
-
- - - - {!isconfigcafeidentityname ? - <> -
-

Alamat kedai

-
-
-

-----------------

-
{ setIsConfigCafeIdentityName(true); cafeIdentifyNameRef.current && cafeIdentifyNameRef.current.focus(); }} style={styles.changeButton}>Ganti -
-
- : ( - <> - -
-

{cafeIdentifyNameUpdate == shop.cafeIdentifyName ? - '-----------------' : - !isLoading && identifyNameResponse == 200 ? - 'Alamat tersedia' - : - !isLoading && identifyNameResponse != 200 ? - 'Alamat terpakai' - : - < ColorRing - height="16" - width="16" style={{ marginTop: '5px' }} /> - - } -

-
- {identifyNameResponse == 199 && -
-
-

-

Perubahan alamat bisnis akan menyebabkan seluruh QR kode identifikasi kedai atau meja yang telah dicetak sebelumnya menjadi tidak berlaku.

-
-
{ setIdentifyNameResponse(200); }} style={styles.changeButton}>Batal -
-
{ setIsConfigCafeIdentityName(false); setIdentifyNameResponse(200); }} style={styles.changeButton}>Terapkan -
- -
-
-
- } -
-

{cafeIdentifyNameUpdate != shop.cafeIdentifyName && identifyNameResponse == 200 && !isLoading ? -

{ setIdentifyNameResponse(199) }} style={styles.changeButton2}>Terapkan -
- : - '----------'}

- {isconfigcafeidentityname ? -
{ setIdentifyNameResponse(0); setCafeIdentifyNameUpdate(shop.cafeIdentifyName); setIsConfigCafeIdentityName(false); }} style={styles.changeButton}>Batal -
- : -
{ setIsConfigCafeIdentityName(true); cafeIdentifyNameRef.current && cafeIdentifyNameRef.current.focus(); }} style={styles.changeButton}>Ganti -
- } -
- {/*
{ }} style={styles.changeButton}>Simpan - {isLoading && - - } -
*/} - - )} - -
-
-
- -  menu -
- -
-
-
- QR Code - QR Code -
50 ? -1 : 1})`, - position: "absolute", - fontSize: `${fontsize * 3}%`, - left: `${initialFontPos.left}%`, - top: `${initialFontPos.top}%`, - textAlign: initialFontPos.left > 50 ? 'right' : 'left' - }}> -

50 ? -1 : 1})`, - width: '200%', - lineHeight: 0, - fontFamily: 'Plus Jakarta Sans', - color: fontcolor - }} >{selectedTable == null ? (isConfigFont ? 'Nomor meja' : '') : selectedTable.tableNo}

-
- -
- {!isViewingQR ? - <> -
-

QR sticker kedai dan meja

-

Background

-
-
-

{ setIsConfig(!isConfig); setIsConfigQR(!isConfigQR); setIsConfigFont(false) }}> {isConfig ? '˅' : '˃'} Konfigurasi

-
qrBackgroundInputRef.current.click()} style={styles.changeButton}>Ganti
-
-
- {isConfig && -
-

{ setIsConfigQR(!isConfigQR); setIsConfigFont(false) }}> - {isConfigQR ? '˅' : '˃'} QR -

- {isConfigQR && <> -
- -
-
- -
-
- -
- } - -

{ setIsConfigFont(!isConfigFont); setIsConfigQR(false) }}> - {isConfigFont ? '˅' : '˃'} Nomor meja -

- {isConfigFont && ( - <> -
- -
-
- -
-
- -
-
- -
- - )} - - -
- } -

setIsViewTables(!isViewTables)}> - {isViewTables ? '˅' : '˃'} Daftar meja -

- - {isViewTables && -
-
- setTableNo(e.target.value)} value={tableNo} /> -
Buat meja
-
- {tables && tables - .filter((table) => table.tableNo !== 0) - .map((table) => ( -
  • setSelectedTable(selectedTable == table ? null : table)} - > -
    - {table.tableNo} -
    -
  • - )) - } -
    - } -
    -
    - - -
    - : - <> -
    -

    Ini adalah QR yang dapat di scan oleh tamu untuk memesan

    - {/*

    QR ini akan menjadi identifikasi bahwa pelanggan memesan dari {selectedTable? `(${selectedTable?.tableNo})` : 'link kafe ini. Untuk mengantarkan pelanggan ke meja yang teridentifikasi, anda perlu membuat meja.'}

    */} -
    -
    - -
    -
    -

    setIsViewingQR(false)} > - Kembali -

    -
    - - } -
    - ); -}; - -// 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: '8px', - position: "relative", - width: "100%", - height: "200px", - backgroundSize: "contain", - overflow: "hidden", - margin: "0 auto", // Center the QR code container - }, - uploadMessage: { - fontWeight: 600, - textAlign: "left", - fontSize: "15px" - }, - changeButton: { - paddingRight: '10px', - backgroundColor: 'rgb(40, 167, 69)', - borderRadius: '30px', - color: 'white', - fontWeight: 700, - height: '31px', - lineHeight: '32px', - paddingLeft: '10px', - paddingHeight: '10px', - marginBottom: '22px', - width: '80px', - textAlign: 'center' - }, - changeButton2: { - color: 'black', - fontWeight: 700, - textAlign: 'left' - }, - uploadButton: { - paddingRight: '10px', - backgroundColor: 'green', - borderRadius: '30px', - color: 'white', - fontWeight: 700, - height: '36px', - lineHeight: '36px', - paddingLeft: '10px', - paddingHeight: '10px', - width: '40%', - textAlign: 'center' - }, - resultMessage: { - marginTop: "-24px", - // marginBottom: "10px", - textAlign: "left", - display: 'flex', - justifyContent: 'space-between' - }, - resultMessage2: { - marginTop: "-24px", - marginBottom: "10px", - textAlign: "left", - display: 'flex', - justifyContent: 'space-evenly' - }, - buttonContainer: { - marginTop: "20px", - textAlign: "left", - display: 'flex', - justifyContent: 'space-evenly' - }, - backButtonContainer: { - marginTop: "2px", - marginBottom: "-10px", - textAlign: "left", - display: 'flex', - justifyContent: 'space-evenly' - }, - saveButton: { - padding: '6px 15px', - fontSize: '13px', - backgroundColor: 'rgb(40, 167, 69)', - color: 'rgb(255, 255, 255)', - border: ' none', - borderRadius: '30px', - cursor: 'pointer', - transition: 'background-color 0.3s', - }, - switchContainer: { - textAlign: "left", - marginTop: '-15px' - }, - description: { - margin: "10px 0", - }, - sliderContainer: { - marginBottom: "20px", - }, - label: { - display: "block", - marginBottom: "10px", - }, - sliderWrapper: { - display: "flex", - alignItems: "center", - }, - input: { - flex: "1", - margin: "0 10px", - }, -}; - -export default SetPaymentQr; diff --git a/src/index.css b/src/index.css index d61228b..c979be0 100644 --- a/src/index.css +++ b/src/index.css @@ -17,15 +17,16 @@ code { /* Ensure proper scrolling behavior */ html, body { - height: 100%; width: 100%; - overflow: hidden; + min-height: 100%; + overflow-x: hidden; + overflow-y: auto; } #root { - height: 100%; width: 100%; - overflow: hidden; + min-height: 100%; + overflow: visible; } /* Custom scrollbar */ @@ -95,4 +96,4 @@ html, body { max-height: 80vh; overflow-y: auto; } -} \ No newline at end of file +} diff --git a/src/pages/Cart.js b/src/pages/Cart.js index 1b95853..e3c6426 100644 --- a/src/pages/Cart.js +++ b/src/pages/Cart.js @@ -1,5 +1,6 @@ import React, { useRef, useEffect, useState } from "react"; import styles from "./Invoice.module.css"; +import cartStyles from "./CartPage.module.css"; import { useParams } from "react-router-dom"; // Changed from useSearchParams to useLocation import { ThreeDots, ColorRing } from "react-loader-spinner"; @@ -384,7 +385,13 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType return (
    0 ? '' : '100vh'), minHeight: (getItemsByCafeId(shopId).length > 0 ? '100vh' : '') }}> -
    Keranjang
    +
    +
    + +
    +
    Keranjang
    +
    +
    {(transactionData == null && getItemsByCafeId(shopId).length < 1) ?
    @@ -443,21 +450,21 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
    )} -
    - Catatan : - -
    - -
    -