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 (
);
}
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]'}`} />
))}
{/* Sidebar */}
{activeTab === 'POS' && (
<>
{/* Cart Section */}
>
)}
{activeTab === 'INVENTORY' && (
)}
{activeTab === 'REPORTS' && (
} />
} color="text-orange-500" />
} color="text-red-500" />
} color="text-white" bg="bg-[#4A5D45]" />
)}
{/* Receipt Modal */}
{receipt && (
)}
);
}
// --- 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 (
Halo, Tim Sesaji!
Siap melayani pelanggan hari ini?
{(['Toko', 'GrabFood', 'GoFood', 'ShopeeFood'] as Channel[]).map(c => (
))}
{products.map(p => (
))}
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()}
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()}
)}
Laporan Laba Rugi
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()}
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!
{icon}
{label}
Rp {value.toLocaleString()}
By : Nunaa
Popular Posts
-
RENCANA PELAKSANAAN PEMBELAJARAN Sekolah : SMA / MA Bintang Bulan Mata Pelajaran ...
-
KOPS SURAT ================================================================ SURAT KETERANGAN Nomor : Yang bertanda tangan di bawah in...
-
SURAT PERNYATAAN DISPENSASI PEMBAYARAN Saya yang bertanda tangan dibawah ini : Nama : NPM ...
-
KOPS SURAT ================================================================ SURAT KETERANGAN Nomor : Yang bertanda tangan di bawah ini...
-
import React, { useState, useEffect, useMemo } from 'react'; import { Lock, LayoutGrid, Package, FileText, Printer, Bluetooth, ...
-
KOP SURAT ================================================ SURAT KETERANGAN Bismillahirrahmanirrahim Ketua program studi pen...
Total Pageviews
Powered by Blogger.




















































