This commit is contained in:
karyamanswasta
2025-08-27 05:54:02 +07:00
parent f58b40c70d
commit df203447a9
20 changed files with 2344 additions and 1701 deletions

View File

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

View File

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

View File

@@ -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 = ({
</div>
<div className={styles.formSection}>
<div className={styles.bannerRow}>
{saveStatus === 'success' && (
<span className={`${styles.banner} ${styles.bannerSuccess}`}>Perubahan disimpan</span>
)}
{saveStatus === 'error' && (
<span className={`${styles.banner} ${styles.bannerError}`}>Gagal menyimpan perubahan</span>
)}
</div>
<div className={styles.formGroup}>
<label className={styles.formLabel}>Nama Item</label>
<input
@@ -167,17 +191,19 @@ const ItemConfig = ({
</div>
<div className={styles.formActions}>
<button
onClick={cancelEdit}
<button
onClick={cancelEdit}
className={`${styles.formButton} ${styles.cancelButton}`}
disabled={saving}
>
Batal
</button>
<button
onClick={() => {isBeingEdit ? handleUpdate() : handleCreate()}}
<button
onClick={() => { isBeingEdit ? handleUpdate() : handleCreate() }}
className={`${styles.formButton} ${styles.saveButton}`}
disabled={saving}
>
{isBeingEdit ? 'Simpan Perubahan' : 'Buat Item'}
{saving ? 'Menyimpan…' : (isBeingEdit ? 'Simpan Perubahan' : 'Buat Item')}
</button>
</div>
</div>
@@ -186,4 +212,4 @@ const ItemConfig = ({
);
};
export default ItemConfig;
export default ItemConfig;

View File

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

View File

@@ -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 (
<div className="item-type-lister" style={{ overflowX: isAddingNewItem ? 'hidden' : 'auto' }}>
<div className="centered-item-type-list">
<div ref={newItemDivRef} className="item-type-list" style={{ display: 'inline-flex' }}>
<div className="compact-centered-list">
<div ref={newItemDivRef} className="compact-item-type-list" style={{ display: 'inline-flex' }}>
{isEditMode && !isAddingNewItem && canManage && (
<ItemType
<div
className="compact-item-type compact-add-item"
onClick={toggleAddNewItem}
name={"buat baru"}
noIcon={true}
compact={false} // Make it larger for better touch targets
/>
>
Buat baru
</div>
)}
{canManage && isAddingNewItem && (
@@ -91,24 +90,22 @@ const ItemTypeLister = ({
)}
{itemTypes && itemTypes.length > 0 && (
<ItemType
name={"semua"}
<div
className={`compact-item-type ${filterId === 0 ? 'selected' : ''}`}
onClick={() => onFilterChange(0)}
selected={filterId === 0}
noIcon={true}
compact={false} // Make it larger for better touch targets
/>
>
Semua
</div>
)}
{itemTypes && itemTypes.map((itemType) => (
<ItemType
<div
key={itemType.itemTypeId}
name={itemType.name}
className={`compact-item-type ${filterId === itemType.itemTypeId ? 'selected' : ''}`}
onClick={() => onFilterChange(itemType.itemTypeId)}
selected={filterId === itemType.itemTypeId}
noIcon={true}
compact={false} // Make it larger for better touch targets
/>
>
{formatName(itemType.name)}
</div>
))}
</div>
</div>

View File

@@ -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 (
<div key={updateKey} onClick={handleOverlayClick} className={styles.modalOverlay}>
<div className={styles.modalContent} onClick={handleContentClick}>
<div className={`${styles.modalContent} ${(modalContent === 'edit_tables' || modalContent === 'payment_option' || modalContent === 'create_clerk') ? styles.modalContentWide : ''}`} onClick={handleContentClick}>
{modalContent === "edit_account" && <AccountUpdatePage user={user} />}
{modalContent === "reset-password" && <ResetPassword />}
@@ -86,7 +86,7 @@ const Modal = ({ user, shop, isOpen, onClose, modalContent, deviceType, setModal
{modalContent === "create_clerk" && <CreateClerk shopId={shop.cafeId} />}
{modalContent === "create_kedai" && <CreateCafe shopId={shop.cafeId} />}
{modalContent === "create_tenant" && <CreateTenant shopId={shop.cafeId} />}
{modalContent === "edit_tables" && <TablesPage shop={shop} />}
{modalContent === "edit_tables" && <IdentifyCafeModal shop={shop} />}
{modalContent === "new_transaction" && (
<Transaction propsShopId={shop.cafeId} handleMoveToTransaction={handleMoveToTransaction} depth={depth} shopImg={shopImg} setModal={setModal}/>
)}

View File

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

View File

@@ -1,18 +1,13 @@
import React, { useState, useRef, useEffect } from "react";
import QrScanner from "qr-scanner"; // Import qr-scanner
import { getImageUrl } from "../helpers/itemHelper";
import {
getCafe,
saveCafeDetails,
setConfirmationStatus,
setOpenBillAvailability
} from "../helpers/cafeHelpers";
import { getCafe, saveCafeDetails } from "../helpers/cafeHelpers";
import Switch from "react-switch"; // Import the Switch component
import styles from "./PaymentOptions.module.css";
const SetPaymentQr = ({ shopId,
qrCodeUrl }) => {
const [qrPosition, setQrPosition] = useState([50, 50]);
const [qrSize, setQrSize] = useState(50);
const SetPaymentQr = ({ shopId, qrCodeUrl }) => {
const [qrPosition, setQrPosition] = useState([50, 50]); // legacy kept for API compatibility
const [qrSize, setQrSize] = useState(50); // legacy kept for API compatibility
const [qrPayment, setQrPayment] = useState();
const [qrPaymentFile, setQrPaymentFile] = useState();
const [qrCodeDetected, setQrCodeDetected] = useState(false);
@@ -26,6 +21,9 @@ const SetPaymentQr = ({ shopId,
const [isConfigQRIS, setIsConfigQRIS] = useState(false);
const [isOpenBillAvailable, setIsOpenBillAvailable] = useState(false);
const [saving, setSaving] = useState(false);
const [saveStatus, setSaveStatus] = useState(null); // 'success' | 'error'
const [copied, setCopied] = useState(false);
useEffect(() => {
const fetchCafe = async () => {
@@ -104,6 +102,8 @@ const SetPaymentQr = ({ shopId,
// Save cafe details
const handleSave = async () => {
setSaving(true);
setSaveStatus(null);
let qrPaymentFileCache;
console.log(qrPaymentFile)
if(qrPaymentFile != null)
@@ -120,240 +120,141 @@ const SetPaymentQr = ({ shopId,
try {
const response = await saveCafeDetails(cafe.cafeId, details);
setIsNeedConfirmationState(response.needsConfirmation ? 1 : 0); // Update state after saving
setIsQRISavailable(response.isQRISavailable ? 1 : 0); // Update state after saving
setIsOpenBillAvailable(response.isOpenBillAvailable ? 1 : 0); // Update state after saving
setIsNeedConfirmationState(response.needsConfirmation ? 1 : 0);
setIsQRISavailable(response.isQRISavailable ? 1 : 0);
setIsOpenBillAvailable(response.isOpenBillAvailable ? 1 : 0);
setSaveStatus('success');
console.log("Cafe details saved:", response);
} catch (error) {
console.error("Error saving cafe details:", error);
setSaveStatus('error');
} finally {
setSaving(false);
}
};
const copyQrData = async () => {
if (!qrCodeData) return;
try {
await navigator.clipboard.writeText(qrCodeData);
setCopied(true);
setTimeout(()=>setCopied(false), 1200);
} catch {}
};
return (
<div style={styles.container}>
<h3 style={styles.title}>Konfigurasi pembayaran</h3>
<div className={styles.container}>
<h3 className={styles.title}>Konfigurasi pembayaran</h3>
<div style={styles.switchContainer}>
<p style={styles.uploadMessage}>
Pembayaran QRIS.
</p>
<div className={styles.section}>
<div className={styles.sectionHeader}>
<div>
<div className={styles.sectionTitle}>Pembayaran QRIS</div>
<div className={styles.sectionDesc}>Aktifkan agar pelanggan dapat membayar via QRIS. Kasir tetap perlu verifikasi rekening.</div>
</div>
<Switch
onChange={(checked) => setIsQRISavailable(checked ? 1 : 0)}
checked={isQRISavailable === 1}
offColor="#888"
onColor="#4CAF50"
uncheckedIcon={false}
checkedIcon={false}
height={25}
width={50}
/>
</div>
{isConfigQRIS ?
<div className={styles.row}>
<button
type="button"
className={`${styles.button} ${styles.configButton}`}
onClick={() => isQRISavailable === 1 && setIsConfigQRIS(true)}
disabled={isQRISavailable !== 1}
>
Konfigurasi QRIS
</button>
</div>
{isConfigQRIS && (
<>
<div
id="qr-code-container"
ref={qrCodeContainerRef}
className={styles.imageBox}
onClick={() => qrPaymentInputRef.current.click()}
style={{
...styles.qrCodeContainer,
backgroundImage: `url(${qrPayment})`,
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
backgroundSize: "contain",
}}
style={{ backgroundImage: `url(${qrPayment})` }}
>
<input
type="file"
accept="image/*"
ref={qrPaymentInputRef}
style={{ display: "none" }}
onChange={handleFileChange}
/>
<input type="file" accept="image/*" ref={qrPaymentInputRef} style={{ display: 'none' }} onChange={handleFileChange} />
</div>
<div style={styles.uploadMessage}>
<p>Klik untuk ganti background</p>
</div>
<div style={styles.resultMessage}>
{qrCodeDetected && qrPayment !== 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPsNr0TPq8dHT3nBwDQ6OHQQTrqzVFoeBOmuWfgyErrLbJi6f6CnnYhpNHEvkJ_2X-kyI&usqp=CAU' ? <p>QR terdeteksi</p> : <p>Tidak ada qr terdeteksi</p>}
{qrCodeDetected && qrPayment !== 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPsNr0TPq8dHT3nBwDQ6OHQQTrqzVFoeBOmuWfgyErrLbJi6f6CnnYhpNHEvkJ_2X-kyI&usqp=CAU' ? <div
onClick={() => qrPaymentInputRef.current.click()} style={styles.uploadButton}>Ganti</div> : <div
onClick={() => qrPaymentInputRef.current.click()} style={styles.uploadButton}>Unggah</div>}
</div>
<div onClick={() => setIsConfigQRIS(false)}
style={{
...styles.qrisConfigButton,
width: '100%',
marginLeft: "0",
}}
>Terapkan</div>
</>
:
<>
<p style={styles.description}>
Aktifkan fitur agar pelanggan dapat menggunakan opsi pembayaran QRIS, namun kasir anda perlu memeriksa rekening untuk memastikan pembayaran.
</p>
<div style={{ display: 'flex' }}>
<Switch
onChange={(checked) => setIsQRISavailable(checked ? 1 : 0)}
checked={isQRISavailable === 1} // Convert to boolean
offColor="#888"
onColor="#4CAF50"
uncheckedIcon={false}
checkedIcon={false}
height={25}
width={50}
/>
<div
onClick={() => setIsConfigQRIS(true)}
style={{
...styles.qrisConfigButton,
backgroundColor: isQRISavailable == 1 ? styles.qrisConfigButton.backgroundColor : 'gray',
}}
>
Konfigurasi QRIS
<div className={styles.smallNote}>Klik area untuk unggah/ganti gambar QR</div>
<div className={styles.detectRow}>
<div className={`${styles.tag} ${qrCodeDetected ? styles.tagOk : styles.tagBad}`}>
{qrCodeDetected && qrPayment !== 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPsNr0TPq8dHT3nBwDQ6OHQQTrqzVFoeBOmuWfgyErrLbJi6f6CnnYhpNHEvkJ_2X-kyI&usqp=CAU' ? 'QR terdeteksi' : 'Tidak ada QR terdeteksi'}
</div>
<button className={styles.button} onClick={() => qrPaymentInputRef.current.click()}>{qrPayment ? 'Ganti' : 'Unggah'}</button>
</div>
{qrCodeDetected && (
<div className={styles.copyRow}>
<input className={styles.linkField} readOnly value={qrCodeData || ''} />
<button className={styles.button} onClick={copyQrData}>{copied ? 'Disalin' : 'Salin'}</button>
</div>
)}
<div className={styles.actionsRight}>
<button className={`${styles.button} ${styles.primary}`} onClick={() => setIsConfigQRIS(false)}>Terapkan</button>
</div>
</>
}
</div>
<div style={styles.switchContainer}>
<p style={styles.uploadMessage}>
Open bill
</p>
<p style={styles.description}>
Aktifkan fitur agar pelanggan dapat menambahkan pesanan selama sesi berlangsung tanpa perlu melakukan transaksi baru dan hanya membayar di akhir.
</p>
<Switch
onChange={(checked) => setIsOpenBillAvailable(checked ? 1 : 0)}
checked={isOpenBillAvailable === 1} // Convert to boolean
offColor="#888"
onColor="#4CAF50"
uncheckedIcon={false}
checkedIcon={false}
height={25}
width={50}
/>
)}
</div>
<div style={styles.switchContainer}>
<p style={styles.uploadMessage}>
Pengecekan ganda
</p>
<p style={styles.description}>
Nyalakan agar kasir memeriksa kembali ketersediaan produk sebelum pelanggan membayar.
</p>
<Switch
onChange={(checked) => setIsNeedConfirmationState(checked ? 1 : 0)}
checked={isNeedConfirmationState === 1} // Convert to boolean
offColor="#888"
onColor="#4CAF50"
uncheckedIcon={false}
checkedIcon={false}
height={25}
width={50}
/>
<div className={styles.section}>
<div className={styles.sectionHeader}>
<div>
<div className={styles.sectionTitle}>Open bill</div>
<div className={styles.sectionDesc}>Izinkan pelanggan menambah pesanan dalam satu sesi dan bayar di akhir.</div>
</div>
<Switch
onChange={(checked) => setIsOpenBillAvailable(checked ? 1 : 0)}
checked={isOpenBillAvailable === 1}
offColor="#888"
onColor="#4CAF50"
uncheckedIcon={false}
checkedIcon={false}
height={25}
width={50}
/>
</div>
</div>
<div style={styles.buttonContainer}>
<button onClick={handleSave} style={styles.saveButton}>
Simpan
<div className={styles.section}>
<div className={styles.sectionHeader}>
<div>
<div className={styles.sectionTitle}>Pengecekan ganda</div>
<div className={styles.sectionDesc}>Kasir memeriksa kembali ketersediaan item sebelum pembayaran.</div>
</div>
<Switch
onChange={(checked) => setIsNeedConfirmationState(checked ? 1 : 0)}
checked={isNeedConfirmationState === 1}
offColor="#888"
onColor="#4CAF50"
uncheckedIcon={false}
checkedIcon={false}
height={25}
width={50}
/>
</div>
</div>
<div className={styles.footer}>
<div>
{saveStatus === 'success' && <span className={`${styles.banner} ${styles.bannerSuccess}`}>Simpan berhasil</span>}
{saveStatus === 'error' && <span className={`${styles.banner} ${styles.bannerError}`}>Gagal menyimpan</span>}
</div>
<button className={`${styles.button} ${styles.primary}`} onClick={handleSave} disabled={saving}>
{saving ? 'Menyimpan…' : 'Simpan'}
</button>
</div>
</div>
);
};
// Styles
const styles = {
container: {
position: 'relative',
overflowY: 'auto',
overflowX: 'hidden',
maxHeight: '80vh',
width: '100%',
backgroundColor: "white",
padding: "20px",
borderRadius: "8px",
boxShadow: "0 2px 10px rgba(0, 0, 0, 0.1)",
textAlign: "center", // Center text and children
},
title: {
marginBottom: "20px",
fontWeight: "bold",
},
qrCodeContainer: {
backgroundColor: '#999999',
borderRadius: '20px',
position: "relative",
width: "100%",
height: "200px",
backgroundSize: "contain",
overflow: "hidden",
margin: "0 auto", // Center the QR code container
marginTop: '10px'
},
uploadMessage: {
fontWeight: 600,
textAlign: "left",
},
qrisConfigButton: {
borderRadius: '15px',
backgroundColor: '#28a745',
width: '144px',
textAlign: 'center',
color: 'white',
lineHeight: '24px',
marginLeft: '14px',
},
uploadButton: {
paddingRight: '10px',
backgroundColor: '#28a745',
borderRadius: '30px',
color: 'white',
fontWeight: 700,
height: '36px',
lineHeight: '36px',
paddingLeft: '10px',
paddingHeight: '10px',
},
resultMessage: {
marginTop: "-24px",
textAlign: "left",
display: 'flex',
justifyContent: 'space-between'
},
buttonContainer: {
marginTop: "20px",
textAlign: "left",
},
saveButton: {
padding: "10px 20px",
fontSize: "16px",
backgroundColor: "#28a745",
color: "#fff",
border: "none",
borderRadius: "30px",
cursor: "pointer",
transition: "background-color 0.3s",
},
switchContainer: {
textAlign: "left",
},
description: {
margin: "10px 0",
fontSize: "14px",
color: "#666",
},
sliderContainer: {
marginBottom: "20px",
},
label: {
display: "block",
marginBottom: "10px",
},
sliderWrapper: {
display: "flex",
alignItems: "center",
},
input: {
flex: "1",
margin: "0 10px",
},
};
export default SetPaymentQr;

View File

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

File diff suppressed because it is too large Load Diff