import React, { useState, createContext, useContext, useEffect } from 'react';
import {
ShoppingCart, Home, LayoutDashboard, Package, Trash2, Plus,
X, Image as ImageIcon, CheckCircle, Clock, ChevronDown, BarChart3,
AlertCircle, Search, Menu, LogOut, User, Settings as SettingsIcon,
Calendar as CalendarIcon, ChevronLeft, ChevronRight, Check,
Upload, Shield, Briefcase, FileText, Database, Download, UploadCloud, Terminal,
UserPlus, Award, Gift, Users, Edit3, ClipboardList, Tag, Percent
} from 'lucide-react';
// ============================================================================
// DATA AWAL (PRODUCTION READY - KOSONG UNTUK DATA ASLI)
// ============================================================================
const initialCategories = ['Kebaya', 'Jas', 'Batik', 'Gaun', 'Aksesoris'];
const initialProducts = [];
const initialOrders = [];
const initialMembers = [];
const initialPrizes = [];
const initialPromos = [];
// Akun default tetap dipertahankan agar sistem RBAC berfungsi
const initialUsers = [
{ id: 'U0', username: 'dev', password: 'dev', name: 'Developer System', role: 'developer' },
{ id: 'U1', username: 'owner', password: 'owner123', name: 'Owner Aisya', role: 'owner' },
{ id: 'U2', username: 'manager', password: 'manager123', name: 'Manajer Operasional', role: 'manager' },
{ id: 'U3', username: 'admin', password: 'admin123', name: 'Admin Kasir', role: 'admin' }
];
const initialBrandConfig = {
appName: 'Aisya Wardrobe',
slogan: 'Sewa Pakaian Premium & Eksklusif',
companyBio: 'Aisya Wardrobe adalah penyedia layanan sewa pakaian premium terpercaya. Kami berdedikasi untuk memberikan pengalaman berpakaian terbaik untuk momen spesial Anda dengan koleksi yang selalu terawat, higienis, dan mengikuti tren terkini.',
logoUrl: '',
themeColor: 'rose',
socialMedia: [
{ type: 'WhatsApp', value: '081234567890', label: 'Hubungi Kami' },
{ type: 'Instagram', value: '@aisyawardrobe', label: 'Follow IG Kami' }
],
companyEmail: 'admin@aisyawardrobe.com'
};
const AppStateContext = createContext();
const formatRupiah = (number) => new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(number);
// ============================================================================
// KOMPONEN GLOBAL & CONTEXT
// ============================================================================
const Toast = ({ message, type, onClose }) => {
useEffect(() => {
const timer = setTimeout(() => onClose(), 3000);
return () => clearTimeout(timer);
}, [onClose]);
return (
{type === 'success' ?
: type === 'error' ?
:
}
{message}
);
};
const AppStateProvider = ({ children }) => {
const [view, setView] = useState('dashboard');
const [products, setProducts] = useState(initialProducts);
const [categories, setCategories] = useState(initialCategories);
const [orders, setOrders] = useState(initialOrders);
const [cart, setCart] = useState([]);
const [users, setUsers] = useState(initialUsers);
const [loggedInUser, setLoggedInUser] = useState(null);
const [members, setMembers] = useState(initialMembers);
const [loggedInMember, setLoggedInMember] = useState(null);
const [prizes, setPrizes] = useState(initialPrizes);
const [promos, setPromos] = useState(initialPromos);
const [brandConfig, setBrandConfig] = useState(initialBrandConfig);
const [approvals, setApprovals] = useState([]);
const [toast, setToast] = useState(null);
const [logs, setLogs] = useState([]);
const showToast = (message, type = 'success') => setToast({ message, type });
const themeColors = {
rose: { bg: 'bg-rose-500', hover: 'hover:bg-rose-600', text: 'text-rose-500', border: 'border-rose-500', light: 'bg-rose-50 text-rose-600', shadow: 'shadow-rose-200' },
blue: { bg: 'bg-blue-500', hover: 'hover:bg-blue-600', text: 'text-blue-500', border: 'border-blue-500', light: 'bg-blue-50 text-blue-600', shadow: 'shadow-blue-200' },
emerald: { bg: 'bg-emerald-500', hover: 'hover:bg-emerald-600', text: 'text-emerald-500', border: 'border-emerald-500', light: 'bg-emerald-50 text-emerald-600', shadow: 'shadow-emerald-200' },
purple: { bg: 'bg-purple-500', hover: 'hover:bg-purple-600', text: 'text-purple-500', border: 'border-purple-500', light: 'bg-purple-50 text-purple-600', shadow: 'shadow-purple-200' },
slate: { bg: 'bg-slate-800', hover: 'hover:bg-slate-900', text: 'text-slate-800', border: 'border-slate-800', light: 'bg-slate-100 text-slate-800', shadow: 'shadow-slate-200' },
};
const cTheme = themeColors[brandConfig.themeColor] || themeColors.rose;
const addLog = (action, detail, user = loggedInUser?.name || 'Sistem Otomatis') => {
const newLog = { id: `LOG-${Date.now()}-${Math.floor(Math.random()*1000)}`, timestamp: new Date().toLocaleString(), user, action, detail };
setLogs(prev => [newLog, ...prev]);
};
const login = (username, password) => {
const user = users.find(u => u.username === username && u.password === password);
if (user) {
setLoggedInUser(user); setView('admin'); showToast(`Akses Diberikan: ${user.name}`, 'success');
addLog('Otorisasi', 'Melakukan login ke Admin Panel', user.name); return true;
}
return false;
};
const logout = () => { addLog('Otorisasi', 'Melakukan logout dari Admin Panel'); setLoggedInUser(null); setView('dashboard'); showToast('Berhasil keluar.', 'info'); };
const memberLogin = (username, password) => {
const member = members.find(m => m.username === username && m.password === password && m.status === 'approved');
if (member) { setLoggedInMember(member); setView('member_profile'); showToast(`Selamat datang, ${member.name}!`, 'success'); return true; }
return false;
};
const memberLogout = () => { setLoggedInMember(null); setView('dashboard'); showToast('Berhasil keluar dari akun.', 'info'); };
const submitMemberRegistration = (formData) => {
const newApproval = { id: `APP-${Date.now()}`, actionType: 'REGISTER_MEMBER', payload: formData, requestedBy: formData.name, requesterRole: 'Calon Member', date: new Date().toLocaleString(), status: 'Menunggu' };
setApprovals(prev => [newApproval, ...prev]); showToast('Pendaftaran diajukan. Menunggu persetujuan.', 'info');
};
const redeemPrize = (prize) => {
if (!loggedInMember) return;
if (loggedInMember.points >= prize.points) {
setMembers(prev => prev.map(m => m.id === loggedInMember.id ? {...m, points: m.points - prize.points} : m));
setLoggedInMember(prev => ({...prev, points: prev.points - prize.points}));
showToast(`Berhasil menukarkan poin dengan ${prize.name}!`, 'success');
} else { showToast('Poin Anda tidak mencukupi.', 'error'); }
};
const exportData = () => {
const data = { products, categories, orders, users, members, brandConfig, logs, promos };
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(data));
const downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute("href", dataStr);
downloadAnchorNode.setAttribute("download", `aisya_backup_${new Date().getTime()}.json`);
document.body.appendChild(downloadAnchorNode); downloadAnchorNode.click(); downloadAnchorNode.remove();
showToast('Data berhasil dibackup.', 'success'); addLog('Developer', 'Mengekspor backup data JSON');
};
const importData = (jsonData) => {
try {
const parsed = JSON.parse(jsonData);
if(parsed.products) setProducts(parsed.products);
if(parsed.categories) setCategories(parsed.categories);
if(parsed.orders) setOrders(parsed.orders);
if(parsed.users) setUsers(parsed.users);
if(parsed.members) setMembers(parsed.members);
if(parsed.brandConfig) setBrandConfig(parsed.brandConfig);
if(parsed.logs) setLogs(parsed.logs);
if(parsed.promos) setPromos(parsed.promos);
showToast('Data berhasil dipulihkan.', 'success'); addLog('Developer', 'Memulihkan (Restore) database');
} catch(e) { showToast('Format file JSON tidak valid!', 'error'); }
};
const requireApproval = (actionType, payload, successMsg, isDestructive = false) => {
if (!loggedInUser) return false;
if (loggedInUser.role === 'developer' || loggedInUser.role === 'owner') { executeAction(actionType, payload); showToast(successMsg, 'success'); return true; }
if (loggedInUser.role === 'manager') {
if (actionType === 'UPDATE_USER' && payload.userId === loggedInUser.id) { requestApproval(actionType, payload, `Perubahan akun Anda memerlukan izin Owner.`); return false; }
executeAction(actionType, payload); showToast(successMsg, 'success'); return true;
}
requestApproval(actionType, payload, `Memerlukan izin Manager/Owner.`);
return false;
};
const requestApproval = (actionType, payload, msg) => {
const newApproval = { id: `APP-${Date.now()}`, actionType, payload, requestedBy: loggedInUser.name, requesterRole: loggedInUser.role, date: new Date().toLocaleString(), status: 'Menunggu' };
setApprovals(prev => [newApproval, ...prev]); showToast(msg, 'info'); addLog('Persetujuan', `Mengajukan izin untuk tindakan: ${actionType}`);
};
const executeAction = (actionType, payload) => {
switch (actionType) {
case 'ADD_PRODUCT': setProducts(prev => [payload, ...prev]); addLog('Inventaris', `Menambahkan produk baru: ${payload.name}`); break;
case 'EDIT_PRODUCT': setProducts(prev => prev.map(p => p.id === payload.id ? payload : p)); addLog('Inventaris', `Mengubah rincian produk: ${payload.id}`); break;
case 'DELETE_PRODUCT': setProducts(prev => prev.filter(p => p.id !== payload.id)); addLog('Inventaris', `Menghapus produk: ${payload.id}`); break;
case 'UPDATE_PRODUCT_STATUS': setProducts(prev => prev.map(p => p.id === payload.id ? { ...p, status: payload.status } : p)); addLog('Inventaris', `Status produk ${payload.id} menjadi ${payload.status}`); break;
case 'ADD_CATEGORY': setCategories(prev => [...prev, payload]); addLog('Kategori', `Menambahkan kategori baru: ${payload}`); break;
case 'DELETE_CATEGORY': setCategories(prev => prev.filter(c => c !== payload)); addLog('Kategori', `Menghapus kategori: ${payload}`); break;
case 'UPDATE_ORDER': setOrders(prev => prev.map(o => o.id === payload.id ? { ...o, status: payload.status } : o)); addLog('Pesanan', `Status pesanan ${payload.id} menjadi ${payload.status}`); break;
case 'ADD_PRIZE': setPrizes(prev => [payload, ...prev]); addLog('Hadiah', `Menambahkan hadiah: ${payload.name}`); break;
case 'DELETE_PRIZE': setPrizes(prev => prev.filter(p => p.id !== payload.id)); addLog('Hadiah', `Menghapus hadiah: ${payload.id}`); break;
case 'ADD_PROMO': setPromos(prev => [payload, ...prev]); addLog('Promo', `Membuat kode promo baru: ${payload.code}`); break;
case 'UPDATE_PROMO_STATUS': setPromos(prev => prev.map(p => p.id === payload.id ? { ...p, status: payload.status } : p)); addLog('Promo', `Mengubah status promo ${payload.code} menjadi ${payload.status}`); break;
case 'DELETE_PROMO': setPromos(prev => prev.filter(p => p.id !== payload.id)); addLog('Promo', `Menghapus kode promo: ${payload.code}`); break;
case 'UPDATE_USER':
setUsers(prev => prev.map(u => u.id === payload.userId ? { ...u, name: payload.name, username: payload.username, password: payload.password, role: payload.role || u.role } : u));
if(loggedInUser && loggedInUser.id === payload.userId) setLoggedInUser(prev => ({...prev, name: payload.name, username: payload.username, password: payload.password}));
addLog('Pengguna', `Mengubah kredensial staf: ${payload.username}`); break;
case 'REGISTER_MEMBER':
const year = new Date().getFullYear().toString().slice(-2);
const seq = String(members.length + 1).padStart(3, '0');
const newMemberId = `MEM-${year}${seq}`;
setMembers(prev => [{ ...payload, id: newMemberId, points: 0, status: 'approved' }, ...prev]);
addLog('Member', `Menyetujui pendaftaran member: ${payload.name} (${newMemberId})`); break;
default: break;
}
};
const handleApproval = (id, action) => {
const req = approvals.find(a => a.id === id); if (!req) return;
if (loggedInUser.role === 'manager' && req.requesterRole === 'manager' && req.actionType === 'UPDATE_USER') return showToast("Hanya Owner yang dapat menyetujui kredensial Manajer.", "error");
if (action === 'approve') { addLog('Persetujuan', `Menyetujui permintaan [${req.actionType}]`); executeAction(req.actionType, req.payload); setApprovals(prev => prev.filter(a => a.id !== id)); showToast('Disetujui.', 'success'); }
else { addLog('Persetujuan', `Menolak permintaan [${req.actionType}]`); setApprovals(prev => prev.filter(a => a.id !== id)); showToast('Ditolak.', 'info'); }
};
const getAvailableStock = (productId) => {
const product = products.find(p => p.id === productId); if (!product || product.status === 'Maintenance') return 0;
const rentedAmount = orders.filter(o => ['Menunggu Konfirmasi', 'Siap Diambil', 'Sedang Disewa'].includes(o.status)).reduce((total, order) => {
const itemInOrder = order.items.find(i => i.id === productId); return total + (itemInOrder ? itemInOrder.quantity : 0);
}, 0);
return product.totalStock - rentedAmount;
};
const updateOrderStatus = (orderId, newStatus) => {
requireApproval('UPDATE_ORDER', { id: orderId, status: newStatus }, `Status diubah menjadi ${newStatus}.`);
if (newStatus === 'Selesai' || newStatus === 'Dibatalkan') {
const order = orders.find(o => o.id === orderId);
if(order) {
order.items.forEach(item => executeAction('UPDATE_PRODUCT_STATUS', { id: item.id, status: 'Maintenance' }));
addLog('Sistem Otomatis', `Memindahkan pakaian di pesanan ${orderId} ke mode Maintenance.`);
}
}
};
const addToCart = (product) => {
if (product.status === 'Maintenance') return showToast('Barang sedang dalam masa perawatan.', 'error');
setCart(prev => {
const exist = prev.find(item => item.id === product.id);
if (exist) return prev.map(item => item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item);
return [...prev, { ...product, quantity: 1 }];
});
showToast(`${product.name} ditambahkan.`, 'success');
};
const processOrder = (formData, promoData) => {
const subtotal = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const totalBase = subtotal * formData.duration;
const isDiscount = formData.duration > 7;
const durationDiscount = isDiscount ? (totalBase * 0.1) : 0;
let promoDiscount = 0;
if(promoData) {
promoDiscount = promoData.discountType === 'percentage' ? (totalBase * (promoData.discountValue / 100)) : promoData.discountValue;
}
const totalBiaya = totalBase - durationDiscount - promoDiscount;
const earnedPoints = loggedInMember ? Math.floor(totalBiaya / 10000) : 0;
const newOrder = {
id: `ORD-${2000 + orders.length + 1}`,
date: new Date().toISOString().split('T')[0],
customer: { name: formData.name, phone: formData.phone, identity: formData.identity, address: formData.address, ktpUrl: formData.ktpUrl },
items: cart.map(c => ({ id: c.id, name: c.name, price: c.price, quantity: c.quantity, image: c.image })),
duration: formData.duration, startDate: formData.startDate, endDate: formData.endDate,
total: totalBiaya, subtotalBase: totalBase, durationDiscount, promoDiscount, appliedPromoCode: promoData?.code || null,
status: 'Menunggu Konfirmasi', memberId: loggedInMember ? loggedInMember.id : null, earnedPoints
};
if (loggedInMember) {
setMembers(prev => prev.map(m => m.id === loggedInMember.id ? {...m, points: m.points + earnedPoints} : m));
setLoggedInMember(prev => ({...prev, points: prev.points + earnedPoints}));
}
setOrders([newOrder, ...orders]); setCart([]); setView('success');
addLog('Pelanggan', `Menerima pesanan baru: ${newOrder.id}`);
};
const value = {
view, setView, products, setProducts, categories, setCategories, orders, setOrders, promos, setPromos,
cart, setCart, addToCart, removeFromCart: (id) => setCart(p => p.filter(i => i.id !== id)), updateCartQuantity: (id, d) => setCart(p => p.map(i => i.id === id && i.quantity + d > 0 ? { ...i, quantity: i.quantity + d } : i)),
processOrder, getAvailableStock, users, setUsers, loggedInUser, login, logout,
members, setMembers, loggedInMember, memberLogin, memberLogout, submitMemberRegistration, prizes, setPrizes, redeemPrize,
brandConfig, setBrandConfig, cTheme, approvals, requireApproval, handleApproval, exportData, importData, showToast, updateOrderStatus, logs, addLog
};
return {toast && setToast(null)} />}{children};
};
// ============================================================================
// PUBLIC WEBSITE LAYOUT
// ============================================================================
const CustomerLayout = ({ children }) => {
const { view, setView, cart, brandConfig, cTheme, loggedInUser, loggedInMember } = useContext(AppStateContext);
const cartCount = cart.reduce((s, i) => s + i.quantity, 0);
const [showNav, setShowNav] = useState(false);
return (
setView('dashboard')}>
{brandConfig.logoUrl ?

:
}
{brandConfig.appName}
{showNav && (
setShowNav(false)}>
Menu Utama
)}
{children}
);
};
// ============================================================================
// CUSTOMER PAGES
// ============================================================================
const DashboardView = () => {
const { setView, brandConfig, cTheme } = useContext(AppStateContext);
return (
{brandConfig.appName}
{brandConfig.slogan}
Kualitas Premium
Koleksi terawat dan dijamin higienis untuk momen spesial Anda.
Bayar di Tempat
Transaksi 100% aman, bayar saat Anda mengambil barang di butik.
Poin Reward
Sewa dan kumpulkan poin berharga untuk ditukar hadiah menarik.
Mengenal {brandConfig.appName}
{brandConfig.companyBio}
{brandConfig.logoUrl ?

:
}
);
};
const CatalogView = () => {
const { products, categories, cart, addToCart, getAvailableStock, cTheme } = useContext(AppStateContext);
const [activeCategory, setActiveCategory] = useState('Semua');
const [searchQuery, setSearchQuery] = useState('');
const filteredProducts = products.filter(p =>
p.status !== 'Maintenance' &&
(activeCategory === 'Semua' || p.category === activeCategory) &&
(p.name.toLowerCase().includes(searchQuery.toLowerCase()) || p.desc.toLowerCase().includes(searchQuery.toLowerCase()) || p.id.toLowerCase().includes(searchQuery.toLowerCase()))
);
return (
Katalog Eksklusif
Temukan pilihan pakaian terbaik untuk momen istimewa Anda.
setSearchQuery(e.target.value)} />
{categories.map(cat => )}
{filteredProducts.length === 0 ? (
Katalog Kosong
Saat ini belum ada produk yang ditambahkan ke kategori ini.
) : (
{filteredProducts.map(product => {
const avail = getAvailableStock(product.id);
const cartItem = cart.find(i => i.id === product.id);
const canAdd = avail > (cartItem ? cartItem.quantity : 0);
return (

{ e.target.src = 'https://placehold.co/400?text=No+Image'; }} />
{product.category}
{avail === 0 &&
Stok Habis
}
{product.name}
{product.id}
{formatRupiah(product.price)} / hari
{product.desc}
Sisa: {avail}
);
})}
)}
);
};
const CartView = () => {
const { cart, removeFromCart, updateCartQuantity, setView, getAvailableStock, cTheme } = useContext(AppStateContext);
const subtotal = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
if (cart.length === 0) return (
Keranjang Kosong
);
return (
Keranjang Sewa
{cart.map(item => {
const avail = getAvailableStock(item.id);
return (
{item.name}
{formatRupiah(item.price)} / hari
{item.quantity}
);
})}
Ringkasan
Total Item{cart.reduce((s, i) => s + i.quantity, 0)} Pcs
Subtotal / hari{formatRupiah(subtotal)}
);
};
const CheckoutView = () => {
const { cart, processOrder, setView, cTheme, loggedInMember, showToast, promos } = useContext(AppStateContext);
const [formData, setFormData] = useState({
name: loggedInMember?.name || '', identity: loggedInMember?.identity || '',
phone: loggedInMember?.phone || '', address: loggedInMember?.address || '',
startDate: '', endDate: '', ktpUrl: loggedInMember?.ktpUrl || null
});
// Promo State
const [promoInput, setPromoInput] = useState('');
const [appliedPromo, setAppliedPromo] = useState(null);
const getDuration = () => {
if(!formData.startDate || !formData.endDate) return 1;
const start = new Date(formData.startDate); const end = new Date(formData.endDate);
const diffDays = Math.ceil(Math.abs(end - start) / (1000 * 60 * 60 * 24)) + 1;
return diffDays > 0 ? diffDays : 1;
};
const duration = getDuration();
const subtotalPerDay = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const totalBase = subtotalPerDay * duration;
const isDiscount = duration > 7;
const durationDiscount = isDiscount ? (totalBase * 0.1) : 0;
// Promo Calculation Safety Check
const isValidPromo = appliedPromo && totalBase >= appliedPromo.minPurchase;
let promoDiscountAmount = 0;
if (isValidPromo) {
promoDiscountAmount = appliedPromo.discountType === 'percentage' ? (totalBase * (appliedPromo.discountValue / 100)) : appliedPromo.discountValue;
}
const totalBiaya = totalBase - durationDiscount - promoDiscountAmount;
const potentialPoints = Math.floor(totalBiaya / 10000);
const handleKtpUpload = (e) => { const file = e.target.files[0]; if (file) { const r = new FileReader(); r.onloadend = () => setFormData(p => ({...p, ktpUrl: r.result})); r.readAsDataURL(file); } };
const handleApplyPromo = () => {
if (!promoInput.trim()) return;
const foundPromo = promos.find(p => p.code.toUpperCase() === promoInput.toUpperCase() && p.status === 'Aktif');
if (!foundPromo) return showToast('Kode voucher tidak ditemukan atau tidak aktif.', 'error');
if (new Date(foundPromo.validUntil) < new Date()) return showToast('Voucher sudah kadaluarsa.', 'error');
if (totalBase < foundPromo.minPurchase) return showToast(`Minimal belanja untuk voucher ini adalah ${formatRupiah(foundPromo.minPurchase)}`, 'error');
setAppliedPromo(foundPromo);
showToast('Voucher berhasil digunakan!', 'success');
};
const removePromo = () => { setAppliedPromo(null); setPromoInput(''); };
const handleSubmit = (e) => {
e.preventDefault();
if(new Date(formData.endDate) < new Date(formData.startDate)) { showToast("Tanggal akhir tidak valid!", "error"); return; }
processOrder({...formData, duration}, isValidPromo ? appliedPromo : null);
};
return (
Checkout
{!loggedInMember && (
Dapatkan {potentialPoints} Poin Reward!
Login/daftar member untuk kumpulkan poin.
)}
Punya Voucher Promo?
{!isValidPromo ? (
setPromoInput(e.target.value)} className="w-full px-4 py-2 bg-gray-50 border border-gray-200 rounded-xl outline-none text-[16px] focus:bg-white focus:border-gray-400 uppercase" />
) : (
Promo Digunakan
{appliedPromo.code}
)}
{appliedPromo && !isValidPromo &&
Voucher dilepas karena minimal belanja tidak terpenuhi.
}
Ringkasan Pesanan
{cart.map(item => (
{item.name}
{item.quantity}x @ {formatRupiah(item.price)}
))}
Subtotal / Hari{formatRupiah(subtotalPerDay)}
Durasi Total{duration} Hari
Total Tagihan Awal{formatRupiah(totalBase)}
{isDiscount &&
Diskon (> 7 Hari)-{formatRupiah(durationDiscount)}
}
{isValidPromo &&
Promo Voucher-{formatRupiah(promoDiscountAmount)}
}
Total Akhir
{formatRupiah(totalBiaya)}
{loggedInMember &&
Poin Didapat +{potentialPoints} }
);
};
const SuccessView = () => {
const { setView, cTheme } = useContext(AppStateContext);
return (
Berhasil!
Pesanan diterima ("Menunggu Konfirmasi").
);
};
const MemberAuthView = () => {
const { memberLogin, submitMemberRegistration, cTheme, showToast } = useContext(AppStateContext);
const [tab, setTab] = useState('login');
const [uname, setUname] = useState(''); const [pwd, setPwd] = useState('');
const [formData, setFormData] = useState({ username: '', password: '', name: '', identity: '', phone: '', birthPlace: '', birthDate: '', gender: 'Perempuan', address: '', email: '', socialMedia: '', photoUrl: null, ktpUrl: null });
const handleLogin = (e) => { e.preventDefault(); if(!memberLogin(uname, pwd)) showToast('Username salah atau belum disetujui.', 'error'); };
const handleRegister = (e) => { e.preventDefault(); submitMemberRegistration(formData); setTab('login'); };
const toBase64 = (file, callback) => { const r = new FileReader(); r.onloadend = () => callback(r.result); r.readAsDataURL(file); };
return (
{tab === 'login' && (
)}
{tab === 'register' && (
)}
);
};
const MemberProfileView = () => {
const { loggedInMember, memberLogout, cTheme, orders, prizes, redeemPrize } = useContext(AppStateContext);
const [tab, setTab] = useState('profile');
if (!loggedInMember) return null;
const myOrders = orders.filter(o => o.memberId === loggedInMember.id);
return (
{loggedInMember.photoUrl ?

:
}
{loggedInMember.name}
ID: {loggedInMember.id}
{tab === 'profile' && (
Informasi Pribadi
Email
{loggedInMember.email}
WhatsApp
{loggedInMember.phone}
NIK
{loggedInMember.identity}
Alamat
{loggedInMember.address}
)}
{tab === 'history' && (
Riwayat Penyewaan
{myOrders.length === 0 ?
Belum ada transaksi.
:
myOrders.map(o => (
{o.id} • {o.date}
{o.status}
{o.items.map(i =>
{i.quantity}x {i.name}
)}
{formatRupiah(o.total)}
+ {o.earnedPoints} Poin
))
}
)}
{tab === 'rewards' && (
Katalog Hadiah
{prizes.length === 0 ?
Katalog hadiah saat ini belum tersedia.
:
{prizes.map(p => {
const canRedeem = loggedInMember.points >= p.points;
return (
{p.name}
{p.desc}
{p.points}
);
})}
}
)}
);
};
// ============================================================================
// ADMIN VIEWS
// ============================================================================
const AdminLogin = () => {
const { login, setView } = useContext(AppStateContext);
const [uname, setUname] = useState(''); const [pwd, setPwd] = useState(''); const [err, setErr] = useState('');
const handleLogin = (e) => { e.preventDefault(); if(!login(uname, pwd)) setErr('Kredensial salah!'); };
return (
Login Admin
Masuk untuk mengelola sistem aplikasi.
{err &&
{err}
}
);
};
const AdminLayout = () => {
const { loggedInUser, logout, brandConfig, setView } = useContext(AppStateContext);
const [activeTab, setActiveTab] = useState('dashboard');
const [showSidebar, setShowSidebar] = useState(false);
const menuItems = [
{ id: 'dashboard', label: 'Statistik Kinerja', icon: BarChart3 },
{ id: 'orders', label: 'Pesanan Masuk', icon: Package },
{ id: 'inventory', label: 'Katalog Barang', icon: Briefcase },
{ id: 'calendar', label: 'Jadwal Sewa', icon: CalendarIcon },
{ id: 'customers', label: 'Database Pelanggan', icon: Users },
{ id: 'prizes', label: 'Katalog Hadiah', icon: Gift },
{ id: 'promos', label: 'Kelola Promo & Voucher', icon: Tag },
{ id: 'approvals', label: 'Persetujuan Sistem', icon: CheckCircle },
{ id: 'account', label: 'Setelan Akun', icon: User },
...(loggedInUser?.role === 'owner' || loggedInUser?.role === 'developer' ? [
{ id: 'logs', label: 'Log Sistem', icon: ClipboardList },
{ id: 'settings', label: 'Pengaturan Sistem', icon: SettingsIcon }
] : []),
...(loggedInUser?.role === 'developer' ? [{ id: 'developer', label: 'Developer Console', icon: Terminal }] : [])
];
return (
{showSidebar &&
setShowSidebar(false)}>
}
{activeTab === 'dashboard' &&
}
{activeTab === 'orders' &&
}
{activeTab === 'inventory' &&
}
{activeTab === 'calendar' &&
}
{activeTab === 'customers' &&
}
{activeTab === 'prizes' &&
}
{activeTab === 'promos' &&
}
{activeTab === 'approvals' &&
}
{activeTab === 'account' &&
}
{activeTab === 'logs' && (loggedInUser.role === 'owner' || loggedInUser.role === 'developer') &&
}
{activeTab === 'settings' && (loggedInUser.role === 'owner' || loggedInUser.role === 'developer') &&
}
{activeTab === 'developer' && loggedInUser.role === 'developer' &&
}
);
};
// --- ADMIN MODULES ---
const AdminStats = () => {
const { orders, members } = useContext(AppStateContext);
const completed = orders.filter(o => o.status === 'Selesai');
const active = orders.filter(o => ['Menunggu Konfirmasi', 'Siap Diambil', 'Sedang Disewa'].includes(o.status));
const revenue = completed.reduce((s, o) => s + o.total, 0);
return (
Pendapatan
{formatRupiah(revenue)}
Pesanan Aktif
{active.length}
Total Transaksi
{orders.length}
Total Member
{members.filter(m=>m.status === 'approved').length}
Aktivitas Terakhir
{orders.length === 0 ?
Belum ada aktivitas pesanan tercatat.
: orders.slice(0, 5).map(o => (
))}
);
};
const AdminOrderManager = () => {
const { orders, updateOrderStatus } = useContext(AppStateContext);
const [tab, setTab] = useState('aktif');
const [expandedId, setExpandedId] = useState(null);
const displayedOrders = tab === 'aktif' ? orders.filter(o => ['Menunggu Konfirmasi', 'Siap Diambil', 'Sedang Disewa'].includes(o.status)) : orders.filter(o => ['Selesai', 'Dibatalkan'].includes(o.status));
return (
{displayedOrders.length === 0 ?
Belum ada data pesanan.
: displayedOrders.map(order => (
setExpandedId(expandedId === order.id ? null : order.id)}>
{order.customer.name} {order.memberId && MEMBER}
{order.id} • {order.startDate}
{order.status}
{expandedId === order.id && (
Pelanggan
{order.customer.name}
{order.customer.phone}
NIK: {order.customer.identity}
Waktu Sewa
Mulai: {order.startDate}
Selesai: {order.endDate}
Durasi: {order.duration} Hari
Tindakan Admin
Total Tagihan{formatRupiah(order.total)}
)}
))}
);
};
const AdminInventory = () => {
const { products, categories, requireApproval, getAvailableStock, cTheme } = useContext(AppStateContext);
const [invTab, setInvTab] = useState('katalog');
const [activeCategory, setActiveCategory] = useState('Semua');
const [searchQuery, setSearchQuery] = useState('');
const [showAddForm, setShowAddForm] = useState(false);
const [newCategoryName, setNewCategoryName] = useState('');
const [expandedId, setExpandedId] = useState(null);
const [editingId, setEditingId] = useState(null);
const [editData, setEditData] = useState({ price: '', totalStock: '' });
const [formData, setFormData] = useState({ name: '', price: '', category: categories[0] || '', desc: '', image: null, totalStock: 1 });
const handleImageUpload = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => setFormData(prev => ({ ...prev, image: reader.result }));
reader.readAsDataURL(file);
}
};
const generateId = (cat) => {
const prefix = cat ? cat.substring(0,2).toUpperCase() : 'XX';
const count = products.filter(p => p.id.startsWith(prefix)).length + 1;
return `${prefix}-${1000 + count}`;
};
const handleAddProduct = (e) => {
e.preventDefault();
const newProduct = { id: generateId(formData.category), ...formData, price: parseInt(formData.price), totalStock: parseInt(formData.totalStock), status: 'Tersedia' };
if(requireApproval('ADD_PRODUCT', newProduct, 'Produk berhasil ditambahkan.')) {
setShowAddForm(false);
setFormData({ name: '', price: '', category: categories[0] || '', desc: '', image: null, totalStock: 1 });
}
};
const handleAddCategory = (e) => {
e.preventDefault();
if (newCategoryName.trim() && !categories.includes(newCategoryName.trim())) {
requireApproval('ADD_CATEGORY', newCategoryName.trim(), 'Kategori baru berhasil ditambahkan.');
setNewCategoryName('');
}
};
const handleSaveEdit = (p) => {
if(editData.price && editData.totalStock) {
requireApproval('EDIT_PRODUCT', {...p, price: parseInt(editData.price), totalStock: parseInt(editData.totalStock)}, 'Perubahan produk disimpan.');
setEditingId(null);
}
};
const filteredProducts = products.filter(p => p.status !== 'Maintenance' && (activeCategory === 'Semua' || p.category === activeCategory) && (p.name.toLowerCase().includes(searchQuery.toLowerCase()) || p.id.toLowerCase().includes(searchQuery.toLowerCase())));
const maintenanceProducts = products.filter(p => p.status === 'Maintenance');
return (
{invTab === 'katalog' && (
<>
Kelola Kategori Pakaian
{categories.length === 0 &&
Belum ada kategori.}
{categories.map(cat => (
))}
{showAddForm && (
)}
{filteredProducts.length === 0 ?
Belum ada barang di katalog.
:
{filteredProducts.map(p => {
const avail = getAvailableStock(p.id);
const isExpanded = expandedId === p.id;
const isEditing = editingId === p.id;
return (
{setExpandedId(isExpanded ? null : p.id); setEditingId(null);}}>
{p.name}
{p.id} • {p.category}
{formatRupiah(p.price)}
0?'bg-green-100 text-green-700':'bg-red-100 text-red-700'}`}>Tersedia: {avail}/{p.totalStock}
{isExpanded && (
{p.desc}
{isEditing ? (
) : (
)}
)}
);
})}
}
>
)}
{invTab === 'maintenance' && (
Barang Dalam Perawatan (Maintenance)
Barang di daftar ini disembunyikan dari katalog pelanggan.
{maintenanceProducts.length === 0 ? (
Semua barang dalam kondisi baik.
) : (
{maintenanceProducts.map(p => (
{p.name}
{p.id}
))}
)}
)}
);
};
const AdminPromo = () => {
const { promos, requireApproval, cTheme } = useContext(AppStateContext);
const [formData, setFormData] = useState({ code: '', discountType: 'percentage', discountValue: '', minPurchase: '', validUntil: '' });
const handleAdd = (e) => {
e.preventDefault();
requireApproval('ADD_PROMO', {
id: `PRM-${Date.now()}`,
code: formData.code.toUpperCase(),
discountType: formData.discountType,
discountValue: parseInt(formData.discountValue),
minPurchase: parseInt(formData.minPurchase),
validUntil: formData.validUntil,
status: 'Aktif'
}, 'Voucher promo berhasil dibuat.');
setFormData({ code: '', discountType: 'percentage', discountValue: '', minPurchase: '', validUntil: '' });
};
return (
{promos.length === 0 ?
Belum ada promo yang dibuat.
:
promos.map(p => (
{p.code}
Diskon {p.discountType === 'percentage' ? `${p.discountValue}%` : formatRupiah(p.discountValue)}
Min. Belanja: {formatRupiah(p.minPurchase)}
Kadaluarsa: {p.validUntil}
))}
);
};
const AdminCalendar = () => {
const { orders } = useContext(AppStateContext);
const [search, setSearch] = useState('');
const [currentMonth, setCurrentMonth] = useState(new Date().getMonth());
const [currentYear, setCurrentYear] = useState(new Date().getFullYear());
const [selectedDate, setSelectedDate] = useState(null);
const filteredOrders = orders.filter(order => {
if (!search) return true;
const s = search.toLowerCase();
return order.items.some(item => item.name.toLowerCase().includes(s) || item.id.toLowerCase().includes(s));
});
const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate();
const firstDay = new Date(currentYear, currentMonth, 1).getDay();
const nextMonth = () => { if (currentMonth === 11) { setCurrentMonth(0); setCurrentYear(currentYear + 1); } else setCurrentMonth(currentMonth + 1); };
const prevMonth = () => { if (currentMonth === 0) { setCurrentMonth(11); setCurrentYear(currentYear - 1); } else setCurrentMonth(currentMonth - 1); };
const monthNames = ["Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember"];
const getOrdersForDate = (day) => {
const dateStr = `${currentYear}-${String(currentMonth + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const targetDate = new Date(dateStr);
return filteredOrders.filter(o => {
const start = new Date(o.startDate);
const end = new Date(o.endDate || o.startDate);
if(!o.endDate) end.setDate(end.getDate() + (o.duration - 1));
return targetDate >= start && targetDate <= end && ['Menunggu Konfirmasi', 'Siap Diambil', 'Sedang Disewa'].includes(o.status);
});
};
return (
{monthNames[currentMonth]} {currentYear}
{['Min', 'Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab'].map(d =>
{d}
)}
{Array.from({ length: firstDay }).map((_, i) =>
)}
{Array.from({ length: daysInMonth }).map((_, i) => {
const day = i + 1;
const dayOrders = getOrdersForDate(day);
const hasOrders = dayOrders.length > 0;
return (
hasOrders && setSelectedDate({ day, month: currentMonth, year: currentYear, orders: dayOrders })} className={`p-2 md:p-4 rounded-xl border flex flex-col items-center justify-center min-h-[60px] md:min-h-[80px] transition-all ${hasOrders ? 'bg-rose-50 border-rose-200 cursor-pointer hover:bg-rose-100 shadow-sm' : 'bg-white border-gray-100 text-gray-400'}`}>
{day}
{hasOrders && {dayOrders.length} Sewa}
);
})}
{selectedDate && (
setSelectedDate(null)}>
e.stopPropagation()}>
Jadwal: {selectedDate.day} {monthNames[selectedDate.month]} {selectedDate.year}
{selectedDate.orders.map(o => (
{o.items.map(item => (
-
{item.quantity}x {item.name}
{item.id}
))}
))}
)}
);
};
const AdminCustomers = () => {
const { members, orders, approvals, handleApproval, loggedInUser, cTheme } = useContext(AppStateContext);
const [tab, setTab] = useState('member');
const approvedMembers = members.filter(m => m.status === 'approved');
const nonMembers = orders.filter(o => !o.memberId).reduce((acc, order) => {
const existing = acc.find(c => c.identity === order.customer.identity);
if (existing) {
existing.totalSpent += order.total;
existing.orderCount += 1;
} else {
acc.push({ ...order.customer, totalSpent: order.total, orderCount: 1 });
}
return acc;
}, []);
const memberApps = approvals.filter(a => a.actionType === 'REGISTER_MEMBER');
return (
{tab === 'member' && (
| Profil | Kontak | Demografi | Poin |
{approvedMembers.length === 0 ? | Belum ada member. |
: approvedMembers.map(m => (
{m.photoUrl ?  :
}
|
{m.phone} {m.email} |
{m.gender} {m.birthPlace}, {m.birthDate} |
{m.points} |
))}
)}
{tab === 'nonmember' && (
| Nama Pelanggan | KTP / NIK | Kontak | Riwayat Order |
{nonMembers.length === 0 ? | Tidak ada data. |
: nonMembers.map((nm, idx) => (
| {nm.name} |
{nm.identity} |
{nm.phone} |
{nm.orderCount}x Transaksi {formatRupiah(nm.totalSpent)} |
))}
)}
{tab === 'approval' && (
{memberApps.length === 0 ?
Tidak ada pendaftaran baru.
: memberApps.map(app => (
{app.payload.photoUrl ?

:
}
{app.requestedBy}
{app.payload.identity} • {app.payload.phone}
))}
)}
);
};
const AdminPrizes = () => {
const { prizes, requireApproval, cTheme } = useContext(AppStateContext);
const [formData, setFormData] = useState({ name: '', points: '', desc: '', image: null });
const handleImageUpload = (e) => {
const file = e.target.files[0];
if (file) { const reader = new FileReader(); reader.onloadend = () => setFormData(p => ({ ...p, image: reader.result })); reader.readAsDataURL(file); }
};
const handleAdd = (e) => {
e.preventDefault();
if(requireApproval('ADD_PRIZE', { id: `PRZ-${Date.now()}`, name: formData.name, points: parseInt(formData.points), desc: formData.desc, image: formData.image || 'https://placehold.co/200?text=Prize' }, 'Hadiah ditambahkan.')) {
setFormData({ name: '', points: '', desc: '', image: null });
}
};
return (
{prizes.length === 0 ?
Katalog hadiah poin kosong.
:
prizes.map(p => (
{p.name}
{p.desc}
{p.points} Poin
))}
);
};
const AdminApprovals = () => {
const { approvals, handleApproval, loggedInUser } = useContext(AppStateContext);
const sysApps = approvals.filter(a => a.actionType !== 'REGISTER_MEMBER');
if (sysApps.length === 0) return Tidak ada antrean sistem.
;
return (
Persetujuan Sistem ({sysApps.length})
{sysApps.map(app => {
const canApprove = loggedInUser.role === 'owner' || loggedInUser.role === 'developer' || (loggedInUser.role === 'manager' && !(app.requesterRole === 'manager' && app.actionType === 'UPDATE_USER'));
return (
Menunggu Izin{app.id}
{app.actionType.replace('_', ' ')}
Oleh: {app.requestedBy} {app.requesterRole}
{canApprove ? (
) : (
Menunggu Owner
)}
);
})}
);
};
const AdminAccountSettings = () => {
const { loggedInUser, requireApproval } = useContext(AppStateContext);
const [formData, setFormData] = useState({ name: loggedInUser.name, username: loggedInUser.username, password: loggedInUser.password });
const handleSubmit = (e) => {
e.preventDefault();
requireApproval('UPDATE_USER', { userId: loggedInUser.id, name: formData.name, username: formData.username, password: formData.password }, 'Profil diupdate.', true);
};
return (
);
};
const AdminLogs = () => {
const { logs } = useContext(AppStateContext);
return (
Log Sistem & Aktivitas
Memantau riwayat perubahan data dan status (Eksklusif untuk Owner & Developer).
| Waktu Kejadian | Pengguna | Kategori Menu | Detail Aktivitas |
{logs.length === 0 ? | Belum ada riwayat aktivitas tercatat. |
:
logs.map(log => (
| {log.timestamp} |
{log.user} |
{log.action} |
{log.detail} |
))
}
);
};
const AdminSystemSettings = () => {
const { brandConfig, setBrandConfig, users, requireApproval, showToast, loggedInUser, addLog } = useContext(AppStateContext);
const [bConfig, setBConfig] = useState(brandConfig);
const handleSaveBrand = (e) => {
e.preventDefault();
setBrandConfig(bConfig);
showToast('Konfigurasi Toko Disimpan.', 'success');
addLog('Pengaturan', 'Menyimpan perubahan konfigurasi dan profil perusahaan toko.');
};
const addSocial = () => setBConfig({...bConfig, socialMedia: [...bConfig.socialMedia, {type: 'Instagram', value: '', label: ''}]});
const updateSocial = (index, field, val) => { const newS = [...bConfig.socialMedia]; newS[index][field] = val; setBConfig({...bConfig, socialMedia: newS}); };
const removeSocial = (index) => setBConfig({...bConfig, socialMedia: bConfig.socialMedia.filter((_, i) => i !== index)});
return (
Identitas Toko (Whitelabel)
Manajemen Otoritas (Staf)
| Staf | Username | Tingkat Akses (Role) |
{users.filter(u => u.role !== 'developer').map(u => (
| {u.name} {u.id === loggedInUser.id && Anda} |
{u.username} |
|
))}
);
};
const AdminDeveloperPanel = () => {
const { users, setUsers, exportData, importData, showToast, addLog } = useContext(AppStateContext);
const handleFileUpload = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { importData(event.target.result); e.target.value = null; }; reader.readAsText(file); };
const handleRoleChange = (userId, newRole) => {
setUsers(prev => prev.map(u => u.id === userId ? { ...u, role: newRole } : u));
showToast(`Otoritas pengguna di-override oleh Developer.`, 'success');
addLog('Developer Override', `Mengubah paksa otoritas akun (${userId}) menjadi ${newRole}`);
};
return (
Developer Console
Akses Root/God Mode. Menimpa data langsung tanpa persetujuan.
Sistem Aplikasi
Versi Saat Iniv5.0.0-stable
Override Otoritas Pihak Lain
| Pengguna | Otoritas |
{users.map(u => (
| {u.name} {u.role === 'developer' && Root} |
|
))}
);
};
export default function App() { return ; }
const AppContent = () => {
const { view, loggedInUser } = useContext(AppStateContext);
if (view === 'admin' && !loggedInUser) return ;
if (view === 'admin' && loggedInUser) return ;
return (
{view === 'dashboard' && }
{view === 'catalog' && }
{view === 'cart' && }
{view === 'checkout' && }
{view === 'success' && }
{view === 'member_auth' && }
{view === 'member_profile' && }
);
};