| Server IP : 173.209.174.21 / Your IP : 216.73.216.89 Web Server : Apache/2.4.58 (Ubuntu) System : Linux wcfs-server 6.8.0-124-generic #124-Ubuntu SMP PREEMPT_DYNAMIC Tue May 26 13:00:45 UTC 2026 x86_64 User : nodor ( 1000) PHP Version : 8.3.6 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/wcfs/NAOPOS/src/components/ |
Upload File : |
import { useEffect, useState } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
CatalogProduct,
defaultCatalog,
saveCatalogDetailed,
} from "@/lib/catalog";
import { reportBuckets } from "@/lib/posSettings";
interface CatalogManagerProps {
open: boolean;
onClose: () => void;
products: CatalogProduct[];
onSaved: (products: CatalogProduct[]) => void;
}
function slugify(value: string): string {
return value
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 40);
}
const CatalogManager = ({
open,
onClose,
products,
onSaved,
}: CatalogManagerProps) => {
const [draft, setDraft] = useState<CatalogProduct[]>(products);
const [status, setStatus] = useState("");
useEffect(() => {
setDraft(products);
}, [products, open]);
const updateProduct = (
id: string,
field: keyof CatalogProduct,
value: string | number | boolean | undefined,
) => {
setDraft((prev) =>
prev.map((product) => {
if (product.id !== id) return product;
const next = { ...product, [field]: value };
if (field === "taxable" && value === false) {
next.taxRate = undefined;
}
if (field === "taxable" && value === true && next.taxRate == null) {
next.taxRate = 0;
}
return next;
}),
);
};
const addProduct = () => {
const index = draft.length + 1;
setDraft((prev) => [
...prev,
{
id: `custom-${index}`,
name: `New Item ${index}`,
price: 0,
category: "General",
emoji: "ITEM",
imageUrl: "",
taxable: false,
reportCategory: "Non-Taxable",
},
]);
};
const removeProduct = (id: string) => {
setDraft((prev) => prev.filter((product) => product.id !== id));
};
const handleSave = async () => {
const cleaned = draft.map((product, index) => ({
...product,
id: slugify(product.id || product.name || `product-${index + 1}`),
name: product.name.trim() || `Item ${index + 1}`,
category: product.category.trim() || "General",
emoji: product.emoji.trim() || "ITEM",
imageUrl: product.imageUrl?.trim() || undefined,
price: Number(product.price) || 0,
taxRate: product.taxable ? Number(product.taxRate || 0) : undefined,
}));
const result = await saveCatalogDetailed(cleaned);
setStatus(result.ok ? "Saved for all devices" : result.error || "Save failed");
if (result.ok) {
onSaved(cleaned);
}
};
const handleResetDefaults = async () => {
const defaults = defaultCatalog();
const result = await saveCatalogDetailed(defaults);
setStatus(result.ok ? "Reset to default catalog" : result.error || "Reset failed");
if (result.ok) {
setDraft(defaults);
onSaved(defaults);
}
};
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-h-[90vh] overflow-y-auto border-border bg-card sm:max-w-5xl">
<DialogHeader>
<DialogTitle className="text-lg font-extrabold text-foreground">
Catalog Manager
</DialogTitle>
<p className="text-sm text-muted-foreground">
Regular product buttons are saved on the server here.
</p>
</DialogHeader>
<div className="space-y-3">
{draft.map((product) => (
<div
key={product.id}
className="grid grid-cols-1 gap-2 rounded-xl border border-border bg-background p-3 md:grid-cols-7"
>
<input
value={product.name}
onChange={(event) =>
updateProduct(product.id, "name", event.target.value)
}
className="rounded-lg border border-border bg-card px-3 py-2 text-sm font-semibold text-foreground"
placeholder="Name"
/>
<input
value={product.category}
onChange={(event) =>
updateProduct(product.id, "category", event.target.value)
}
className="rounded-lg border border-border bg-card px-3 py-2 text-sm font-semibold text-foreground"
placeholder="Category"
/>
<input
type="number"
min="0"
step="0.01"
value={product.price}
onChange={(event) =>
updateProduct(
product.id,
"price",
parseFloat(event.target.value || "0"),
)
}
className="rounded-lg border border-border bg-card px-3 py-2 text-sm font-semibold text-foreground"
placeholder="Price"
/>
<input
value={product.emoji}
onChange={(event) =>
updateProduct(product.id, "emoji", event.target.value)
}
className="rounded-lg border border-border bg-card px-3 py-2 text-sm font-semibold text-foreground"
placeholder="Button tag"
/>
<input
value={product.imageUrl ?? ""}
onChange={(event) =>
updateProduct(product.id, "imageUrl", event.target.value)
}
className="rounded-lg border border-border bg-card px-3 py-2 text-sm font-semibold text-foreground"
placeholder="Image URL"
/>
<select
value={product.reportCategory}
onChange={(event) =>
updateProduct(
product.id,
"reportCategory",
event.target.value,
)
}
className="rounded-lg border border-border bg-card px-3 py-2 text-sm font-semibold text-foreground"
>
{reportBuckets.map((bucket) => (
<option key={bucket} value={bucket}>
{bucket}
</option>
))}
</select>
<div className="flex gap-2">
<select
value={product.taxable ? "taxable" : "non-taxable"}
onChange={(event) =>
updateProduct(
product.id,
"taxable",
event.target.value === "taxable",
)
}
className="flex-1 rounded-lg border border-border bg-card px-3 py-2 text-sm font-semibold text-foreground"
>
<option value="taxable">Taxable</option>
<option value="non-taxable">Non-Taxable</option>
</select>
<input
type="number"
min="0"
step="0.01"
disabled={!product.taxable}
value={product.taxRate ?? ""}
onChange={(event) =>
updateProduct(
product.id,
"taxRate",
event.target.value === ""
? undefined
: parseFloat(event.target.value),
)
}
className="w-28 rounded-lg border border-border bg-card px-3 py-2 text-sm font-semibold text-foreground disabled:opacity-50"
placeholder="Tax %"
/>
<button
onClick={() => removeProduct(product.id)}
className="rounded-lg border border-destructive/20 bg-destructive/10 px-3 py-2 text-xs font-bold text-destructive"
>
Remove
</button>
</div>
</div>
))}
</div>
<div className="flex items-center justify-between gap-2 pt-2">
<div className="text-xs font-semibold text-muted-foreground">
{status}
</div>
<div className="flex gap-2">
<button
onClick={addProduct}
className="rounded-lg bg-secondary px-4 py-2 text-sm font-bold text-secondary-foreground"
>
Add Product
</button>
<button
onClick={handleResetDefaults}
className="rounded-lg border border-border bg-background px-4 py-2 text-sm font-bold text-foreground"
>
Reset Defaults
</button>
<button
onClick={handleSave}
className="rounded-lg bg-primary px-4 py-2 text-sm font-bold text-primary-foreground"
>
Save Catalog
</button>
</div>
</div>
</DialogContent>
</Dialog>
);
};
export default CatalogManager;