Archive for April 2026

  • Sesaji Kasir

    import React, { useState, useEffect, useMemo } from 'react'; import { Lock, LayoutGrid, Package, FileText, Printer, Bluetooth, Trash, Plus, Minus, ShoppingCart, Coffee, TrendingUp, DollarSign, LogOut, Check, Save } from 'lucide-react'; // --- STYLES INJECTION --- const injectStyles = () => { if (document.getElementById('sesaji-styles')) return; const style = document.createElement('style'); style.id = 'sesaji-styles'; style.innerHTML = ` body { background-color: #F8FAF8; color: #4A5D45; font-family: 'Inter', sans-serif; height: 100vh; overflow: hidden; margin: 0; } .cute-card { background: white; border-radius: 32px; border: 1px solid #E0E7DE; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05); transition: all 0.2s; } .receipt-card { background: white; border-radius: 40px; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); border: 1px solid #E0E7DE; } .btn-primary { background-color: #4A5D45; color: white; font-weight: bold; padding: 0.75rem 1.5rem; border-radius: 9999px; transition: all 0.2s; border: none; cursor: pointer; } .btn-primary:hover { background-color: #3d4d39; transform: translateY(-1px); } .btn-primary:active { transform: scale(0.95); } @media print { body * { visibility: hidden; } #printable-receipt, #printable-receipt * { visibility: visible; } #printable-receipt { position: absolute; left: 0; top: 0; width: 80mm; margin: 0; padding: 10px; border: none; } } `; document.head.appendChild(style); }; // --- TYPES --- type IngredientType = 'raw' | 'self_made'; type Channel = 'Toko' | 'GrabFood' | 'GoFood' | 'ShopeeFood'; interface RecipeItem { ingredientId: string; qty: number; } interface Ingredient { id: string; name: string; unit: string; costPerUnit: number; stock: number; type: IngredientType; recipe?: RecipeItem[]; } interface Product { id: string; name: string; basePrice: number; recipe: RecipeItem[]; } interface CartItem { product: Product; qty: number; } interface Transaction { id: string; date: string; items: CartItem[]; totalAmount: number; totalHPP: number; channel: Channel; } interface WasteLog { id: string; date: string; ingredientId: string; qty: number; cost: number; } // --- INITIAL DATA --- const INITIAL_INGREDIENTS: Ingredient[] = [ { id: 'i1', name: 'Biji Kopi Arabica', unit: 'gr', costPerUnit: 200, stock: 5000, type: 'raw' }, { id: 'i2', name: 'Susu Segar', unit: 'ml', costPerUnit: 18, stock: 10000, type: 'raw' }, { id: 'i3', name: 'Gula Pasir', unit: 'gr', costPerUnit: 15, stock: 2000, type: 'raw' }, { id: 'i4', name: 'Air Mineral', unit: 'ml', costPerUnit: 2, stock: 20000, type: 'raw' }, { id: 'i5', name: 'Syrup Gula (Internal)', unit: 'ml', costPerUnit: 0, stock: 1000, type: 'self_made', recipe: [{ ingredientId: 'i3', qty: 500 }, { ingredientId: 'i4', qty: 500 }] } ]; const INITIAL_PRODUCTS: Product[] = [ { id: 'p1', name: 'Iced Kopi Susu Sesaji', basePrice: 18000, recipe: [{ ingredientId: 'i1', qty: 18 }, { ingredientId: 'i2', qty: 150 }, { ingredientId: 'i5', qty: 30 }] }, { id: 'p2', name: 'Manual Brew V60', basePrice: 25000, recipe: [{ ingredientId: 'i1', qty: 15 }, { ingredientId: 'i4', qty: 225 }] } ]; // --- MAIN APP --- export default function SesajiPOS() { injectStyles(); // AUTH STATE const [isAuthenticated, setIsAuthenticated] = useState(false); const [pin, setPin] = useState(''); // NAV STATE const [activeTab, setActiveTab] = useState<'POS' | 'INVENTORY' | 'REPORTS'>('POS'); // DATA STATE const [ingredients, setIngredients] = useState(() => JSON.parse(localStorage.getItem('sesaji_ing') || JSON.stringify(INITIAL_INGREDIENTS))); const [products, setProducts] = useState(() => JSON.parse(localStorage.getItem('sesaji_prod') || JSON.stringify(INITIAL_PRODUCTS))); const [transactions, setTransactions] = useState(() => JSON.parse(localStorage.getItem('sesaji_tx') || '[]')); const [wasteLogs, setWasteLogs] = useState(() => JSON.parse(localStorage.getItem('sesaji_waste') || '[]')); // POS STATE const [cart, setCart] = useState([]); const [channel, setChannel] = useState('Toko'); const [receipt, setReceipt] = useState(null); // Persistence useEffect(() => { localStorage.setItem('sesaji_ing', JSON.stringify(ingredients)); }, [ingredients]); useEffect(() => { localStorage.setItem('sesaji_prod', JSON.stringify(products)); }, [products]); useEffect(() => { localStorage.setItem('sesaji_tx', JSON.stringify(transactions)); }, [transactions]); useEffect(() => { localStorage.setItem('sesaji_waste', JSON.stringify(wasteLogs)); }, [wasteLogs]); // --- LOGIC: HPP REAKTIF --- const calculateIngredientCost = (ingId: string): number => { const ing = ingredients.find(i => i.id === ingId); if (!ing) return 0; if (ing.type === 'raw') return ing.costPerUnit; // Jika self_made, hitung berdasarkan resepnya if (ing.recipe) { const totalRecipeCost = ing.recipe.reduce((acc, item) => { return acc + (calculateIngredientCost(item.ingredientId) * item.qty); }, 0); // Asumsi: resep self_made menghasilkan 1000 unit/ml (batch standard) return totalRecipeCost / 1000; } return 0; }; const calculateProductHPP = (recipe: RecipeItem[]) => { return recipe.reduce((acc, item) => acc + (calculateIngredientCost(item.ingredientId) * item.qty), 0); }; const channelMarkup = { 'Toko': 1, 'GrabFood': 1.2, 'GoFood': 1.2, 'ShopeeFood': 1.2 }; // --- ACTIONS --- const handleCheckout = () => { if (cart.length === 0) return; let currentTotalHPP = 0; let currentTotalAmount = 0; const nextIngredients = [...ingredients]; cart.forEach(item => { const hppPerUnit = calculateProductHPP(item.product.recipe); const pricePerUnit = item.product.basePrice * channelMarkup[channel]; currentTotalHPP += hppPerUnit * item.qty; currentTotalAmount += pricePerUnit * item.qty; // Deduct Stock item.product.recipe.forEach(recipeItem => { const idx = nextIngredients.findIndex(i => i.id === recipeItem.ingredientId); if (idx !== -1) nextIngredients[idx].stock -= (recipeItem.qty * item.qty); }); }); const newTx: Transaction = { id: `SES-${Date.now()}`, date: new Date().toISOString(), items: cart, totalAmount: currentTotalAmount, totalHPP: currentTotalHPP, channel: channel }; setTransactions([newTx, ...transactions]); setIngredients(nextIngredients); setReceipt(newTx); setCart([]); }; const reportData = useMemo(() => { const revenue = transactions.reduce((a, b) => a + b.totalAmount, 0); const hpp = transactions.reduce((a, b) => a + b.totalHPP, 0); const waste = wasteLogs.reduce((a, b) => a + b.cost, 0); return { revenue, hpp, waste, profit: revenue - (hpp + waste) }; }, [transactions, wasteLogs]); // --- UI COMPONENTS --- if (!isAuthenticated) { return (

    Sesaji POS

    Masukkan PIN Keamanan

    {[1, 2, 3, 4, 5, 6, 7, 8, 9, 'C', 0, 'Go'].map(k => ( ))}
    {[...Array(4)].map((_, i) => (
    i ? 'bg-[#4A5D45]' : 'bg-[#E0E7DE]'}`} /> ))}
    ); } return (
    {/* Sidebar */}
    {activeTab === 'POS' && ( <>

    Halo, Tim Sesaji!

    Siap melayani pelanggan hari ini?

    {(['Toko', 'GrabFood', 'GoFood', 'ShopeeFood'] as Channel[]).map(c => ( ))}
    {products.map(p => ( ))}
    {/* Cart Section */}

    Pesanan Aktif

    {cart.map(item => (

    {item.product.name}

    Rp {(item.product.basePrice * channelMarkup[channel]).toLocaleString()}

    {item.qty}
    ))}
    Subtotal Rp {cart.reduce((a, b) => a + (b.product.basePrice * channelMarkup[channel] * b.qty), 0).toLocaleString()}
    Total Rp {cart.reduce((a, b) => a + (b.product.basePrice * channelMarkup[channel] * b.qty), 0).toLocaleString()}
    )} {activeTab === 'INVENTORY' && (

    Gudang & Bahan Baku

    Ubah harga di sini, maka HPP produk otomatis terupdate.

    {ingredients.map(ing => (

    {ing.name}

    {ing.type === 'raw' ? 'Bahan Dasar' : 'Self-Made'}

    Stok: {ing.stock} {ing.unit}
    Rp {ing.type === 'raw' ? ( setIngredients(ingredients.map(i => i.id === ing.id ? {...i, costPerUnit: Number(e.target.value)} : i))} className="bg-transparent font-bold text-xl w-full outline-none focus:text-[#8EA985]" /> ) : ( {calculateIngredientCost(ing.id).toLocaleString()} )}
    ))}
    )} {activeTab === 'REPORTS' && (

    Laporan Laba Rugi

    } /> } color="text-orange-500" /> } color="text-red-500" /> } color="text-white" bg="bg-[#4A5D45]" />
    Riwayat Penjualan Terakhir
    {transactions.slice(0, 10).map(tx => (

    {tx.id}

    {new Date(tx.date).toLocaleString()} • {tx.channel}

    Rp {tx.totalAmount.toLocaleString()}

    Profit: Rp {(tx.totalAmount - tx.totalHPP).toLocaleString()}

    ))}
    )}
    {/* Receipt Modal */} {receipt && (

    SESAJI POS

    Jl. Kopi No. 12, Jakarta
    0812-3456-7890

    {receipt.items.map(i => (
    {i.qty}x {i.product.name} {(i.product.basePrice * channelMarkup[receipt.channel] * i.qty).toLocaleString()}
    ))}
    TOTAL Rp {receipt.totalAmount.toLocaleString()}

    Terima kasih telah berkunjung!

    )}
    ); } // --- SUB COMPONENTS --- function NavBtn({ icon, active, onClick }: { icon: React.ReactNode, active: boolean, onClick: () => void }) { return ( ); } function ReportCard({ label, value, icon, color = 'text-[#4A5D45]', bg = 'bg-white' }: any) { return (
    {icon}

    {label}

    Rp {value.toLocaleString()}

    ); }
  • Copyright © - Nungky's Blog

    Nungky's Blog - Powered by Blogger - Designed by Johanes Djogan