ok
This commit is contained in:
348
src/components/IdentifyCafeModal.js
Normal file
348
src/components/IdentifyCafeModal.js
Normal file
@@ -0,0 +1,348 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import styles from "./IdentifyCafeModal.module.css";
|
||||
import API_BASE_URL from "../config.js";
|
||||
import { getTables, createTable } from "../helpers/tableHelper";
|
||||
import { saveCafeDetails } from "../helpers/cafeHelpers";
|
||||
import { getImageUrl } from "../helpers/itemHelper";
|
||||
import { toPng } from "html-to-image";
|
||||
|
||||
export default function IdentifyCafeModal({ shop }) {
|
||||
const [cafeIdentifyName, setCafeIdentifyName] = useState(shop.cafeIdentifyName || "");
|
||||
const [availability, setAvailability] = useState(null); // 200 ok, 409 taken
|
||||
const [checking, setChecking] = useState(false);
|
||||
|
||||
const [tables, setTables] = useState([]);
|
||||
const [selectedTable, setSelectedTable] = useState(null);
|
||||
|
||||
const [qrSize, setQrSize] = useState(Number(shop.scale) || 1);
|
||||
const [qrX, setQrX] = useState(Number(shop.xposition) || 50);
|
||||
const [qrY, setQrY] = useState(Number(shop.yposition) || 50);
|
||||
const [fontSize, setFontSize] = useState(Number(shop.fontsize) || 16);
|
||||
const [fontColor, setFontColor] = useState(shop.fontcolor || "#FFFFFF");
|
||||
const [fontX, setFontX] = useState(Number(shop.fontxposition) || 50);
|
||||
const [fontY, setFontY] = useState(Number(shop.fontyposition) || 85);
|
||||
const [bgImageUrl, setBgImageUrl] = useState(getImageUrl(shop.qrBackground));
|
||||
const bgFileRef = useRef(null);
|
||||
|
||||
const [newTableNo, setNewTableNo] = useState("");
|
||||
const previewRef = useRef(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [currentStep, setCurrentStep] = useState(1); // 1=Alamat, 2=Desain QR, 3=Meja
|
||||
const [saveStatus, setSaveStatus] = useState(null); // 'success' | 'error'
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const initialDesignRef = useRef({
|
||||
qrSize: Number(shop.scale) || 1,
|
||||
qrX: Number(shop.xposition) || 50,
|
||||
qrY: Number(shop.yposition) || 50,
|
||||
fontSize: Number(shop.fontsize) || 16,
|
||||
fontColor: shop.fontcolor || "#FFFFFF",
|
||||
fontX: Number(shop.fontxposition) || 50,
|
||||
fontY: Number(shop.fontyposition) || 85,
|
||||
bgImageUrl: getImageUrl(shop.qrBackground),
|
||||
});
|
||||
|
||||
const shopHost = useMemo(() => window.location.hostname, []);
|
||||
const fullLink = useMemo(() => `${shopHost}/${cafeIdentifyName}`, [shopHost, cafeIdentifyName]);
|
||||
|
||||
// Debounced availability check
|
||||
const debounceRef = useRef(null);
|
||||
const handleIdentifyChange = (e) => {
|
||||
const val = e.target.value
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, "_")
|
||||
.replace(/[^a-z0-9_]/g, "");
|
||||
setCafeIdentifyName(val);
|
||||
setChecking(true);
|
||||
setAvailability(null);
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||
debounceRef.current = setTimeout(async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/cafe/check-identifyName/${val}`);
|
||||
setAvailability(res.ok ? 200 : 409);
|
||||
} catch (_) {
|
||||
setAvailability(409);
|
||||
} finally {
|
||||
setChecking(false);
|
||||
}
|
||||
}, 600);
|
||||
};
|
||||
|
||||
// Load tables
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const fetched = await getTables(shop.cafeId);
|
||||
setTables(fetched || []);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
})();
|
||||
}, [shop.cafeId]);
|
||||
|
||||
const handleUploadBg = (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
const url = URL.createObjectURL(file);
|
||||
setBgImageUrl(url);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateTable = async () => {
|
||||
if (!newTableNo) return;
|
||||
try {
|
||||
const created = await createTable(shop.cafeId, { tableNo: newTableNo });
|
||||
setTables((t) => [...t, created]);
|
||||
setNewTableNo("");
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
setSaveStatus(null);
|
||||
const qrBackgroundFile = bgFileRef.current?.files?.[0];
|
||||
const details = {
|
||||
qrSize,
|
||||
qrPosition: { left: qrX, top: qrY },
|
||||
qrBackgroundFile,
|
||||
fontsize: fontSize,
|
||||
fontcolor: fontColor,
|
||||
fontPosition: { left: fontX, top: fontY },
|
||||
cafeIdentifyName: shop.cafeIdentifyName !== cafeIdentifyName ? cafeIdentifyName : null,
|
||||
};
|
||||
try {
|
||||
await saveCafeDetails(shop.cafeId, details);
|
||||
setSaveStatus('success');
|
||||
} catch (e) {
|
||||
setSaveStatus('error');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const downloadPreview = async () => {
|
||||
if (!previewRef.current) return;
|
||||
const node = previewRef.current;
|
||||
const originalBg = node.style.backgroundColor;
|
||||
node.style.backgroundColor = "transparent";
|
||||
try {
|
||||
const dataUrl = await toPng(node, { pixelRatio: 2 });
|
||||
const link = document.createElement("a");
|
||||
link.href = dataUrl;
|
||||
link.download = selectedTable ? `QR Meja (${selectedTable.tableNo}).png` : `QR ${shop.name}.png`;
|
||||
link.click();
|
||||
} catch (e) {
|
||||
// noop
|
||||
} finally {
|
||||
node.style.backgroundColor = originalBg;
|
||||
}
|
||||
};
|
||||
|
||||
const copyLink = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(fullLink);
|
||||
setCopied(true);
|
||||
setTimeout(()=>setCopied(false), 1400);
|
||||
} catch (_) {}
|
||||
};
|
||||
|
||||
const applyPreset = (preset) => {
|
||||
if (preset === 'center') {
|
||||
setQrX(50); setQrY(50); setQrSize(1);
|
||||
setFontX(50); setFontY(85);
|
||||
} else if (preset === 'topLeft') {
|
||||
setQrX(25); setQrY(30); setQrSize(1);
|
||||
setFontX(50); setFontY(85);
|
||||
} else if (preset === 'bottomRight') {
|
||||
setQrX(75); setQrY(70); setQrSize(1);
|
||||
setFontX(50); setFontY(15);
|
||||
}
|
||||
};
|
||||
|
||||
const resetDesign = () => {
|
||||
const d = initialDesignRef.current;
|
||||
setQrSize(d.qrSize);
|
||||
setQrX(d.qrX);
|
||||
setQrY(d.qrY);
|
||||
setFontSize(d.fontSize);
|
||||
setFontColor(d.fontColor);
|
||||
setFontX(d.fontX);
|
||||
setFontY(d.fontY);
|
||||
setBgImageUrl(d.bgImageUrl);
|
||||
if (bgFileRef.current) bgFileRef.current.value = '';
|
||||
};
|
||||
|
||||
// Positioning helpers
|
||||
const qrStyle = {
|
||||
left: `${qrX}%`,
|
||||
top: `${qrY}%`,
|
||||
transform: `translate(-50%, -50%) scale(${qrSize})`,
|
||||
};
|
||||
const fontStyle = {
|
||||
left: `${fontX}%`,
|
||||
top: `${fontY}%`,
|
||||
transform: `translate(-50%, -50%)`,
|
||||
color: fontColor,
|
||||
fontSize: `${fontSize}px`,
|
||||
position: "absolute",
|
||||
fontWeight: 700,
|
||||
};
|
||||
|
||||
const qrData = selectedTable ? `${fullLink}/${selectedTable.tableNo}` : fullLink;
|
||||
|
||||
const canProceedFromStep1 = () => {
|
||||
if (!cafeIdentifyName) return false;
|
||||
if (cafeIdentifyName === (shop.cafeIdentifyName || '')) return true;
|
||||
return availability === 200 && !checking;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.title}>Identifikasi kedai</div>
|
||||
<div className={styles.steps}>
|
||||
<div className={`${styles.step} ${currentStep === 1 ? styles.stepActive : ''}`}>
|
||||
<span className={styles.stepNumber}>1</span> Alamat
|
||||
</div>
|
||||
<div className={`${styles.step} ${currentStep === 2 ? styles.stepActive : ''}`}>
|
||||
<span className={styles.stepNumber}>2</span> Desain QR
|
||||
</div>
|
||||
<div className={`${styles.step} ${currentStep === 3 ? styles.stepActive : ''}`}>
|
||||
<span className={styles.stepNumber}>3</span> Meja
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.scrollArea}>
|
||||
{currentStep === 1 && (
|
||||
<div className={styles.section}>
|
||||
<div className={styles.sectionTitle}>Alamat kedai</div>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.domainPrefix}>{shopHost}/</div>
|
||||
<input
|
||||
value={cafeIdentifyName}
|
||||
onChange={handleIdentifyChange}
|
||||
className={styles.input}
|
||||
placeholder="alamat_kedai"
|
||||
/>
|
||||
<div className={`${styles.status} ${availability === 200 ? styles.statusOk : availability === 409 ? styles.statusBad : ''}`}>
|
||||
{checking ? 'Memeriksa…' : availability === 200 ? 'Tersedia' : availability === 409 ? 'Terpakai' : 'Menunggu input'}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.helpText}>Gunakan huruf kecil, angka, dan garis bawah (_). Contoh: kopikenangan_malam</div>
|
||||
<div className={styles.copyRow}>
|
||||
<input className={styles.linkField} readOnly value={fullLink} />
|
||||
<button className={styles.button} onClick={copyLink}>{copied ? 'Disalin' : 'Salin'}</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 2 && (
|
||||
<div className={styles.section}>
|
||||
<div className={styles.sectionTitle}>Desain QR</div>
|
||||
<div className={styles.presets}>
|
||||
<button className={styles.presetButton} onClick={()=>applyPreset('center')}>Tengah</button>
|
||||
<button className={styles.presetButton} onClick={()=>applyPreset('topLeft')}>Atas-Kiri</button>
|
||||
<button className={styles.presetButton} onClick={()=>applyPreset('bottomRight')}>Bawah-Kanan</button>
|
||||
<button className={styles.presetButton} onClick={resetDesign}>Reset desain</button>
|
||||
</div>
|
||||
<div className={styles.grid}>
|
||||
<div>
|
||||
<div ref={previewRef} id="qr-code-container" className={styles.previewBox} style={{ backgroundColor: '#fff' }}>
|
||||
{bgImageUrl && <img src={bgImageUrl} alt="Background" className={styles.bgPreview} />}
|
||||
<img
|
||||
className={styles.qrLayer}
|
||||
style={qrStyle}
|
||||
alt="QR"
|
||||
src={`https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=${encodeURIComponent(qrData)}`}
|
||||
/>
|
||||
<div style={fontStyle}>{selectedTable ? selectedTable.tableNo : 'Kedai'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.controls}>
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label}>Gambar latar</label>
|
||||
<input ref={bgFileRef} type="file" accept="image/*" onChange={handleUploadBg} />
|
||||
</div>
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label}>Skala QR ({qrSize}x)</label>
|
||||
<input className={styles.range} type="range" min="0.5" max="3" step="0.1" value={qrSize} onChange={(e)=>setQrSize(parseFloat(e.target.value))} />
|
||||
</div>
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label}>Posisi QR - horizontal ({qrX}%)</label>
|
||||
<input className={styles.range} type="range" min="0" max="100" step="1" value={qrX} onChange={(e)=>setQrX(parseInt(e.target.value))} />
|
||||
</div>
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label}>Posisi QR - vertikal ({qrY}%)</label>
|
||||
<input className={styles.range} type="range" min="0" max="100" step="1" value={qrY} onChange={(e)=>setQrY(parseInt(e.target.value))} />
|
||||
</div>
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label}>Ukuran teks ({fontSize}px)</label>
|
||||
<input className={styles.range} type="range" min="8" max="48" step="1" value={fontSize} onChange={(e)=>setFontSize(parseInt(e.target.value))} />
|
||||
</div>
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label}>Warna teks</label>
|
||||
<input className={styles.colorInput} type="color" value={fontColor} onChange={(e)=>setFontColor(e.target.value)} />
|
||||
</div>
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label}>Posisi teks - horizontal ({fontX}%)</label>
|
||||
<input className={styles.range} type="range" min="0" max="100" step="1" value={fontX} onChange={(e)=>setFontX(parseInt(e.target.value))} />
|
||||
</div>
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label}>Posisi teks - vertikal ({fontY}%)</label>
|
||||
<input className={styles.range} type="range" min="0" max="100" step="1" value={fontY} onChange={(e)=>setFontY(parseInt(e.target.value))} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.helpText}>Tip: Gunakan latar yang kontras agar QR mudah dipindai.</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 3 && (
|
||||
<div className={styles.section}>
|
||||
<div className={styles.sectionTitle}>Daftar meja</div>
|
||||
<div className={styles.tables}>
|
||||
<input className={styles.input} placeholder="Meja A1" value={newTableNo} onChange={(e)=>setNewTableNo(e.target.value)} />
|
||||
<button className={styles.button} onClick={handleCreateTable}>Buat meja</button>
|
||||
</div>
|
||||
<div className={styles.tableList}>
|
||||
{tables && tables
|
||||
.filter((t)=>t.tableNo !== 0)
|
||||
.map((t)=>{
|
||||
const active = selectedTable && selectedTable.tableId === t.tableId;
|
||||
return (
|
||||
<div
|
||||
key={t.tableId}
|
||||
className={`${styles.tableItem} ${active ? styles.tableItemActive : ''}`}
|
||||
onClick={()=>setSelectedTable(active ? null : t)}
|
||||
>
|
||||
{t.tableNo}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className={styles.helpText}>Pilih meja untuk membuat QR khusus meja (opsional).</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.footer}>
|
||||
<div className={styles.actions}>
|
||||
<button className={`${styles.button} ${styles.secondary}`} disabled={currentStep === 1} onClick={()=>setCurrentStep((s)=>Math.max(1, s-1))}>Kembali</button>
|
||||
{currentStep < 3 ? (
|
||||
<button className={`${styles.button} ${styles.primary}`} disabled={currentStep === 1 && !canProceedFromStep1()} onClick={()=>setCurrentStep((s)=>Math.min(3, s+1))}>Lanjut</button>
|
||||
) : (
|
||||
<button className={`${styles.button} ${styles.primary}`} onClick={handleSave} disabled={saving}>{saving ? 'Menyimpan…' : 'Simpan'}</button>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
{saveStatus === 'success' && <div className={`${styles.banner} ${styles.bannerSuccess}`}>Simpan berhasil</div>}
|
||||
{saveStatus === 'error' && <div className={`${styles.banner} ${styles.bannerError}`}>Gagal menyimpan</div>}
|
||||
<button className={`${styles.button} ${styles.muted}`} onClick={downloadPreview}>Download QR {selectedTable ? 'meja' : 'kedai'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
299
src/components/IdentifyCafeModal.module.css
Normal file
299
src/components/IdentifyCafeModal.module.css
Normal file
@@ -0,0 +1,299 @@
|
||||
/* IdentifyCafeModal.module.css */
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.steps {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #e6e6e6;
|
||||
background: #fafafa;
|
||||
color: #666;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.stepNumber {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: #e9ecef;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stepActive {
|
||||
border-color: #cdebd8;
|
||||
background: #e9f7ef;
|
||||
color: #245c3d;
|
||||
}
|
||||
|
||||
.helpText {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.scrollArea {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: #fafafa;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
margin: 0 0 10px 0;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.domainPrefix {
|
||||
background: #f0f0f0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 10px 0 0 10px;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 0 10px 10px 0;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.statusOk {
|
||||
color: #155724;
|
||||
background: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.statusBad {
|
||||
color: #721c24;
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.copyRow {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.linkField {
|
||||
flex: 1;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #ddd;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.3fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.previewBox {
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
height: 280px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bgPreview {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.qrLayer {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.presets {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin: 8px 0 4px 0;
|
||||
}
|
||||
|
||||
.presetButton {
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e6e6e6;
|
||||
background: #fff;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.range {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.colorInput {
|
||||
height: 38px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.tables {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tableList {
|
||||
max-height: 180px;
|
||||
overflow: auto;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.tableItem {
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tableItemActive {
|
||||
background: #e9f7ef;
|
||||
border: 1px solid #cdebd8;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.primary {
|
||||
background: #28a745;
|
||||
color: #fff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.muted {
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.banner {
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bannerSuccess {
|
||||
color: #155724;
|
||||
background: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.bannerError {
|
||||
color: #721c24;
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.grid { grid-template-columns: 1fr; }
|
||||
.controls { grid-template-columns: 1fr; }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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}/>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
171
src/components/PaymentOptions.module.css
Normal file
171
src/components/PaymentOptions.module.css
Normal file
@@ -0,0 +1,171 @@
|
||||
/* PaymentOptions.module.css */
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: 80vh;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: #fafafa;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.sectionDesc {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #ddd;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.primary {
|
||||
background: #28a745;
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.configButton {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.imageBox {
|
||||
background-color: #999999;
|
||||
border-radius: 12px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 220px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
overflow: hidden;
|
||||
margin: 10px 0 6px 0;
|
||||
}
|
||||
|
||||
.smallNote {
|
||||
font-size: 12px;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.detectRow {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tagOk {
|
||||
color: #155724;
|
||||
background: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.tagBad {
|
||||
color: #721c24;
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.copyRow {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.linkField {
|
||||
flex: 1;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.actionsRight {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.banner {
|
||||
padding: 8px 12px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bannerSuccess {
|
||||
color: #155724;
|
||||
background: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.bannerError {
|
||||
color: #721c24;
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.container { padding: 16px; }
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user