/* global React */
const { useState: useStateCfg, useEffect: useEffectCfg, useMemo: useMemoCfg } = React;
// =============================================================================
// OFFERS (banner toggle + live preview)
// =============================================================================
window.OffersSection = function OffersSection() {
const [offers, setOffers] = useStateCfg(null);
const [saving, setSaving] = useStateCfg(false);
const [savedTick, setSavedTick] = useStateCfg(0);
useEffectCfg(() => {
window.LQ.request('/settings/offers_banner').then(setOffers).catch(() => setOffers({ enabled: false, text: '' }));
}, []);
useEffectCfg(() => {
if (!offers) return;
const t = setTimeout(async () => {
setSaving(true);
try {
await window.LQ.request('/settings/offers_banner', { method: 'PUT', body: offers });
setSavedTick(savedTick + 1);
} catch {}
setSaving(false);
}, 600);
return () => clearTimeout(t);
}, [offers && offers.enabled, offers && offers.text]);
if (!offers) return
Loading…
;
const previewText = (offers.text || '').trim();
return (
<>
Live Preview
This is exactly how the banner appears on the public site.
{offers.enabled && previewText ? (
{previewText} ✦ {previewText} ✦ {previewText} ✦ {previewText}
) : (
Banner is off — toggle above to show on public site.
)}
Public pages refresh banner data on every load — changes go live immediately.
>
);
};
// =============================================================================
// BLOG
// =============================================================================
window.BlogSection = function BlogSection() {
const [list, setList] = useStateCfg([]);
const [editing, setEditing] = useStateCfg(null);
const [adding, setAdding] = useStateCfg(false);
const [loading, setLoading] = useStateCfg(true);
function load() {
setLoading(true);
window.LQ.request('/blog').then((r) => { setList(r || []); setLoading(false); });
}
useEffectCfg(load, []);
async function del(id) {
if (!confirm('Delete this post permanently?')) return;
await window.LQ.request(`/blog/${id}`, { method: 'DELETE' });
if (editing && editing.id === id) setEditing(null);
load();
}
return (
<>
{list.length} posts.
{loading ?
Loading…
:
list.length === 0 ?
No posts yet.
:
list.map((p) => (
))
}
{(editing || adding) ?
{ setAdding(false); setEditing(updated); load(); }}
onDelete={() => editing && del(editing.id)}
/> :
Pick a post on the left, or create a new one.
}
>
);
};
window.BlogEditor = function BlogEditor({ post, isNew, onSaved, onDelete }) {
const [form, setForm] = useStateCfg(null);
const [saving, setSaving] = useStateCfg(false);
const [err, setErr] = useStateCfg('');
useEffectCfg(() => {
if (isNew) {
setForm({ title: '', slug: '', meta_desc: '', body: '', status: 'draft' });
} else if (post) {
// Fetch full post (list view doesn't include body)
window.LQ.request(`/blog/${post.id}`).then((p) => setForm({
title: p.title, slug: p.slug, meta_desc: p.meta_desc || '', body: p.body || '', status: p.status
}));
}
}, [post && post.id, isNew]);
if (!form) return Loading…
;
function set(k, v) {
setForm((f) => {
const next = { ...f, [k]: v };
if (k === 'title' && (!post || !post.slug || f.slug === window.LQ_slug(f.title || ''))) {
next.slug = window.LQ_slug(v);
}
return next;
});
}
async function save(e) {
e && e.preventDefault();
setSaving(true); setErr('');
try {
let saved;
if (post && !isNew) {
await window.LQ.request(`/blog/${post.id}`, { method: 'PATCH', body: form });
saved = { ...post, ...form };
} else {
const r = await window.LQ.request('/blog', { method: 'POST', body: form });
saved = { id: r.id, ...form };
}
onSaved(saved);
} catch (e) {
setErr(e.message);
} finally {
setSaving(false);
}
}
return (
);
};
window.LQ_slug = function (s) {
return (s || '').toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
};
// =============================================================================
// PAYMENT (RazorPay)
// =============================================================================
window.PaymentSection = function PaymentSection() {
const [payment, setPayment] = useStateCfg(null);
const [saving, setSaving] = useStateCfg(false);
useEffectCfg(() => {
window.LQ.request('/settings/payment').then(setPayment).catch(() => setPayment({}));
}, []);
async function save() {
setSaving(true);
await window.LQ.request('/settings/payment', { method: 'PUT', body: payment });
setSaving(false);
alert('Payment settings saved.');
}
if (!payment) return Loading…
;
function set(k, v) { setPayment({ ...payment, [k]: v }); }
return (
<>
Razorpay Credentials
Get these from your Razorpay Dashboard → Settings → API Keys.
>
);
};
// =============================================================================
// SITE SETTINGS + REFILL RULES + BACKUP
// =============================================================================
window.SettingsSection = function SettingsSection() {
const [site, setSite] = useStateCfg(null);
const [refill, setRefill] = useStateCfg(null);
const [savingS, setSavingS] = useStateCfg(false);
const [savingR, setSavingR] = useStateCfg(false);
useEffectCfg(() => {
Promise.all([
window.LQ.request('/settings/site').catch(() => ({})),
window.LQ.request('/settings/refill_rules').catch(() => ({ days: 21, services: [] })),
]).then(([s, r]) => { setSite(s); setRefill(r); });
}, []);
async function saveSite() {
setSavingS(true);
await window.LQ.request('/settings/site', { method: 'PUT', body: site });
setSavingS(false);
alert('Site settings saved. Public pages will read these on next load.');
}
async function saveRefill() {
setSavingR(true);
await window.LQ.request('/settings/refill_rules', { method: 'PUT', body: refill });
setSavingR(false);
alert('Refill rules saved.');
}
function exportBackup() {
window.LQ.downloadFile('/backup', `lacquery-backup-${new Date().toISOString().slice(0, 10)}.json`);
}
async function importBackup(file) {
if (!confirm('Restore from this file? It REPLACES clients, appointments, products, orders, photos, blog, and settings. Users + audit log are preserved.')) return;
const text = await file.text();
try {
const json = JSON.parse(text);
await window.LQ.request('/backup', { method: 'POST', body: json });
alert('Backup restored.');
location.reload();
} catch (e) {
alert('Restore failed: ' + e.message);
}
}
if (!site || !refill) return Loading…
;
return (
<>
Site Settings
Single source of truth for the contact details that appear on the public site, in WhatsApp links, and in email signatures.
Refill Rules
When a client completes one of these services, a refill reminder is auto-flagged after the chosen number of days.
setRefill({ ...refill, days: Number(e.target.value) })} />
{['Gel Extensions','Nail Extensions','Acrylic Extensions','Permanent Extensions','Builder Gel','Refill','Gel Polish','Nail Art'].map((s) => {
const on = (refill.services || []).includes(s);
return (
);
})}
Backup & Restore
Download a JSON snapshot of all data, or restore from a previous backup. Take one weekly to be safe.
>
);
};
// =============================================================================
// USERS (admin manages staff + permissions)
// =============================================================================
window.UsersSection = function UsersSection({ user: me }) {
const [list, setList] = useStateCfg([]);
const [adding, setAdding] = useStateCfg(false);
const [editing, setEditing] = useStateCfg(null);
const [loading, setLoading] = useStateCfg(true);
function load() {
setLoading(true);
window.LQ.request('/users').then((r) => { setList(r || []); setLoading(false); });
}
useEffectCfg(load, []);
async function deactivate(id) {
if (!confirm('Deactivate this user? They will lose access immediately.')) return;
await window.LQ.request(`/users/${id}`, { method: 'DELETE' });
load();
}
async function reactivate(id) {
await window.LQ.request(`/users/${id}`, { method: 'PATCH', body: { is_active: true } });
load();
}
if (loading) return Loading…
;
return (
<>
{list.filter((u) => u.is_active).length} active · {list.filter((u) => !u.is_active).length} inactive
| Name | Email | Role | Tabs | Last login | Actions |
{list.map((u) => (
| {u.name}{u.id === me.id && You} |
{u.email} |
{u.role} |
{u.role === 'admin' ? All :
(u.permissions && u.permissions.length) ?
{u.permissions.map((p) => {p})}
:
None}
|
{u.last_login_at ? window.LQ_fmtDateTime(u.last_login_at) : '—'} |
{u.is_active && u.id !== me.id && }
{!u.is_active && }
|
))}
{(adding || editing) && { setAdding(false); setEditing(null); }} onSaved={() => { setAdding(false); setEditing(null); load(); }} />}
>
);
};
window.UserModal = function UserModal({ user, isNew, onClose, onSaved }) {
const [form, setForm] = useStateCfg(() => ({
name: user?.name || '',
email: user?.email || '',
password: '',
role: user?.role || 'staff',
permissions: user?.permissions || window.LQ_TABS.filter((t) => t.defaultStaff && !t.adminOnly).map((t) => t.key),
send_invite: true,
}));
const [saving, setSaving] = useStateCfg(false);
const [err, setErr] = useStateCfg('');
function togglePerm(k) {
setForm((f) => ({ ...f, permissions: f.permissions.includes(k) ? f.permissions.filter((x) => x !== k) : [...f.permissions, k] }));
}
function generatePassword() {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789';
let p = '';
for (let i = 0; i < 12; i++) p += chars[Math.floor(Math.random() * chars.length)];
setForm((f) => ({ ...f, password: p }));
}
async function save(e) {
e.preventDefault();
setSaving(true); setErr('');
try {
if (isNew) {
await window.LQ.request('/users', { method: 'POST', body: form });
} else {
const body = { name: form.name, role: form.role, permissions: form.permissions };
if (form.email !== user.email) body.email = form.email;
if (form.password) body.password = form.password;
await window.LQ.request(`/users/${user.id}`, { method: 'PATCH', body });
}
onSaved();
} catch (e) {
setErr(e.message);
} finally {
setSaving(false);
}
}
return (
e.stopPropagation()}>
{isNew ? 'Add User' : 'Edit User'}
);
};
// =============================================================================
// AUDIT LOG
// =============================================================================
window.AuditSection = function AuditSection() {
const [rows, setRows] = useStateCfg([]);
const [loading, setLoading] = useStateCfg(true);
const [filterAction, setFilterAction] = useStateCfg('');
function load() {
setLoading(true);
const qs = filterAction ? '?action=' + encodeURIComponent(filterAction) : '';
window.LQ.request('/audit' + qs).then((r) => { setRows(r || []); setLoading(false); });
}
useEffectCfg(load, [filterAction]);
if (loading) return Loading…
;
return (
<>
{rows.length} entries · most recent first.
| When | Who | Action | Entity | Details | IP |
{rows.map((r) => (
| {window.LQ_fmtDateTime(r.created_at)} |
{r.user_email || —} |
{r.action} |
{r.entity_type ? `${r.entity_type}#${r.entity_id ?? '—'}` : '—'} |
{r.meta ? JSON.stringify(r.meta) : ''}
|
{r.ip} |
))}
>
);
};