| 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/pages/ |
Upload File : |
import { useCallback, useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import {
ClipboardList,
Printer,
PrinterCheck,
Settings2,
Store,
} from "lucide-react";
import CategoryBar from "@/components/CategoryBar";
import ProductGrid from "@/components/ProductGrid";
import Cart, { CartItem } from "@/components/Cart";
import Numpad from "@/components/Numpad";
import PaymentModal from "@/components/PaymentModal";
import SalesReport from "@/components/SalesReport";
import CatalogManager from "@/components/CatalogManager";
import {
CatalogProduct,
defaultCatalog,
deriveCategories,
loadCatalog,
saveCatalog,
} from "@/lib/catalog";
import { Product } from "@/data/products";
import {
PaymentMethod,
ReportCategory,
SaleItem,
SaleRecord,
} from "@/types/sales";
import { formatBusinessDate, normalizeReportCategory } from "@/lib/reporting";
import {
clearSalesOnServer,
clearStoredSales,
loadSalesFromServer,
loadStoredSales,
saveSaleToServer,
saveStoredSales,
} from "@/lib/salesStore";
import {
ReceiptData,
connectPrinter,
disconnectPrinter,
isPrinterConnected,
isWebSerialSupported,
kickDrawer,
printReceiptAndKickDrawer,
} from "@/lib/printer";
let nextManualId = 1000;
function getProductReportCategory(product: Product): ReportCategory {
if (product.reportCategory) return product.reportCategory;
return normalizeReportCategory(product.category, Boolean(product.taxRate));
}
const Index = () => {
const businessDate = formatBusinessDate(new Date());
const [category, setCategory] = useState("All");
const [cart, setCart] = useState<CartItem[]>([]);
const [paymentOpen, setPaymentOpen] = useState(false);
const [reportOpen, setReportOpen] = useState(false);
const [catalogOpen, setCatalogOpen] = useState(false);
const [sales, setSales] = useState<SaleRecord[]>([]);
const [catalog, setCatalog] = useState<CatalogProduct[]>(defaultCatalog());
const [printerConnected, setPrinterConnected] = useState(false);
const categories = useMemo(() => deriveCategories(catalog), [catalog]);
const filtered = useMemo(
() =>
category === "All"
? catalog
: catalog.filter((product) => product.category === category),
[catalog, category],
);
const addToCart = useCallback((product: Product) => {
setCart((prev) => {
const existing = prev.find((item) => item.id === product.id);
if (existing) {
return prev.map((item) =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item,
);
}
return [
...prev,
{
...product,
quantity: 1,
taxRate: product.taxRate,
reportCategory: getProductReportCategory(product),
taxable: Boolean(product.taxRate),
},
];
});
}, []);
const addManualItem = useCallback(
(
amount: number,
label: string,
taxable: boolean,
taxRate?: number,
reportCategory?: ReportCategory,
) => {
const id = `manual-${nextManualId++}`;
const displayLabel = taxable ? `${label} (T)` : label;
setCart((prev) => [
...prev,
{
id,
name: displayLabel,
price: amount,
emoji: taxable ? "TAX" : "NTX",
quantity: 1,
taxRate: taxable ? taxRate : undefined,
category: label,
reportCategory:
reportCategory ?? normalizeReportCategory(label, taxable),
taxable,
},
]);
},
[],
);
const updateQty = useCallback((id: string, delta: number) => {
setCart((prev) =>
prev
.map((item) =>
item.id === id ? { ...item, quantity: item.quantity + delta } : item,
)
.filter((item) => item.quantity > 0),
);
}, []);
const removeItem = useCallback((id: string) => {
setCart((prev) => prev.filter((item) => item.id !== id));
}, []);
const clearCart = useCallback(() => setCart([]), []);
const handleCheckout = useCallback(() => {
if (cart.length === 0) return;
setPaymentOpen(true);
}, [cart]);
const handleConnectPrinter = useCallback(async () => {
if (isPrinterConnected()) {
await disconnectPrinter();
setPrinterConnected(false);
toast.info("Printer disconnected");
return;
}
if (!isWebSerialSupported()) {
toast.error("WebSerial not supported. Use Chrome or Edge.");
return;
}
const connected = await connectPrinter();
setPrinterConnected(connected);
if (connected) {
toast.success("Printer connected");
return;
}
toast.error("Could not connect to printer");
}, []);
const handleOpenDrawer = useCallback(async () => {
if (!isPrinterConnected()) {
toast.error("Connect a printer first");
return;
}
try {
await kickDrawer();
toast.success("Drawer opened");
} catch {
toast.error("Failed to open drawer");
}
}, []);
useEffect(() => {
let mounted = true;
const fetchCatalog = async () => {
const serverCatalog = await loadCatalog();
if (!mounted) return;
if (serverCatalog && serverCatalog.length > 0) {
setCatalog(serverCatalog);
return;
}
setCatalog(defaultCatalog());
};
fetchCatalog();
return () => {
mounted = false;
};
}, []);
useEffect(() => {
let mounted = true;
const loadSales = async () => {
const serverSales = await loadSalesFromServer(businessDate);
const nextSales = serverSales ?? loadStoredSales(businessDate);
if (!mounted) return;
setSales(nextSales);
};
loadSales();
return () => {
mounted = false;
};
}, [businessDate]);
useEffect(() => {
saveStoredSales(sales);
}, [sales]);
const completePayment = useCallback(
async (method: PaymentMethod, amountTendered?: number) => {
const subtotal = cart.reduce(
(sum, item) => sum + item.price * item.quantity,
0,
);
const tax = cart.reduce((sum, item) => {
if (item.taxRate) {
return sum + item.price * item.quantity * (item.taxRate / 100);
}
return sum;
}, 0);
const total = subtotal + tax;
const changeDue = amountTendered ? amountTendered - total : undefined;
const saleItems: SaleItem[] = cart.map((item) => {
const lineTotal = item.price * item.quantity;
const taxAmount = item.taxRate
? lineTotal * (item.taxRate / 100)
: 0;
return {
name: item.name,
category: item.category || "Other",
reportCategory:
item.reportCategory ||
normalizeReportCategory(
item.category || item.name,
Boolean(item.taxRate),
),
price: item.price,
quantity: item.quantity,
taxable: Boolean(item.taxRate || item.taxable),
taxRate: item.taxRate,
taxAmount,
lineTotal,
};
});
const timestamp = new Date();
const record: SaleRecord = {
id: `sale-${Date.now()}`,
items: saleItems,
subtotal,
tax,
total,
paymentMethod: method,
amountTendered,
changeDue,
timestamp,
businessDate: formatBusinessDate(timestamp),
};
const nextSales = [...sales, record];
setSales(nextSales);
setPaymentOpen(false);
setCart([]);
const savedToServer = await saveSaleToServer(record);
if (isPrinterConnected()) {
try {
const receiptData: ReceiptData = {
storeName: "My Store",
items: saleItems.map((item) => ({
name: item.name,
quantity: item.quantity,
price: item.price,
taxRate: item.taxRate,
taxAmount: item.taxAmount,
})),
subtotal,
tax,
total,
paymentMethod: method,
amountTendered,
changeDue:
changeDue !== undefined && changeDue > 0 ? changeDue : undefined,
timestamp,
};
await printReceiptAndKickDrawer(receiptData);
} catch (error) {
console.error("Print error:", error);
toast.error("Failed to print receipt");
}
}
const methodLabel = method === "cash" ? "Cash" : "Credit Card";
const changeLabel =
changeDue !== undefined && changeDue > 0
? ` | Change $${changeDue.toFixed(2)}`
: "";
const savedLabel = savedToServer ? "" : " | saved locally";
toast.success(
`Sale complete: ${methodLabel} $${total.toFixed(2)}${changeLabel}${savedLabel}`,
{ duration: 4000 },
);
},
[cart, sales],
);
const handleClearSales = useCallback(async () => {
const clearedOnServer = await clearSalesOnServer(businessDate);
clearStoredSales(businessDate);
setSales([]);
if (clearedOnServer) {
toast.success("Started a new business day");
return;
}
toast.success("Started a new business day locally");
}, [businessDate]);
const handleCatalogSaved = useCallback((products: CatalogProduct[]) => {
setCatalog(products);
if (category !== "All" && !products.some((item) => item.category === category)) {
setCategory("All");
}
toast.success("Catalog saved for all devices");
}, [category]);
const handleCatalogReorder = useCallback(
async (activeId: string, overId: string) => {
const activeIndex = catalog.findIndex((item) => item.id === activeId);
const overIndex = catalog.findIndex((item) => item.id === overId);
if (activeIndex < 0 || overIndex < 0 || activeIndex === overIndex) {
return;
}
const nextCatalog = [...catalog];
const [moved] = nextCatalog.splice(activeIndex, 1);
nextCatalog.splice(overIndex, 0, moved);
setCatalog(nextCatalog);
const saved = await saveCatalog(nextCatalog);
if (!saved) {
toast.error("Could not save button order");
return;
}
toast.success("Product button order saved");
},
[catalog],
);
return (
<div className="flex h-screen flex-col bg-background">
<header className="flex items-center gap-3 border-b border-border bg-card px-4 py-3 shadow-sm">
<Store className="h-7 w-7 text-primary" />
<h1 className="text-xl font-extrabold tracking-tight text-foreground">
My Store
</h1>
<div className="ml-auto flex items-center gap-2">
<button
onClick={() => setCatalogOpen(true)}
className="flex items-center gap-1.5 rounded-lg border border-border bg-secondary px-3 py-1.5 text-sm font-bold text-foreground transition-all hover:bg-primary hover:text-primary-foreground"
>
<Settings2 className="h-4 w-4" />
<span>Catalog</span>
</button>
<button
onClick={handleConnectPrinter}
className={`flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-bold transition-all ${
printerConnected
? "border-success/30 bg-success/15 text-success hover:bg-success hover:text-success-foreground"
: "border-border bg-secondary text-foreground hover:bg-primary hover:text-primary-foreground"
}`}
>
{printerConnected ? (
<PrinterCheck className="h-4 w-4" />
) : (
<Printer className="h-4 w-4" />
)}
<span className="hidden sm:inline">
{printerConnected ? "Printer Connected" : "Connect Printer"}
</span>
</button>
{printerConnected && (
<button
onClick={handleOpenDrawer}
className="flex items-center gap-1.5 rounded-lg border border-border bg-secondary px-3 py-1.5 text-sm font-bold text-foreground transition-all hover:bg-primary hover:text-primary-foreground"
>
<span>Open Drawer</span>
</button>
)}
<button
onClick={() => setReportOpen(true)}
className="flex items-center gap-1.5 rounded-lg border border-border bg-secondary px-3 py-1.5 text-sm font-bold text-foreground transition-all hover:bg-primary hover:text-primary-foreground"
>
<ClipboardList className="h-4 w-4" />
<span>Report</span>
{sales.length > 0 && (
<span className="rounded-full bg-primary px-1.5 py-0.5 text-[10px] font-bold text-primary-foreground">
{sales.length}
</span>
)}
</button>
<span className="text-sm text-muted-foreground">Point of Sale</span>
</div>
</header>
<div className="flex flex-1 overflow-hidden">
<div className="hidden w-[260px] flex-shrink-0 overflow-y-auto p-3 md:block lg:w-[280px]">
<Numpad onAddItem={addManualItem} />
</div>
<div className="flex flex-1 flex-col gap-3 overflow-y-auto p-4">
<CategoryBar
categories={categories}
selected={category}
onSelect={setCategory}
/>
<ProductGrid
products={filtered}
onAdd={addToCart}
onReorder={handleCatalogReorder}
/>
</div>
<div className="hidden w-[340px] flex-shrink-0 p-3 pl-0 md:flex lg:w-[380px]">
<Cart
items={cart}
onUpdateQty={updateQty}
onRemove={removeItem}
onCheckout={handleCheckout}
onClear={clearCart}
/>
</div>
</div>
{cart.length > 0 && (
<div className="border-t border-border bg-card px-4 py-3 md:hidden">
<button
onClick={handleCheckout}
className="w-full rounded-xl bg-success py-3 text-base font-bold text-success-foreground shadow-md active:scale-[0.98]"
>
Checkout | {cart.reduce((sum, item) => sum + item.quantity, 0)} items
{" | $"}
{cart
.reduce((sum, item) => sum + item.price * item.quantity, 0)
.toFixed(2)}
</button>
</div>
)}
<PaymentModal
open={paymentOpen}
onClose={() => setPaymentOpen(false)}
onComplete={completePayment}
items={cart}
/>
<SalesReport
open={reportOpen}
onClose={() => setReportOpen(false)}
sales={sales}
onClearSales={handleClearSales}
/>
<CatalogManager
open={catalogOpen}
onClose={() => setCatalogOpen(false)}
products={catalog}
onSaved={handleCatalogSaved}
/>
</div>
);
};
export default Index;