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
+
+
+ {copied ? 'Disalin' : 'Salin'}
+
+
+ )}
+
+ {currentStep === 2 && (
+
+
Desain QR
+
+ applyPreset('center')}>Tengah
+ applyPreset('topLeft')}>Atas-Kiri
+ applyPreset('bottomRight')}>Bawah-Kanan
+ Reset desain
+
+
+
+
+ {bgImageUrl &&
}
+
+
{selectedTable ? selectedTable.tableNo : 'Kedai'}
+
+
+
+
+
Tip: Gunakan latar yang kontras agar QR mudah dipindai.
+
+ )}
+
+ {currentStep === 3 && (
+
+
Daftar meja
+
+ setNewTableNo(e.target.value)} />
+ Buat meja
+
+
+ {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).
+
+ )}
+
+
+
+
+ setCurrentStep((s)=>Math.max(1, s-1))}>Kembali
+ {currentStep < 3 ? (
+ setCurrentStep((s)=>Math.min(3, s+1))}>Lanjut
+ ) : (
+ {saving ? 'Menyimpan…' : 'Simpan'}
+ )}
+
+
+ {saveStatus === 'success' &&
Simpan berhasil
}
+ {saveStatus === 'error' &&
Gagal menyimpan
}
+
Download QR {selectedTable ? 'meja' : 'kedai'}
+
+
+
+ );
+}
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 ?
+
+ isQRISavailable === 1 && setIsConfigQRIS(true)}
+ disabled={isQRISavailable !== 1}
+ >
+ Konfigurasi QRIS
+
+
+
+ {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'}
-
+
qrPaymentInputRef.current.click()}>{qrPayment ? 'Ganti' : 'Unggah'}
+
+ {qrCodeDetected && (
+
+
+ {copied ? 'Disalin' : 'Salin'}
+
+ )}
+
+ setIsConfigQRIS(false)}>Terapkan
>
- }
-
-
-
- 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}
+ />
+
-
-
- Simpan
+
+
+
+
Pengecekan ganda
+
Kasir memeriksa kembali ketersediaan item sebelum pembayaran.
+
+
setIsNeedConfirmationState(checked ? 1 : 0)}
+ checked={isNeedConfirmationState === 1}
+ offColor="#888"
+ onColor="#4CAF50"
+ uncheckedIcon={false}
+ checkedIcon={false}
+ height={25}
+ width={50}
+ />
+
+
+
+
+
+ {saveStatus === 'success' && Simpan berhasil }
+ {saveStatus === 'error' && Gagal menyimpan }
+
+
+ {saving ? 'Menyimpan…' : 'Simpan'}
);
};
-// 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 ?
- <>
-
-
-
-----------------
-
{ 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
-
-
-
-
-
-
-
-
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 && <>
-
-
- QR Code Size:
-
- 10%
-
- {initialSize}%
-
-
-
-
-
- QR Code Position X:
-
- 0%
-
- {initialPos.left}%
-
-
-
-
-
- QR Code Position Y:
-
- 0%
-
- {initialPos.top}%
-
-
-
- >}
-
-
{ setIsConfigFont(!isConfigFont); setIsConfigQR(false) }}>
- {isConfigFont ? '˅' : '˃'} Nomor meja
-
- {isConfigFont && (
- <>
-
-
- Ukuran nomor meja:
-
- 10%
-
- {fontsize}%
-
-
-
-
-
- Posisi nomor meja - horizontal:
-
- 0%
-
- {initialFontPos.left}%
-
-
-
-
-
- Posisi nomor meja - vertikal:
-
- 0%
-
- {initialFontPos.top}%
-
-
-
-
-
- Warna nomor meja:
-
- setfontcolor(e.target.value)} // Update the font color state
- style={styles.colorInput} // Add your styles for the color input if needed
- />
- {fontcolor} {/* Display the selected color value */}
-
-
-
- >
- )}
-
-
-
- }
-
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}
-
-
- ))
- }
-
- }
-
-
-
- Simpan
-
- setIsViewingQR(true)} style={styles.saveButton}>
- Download QR {selectedTable ? 'meja' : 'kedai'}
-
-
- > :
- <>
-
-
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.'}
*/}
-
-
- downloadQrCodeContainer({ selectedTable, shop })} style={styles.saveButton}>
- Download QR {selectedTable ? 'meja' : 'kedai'}
-
-
-
-
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' : '') }}>
-
+
+
{(transactionData == null && getItemsByCafeId(shopId).length < 1) ?
@@ -443,21 +450,21 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
)}
-
- Catatan :
-
-
-
-
-
-
}
+ {getItemsByCafeId(shopId).length > 0 && (
+
+
Catatan Untuk Kasir
+
+
+
+ )}
+
{transactionData &&
{transactionData.payment_type != 'paylater' ?
@@ -575,6 +582,7 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
>
)
}
+
);
}
diff --git a/src/pages/CartPage.module.css b/src/pages/CartPage.module.css
new file mode 100644
index 0000000..8b253ae
--- /dev/null
+++ b/src/pages/CartPage.module.css
@@ -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;
+}
diff --git a/src/pages/CreateClerk.js b/src/pages/CreateClerk.js
index 12836b1..3fa4c64 100644
--- a/src/pages/CreateClerk.js
+++ b/src/pages/CreateClerk.js
@@ -1,119 +1,153 @@
-import React, { useState } from 'react';
-import { createClerks } from '../helpers/userHelpers'; // Adjust the import path as needed
+import React, { useEffect, useMemo, useState } from 'react';
+import { createClerks, getClerks } from '../helpers/userHelpers';
import { useLocation } from "react-router-dom";
+import styles from './CreateClerk.module.css';
const CreateClerk = ({ shopId }) => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
- const [loading, setLoading] = useState(false);
- const [message, setMessage] = useState('');
+ const [showPassword, setShowPassword] = useState(false);
+ const [saving, setSaving] = useState(false);
+ const [banner, setBanner] = useState(null); // { type: 'success'|'error', text: string }
+ const [clerks, setClerks] = useState([]);
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const cafeIdParam = queryParams.get("cafeId");
+ const effectiveShopId = useMemo(()=> shopId || cafeIdParam, [shopId, cafeIdParam]);
+
+ useEffect(()=>{
+ const load = async ()=>{
+ if (!effectiveShopId) return;
+ try {
+ const data = await getClerks(effectiveShopId);
+ if (data && Array.isArray(data)) setClerks(data);
+ } catch (e) {}
+ };
+ load();
+ }, [effectiveShopId]);
+
+ const generatePassword = () => {
+ const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%';
+ let pwd = '';
+ for (let i = 0; i < 12; i++) pwd += chars[Math.floor(Math.random()*chars.length)];
+ setPassword(pwd);
+ };
const handleSubmit = async (event) => {
event.preventDefault();
- setLoading(true);
- setMessage('');
+ setSaving(true);
+ setBanner(null);
// Basic validation
if (!username || !password) {
- setMessage('Username and password are required');
- setLoading(false);
+ setBanner({ type: 'error', text: 'Username dan password wajib diisi' });
+ setSaving(false);
+ return;
+ }
+ if (username.length < 3) {
+ setBanner({ type: 'error', text: 'Username minimal 3 karakter' });
+ setSaving(false);
+ return;
+ }
+ if (password.length < 6) {
+ setBanner({ type: 'error', text: 'Password minimal 6 karakter' });
+ setSaving(false);
return;
}
try {
- const create = await createClerks(shopId || cafeIdParam, username, password);
-
- if (create) setMessage('Clerk created successfully');
- else setMessage('Failed to create clerk');
+ const create = await createClerks(effectiveShopId, username, password);
+ if (create) {
+ setBanner({ type: 'success', text: 'Kasir berhasil ditambahkan' });
+ // Refresh list
+ try {
+ const data = await getClerks(effectiveShopId);
+ if (data && Array.isArray(data)) setClerks(data);
+ } catch {}
+ // Clear form
+ setUsername('');
+ setPassword('');
+ } else {
+ setBanner({ type: 'error', text: 'Gagal menambahkan kasir' });
+ }
} catch (error) {
- setMessage('Error creating clerk');
+ setBanner({ type: 'error', text: 'Terjadi kesalahan saat menambahkan kasir' });
} finally {
- setLoading(false);
+ setSaving(false);
}
};
return (
-