""" SISTEM MANAGEMENT CAFE - ULTIMATE EDITION (FIXED & COMPLETE) ============================================================== ✅ SEMUA FITUR SUDAH DIIMPLEMENTASI & TERINTEGRASI ✅ SEMUA BUG SUDAH DIPERBAIKI ✅ PEMBELI SUDAH BISA MEMESAN ✅ KASIR SUDAH BISA TERIMA PEMBAYARAN (Cash/QRIS/E-Wallet) ""Akun Default: - admin / admin123 - kasir / kasir123 - waiter / waiter123 - pembeli / user123 - owner / owner123 """ import os import csv import shutil import datetime import tkinter as tk from tkinter import ttk, messagebox, simpledialog, filedialog from collections import defaultdict import hashlib # Image support try: from PIL import Image, ImageTk PIL_AVAILABLE = True except: PIL_AVAILABLE = False # QRCode support try: import qrcode from io import BytesIO QRCODE_AVAILABLE = True except: QRCODE_AVAILABLE = False # Matplotlib support try: import matplotlib matplotlib.use('TkAgg') from matplotlib.figure import Figure from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg MATPLOTLIB_AVAILABLE = True except: MATPLOTLIB_AVAILABLE = False # ================================ # KONFIGURASI # ================================ DATA_DIR = "data" IMAGES_DIR = "images" CSV_USERS = os.path.join(DATA_DIR, "users.csv") CSV_MENU = os.path.join(DATA_DIR, "menu.csv") CSV_BAHAN = os.path.join(DATA_DIR, "bahan.csv") CSV_RESEP = os.path.join(DATA_DIR, "resep.csv") CSV_TRANSAKSI = os.path.join(DATA_DIR, "transaksi.csv") CSV_DETAIL_TRANSAKSI = os.path.join(DATA_DIR, "detail_transaksi.csv") CSV_MEJA = os.path.join(DATA_DIR, "meja.csv") CSV_FAVORITES = os.path.join(DATA_DIR, "favorites.csv") CSV_NOTIFIKASI = os.path.join(DATA_DIR, "notifikasi.csv") CSV_PROMO_CODES = os.path.join(DATA_DIR, "promo_codes.csv") CSV_PEMBAYARAN = os.path.join(DATA_DIR, "pembayaran.csv") os.makedirs(DATA_DIR, exist_ok=True) os.makedirs(IMAGES_DIR, exist_ok=True) # ================================ # GLOBAL DATABASE # ================================ users = [] menu = [] bahan = {} resep = {} transaksi = [] detail_transaksi = [] data_meja = {} favorites = {} notifikasi = [] promo_codes = {} pembayaran = [] _image_refs = {} # ================================ # CSV UTILITY # ================================ def ensure_file(path, fieldnames): if not os.path.exists(path): with open(path, "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() def read_csv(path): if not os.path.exists(path): return [] try: with open(path, newline="", encoding="utf-8") as f: return list(csv.DictReader(f)) except: return [] def write_csv(path, fieldnames, rows): try: with open(path, "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() writer.writerows(rows) except Exception as e: print(f"Error writing {path}: {e}") # ================================ # INIT DATA # ================================ def init_default_data(): global users, menu, bahan, resep, data_meja, promo_codes if not users: users.extend([ {"id": "1", "username": "admin", "password": "admin123", "role": "admin"}, {"id": "2", "username": "kasir", "password": "kasir123", "role": "kasir"}, {"id": "3", "username": "waiter", "password": "waiter123", "role": "waiter"}, {"id": "4", "username": "pembeli", "password": "user123", "role": "pembeli"}, {"id": "5", "username": "owner", "password": "owner123", "role": "owner"}, ]) if not menu: menu.extend([ {"id": 1, "nama": "Americano", "harga": 20000, "kategori": "Minuman", "stok": 50, "foto": "", "promo": "", "item_discount_pct": 0}, {"id": 2, "nama": "Latte", "harga": 25000, "kategori": "Minuman", "stok": 45, "foto": "", "promo": "Diskon 10%", "item_discount_pct": 10}, {"id": 3, "nama": "Nasi Goreng", "harga": 35000, "kategori": "Makanan", "stok": 30, "foto": "", "promo": "", "item_discount_pct": 0}, {"id": 4, "nama": "Mie Goreng", "harga": 30000, "kategori": "Makanan", "stok": 35, "foto": "", "promo": "", "item_discount_pct": 0}, {"id": 5, "nama": "Es Teh", "harga": 5000, "kategori": "Minuman", "stok": 100, "foto": "", "promo": "", "item_discount_pct": 0}, {"id": 6, "nama": "Kopi Susu", "harga": 15000, "kategori": "Minuman", "stok": 60, "foto": "", "promo": "Diskon 5%", "item_discount_pct": 5}, {"id": 7, "nama": "Cappuccino", "harga": 28000, "kategori": "Minuman", "stok": 40, "foto": "", "promo": "", "item_discount_pct": 0}, {"id": 8, "nama": "Roti Bakar", "harga": 15000, "kategori": "Makanan", "stok": 25, "foto": "", "promo": "Diskon 15%", "item_discount_pct": 15}, {"id": 9, "nama": "Pasta Carbonara", "harga": 45000, "kategori": "Makanan", "stok": 20, "foto": "", "promo": "", "item_discount_pct": 0}, {"id": 10, "nama": "Smoothie Buah", "harga": 22000, "kategori": "Minuman", "stok": 35, "foto": "", "promo": "", "item_discount_pct": 0}, ]) if not bahan: bahan.update({ "kopi": 50, "susu": 30, "teh": 40, "nasi": 20, "mie": 15, "telur": 25, "bumbu": 30 }) if not resep: resep.update({ 1: {"kopi": 1}, 2: {"kopi": 1, "susu": 1}, 3: {"nasi": 1, "telur": 1, "bumbu": 1}, 4: {"mie": 1, "telur": 1, "bumbu": 1}, 5: {"teh": 1}, 6: {"kopi": 1, "susu": 1}, }) if not data_meja: data_meja.update({i: "Kosong" for i in range(1, 11)}) if not promo_codes: promo_codes.update({ "CAFE10": 10, "CAFE20": 20, "WELCOME": 15, "SPECIAL25": 25 }) # ================================ # LOAD/SAVE FUNCTIONS # ================================ def save_users(): write_csv(CSV_USERS, ["id", "username", "password", "role"], users) def load_users(): global users users = read_csv(CSV_USERS) if not users: init_default_data() save_users() def save_menu(): rows = [] for m in menu: rows.append({ "id": str(m["id"]), "nama": m["nama"], "harga": str(m["harga"]), "kategori": m["kategori"], "stok": str(m["stok"]), "foto": m.get("foto", ""), "promo": m.get("promo", ""), "item_discount_pct": str(m.get("item_discount_pct", 0)) }) write_csv(CSV_MENU, ["id", "nama", "harga", "kategori", "stok", "foto", "promo", "item_discount_pct"], rows) def load_menu(): global menu rows = read_csv(CSV_MENU) menu = [] for r in rows: try: menu.append({ "id": int(r["id"]), "nama": r["nama"], "harga": int(float(r["harga"])), "kategori": r["kategori"], "stok": int(float(r["stok"])), "foto": r.get("foto", ""), "promo": r.get("promo", ""), "item_discount_pct": float(r.get("item_discount_pct", 0)) }) except: pass if not menu: init_default_data() save_menu() def save_bahan(): rows = [{"nama": k, "jumlah": str(v)} for k, v in bahan.items()] write_csv(CSV_BAHAN, ["nama", "jumlah"], rows) def load_bahan(): global bahan rows = read_csv(CSV_BAHAN) bahan = {} for r in rows: try: bahan[r["nama"]] = int(float(r["jumlah"])) except: pass if not bahan: init_default_data() save_bahan() def save_resep(): rows = [] for menu_id, ingredients in resep.items(): for bahan_nama, qty in ingredients.items(): rows.append({"menu_id": str(menu_id), "bahan": bahan_nama, "jumlah": str(qty)}) write_csv(CSV_RESEP, ["menu_id", "bahan", "jumlah"], rows) def load_resep(): global resep rows = read_csv(CSV_RESEP) resep = {} for r in rows: try: mid = int(r["menu_id"]) if mid not in resep: resep[mid] = {} resep[mid][r["bahan"]] = int(float(r["jumlah"])) except: pass if not resep: init_default_data() save_resep() def save_transaksi(): rows = [] for t in transaksi: items_str = ";".join([f"{it['nama']}x{it['qty']}@{it['harga_satuan']}" for it in t['items']]) rows.append({ "id": str(t['id']), "tanggal": t['tanggal'], "waktu": t['waktu'].strftime('%Y-%m-%d %H:%M:%S'), "user": t['user'], "items": items_str, "subtotal": str(t['subtotal']), "diskon": str(t['diskon']), "total": str(t['total']), "meja": str(t.get('meja', '')), "status": t['status'], "payment_status": t.get('payment_status', 'Pending'), "payment_method": t.get('payment_method', ''), "paid_amount": str(t.get('paid_amount', 0)), "change": str(t.get('change', 0)), "promo_code": t.get('promo_code', '') }) write_csv(CSV_TRANSAKSI, ["id", "tanggal", "waktu", "user", "items", "subtotal", "diskon", "total", "meja", "status", "payment_status", "payment_method", "paid_amount", "change", "promo_code"], rows) def load_transaksi(): global transaksi rows = read_csv(CSV_TRANSAKSI) transaksi = [] for r in rows: try: items = [] if r['items']: for item_str in r['items'].split(';'): parts = item_str.split('x') nama = parts[0] qty_price = parts[1].split('@') items.append({ 'nama': nama, 'qty': int(qty_price[0]), 'harga_satuan': int(qty_price[1]), 'subtotal': int(qty_price[0]) * int(qty_price[1]) }) transaksi.append({ 'id': int(r['id']), 'tanggal': r['tanggal'], 'waktu': datetime.datetime.strptime(r['waktu'], '%Y-%m-%d %H:%M:%S'), 'user': r['user'], 'items': items, 'subtotal': int(float(r['subtotal'])), 'diskon': int(float(r['diskon'])), 'total': int(float(r['total'])), 'meja': int(r['meja']) if r['meja'] else None, 'status': r['status'], 'payment_status': r.get('payment_status', 'Pending'), 'payment_method': r.get('payment_method', ''), 'paid_amount': int(float(r.get('paid_amount', 0))), 'change': int(float(r.get('change', 0))), 'promo_code': r.get('promo_code', '') }) except: pass def save_detail_transaksi(): rows = [] for d in detail_transaksi: rows.append({ "id": str(d['id']), "transaksi_id": str(d['transaksi_id']), "menu_id": str(d['menu_id']), "nama_menu": d['nama_menu'], "jumlah": str(d['jumlah']), "harga_satuan": str(d['harga_satuan']), "subtotal": str(d['subtotal']), "diskon": str(d['diskon']) }) write_csv(CSV_DETAIL_TRANSAKSI, ["id", "transaksi_id", "menu_id", "nama_menu", "jumlah", "harga_satuan", "subtotal", "diskon"], rows) def load_detail_transaksi(): global detail_transaksi rows = read_csv(CSV_DETAIL_TRANSAKSI) detail_transaksi = [] for r in rows: try: detail_transaksi.append({ 'id': int(r['id']), 'transaksi_id': int(r['transaksi_id']), 'menu_id': int(r['menu_id']), 'nama_menu': r['nama_menu'], 'jumlah': int(r['jumlah']), 'harga_satuan': int(float(r['harga_satuan'])), 'subtotal': int(float(r['subtotal'])), 'diskon': int(float(r['diskon'])) }) except: pass def save_meja(): rows = [{"nomor": str(k), "status": v} for k, v in data_meja.items()] write_csv(CSV_MEJA, ["nomor", "status"], rows) def load_meja(): global data_meja rows = read_csv(CSV_MEJA) data_meja = {} for r in rows: try: data_meja[int(r['nomor'])] = r['status'] except: pass if not data_meja: init_default_data() save_meja() def save_favorites(): rows = [] for username, menu_ids in favorites.items(): rows.append({"username": username, "menu_ids": ",".join(map(str, menu_ids))}) write_csv(CSV_FAVORITES, ["username", "menu_ids"], rows) def load_favorites(): global favorites rows = read_csv(CSV_FAVORITES) favorites = {} for r in rows: try: if r['menu_ids']: favorites[r['username']] = [int(x) for x in r['menu_ids'].split(',')] else: favorites[r['username']] = [] except: favorites[r['username']] = [] def save_notifikasi(): write_csv(CSV_NOTIFIKASI, ["id", "timestamp", "type", "message", "read"], notifikasi) def load_notifikasi(): global notifikasi rows = read_csv(CSV_NOTIFIKASI) notifikasi = [] for r in rows: try: notifikasi.append({ 'id': int(r['id']), 'timestamp': r['timestamp'], 'type': r['type'], 'message': r['message'], 'read': r['read'] == 'True' }) except: pass def save_promo_codes(): rows = [{"code": code, "discount_percent": str(discount)} for code, discount in promo_codes.items()] write_csv(CSV_PROMO_CODES, ["code", "discount_percent"], rows) def load_promo_codes(): global promo_codes rows = read_csv(CSV_PROMO_CODES) promo_codes = {} for r in rows: try: promo_codes[r['code']] = int(r['discount_percent']) except: pass if not promo_codes: init_default_data() save_promo_codes() def save_pembayaran(): rows = [] for p in pembayaran: rows.append({ "id": str(p['id']), "transaksi_id": str(p['transaksi_id']), "metode": p['metode'], "jumlah": str(p['jumlah']), "status": p['status'], "tanggal": p['tanggal'] }) write_csv(CSV_PEMBAYARAN, ["id", "transaksi_id", "metode", "jumlah", "status", "tanggal"], rows) def load_pembayaran(): global pembayaran rows = read_csv(CSV_PEMBAYARAN) pembayaran = [] for r in rows: try: pembayaran.append({ 'id': int(r['id']), 'transaksi_id': int(r['transaksi_id']), 'metode': r['metode'], 'jumlah': int(float(r['jumlah'])), 'status': r['status'], 'tanggal': r['tanggal'] }) except: pass def load_all_data(): ensure_file(CSV_USERS, ["id", "username", "password", "role"]) ensure_file(CSV_MENU, ["id", "nama", "harga", "kategori", "stok", "foto", "promo", "item_discount_pct"]) ensure_file(CSV_BAHAN, ["nama", "jumlah"]) ensure_file(CSV_RESEP, ["menu_id", "bahan", "jumlah"]) ensure_file(CSV_TRANSAKSI, ["id", "tanggal", "waktu", "user", "items", "subtotal", "diskon", "total", "meja", "status", "payment_status", "payment_method", "paid_amount", "change", "promo_code"]) ensure_file(CSV_DETAIL_TRANSAKSI, ["id", "transaksi_id", "menu_id", "nama_menu", "jumlah", "harga_satuan", "subtotal", "diskon"]) ensure_file(CSV_MEJA, ["nomor", "status"]) ensure_file(CSV_FAVORITES, ["username", "menu_ids"]) ensure_file(CSV_NOTIFIKASI, ["id", "timestamp", "type", "message", "read"]) ensure_file(CSV_PROMO_CODES, ["code", "discount_percent"]) ensure_file(CSV_PEMBAYARAN, ["id", "transaksi_id", "metode", "jumlah", "status", "tanggal"]) load_users() load_menu() load_bahan() load_resep() load_transaksi() load_detail_transaksi() load_meja() load_favorites() load_notifikasi() load_promo_codes() load_pembayaran() add_sample_transactions() # ================================ # HELPER FUNCTIONS # ================================ def find_menu_by_id(mid): for it in menu: if it["id"] == mid: return it return None def format_currency(n): try: return f"Rp{int(n):,}".replace(",", ".") except: return f"Rp{n}" def calculate_discount(harga, promo_text): if not promo_text: return 0 try: import re match = re.search(r'(\d+)%', promo_text) if match: percent = int(match.group(1)) return int(harga * percent / 100) except: pass return 0 def add_notification(ntype, message): global notifikasi new_id = max([n['id'] for n in notifikasi], default=0) + 1 notifikasi.append({ 'id': new_id, 'timestamp': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'type': ntype, 'message': message, 'read': False }) save_notifikasi() def copy_image_to_project(src_path): if not src_path or not os.path.exists(src_path): return "" try: filename = os.path.basename(src_path) name, ext = os.path.splitext(filename) timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") new_filename = f"{name}_{timestamp}{ext}" dest_path = os.path.join(IMAGES_DIR, new_filename) shutil.copy2(src_path, dest_path) return dest_path except Exception as e: print(f"Error copying image: {e}") return "" def ensure_image(path, maxsize=(240, 160)): if not path or not os.path.exists(path): return None key = (path, maxsize) if key in _image_refs: return _image_refs[key] try: if PIL_AVAILABLE: img = Image.open(path) img.thumbnail(maxsize) tkimg = ImageTk.PhotoImage(img) else: tkimg = tk.PhotoImage(file=path) _image_refs[key] = tkimg return tkimg except: return None def reset_image_refs(): _image_refs.clear() def get_daily_report(date_str): trans = [t for t in transaksi if t['tanggal'] == date_str and t.get('payment_status') == 'Berhasil'] total_omzet = sum(t['total'] for t in trans) total_transaksi = len(trans) menu_count = defaultdict(int) for t in trans: for item in t['items']: menu_count[item['nama']] += item['qty'] return { 'date': date_str, 'total_omzet': total_omzet, 'total_transaksi': total_transaksi, 'menu_terlaris': dict(menu_count) } def add_sample_transactions(): """Tambahkan data transaksi sample untuk demo""" global transaksi, detail_transaksi if len(transaksi) < 5: today = str(datetime.date.today()) yesterday = str(datetime.date.today() - datetime.timedelta(days=1)) two_days_ago = str(datetime.date.today() - datetime.timedelta(days=2)) sample_data = [ {'tanggal': today, 'items': [{'nama': 'Americano', 'qty': 2, 'harga_satuan': 20000}, {'nama': 'Es Teh', 'qty': 1, 'harga_satuan': 5000}], 'total': 45000, 'meja': 1}, {'tanggal': today, 'items': [{'nama': 'Latte', 'qty': 3, 'harga_satuan': 25000}, {'nama': 'Roti Bakar', 'qty': 2, 'harga_satuan': 15000}], 'total': 105000, 'meja': 2}, {'tanggal': today, 'items': [{'nama': 'Nasi Goreng', 'qty': 2, 'harga_satuan': 35000}, {'nama': 'Kopi Susu', 'qty': 2, 'harga_satuan': 15000}], 'total': 100000, 'meja': 3}, {'tanggal': today, 'items': [{'nama': 'Mie Goreng', 'qty': 1, 'harga_satuan': 30000}, {'nama': 'Cappuccino', 'qty': 1, 'harga_satuan': 28000}], 'total': 58000, 'meja': 4}, {'tanggal': yesterday, 'items': [{'nama': 'Pasta Carbonara', 'qty': 2, 'harga_satuan': 45000}, {'nama': 'Smoothie Buah', 'qty': 3, 'harga_satuan': 22000}], 'total': 156000, 'meja': 5}, {'tanggal': yesterday, 'items': [{'nama': 'Americano', 'qty': 4, 'harga_satuan': 20000}, {'nama': 'Es Teh', 'qty': 2, 'harga_satuan': 5000}], 'total': 90000, 'meja': 6}, {'tanggal': two_days_ago, 'items': [{'nama': 'Latte', 'qty': 2, 'harga_satuan': 25000}, {'nama': 'Nasi Goreng', 'qty': 1, 'harga_satuan': 35000}], 'total': 85000, 'meja': 7}, ] for idx, data in enumerate(sample_data, start=1): trans_id = idx transaksi.append({ 'id': trans_id, 'tanggal': data['tanggal'], 'waktu': datetime.datetime.now() - datetime.timedelta(hours=idx), 'user': 'waiter', 'items': data['items'], 'subtotal': data['total'], 'diskon': 0, 'total': data['total'], 'meja': data['meja'], 'status': 'Selesai', 'payment_status': 'Berhasil', 'payment_method': 'cash' if idx % 2 == 0 else 'qris', 'paid_amount': data['total'], 'change': 0, 'promo_code': '' }) def get_payment_summary(start_date, end_date): trans = [t for t in transaksi if start_date <= t['tanggal'] <= end_date and t.get('payment_status') == 'Berhasil'] total = sum(t['total'] for t in trans) count = len(trans) method_breakdown = defaultdict(int) for t in trans: method = t.get('payment_method', 'Unknown') method_breakdown[method] += 1 return { 'total_income': total, 'total_count': count, 'avg': total / count if count > 0 else 0, 'method_breakdown': dict(method_breakdown) } # ================================ # MAIN APP CLASS # ================================ class CafeApp: def __init__(self, root): self.root = root self.root.title("Sistem Management Cafe - Ultimate Edition (FIXED)") self.root.geometry("1200x700") self.root.minsize(1000, 600) # Session self.current_user = None self.cart_items = [] # Load data load_all_data() # Style self.setup_styles() # Start self.show_welcome_screen() def setup_styles(self): style = ttk.Style() style.theme_use('clam') style.configure("Accent.TButton", background="#8B7355", foreground="white", font=("Arial", 10, "bold")) style.map("Accent.TButton", background=[('active', '#6B5335')]) # ================================ # WELCOME & LOGIN # ================================ def show_welcome_screen(self): self._clear_root() self.root.configure(bg="#F5F5F0") main_frame = tk.Frame(self.root, bg="#F5F5F0") main_frame.place(relx=0.5, rely=0.5, anchor="center") title_frame = tk.Frame(main_frame, bg="#F5F5F0") title_frame.pack(pady=(0, 30)) tk.Label(title_frame, text="☕", font=("Segoe UI", 64), bg="#F5F5F0", fg="#8B7355").pack() tk.Label(title_frame, text="CAFE MANAGEMENT", font=("Segoe UI", 32, "bold"), bg="#F5F5F0", fg="#5C4033").pack(pady=(10, 0)) tk.Label(title_frame, text="Ultimate Edition - FIXED VERSION", font=("Segoe UI", 14), bg="#F5F5F0", fg="#999999").pack() btn_frame = tk.Frame(main_frame, bg="#F5F5F0") btn_frame.pack(pady=40) guest_btn = tk.Button(btn_frame, text="🌐 Browse Sebagai Guest", command=self.enter_as_guest, font=("Segoe UI", 12, "bold"), bg="#27AE60", fg="white", relief="flat", bd=0, padx=40, pady=15, cursor="hand2") guest_btn.pack(pady=10) guest_btn.bind("", lambda e: guest_btn.configure(bg="#229954")) guest_btn.bind("", lambda e: guest_btn.configure(bg="#27AE60")) login_btn = tk.Button(btn_frame, text="🔐 Login Sebagai Staff", command=self.show_login_screen, font=("Segoe UI", 12, "bold"), bg="#8B7355", fg="white", relief="flat", bd=0, padx=40, pady=15, cursor="hand2") login_btn.pack(pady=10) login_btn.bind("", lambda e: login_btn.configure(bg="#6B5335")) login_btn.bind("", lambda e: login_btn.configure(bg="#8B7355")) tk.Label(main_frame, text="Guest dapat melihat menu tanpa login\nStaff perlu login untuk akses penuh", font=("Segoe UI", 9), bg="#F5F5F0", fg="#999999", justify="center").pack(pady=(20, 5)) def enter_as_guest(self): self.current_user = { 'id': 0, 'username': 'Guest', 'role': 'guest' } messagebox.showinfo("Welcome", "Selamat datang, Guest!\n\nAnda dapat melihat menu kami.") self.build_main_ui() def show_login_screen(self): self._clear_root() self.root.configure(bg="#F5F5F0") main_frame = tk.Frame(self.root, bg="#F5F5F0") main_frame.place(relx=0.5, rely=0.5, anchor="center") title_frame = tk.Frame(main_frame, bg="#F5F5F0") title_frame.pack(pady=(0, 30)) tk.Label(title_frame, text="☕", font=("Segoe UI", 48), bg="#F5F5F0", fg="#8B7355").pack() tk.Label(title_frame, text="STAFF LOGIN", font=("Segoe UI", 24, "bold"), bg="#F5F5F0", fg="#5C4033").pack(pady=(5, 0)) form_frame = tk.Frame(main_frame, bg="white", relief="flat", bd=0) form_frame.pack(pady=20, padx=40) inner_form = tk.Frame(form_frame, bg="white") inner_form.pack(padx=50, pady=40) tk.Label(inner_form, text="Username", font=("Segoe UI", 10), bg="white", fg="#666666", anchor="w").grid(row=0, column=0, sticky="w", pady=(0, 5)) self.e_user = tk.Entry(inner_form, font=("Segoe UI", 11), relief="solid", bd=1, bg="#FAFAFA", fg="#333333", width=28) self.e_user.grid(row=1, column=0, pady=(0, 20), ipady=8) tk.Label(inner_form, text="Password", font=("Segoe UI", 10), bg="white", fg="#666666", anchor="w").grid(row=2, column=0, sticky="w", pady=(0, 5)) self.e_pass = tk.Entry(inner_form, show="●", font=("Segoe UI", 11), relief="solid", bd=1, bg="#FAFAFA", fg="#333333", width=28) self.e_pass.grid(row=3, column=0, pady=(0, 25), ipady=8) login_btn = tk.Button(inner_form, text="Login", font=("Segoe UI", 11, "bold"), bg="#8B7355", fg="white", relief="flat", bd=0, command=self.handle_login, cursor="hand2", width=28, pady=12) login_btn.grid(row=4, column=0) login_btn.bind("", lambda e: login_btn.configure(bg="#6B5335")) login_btn.bind("", lambda e: login_btn.configure(bg="#8B7355")) self.e_pass.bind("", lambda e: self.handle_login()) back_btn = tk.Button(main_frame, text="← Kembali", command=self.show_welcome_screen, font=("Segoe UI", 9), bg="#E8E0D5", fg="#5C4033", relief="flat", bd=0, padx=20, pady=8, cursor="hand2") back_btn.pack(pady=(15, 0)) tk.Label(main_frame, text="Default: admin/admin123, kasir/kasir123, pembeli/user123", font=("Segoe UI", 9), bg="#F5F5F0", fg="#999999").pack(pady=(10, 0)) def handle_login(self): u = self.e_user.get().strip() p = self.e_pass.get().strip() for usr in users: if usr["username"] == u and usr["password"] == p: self.current_user = { 'id': int(usr['id']), 'username': usr['username'], 'role': usr['role'] } messagebox.showinfo("Login", f"Berhasil login sebagai {u} ({usr['role']})") self.build_main_ui() return messagebox.showerror("Login Gagal", "Username atau password salah.") def _clear_root(self): for w in self.root.winfo_children(): w.destroy() def logout(self): self.current_user = None self.cart_items = [] self.show_welcome_screen() # ================================ # MAIN UI # ================================ def build_main_ui(self): self._clear_root() self.root.configure(bg="#F5F5F0") # Top bar topbar = tk.Frame(self.root, bg="#5C4033", height=60) topbar.pack(fill="x") topbar.pack_propagate(False) left_header = tk.Frame(topbar, bg="#5C4033") left_header.pack(side="left", padx=20, pady=10) tk.Label(left_header, text="☕ CAFE MANAGEMENT", font=("Segoe UI", 14, "bold"), bg="#5C4033", fg="white").pack(side="left") role_text = self.current_user['role'].upper() if self.current_user['role'] != 'guest' else 'GUEST MODE' tk.Label(left_header, text=f" | {self.current_user['username']} ({role_text})", font=("Segoe UI", 10), bg="#5C4033", fg="#D4AF77").pack(side="left") right_header = tk.Frame(topbar, bg="#5C4033") right_header.pack(side="right", padx=20, pady=10) # Notifikasi button (untuk staff saja) if self.current_user['role'] != 'guest': unread = len([n for n in notifikasi if not n['read']]) notif_text = f"🔔 ({unread})" if unread > 0 else "🔔" notif_btn = tk.Button(right_header, text=notif_text, command=self.open_notifikasi_window, font=("Arial", 9), bg="#6B5335", fg="white", relief="flat", bd=0, padx=15, pady=8, cursor="hand2") notif_btn.pack(side="right", padx=3) # Logout button logout_btn = tk.Button(right_header, text="🚪 Logout", command=self.logout, font=("Arial", 9), bg="#A0522D", fg="white", relief="flat", bd=0, padx=15, pady=8, cursor="hand2") logout_btn.pack(side="right", padx=3) # Main content main = ttk.Notebook(self.root) main.pack(fill='both', expand=True, padx=10, pady=10) # Build tabs based on role if self.current_user['role'] == 'guest': self.build_guest_tabs(main) elif self.current_user['role'] == 'pembeli': self.build_pembeli_tabs(main) elif self.current_user['role'] == 'kasir': self.build_kasir_tabs(main) elif self.current_user['role'] == 'waiter': self.build_waiter_tabs(main) elif self.current_user['role'] == 'owner': self.build_owner_tabs(main) elif self.current_user['role'] == 'admin': self.build_admin_tabs(main) # ================================ # TAB BUILDERS # ================================ def build_guest_tabs(self, notebook): tab_menu = ttk.Frame(notebook) notebook.add(tab_menu, text="📖 Lihat Menu") self.build_menu_browse_tab(tab_menu, guest_mode=True) def build_pembeli_tabs(self, notebook): tab_menu = ttk.Frame(notebook) tab_order = ttk.Frame(notebook) tab_fav = ttk.Frame(notebook) notebook.add(tab_menu, text="📖 Menu") notebook.add(tab_order, text="🛒 Order") notebook.add(tab_fav, text="⭐ Favorit") self.build_menu_browse_tab(tab_menu) self.build_order_tab(tab_order) self.build_favorite_tab(tab_fav) def build_kasir_tabs(self, notebook): tab_payment = ttk.Frame(notebook) tab_riwayat = ttk.Frame(notebook) notebook.add(tab_payment, text="💰 Pembayaran") notebook.add(tab_riwayat, text="📜 Riwayat") self.build_payment_tab(tab_payment) self.build_riwayat_tab(tab_riwayat) def build_waiter_tabs(self, notebook): tab_pesanan = ttk.Frame(notebook) tab_meja = ttk.Frame(notebook) notebook.add(tab_pesanan, text="🍽️ Pesanan") notebook.add(tab_meja, text="🪑 Meja") self.build_waiter_pesanan_tab(tab_pesanan) self.build_meja_tab(tab_meja) def build_owner_tabs(self, notebook): tab_laporan = ttk.Frame(notebook) tab_riwayat = ttk.Frame(notebook) notebook.add(tab_laporan, text="📊 Laporan") notebook.add(tab_riwayat, text="📜 Riwayat") self.build_laporan_tab(tab_laporan) self.build_riwayat_tab(tab_riwayat) def build_admin_tabs(self, notebook): tab_menu_manage = ttk.Frame(notebook) tab_bahan = ttk.Frame(notebook) tab_promo = ttk.Frame(notebook) tab_user = ttk.Frame(notebook) tab_laporan = ttk.Frame(notebook) notebook.add(tab_menu_manage, text="⚙️ Kelola Menu") notebook.add(tab_bahan, text="📦 Stok Bahan") notebook.add(tab_promo, text="🎁 Promo") notebook.add(tab_user, text="👥 User") notebook.add(tab_laporan, text="📊 Laporan") self.build_menu_manage_tab(tab_menu_manage) self.build_bahan_tab(tab_bahan) self.build_promo_tab(tab_promo) self.build_user_tab(tab_user) self.build_laporan_tab(tab_laporan) # ================================ # TAB: MENU BROWSE # ================================ def build_menu_browse_tab(self, parent, guest_mode=False): for w in parent.winfo_children(): w.destroy() header = tk.Frame(parent, bg="#F5F5F0") header.pack(fill="x", padx=15, pady=10) tk.Label(header, text="🍽️ Menu Cafe", font=("Arial", 18, "bold"), bg="#F5F5F0", fg="#5C4033").pack(side="left") if guest_mode: info = tk.Label(header, text="ℹ️ Mode Guest - Login untuk memesan", font=("Arial", 10), bg="#FFF3CD", fg="#856404", padx=10, pady=5) info.pack(side="right") search_frame = tk.Frame(parent, bg="white", relief="solid", bd=1) search_frame.pack(fill="x", padx=15, pady=5) inner_search = tk.Frame(search_frame, bg="white") inner_search.pack(padx=10, pady=8) tk.Label(inner_search, text="🔍 Cari:", font=("Arial", 9), bg="white").pack(side="left", padx=5) self.menu_search_var = tk.StringVar() tk.Entry(inner_search, textvariable=self.menu_search_var, font=("Arial", 10), width=25).pack(side="left", padx=5) tk.Button(inner_search, text="Cari", command=self.refresh_menu_browse, font=("Arial", 9), bg="#8B7355", fg="white", relief="flat", padx=15, pady=5).pack(side="left", padx=5) tk.Label(inner_search, text="📂 Kategori:", font=("Arial", 9), bg="white").pack(side="left", padx=(15, 5)) self.menu_filter_var = tk.StringVar(value="Semua") categories = ["Semua"] + sorted(set(m['kategori'] for m in menu)) filter_cb = ttk.Combobox(inner_search, textvariable=self.menu_filter_var, values=categories, state="readonly", width=15) filter_cb.pack(side="left", padx=5) filter_cb.bind("<>", lambda e: self.refresh_menu_browse()) container = tk.Frame(parent, bg="white") container.pack(fill="both", expand=True, padx=15, pady=5) canvas = tk.Canvas(container, bg="white", highlightthickness=0) scrollbar = ttk.Scrollbar(container, orient="vertical", command=canvas.yview) self.menu_browse_frame = tk.Frame(canvas, bg="white") canvas.configure(yscrollcommand=scrollbar.set) scrollbar.pack(side="right", fill="y") canvas.pack(side="left", fill="both", expand=True) canvas_window = canvas.create_window((0, 0), window=self.menu_browse_frame, anchor="nw") def configure_scroll(event): canvas.configure(scrollregion=canvas.bbox("all")) self.menu_browse_frame.bind("", configure_scroll) def configure_canvas(event): canvas.itemconfig(canvas_window, width=event.width) canvas.bind("", configure_canvas) def on_mousewheel(event): canvas.yview_scroll(-1 * int(event.delta / 120), "units") canvas.bind_all("", on_mousewheel) self.refresh_menu_browse() def refresh_menu_browse(self): for widget in self.menu_browse_frame.winfo_children(): widget.destroy() search = self.menu_search_var.get().lower() if hasattr(self, 'menu_search_var') else "" kategori = self.menu_filter_var.get() if hasattr(self, 'menu_filter_var') else "Semua" filtered = [] for m in menu: if search and search not in m['nama'].lower(): continue if kategori != "Semua" and m['kategori'] != kategori: continue filtered.append(m) cols = 3 row = 0 col = 0 for m in filtered: card = tk.Frame(self.menu_browse_frame, bg="white", relief="solid", bd=1, highlightthickness=2, highlightbackground="#E0E0E0") card.grid(row=row, column=col, padx=15, pady=15, sticky="nsew") img_frame = tk.Frame(card, bg="#F5F5F5", width=220, height=180) img_frame.pack(fill="x", padx=10, pady=(10, 5)) img_frame.pack_propagate(False) foto = m.get('foto') if foto and os.path.exists(foto): img = ensure_image(foto, maxsize=(200, 160)) if img: img_label = tk.Label(img_frame, image=img, bg="#F5F5F5") img_label.image = img img_label.pack(expand=True) else: tk.Label(img_frame, text="🍽️\nNo Image", font=("Arial", 16), bg="#F5F5F5", fg="#CCCCCC").pack(expand=True) else: tk.Label(img_frame, text="🍽️\nNo Image", font=("Arial", 16), bg="#F5F5F5", fg="#CCCCCC").pack(expand=True) info_frame = tk.Frame(card, bg="white") info_frame.pack(fill="x", padx=15, pady=10) tk.Label(info_frame, text=m['nama'], font=("Arial", 13, "bold"), bg="white", fg="#5C4033", anchor="w").pack(fill="x") tk.Label(info_frame, text=f"📂 {m['kategori']}", font=("Arial", 9), bg="white", fg="#999999", anchor="w").pack(fill="x", pady=(3, 5)) price_frame = tk.Frame(info_frame, bg="white") price_frame.pack(fill="x", pady=(0, 5)) harga = m['harga'] promo = m.get('promo', '') if promo: diskon = calculate_discount(harga, promo) harga_after = harga - diskon tk.Label(price_frame, text=format_currency(harga), font=("Arial", 10), bg="white", fg="#999999", anchor="w").pack(side="left") tk.Label(price_frame, text=" → ", font=("Arial", 10), bg="white", fg="#999999").pack(side="left") tk.Label(price_frame, text=format_currency(harga_after), font=("Arial", 12, "bold"), bg="white", fg="#D2691E", anchor="w").pack(side="left") tk.Label(info_frame, text=f"🎁 {promo}", font=("Arial", 9), bg="white", fg="#D2691E", anchor="w").pack(fill="x") else: tk.Label(price_frame, text=format_currency(harga), font=("Arial", 13, "bold"), bg="white", fg="#8B7355", anchor="w").pack(side="left") stok = m.get('stok', 0) stock_color = "#E74C3C" if stok < 5 else "#27AE60" tk.Label(info_frame, text=f"📦 Stok: {stok}", font=("Arial", 9), bg="white", fg=stock_color, anchor="w").pack(fill="x", pady=(3, 0)) col += 1 if col >= cols: col = 0 row += 1 # ================================ # TAB: ORDER (FIXED & INTEGRATED) # ================================ def build_order_tab(self, parent): for w in parent.winfo_children(): w.destroy() left = ttk.Frame(parent, width=600) right = ttk.Frame(parent, width=380) left.pack(side='left', fill='both', expand=True, padx=6, pady=6) right.pack(side='right', fill='both', padx=6, pady=6) tk.Label(left, text="🍽️ Daftar Menu", font=("Arial", 12, "bold")).pack(pady=4) search_frame = ttk.Frame(left) search_frame.pack(fill='x', pady=4) tk.Label(search_frame, text="🔍 Cari:").pack(side='left', padx=3) self.order_search_var = tk.StringVar() ttk.Entry(search_frame, textvariable=self.order_search_var, width=25).pack(side='left', padx=3) ttk.Button(search_frame, text="Cari", command=self.reload_order_menu_cards).pack(side='left', padx=3) ttk.Button(search_frame, text="Reset", command=self.reset_order_search).pack(side='left', padx=3) canvas = tk.Canvas(left, bg='#f5f5f5', height=450, highlightthickness=0) scrollbar = ttk.Scrollbar(left, orient="vertical", command=canvas.yview) self.menu_cards_frame = ttk.Frame(canvas) self.menu_cards_frame.bind( "", lambda e: canvas.configure(scrollregion=canvas.bbox("all")) ) canvas.create_window((0, 0), window=self.menu_cards_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") def _on_mousewheel(event): canvas.yview_scroll(int(-1*(event.delta/120)), "units") canvas.bind_all("", _on_mousewheel) tk.Label(right, text="🛒 Keranjang Belanja", font=("Arial", 12, "bold")).pack(pady=4) cart_cols = ("Menu", "Qty", "Harga", "Subtotal") self.cart_tree = ttk.Treeview(right, columns=cart_cols, show='headings', height=8) for c in cart_cols: self.cart_tree.heading(c, text=c) if c == "Menu": self.cart_tree.column(c, width=150) else: self.cart_tree.column(c, width=70) self.cart_tree.pack(fill='x', pady=4) cart_btn_frame = ttk.Frame(right) cart_btn_frame.pack(pady=4) ttk.Button(cart_btn_frame, text="🗑️ Hapus Item", command=self.remove_cart_item).pack(side='left', padx=3) ttk.Button(cart_btn_frame, text="🗑️ Kosongkan", command=self.clear_cart).pack(side='left', padx=3) total_frame = ttk.Frame(right) total_frame.pack(fill='x', pady=4) self.cart_subtotal_label = ttk.Label(total_frame, text="Subtotal: Rp 0", font=("Arial", 9)) self.cart_subtotal_label.pack() self.cart_discount_label = ttk.Label(total_frame, text="Diskon Item: Rp 0", font=("Arial", 9)) self.cart_discount_label.pack() self.cart_promo_label = ttk.Label(total_frame, text="Diskon Promo: Rp 0", font=("Arial", 9)) self.cart_promo_label.pack() self.cart_total_label = ttk.Label(total_frame, text="TOTAL: Rp 0", font=("Arial", 11, "bold")) self.cart_total_label.pack(pady=2) checkout_frame = ttk.Frame(right) checkout_frame.pack(fill='x', pady=6, padx=10) ttk.Label(checkout_frame, text="No. Meja:", font=("Arial", 9)).grid(row=0, column=0, sticky='w', padx=3, pady=3) self.order_meja_var = tk.StringVar() meja_entry = ttk.Entry(checkout_frame, textvariable=self.order_meja_var, width=20) meja_entry.grid(row=0, column=1, pady=3, sticky='ew') ttk.Label(checkout_frame, text="Kode Promo:", font=("Arial", 9)).grid(row=1, column=0, sticky='w', padx=3, pady=3) self.order_promo_var = tk.StringVar() ttk.Entry(checkout_frame, textvariable=self.order_promo_var, width=12).grid(row=1, column=1, pady=3, sticky='ew') ttk.Button(checkout_frame, text="Terapkan", command=self.update_cart_display, width=10).grid(row=1, column=2, padx=3, sticky='e') checkout_frame.columnconfigure(1, weight=1) checkout_btn_frame = ttk.Frame(right) checkout_btn_frame.pack(fill='x', pady=10, padx=20) ttk.Button( checkout_btn_frame, text="🛒 CHECKOUT PESANAN", command=self.checkout_order, width=30 ).pack() self.cart_items = [] self.reload_order_menu_cards() def reload_order_menu_cards(self): for widget in self.menu_cards_frame.winfo_children(): widget.destroy() search = self.order_search_var.get().strip() or None results = [m for m in menu if m.get('stok', 0) > 0] if search: results = [m for m in results if search.lower() in m['nama'].lower()] cart_dict = {} for cart_item in self.cart_items: cart_dict[cart_item['menu_id']] = cart_item['qty'] row = 0 col = 0 for m in results: mid = m['id'] nama = m['nama'] kategori = m['kategori'] harga = m['harga'] stok = m['stok'] foto = m.get('foto') item_disc = m.get('item_discount_pct', 0) promo = m.get('promo', '') card = tk.Frame( self.menu_cards_frame, relief='solid', borderwidth=1, bg='white', padx=10, pady=10 ) card.grid(row=row, column=col, padx=8, pady=8, sticky='nsew') if foto and os.path.exists(foto): try: if PIL_AVAILABLE: img = Image.open(foto) img = img.resize((150, 100), Image.Resampling.LANCZOS) photo = ImageTk.PhotoImage(img) img_label = tk.Label(card, image=photo, bg='white') img_label.image = photo img_label.pack() else: tk.Label(card, text="[No Image]", bg='#e0e0e0', width=20, height=6).pack() except: tk.Label(card, text="[No Image]", bg='#e0e0e0', width=20, height=6).pack() else: tk.Label(card, text="[No Image]", bg='#e0e0e0', width=20, height=6).pack() tk.Label(card, text=nama, font=("Arial", 11, "bold"), bg='white', wraplength=150).pack(pady=(5, 2)) tk.Label(card, text=kategori, font=("Arial", 9), fg='gray', bg='white').pack() harga_text = f"Rp {harga:,}" if item_disc > 0: harga_after = harga - (harga * item_disc / 100) harga_text = f"Rp {harga:,} → Rp {int(harga_after):,}" tk.Label(card, text=harga_text, font=("Arial", 9, "bold"), fg='#E67E22', bg='white').pack(pady=2) if promo: tk.Label(card, text=f"🎁 {promo}", font=("Arial", 8), fg='#D35400', bg='white').pack() else: tk.Label(card, text=harga_text, font=("Arial", 10, "bold"), fg='green', bg='white').pack(pady=2) tk.Label(card, text=f"📦 Stok: {stok}", font=("Arial", 8), fg='blue', bg='white').pack(pady=2) qty_in_cart = cart_dict.get(mid, 0) btn_frame = tk.Frame(card, bg='white') btn_frame.pack(pady=5) if qty_in_cart > 0: tk.Button( btn_frame, text="➖", font=("Arial", 12, "bold"), bg='#E74C3C', fg='white', width=3, borderwidth=0, cursor='hand2', command=lambda m=mid: self.decrease_from_card(m) ).pack(side='left', padx=2) tk.Label( btn_frame, text=str(qty_in_cart), font=("Arial", 12, "bold"), bg='white', width=3 ).pack(side='left', padx=5) tk.Button( btn_frame, text="➕", font=("Arial", 12, "bold"), bg='#27AE60', fg='white', width=3, borderwidth=0, cursor='hand2', command=lambda m=mid, s=stok: self.increase_from_card(m, s) ).pack(side='left', padx=2) else: tk.Button( btn_frame, text="➕ Tambah", font=("Arial", 10, "bold"), bg='#27AE60', fg='white', width=12, borderwidth=0, cursor='hand2', command=lambda m=mid, s=stok: self.increase_from_card(m, s) ).pack() col += 1 if col >= 2: col = 0 row += 1 def reset_order_search(self): self.order_search_var.set("") self.reload_order_menu_cards() def increase_from_card(self, menu_id, stok): current_qty = 0 cart_item_found = None for cart_item in self.cart_items: if cart_item['menu_id'] == menu_id: current_qty = cart_item['qty'] cart_item_found = cart_item break if current_qty >= stok: messagebox.showwarning("Stok Habis", f"Stok hanya tersisa {stok}") return if cart_item_found: cart_item_found['qty'] += 1 else: self.cart_items.append({'menu_id': menu_id, 'qty': 1}) self.reload_order_menu_cards() self.update_cart_display() def decrease_from_card(self, menu_id): for i, cart_item in enumerate(self.cart_items): if cart_item['menu_id'] == menu_id: cart_item['qty'] -= 1 if cart_item['qty'] <= 0: del self.cart_items[i] self.reload_order_menu_cards() self.update_cart_display() return def update_cart_display(self): for r in self.cart_tree.get_children(): self.cart_tree.delete(r) if not self.cart_items: self.cart_subtotal_label.config(text="Subtotal: Rp 0") self.cart_discount_label.config(text="Diskon Item: Rp 0") self.cart_promo_label.config(text="Diskon Promo: Rp 0") self.cart_total_label.config(text="TOTAL: Rp 0") return subtotal = 0 item_discount_total = 0 for cart_item in self.cart_items: menu_data = find_menu_by_id(cart_item['menu_id']) if not menu_data: continue harga = menu_data['harga'] qty = cart_item['qty'] item_disc_pct = menu_data.get('item_discount_pct', 0) line_subtotal = harga * qty subtotal += line_subtotal if item_disc_pct > 0: item_discount = int(line_subtotal * item_disc_pct / 100) item_discount_total += item_discount self.cart_tree.insert("", tk.END, values=( menu_data['nama'], qty, f"{harga:,}", f"{line_subtotal:,}" )) promo_code = self.order_promo_var.get().strip().upper() promo_discount = 0 if promo_code and promo_code in promo_codes: discount_pct = promo_codes[promo_code] promo_discount = int((subtotal - item_discount_total) * discount_pct / 100) total = subtotal - item_discount_total - promo_discount self.cart_subtotal_label.config(text=f"Subtotal: Rp {subtotal:,}") self.cart_discount_label.config(text=f"Diskon Item: Rp {item_discount_total:,}") self.cart_promo_label.config(text=f"Diskon Promo: Rp {promo_discount:,}") self.cart_total_label.config(text=f"TOTAL: Rp {total:,}") def remove_cart_item(self): sel = self.cart_tree.selection() if not sel: messagebox.showwarning("Pilih Item", "Pilih item di keranjang") return idx = self.cart_tree.index(sel) if idx >= len(self.cart_items): return del self.cart_items[idx] self.reload_order_menu_cards() self.update_cart_display() def clear_cart(self): if not self.cart_items: return if messagebox.askyesno("Konfirmasi", "Kosongkan keranjang?"): self.cart_items = [] self.reload_order_menu_cards() self.update_cart_display() def checkout_order(self): if not self.cart_items: messagebox.showwarning("Keranjang Kosong", "Tambahkan item terlebih dahulu") return nomor_meja = self.order_meja_var.get().strip() if not nomor_meja: messagebox.showwarning("Input Error", "Masukkan nomor meja") return try: nomor_meja = int(nomor_meja) except: messagebox.showerror("Input Error", "Nomor meja harus angka") return if nomor_meja < 1 or nomor_meja > 10: messagebox.showerror("Invalid", "Nomor meja harus 1-10") return promo_code = self.order_promo_var.get().strip().upper() or None if promo_code and promo_code not in promo_codes: messagebox.showwarning("Promo Invalid", "Kode promo tidak ditemukan") return # Cek stok bahan kurang_bahan = [] for cart_item in self.cart_items: mid = cart_item['menu_id'] qty = cart_item['qty'] if mid in resep: for bahan_nama, need in resep[mid].items(): if bahan.get(bahan_nama, 0) < need * qty: kurang_bahan.append((bahan_nama, bahan.get(bahan_nama, 0), need * qty)) if kurang_bahan: msg = "❌ Stok bahan tidak cukup untuk pesanan:\n\n" for bn, avail, need in kurang_bahan: msg += f"• {bn}: tersedia {avail}, dibutuhkan {need}\n" messagebox.showerror("Stok Bahan Kurang", msg) add_notification("warning", f"Stok bahan kurang untuk pesanan dari {self.current_user['username']}") return # Hitung total subtotal = 0 item_discount_total = 0 items_for_trans = [] for cart_item in self.cart_items: menu_data = find_menu_by_id(cart_item['menu_id']) if not menu_data: continue qty = cart_item['qty'] harga = menu_data['harga'] item_disc_pct = menu_data.get('item_discount_pct', 0) line_subtotal = harga * qty subtotal += line_subtotal if item_disc_pct > 0: item_discount = int(line_subtotal * item_disc_pct / 100) item_discount_total += item_discount menu_data['stok'] -= qty if menu_data['stok'] < 0: menu_data['stok'] = 0 if menu_data['stok'] < 5: add_notification("warning", f"Stok {menu_data['nama']} tinggal {menu_data['stok']}") if menu_data['id'] in resep: for bahan_nama, kebutuhan in resep[menu_data['id']].items(): if bahan_nama in bahan: bahan[bahan_nama] -= kebutuhan * qty if bahan[bahan_nama] < 0: bahan[bahan_nama] = 0 if bahan[bahan_nama] < 10: add_notification("warning", f"Stok bahan {bahan_nama} tinggal {bahan[bahan_nama]}") items_for_trans.append({ 'menu_id': menu_data['id'], 'nama': menu_data['nama'], 'qty': qty, 'harga_satuan': harga, 'subtotal': line_subtotal }) promo_discount = 0 if promo_code: discount_pct = promo_codes[promo_code] promo_discount = int((subtotal - item_discount_total) * discount_pct / 100) total = subtotal - item_discount_total - promo_discount new_trans_id = max([t['id'] for t in transaksi], default=0) + 1 transaksi.append({ 'id': new_trans_id, 'tanggal': str(datetime.date.today()), 'waktu': datetime.datetime.now(), 'user': self.current_user['username'], 'items': items_for_trans, 'subtotal': subtotal, 'diskon': item_discount_total + promo_discount, 'total': total, 'meja': nomor_meja, 'status': 'Baru', 'payment_status': 'Pending', 'payment_method': None, 'paid_amount': 0, 'change': 0, 'promo_code': promo_code or '' }) detail_id = max([d['id'] for d in detail_transaksi], default=0) + 1 for item in items_for_trans: detail_transaksi.append({ 'id': detail_id, 'transaksi_id': new_trans_id, 'menu_id': item['menu_id'], 'nama_menu': item['nama'], 'jumlah': item['qty'], 'harga_satuan': item['harga_satuan'], 'subtotal': item['subtotal'], 'diskon': 0 }) detail_id += 1 data_meja[nomor_meja] = "Terisi" add_notification("info", f"Pesanan baru dari {self.current_user['username']} - Total: Rp {total:,}") save_menu() save_bahan() save_transaksi() save_detail_transaksi() save_meja() self.cart_items = [] self.order_promo_var.set("") self.reload_order_menu_cards() self.update_cart_display() messagebox.showinfo("✅ Sukses", f"Pesanan berhasil dibuat!\n\n" f"ID Transaksi: #{new_trans_id}\n" f"Total: Rp {total:,}\n\n" f"Silakan lakukan pembayaran di kasir.") # ================================ # TAB: PAYMENT (FIXED & INTEGRATED) # ================================ def build_payment_tab(self, parent): for w in parent.winfo_children(): w.destroy() header = ttk.Frame(parent) header.pack(fill='x', padx=10, pady=8) ttk.Label(header, text="💰 Pembayaran Transaksi", font=("Arial", 14, "bold")).pack(side='left') ttk.Button(header, text="🔄 Refresh", command=self.reload_payment_orders).pack(side='right', padx=6) search_frame = ttk.LabelFrame(parent, text="🔍 Cari Transaksi by Nomor Meja", padding=10) search_frame.pack(fill='x', padx=10, pady=6) search_inner = ttk.Frame(search_frame) search_inner.pack() ttk.Label(search_inner, text="Nomor Meja:", font=("Arial", 9)).grid(row=0, column=0, padx=5) self.search_meja_var = tk.StringVar() ttk.Entry(search_inner, textvariable=self.search_meja_var, width=15).grid(row=0, column=1, padx=5) ttk.Button(search_inner, text="🔍 Cari Tagihan", command=self.search_by_meja).grid(row=0, column=2, padx=5) ttk.Button(search_inner, text="🔄 Tampilkan Semua", command=self.reload_payment_orders).grid(row=0, column=3, padx=5) summary_frame = ttk.LabelFrame(parent, text="📊 TOTAL PERHITUNGAN - Penjualan Hari Ini", padding=12) summary_frame.pack(fill='x', padx=10, pady=6) summary_inner = ttk.Frame(summary_frame) summary_inner.pack() today = str(datetime.date.today()) today_report = get_daily_report(today) ttk.Label(summary_inner, text="Total Transaksi Hari Ini:", font=("Arial", 11, "bold")).grid(row=0, column=0, sticky='w', padx=10, pady=5) ttk.Label(summary_inner, text=str(today_report['total_transaksi']), font=("Arial", 12, "bold"), foreground='blue').grid(row=0, column=1, sticky='w', padx=10, pady=5) ttk.Label(summary_inner, text="Total Pendapatan Hari Ini:", font=("Arial", 11, "bold")).grid(row=1, column=0, sticky='w', padx=10, pady=5) ttk.Label(summary_inner, text=format_currency(today_report['total_omzet']), font=("Arial", 12, "bold"), foreground='green').grid(row=1, column=1, sticky='w', padx=10, pady=5) pending_total = sum(t['total'] for t in transaksi if t.get('payment_status') == 'Pending' and t['tanggal'] == today) ttk.Label(summary_inner, text="Total Tagihan Pending:", font=("Arial", 11, "bold")).grid(row=2, column=0, sticky='w', padx=10, pady=5) ttk.Label(summary_inner, text=format_currency(pending_total), font=("Arial", 12, "bold"), foreground='orange').grid(row=2, column=1, sticky='w', padx=10, pady=5) # Breakdown QRIS vs CASH today_berhasil = [t for t in transaksi if t['tanggal'] == today and t.get('payment_status') == 'Berhasil'] qris_total = sum(t['total'] for t in today_berhasil if t.get('payment_method', '').lower() == 'qris') cash_total = sum(t['total'] for t in today_berhasil if t.get('payment_method', '').lower() == 'cash') qris_count = len([t for t in today_berhasil if t.get('payment_method', '').lower() == 'qris']) cash_count = len([t for t in today_berhasil if t.get('payment_method', '').lower() == 'cash']) ttk.Separator(summary_inner, orient='horizontal').grid(row=3, column=0, columnspan=2, sticky='ew', padx=10, pady=8) ttk.Label(summary_inner, text="📱 QRIS:", font=("Arial", 10, "bold"), foreground='#FF6B6B').grid(row=4, column=0, sticky='w', padx=10, pady=3) ttk.Label(summary_inner, text=f"{format_currency(qris_total)} ({qris_count}x)", font=("Arial", 10, "bold"), foreground='#FF6B6B').grid(row=4, column=1, sticky='w', padx=10, pady=3) ttk.Label(summary_inner, text="💵 CASH:", font=("Arial", 10, "bold"), foreground='#4CAF50').grid(row=5, column=0, sticky='w', padx=10, pady=3) ttk.Label(summary_inner, text=f"{format_currency(cash_total)} ({cash_count}x)", font=("Arial", 10, "bold"), foreground='#4CAF50').grid(row=5, column=1, sticky='w', padx=10, pady=3) main_container = ttk.Frame(parent) main_container.pack(fill='both', expand=True, padx=10, pady=6) left = ttk.LabelFrame(main_container, text="📋 Daftar Transaksi Belum Dibayar", padding=5) left.pack(side='left', fill='both', expand=True, padx=(0, 5)) tree_scroll = ttk.Scrollbar(left, orient='vertical') tree_scroll.pack(side='right', fill='y') cols = ("ID", "Meja", "Total", "Status", "Tanggal") self.payment_tree = ttk.Treeview( left, columns=cols, show='headings', height=8, yscrollcommand=tree_scroll.set ) tree_scroll.config(command=self.payment_tree.yview) self.payment_tree.heading("ID", text="ID") self.payment_tree.heading("Meja", text="Meja") self.payment_tree.heading("Total", text="Total") self.payment_tree.heading("Status", text="Status") self.payment_tree.heading("Tanggal", text="Tanggal") self.payment_tree.column("ID", width=50) self.payment_tree.column("Meja", width=60) self.payment_tree.column("Total", width=100) self.payment_tree.column("Status", width=80) self.payment_tree.column("Tanggal", width=120) self.payment_tree.pack(side='left', fill='both', expand=True) self.payment_tree.bind("<>", self.on_payment_select) detail_label = ttk.Label(left, text="📄 Detail Transaksi", font=("Arial", 10, "bold")) detail_label.pack(anchor='w', pady=(10, 5)) detail_frame = ttk.Frame(left) detail_frame.pack(fill='both', expand=True) detail_scroll = ttk.Scrollbar(detail_frame, orient='vertical') detail_scroll.pack(side='right', fill='y') self.payment_detail_text = tk.Text(detail_frame, height=10, font=("Courier New", 9), wrap='word', yscrollcommand=detail_scroll.set) detail_scroll.config(command=self.payment_detail_text.yview) self.payment_detail_text.pack(side='left', fill='both', expand=True) right = ttk.LabelFrame(main_container, text="💳 Form Pembayaran", padding=10) right.pack(side='right', fill='both', expand=True, padx=(5, 0)) self.selected_transaksi_label = ttk.Label( right, text="❌ Belum ada transaksi dipilih", font=("Arial", 10, "bold"), foreground='red' ) self.selected_transaksi_label.pack(pady=5) self.selected_total_label = ttk.Label( right, text="Total: Rp 0", font=("Arial", 14, "bold"), foreground='green' ) self.selected_total_label.pack(pady=5) ttk.Separator(right, orient='horizontal').pack(fill='x', pady=10) method_label = ttk.Label(right, text="💳 Pilih Metode Pembayaran:", font=("Arial", 10, "bold")) method_label.pack(anchor='w', pady=5) self.payment_method_var = tk.StringVar(value='cash') method_frame = ttk.Frame(right) method_frame.pack(fill='x', pady=5) ttk.Radiobutton(method_frame, text="💵 Cash", variable=self.payment_method_var, value='cash', command=self.on_payment_method_change).pack(anchor='w', pady=2) ttk.Radiobutton(method_frame, text="📱 QRIS", variable=self.payment_method_var, value='qris', command=self.on_payment_method_change).pack(anchor='w', pady=2) ttk.Radiobutton(method_frame, text="💳 E-Wallet", variable=self.payment_method_var, value='ewallet', command=self.on_payment_method_change).pack(anchor='w', pady=2) ttk.Separator(right, orient='horizontal').pack(fill='x', pady=10) # Container untuk payment input + button payment_container = tk.Frame(right, bg='white') payment_container.pack(fill='both', expand=True, pady=5) # Canvas dengan scrollbar untuk payment input (max height 250px) self.payment_canvas = tk.Canvas(payment_container, bg='white', highlightthickness=0, height=250) scrollbar = ttk.Scrollbar(payment_container, orient='vertical', command=self.payment_canvas.yview) self.payment_input_frame = tk.Frame(self.payment_canvas, bg='white', relief='solid', bd=1) self.payment_input_frame.bind( "", lambda e: self.payment_canvas.configure(scrollregion=self.payment_canvas.bbox("all")) ) self.payment_canvas.create_window((0, 0), window=self.payment_input_frame, anchor='nw') self.payment_canvas.configure(yscrollcommand=scrollbar.set) self.payment_canvas.pack(fill='both', expand=False, padx=2, pady=5, side='left') scrollbar.pack(fill='y', side='right', pady=5) # Mouse wheel scrolling def _on_mousewheel(event): self.payment_canvas.yview_scroll(int(-1*(event.delta/120)), "units") self.payment_canvas.bind_all("", _on_mousewheel) self.build_cash_input() # Separator ttk.Separator(payment_container, orient='horizontal').pack(fill='x', pady=8) # Button PROSES PEMBAYARAN self.process_btn = tk.Button( payment_container, text="✅ PROSES PEMBAYARAN", command=self.process_payment, state='disabled', font=("Arial", 11, "bold"), bg='#4CAF50', fg='white', relief='raised', bd=2, padx=15, pady=10, cursor='hand2' ) self.process_btn.pack(fill='x', padx=5, pady=10) self.reload_payment_orders() def reload_payment_orders(self): for r in self.payment_tree.get_children(): self.payment_tree.delete(r) for t in transaksi: if t.get('payment_status') == 'Pending': self.payment_tree.insert("", tk.END, values=( t['id'], t.get('meja', '-'), format_currency(t['total']), t['status'], t['tanggal'] )) def search_by_meja(self): nomor_meja = self.search_meja_var.get().strip() if not nomor_meja: messagebox.showwarning("Input Error", "Masukkan nomor meja") return try: nomor_meja = int(nomor_meja) except: messagebox.showerror("Input Error", "Nomor meja harus angka") return if nomor_meja < 1 or nomor_meja > 10: messagebox.showwarning("Invalid", "Nomor meja harus 1-10") return for r in self.payment_tree.get_children(): self.payment_tree.delete(r) found = False for t in transaksi: if t.get('meja') == nomor_meja and t.get('payment_status') == 'Pending': self.payment_tree.insert("", tk.END, values=( t['id'], t.get('meja', '-'), format_currency(t['total']), t['status'], t['tanggal'] )) found = True if not found: messagebox.showinfo("Tidak Ditemukan", f"❌ Tidak ada tagihan aktif untuk Meja {nomor_meja}") self.reload_payment_orders() def on_payment_select(self, event): sel = self.payment_tree.selection() if not sel: return item = self.payment_tree.item(sel)['values'] transaksi_id = item[0] t = None for trans in transaksi: if trans['id'] == transaksi_id: t = trans break if not t: return self.selected_transaksi_label.config( text=f"✅ Transaksi #{t['id']} - Meja {t.get('meja', '-')}", foreground='green' ) self.selected_total_label.config(text=f"Total: {format_currency(t['total'])}") self.process_btn.config(state='normal') detail_text = f"═══════════════════════════════════════\n" detail_text += f"TRANSAKSI #{t['id']}\n" detail_text += f"═══════════════════════════════════════\n\n" detail_text += f"Tanggal : {t['tanggal']}\n" detail_text += f"User : {t['user']}\n" detail_text += f"Meja : {t.get('meja', '-')}\n" detail_text += f"Status : {t['status']}\n\n" detail_text += f"───────────────────────────────────────\n" detail_text += f"ITEM PESANAN:\n" detail_text += f"───────────────────────────────────────\n" for item in t['items']: detail_text += f"• {item['nama']}\n" detail_text += f" {item['qty']} x {format_currency(item['harga_satuan'])} = {format_currency(item['subtotal'])}\n\n" detail_text += f"───────────────────────────────────────\n" detail_text += f"Subtotal : {format_currency(t['subtotal'])}\n" detail_text += f"Diskon : {format_currency(t['diskon'])}\n" detail_text += f"───────────────────────────────────────\n" detail_text += f"TOTAL : {format_currency(t['total'])}\n" detail_text += f"═══════════════════════════════════════\n" self.payment_detail_text.delete('1.0', tk.END) self.payment_detail_text.insert('1.0', detail_text) self.selected_payment_transaksi = t def on_payment_method_change(self): for widget in self.payment_input_frame.winfo_children(): widget.destroy() method = self.payment_method_var.get() if method == 'cash': self.build_cash_input() elif method == 'qris': self.build_qris_input() elif method == 'ewallet': self.build_ewallet_input() def build_cash_input(self): ttk.Label(self.payment_input_frame, text="💵 PEMBAYARAN CASH", font=("Arial", 11, "bold"), background='white').pack(pady=8) ttk.Label(self.payment_input_frame, text="Jumlah Bayar:", font=("Arial", 9), background='white').pack(anchor='w', padx=10, pady=(5, 2)) self.cash_amount_var = tk.StringVar() cash_entry = ttk.Entry(self.payment_input_frame, textvariable=self.cash_amount_var, font=("Arial", 11), width=25) cash_entry.pack(pady=5, padx=10) self.cash_change_label = ttk.Label(self.payment_input_frame, text="Kembalian: Rp 0", font=("Arial", 10, "bold"), foreground='blue', background='white') self.cash_change_label.pack(pady=8, padx=10) def calculate_change(*args): if not hasattr(self, 'selected_payment_transaksi'): return try: paid = int(self.cash_amount_var.get()) total = self.selected_payment_transaksi['total'] change = paid - total if change >= 0: self.cash_change_label.config( text=f"Kembalian: {format_currency(change)}", foreground='green' ) else: self.cash_change_label.config( text=f"Kurang: {format_currency(abs(change))}", foreground='red' ) except: self.cash_change_label.config(text="Kembalian: Rp 0", foreground='blue') self.cash_amount_var.trace('w', calculate_change) def build_qris_input(self): ttk.Label(self.payment_input_frame, text="📱 PEMBAYARAN QRIS", font=("Arial", 9, "bold"), background='white').pack(pady=2) if not hasattr(self, 'selected_payment_transaksi'): ttk.Label(self.payment_input_frame, text="Pilih transaksi", foreground='red', background='white', font=("Arial", 8)).pack() return t = self.selected_payment_transaksi if QRCODE_AVAILABLE: ttk.Label(self.payment_input_frame, text="📲 Scan QR Code:", font=("Arial", 8), background='white').pack(pady=1) qr_data = f"CAFE-TRX-{t['id']}-{t['total']}" try: qr = qrcode.QRCode(version=1, box_size=4, border=1) qr.add_data(qr_data) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") buffer = BytesIO() img.save(buffer, format='PNG') buffer.seek(0) if PIL_AVAILABLE: qr_img = Image.open(buffer) qr_img = qr_img.resize((120, 120)) qr_photo = ImageTk.PhotoImage(qr_img) qr_label = tk.Label(self.payment_input_frame, image=qr_photo, bg='white') qr_label.image = qr_photo qr_label.pack(pady=2) ttk.Label(self.payment_input_frame, text=f"Total: Rp {t['total']:,}".replace(",", "."), font=("Arial", 8, "bold"), background='white', foreground='green').pack(pady=1) except Exception as e: ttk.Label(self.payment_input_frame, text=f"Error: {str(e)[:20]}", foreground='red', font=("Arial", 8), background='white').pack() else: ttk.Label(self.payment_input_frame, text="qrcode tidak tersedia", foreground='red', font=("Arial", 8), background='white').pack() def build_ewallet_input(self): ttk.Label(self.payment_input_frame, text="💳 PEMBAYARAN E-WALLET", font=("Arial", 10, "bold"), background='white').pack(pady=3) ttk.Label(self.payment_input_frame, text="Pilih E-Wallet:", font=("Arial", 8), background='white').pack(anchor='w', padx=5, pady=(3, 2)) self.ewallet_type_var = tk.StringVar(value='gopay') wallets = [ ('GoPay', 'gopay'), ('OVO', 'ovo'), ('DANA', 'dana'), ('ShopeePay', 'shopeepay') ] wallet_frame = tk.Frame(self.payment_input_frame, bg='white') wallet_frame.pack(fill='x', padx=5, pady=2) for label, value in wallets: ttk.Radiobutton(wallet_frame, text=f"💰 {label}", variable=self.ewallet_type_var, value=value).pack(anchor='w', pady=2) if hasattr(self, 'selected_payment_transaksi'): t = self.selected_payment_transaksi ttk.Label(self.payment_input_frame, text=f"Total Bayar: {format_currency(t['total'])}", font=("Arial", 10, "bold"), foreground='green', background='white').pack(pady=10) def process_payment(self): if not hasattr(self, 'selected_payment_transaksi'): messagebox.showwarning("Error", "Pilih transaksi terlebih dahulu") return t = self.selected_payment_transaksi method = self.payment_method_var.get() if method == 'cash': try: paid = int(self.cash_amount_var.get()) except: messagebox.showerror("Input Error", "Masukkan jumlah pembayaran yang valid") return if paid < t['total']: messagebox.showerror("Pembayaran Kurang", f"Pembayaran kurang!\nTotal: {format_currency(t['total'])}\nBayar: {format_currency(paid)}") return change = paid - t['total'] t['payment_status'] = 'Berhasil' t['payment_method'] = 'Cash' t['paid_amount'] = paid t['change'] = change t['status'] = 'Selesai' if t.get('meja'): data_meja[t['meja']] = 'Kosong' payment_id = max([p['id'] for p in pembayaran], default=0) + 1 pembayaran.append({ 'id': payment_id, 'transaksi_id': t['id'], 'metode': 'Cash', 'jumlah': t['total'], 'status': 'Berhasil', 'tanggal': str(datetime.date.today()) }) save_transaksi() save_meja() save_pembayaran() add_notification("success", f"Pembayaran Cash #{t['id']} berhasil - Total: {format_currency(t['total'])}") messagebox.showinfo("✅ Pembayaran Berhasil", f"Transaksi #{t['id']}\n\n" f"Metode: Cash\n" f"Total: {format_currency(t['total'])}\n" f"Bayar: {format_currency(paid)}\n" f"Kembalian: {format_currency(change)}") self.reload_payment_orders() self.selected_transaksi_label.config(text="❌ Belum ada transaksi dipilih", foreground='red') self.selected_total_label.config(text="Total: Rp 0") self.payment_detail_text.delete('1.0', tk.END) self.process_btn.config(state='disabled') self.cash_amount_var.set("") elif method == 'qris': confirm = messagebox.askyesno("Konfirmasi QRIS", f"Apakah pembayaran QRIS sebesar {format_currency(t['total'])} sudah diterima?") if not confirm: return t['payment_status'] = 'Berhasil' t['payment_method'] = 'QRIS' t['paid_amount'] = t['total'] t['change'] = 0 t['status'] = 'Selesai' if t.get('meja'): data_meja[t['meja']] = 'Kosong' payment_id = max([p['id'] for p in pembayaran], default=0) + 1 pembayaran.append({ 'id': payment_id, 'transaksi_id': t['id'], 'metode': 'QRIS', 'jumlah': t['total'], 'status': 'Berhasil', 'tanggal': str(datetime.date.today()) }) save_transaksi() save_meja() save_pembayaran() add_notification("success", f"Pembayaran QRIS #{t['id']} berhasil - Total: {format_currency(t['total'])}") messagebox.showinfo("✅ Pembayaran Berhasil", f"Transaksi #{t['id']}\n\n" f"Metode: QRIS\n" f"Total: {format_currency(t['total'])}") self.reload_payment_orders() self.selected_transaksi_label.config(text="❌ Belum ada transaksi dipilih", foreground='red') self.selected_total_label.config(text="Total: Rp 0") self.payment_detail_text.delete('1.0', tk.END) self.process_btn.config(state='disabled') elif method == 'ewallet': wallet_type = self.ewallet_type_var.get() wallet_names = { 'gopay': 'GoPay', 'ovo': 'OVO', 'dana': 'DANA', 'shopeepay': 'ShopeePay' } wallet_name = wallet_names.get(wallet_type, 'E-Wallet') confirm = messagebox.askyesno("Konfirmasi E-Wallet", f"Apakah pembayaran {wallet_name} sebesar {format_currency(t['total'])} sudah diterima?") if not confirm: return t['payment_status'] = 'Berhasil' t['payment_method'] = wallet_name t['paid_amount'] = t['total'] t['change'] = 0 t['status'] = 'Selesai' if t.get('meja'): data_meja[t['meja']] = 'Kosong' payment_id = max([p['id'] for p in pembayaran], default=0) + 1 pembayaran.append({ 'id': payment_id, 'transaksi_id': t['id'], 'metode': wallet_name, 'jumlah': t['total'], 'status': 'Berhasil', 'tanggal': str(datetime.date.today()) }) save_transaksi() save_meja() save_pembayaran() add_notification("success", f"Pembayaran {wallet_name} #{t['id']} berhasil - Total: {format_currency(t['total'])}") messagebox.showinfo("✅ Pembayaran Berhasil", f"Transaksi #{t['id']}\n\n" f"Metode: {wallet_name}\n" f"Total: {format_currency(t['total'])}") self.reload_payment_orders() self.selected_transaksi_label.config(text="❌ Belum ada transaksi dipilih", foreground='red') self.selected_total_label.config(text="Total: Rp 0") self.payment_detail_text.delete('1.0', tk.END) self.process_btn.config(state='disabled') # ================================ # TAB: FAVORITE # ================================ def build_favorite_tab(self, parent): for w in parent.winfo_children(): w.destroy() header = tk.Frame(parent, bg="#F5F5F0") header.pack(fill="x", padx=15, pady=10) tk.Label(header, text="⭐ Menu Favorit Saya", font=("Arial", 18, "bold"), bg="#F5F5F0", fg="#5C4033").pack(side="left") ttk.Button(header, text="🔄 Refresh", command=lambda: self.build_favorite_tab(parent)).pack(side="right") username = self.current_user['username'] user_favs = favorites.get(username, []) if not user_favs: empty_frame = tk.Frame(parent, bg="white") empty_frame.pack(fill="both", expand=True, padx=20, pady=20) tk.Label(empty_frame, text="⭐", font=("Arial", 64), bg="white", fg="#CCCCCC").pack(pady=(50, 20)) tk.Label(empty_frame, text="Belum ada menu favorit", font=("Arial", 16), bg="white", fg="#999999").pack() tk.Label(empty_frame, text="Tandai menu favorit Anda dari tab Menu", font=("Arial", 11), bg="white", fg="#CCCCCC").pack(pady=10) return container = tk.Frame(parent, bg="white") container.pack(fill="both", expand=True, padx=15, pady=5) canvas = tk.Canvas(container, bg="white", highlightthickness=0) scrollbar = ttk.Scrollbar(container, orient="vertical", command=canvas.yview) fav_frame = tk.Frame(canvas, bg="white") canvas.configure(yscrollcommand=scrollbar.set) scrollbar.pack(side="right", fill="y") canvas.pack(side="left", fill="both", expand=True) canvas_window = canvas.create_window((0, 0), window=fav_frame, anchor="nw") fav_frame.bind("", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) canvas.bind("", lambda e: canvas.itemconfig(canvas_window, width=e.width)) canvas.bind_all("", lambda e: canvas.yview_scroll(-1 * int(e.delta / 120), "units")) row = 0 col = 0 for fav_id in user_favs: m = find_menu_by_id(fav_id) if not m: continue card = tk.Frame(fav_frame, bg="white", relief="solid", bd=1, highlightthickness=2, highlightbackground="#FFD700") card.grid(row=row, column=col, padx=15, pady=15, sticky="nsew") img_frame = tk.Frame(card, bg="#F5F5F5", width=220, height=180) img_frame.pack(fill="x", padx=10, pady=(10, 5)) img_frame.pack_propagate(False) foto = m.get('foto') if foto and os.path.exists(foto): img = ensure_image(foto, maxsize=(200, 160)) if img: img_label = tk.Label(img_frame, image=img, bg="#F5F5F5") img_label.image = img img_label.pack(expand=True) else: tk.Label(img_frame, text="⭐\nFavorite", font=("Arial", 16), bg="#F5F5F5", fg="#FFD700").pack(expand=True) else: tk.Label(img_frame, text="⭐\nFavorite", font=("Arial", 16), bg="#F5F5F5", fg="#FFD700").pack(expand=True) info_frame = tk.Frame(card, bg="white") info_frame.pack(fill="x", padx=15, pady=10) tk.Label(info_frame, text=m['nama'], font=("Arial", 13, "bold"), bg="white", fg="#5C4033", anchor="w").pack(fill="x") tk.Label(info_frame, text=f"📂 {m['kategori']}", font=("Arial", 9), bg="white", fg="#999999", anchor="w").pack(fill="x", pady=(3, 5)) tk.Label(info_frame, text=format_currency(m['harga']), font=("Arial", 13, "bold"), bg="white", fg="#8B7355", anchor="w").pack(fill="x") btn_frame = tk.Frame(info_frame, bg="white") btn_frame.pack(fill="x", pady=(10, 0)) tk.Button(btn_frame, text="💔 Hapus dari Favorit", command=lambda mid=m['id']: self.remove_from_favorites(mid, parent), font=("Arial", 9), bg="#E74C3C", fg="white", relief="flat", padx=10, pady=5).pack() col += 1 if col >= 3: col = 0 row += 1 def remove_from_favorites(self, menu_id, parent): username = self.current_user['username'] if username in favorites and menu_id in favorites[username]: favorites[username].remove(menu_id) save_favorites() messagebox.showinfo("Success", "Menu dihapus dari favorit") self.build_favorite_tab(parent) # ================================ # TAB: RIWAYAT # ================================ def build_riwayat_tab(self, parent): for w in parent.winfo_children(): w.destroy() header = ttk.Frame(parent) header.pack(fill='x', padx=10, pady=8) ttk.Label(header, text="📜 Riwayat Transaksi", font=("Arial", 14, "bold")).pack(side='left') ttk.Button(header, text="🔄 Refresh", command=lambda: self.build_riwayat_tab(parent)).pack(side='right', padx=5) filter_frame = ttk.LabelFrame(parent, text="🔍 Filter Riwayat", padding=10) filter_frame.pack(fill='x', padx=10, pady=5) filter_inner = ttk.Frame(filter_frame) filter_inner.pack() ttk.Label(filter_inner, text="Status:").grid(row=0, column=0, padx=5) self.riwayat_status_var = tk.StringVar(value="Semua") status_cb = ttk.Combobox(filter_inner, textvariable=self.riwayat_status_var, values=["Semua", "Pending", "Berhasil", "Batal"], state="readonly", width=15) status_cb.grid(row=0, column=1, padx=5) ttk.Label(filter_inner, text="Tanggal:").grid(row=0, column=2, padx=5) self.riwayat_date_var = tk.StringVar(value=str(datetime.date.today())) ttk.Entry(filter_inner, textvariable=self.riwayat_date_var, width=15).grid(row=0, column=3, padx=5) ttk.Button(filter_inner, text="🔍 Tampilkan", command=lambda: self.refresh_riwayat_tree()).grid(row=0, column=4, padx=5) ttk.Button(filter_inner, text="🔄 Reset", command=self.reset_riwayat_filter).grid(row=0, column=5, padx=5) tree_frame = ttk.Frame(parent) tree_frame.pack(fill='both', expand=True, padx=10, pady=5) tree_scroll = ttk.Scrollbar(tree_frame, orient='vertical') tree_scroll.pack(side='right', fill='y') cols = ("ID", "Tanggal", "User", "Total", "Metode", "Status", "Meja") self.riwayat_tree = ttk.Treeview( tree_frame, columns=cols, show='headings', height=12, yscrollcommand=tree_scroll.set ) tree_scroll.config(command=self.riwayat_tree.yview) for c in cols: self.riwayat_tree.heading(c, text=c) if c == "ID": self.riwayat_tree.column(c, width=50) elif c == "Total": self.riwayat_tree.column(c, width=100) elif c == "Meja": self.riwayat_tree.column(c, width=60) else: self.riwayat_tree.column(c, width=100) self.riwayat_tree.pack(side='left', fill='both', expand=True) self.riwayat_tree.bind("", self.show_riwayat_detail) detail_frame = ttk.LabelFrame(parent, text="📄 Detail Transaksi", padding=10) detail_frame.pack(fill='x', padx=10, pady=5) self.riwayat_detail_text = tk.Text(detail_frame, height=8, font=("Courier New", 9), wrap='word', state='disabled') self.riwayat_detail_text.pack(fill='x') self.refresh_riwayat_tree() def refresh_riwayat_tree(self): for r in self.riwayat_tree.get_children(): self.riwayat_tree.delete(r) status_filter = self.riwayat_status_var.get() date_filter = self.riwayat_date_var.get().strip() for t in reversed(transaksi): payment_status = t.get('payment_status', 'Pending') if status_filter != "Semua" and payment_status != status_filter: continue if date_filter and t['tanggal'] != date_filter: continue self.riwayat_tree.insert("", tk.END, values=( t['id'], t['tanggal'], t['user'], format_currency(t['total']), t.get('payment_method', '-'), payment_status, t.get('meja', '-') )) def reset_riwayat_filter(self): self.riwayat_status_var.set("Semua") self.riwayat_date_var.set(str(datetime.date.today())) self.refresh_riwayat_tree() def show_riwayat_detail(self, event): sel = self.riwayat_tree.selection() if not sel: return item = self.riwayat_tree.item(sel)['values'] transaksi_id = item[0] t = None for trans in transaksi: if trans['id'] == transaksi_id: t = trans break if not t: return detail_text = f"═══════════════════════════════════════\n" detail_text += f"DETAIL TRANSAKSI #{t['id']}\n" detail_text += f"═══════════════════════════════════════\n\n" detail_text += f"Tanggal : {t['tanggal']}\n" detail_text += f"Waktu : {t['waktu'].strftime('%H:%M:%S')}\n" detail_text += f"User : {t['user']}\n" detail_text += f"Meja : {t.get('meja', '-')}\n" detail_text += f"Status Pesanan : {t['status']}\n" detail_text += f"Status Bayar : {t.get('payment_status', 'Pending')}\n" detail_text += f"Metode Bayar : {t.get('payment_method', '-')}\n\n" detail_text += f"───────────────────────────────────────\n" detail_text += f"ITEM:\n" detail_text += f"───────────────────────────────────────\n" for item in t['items']: detail_text += f"• {item['nama']}\n" detail_text += f" {item['qty']} x {format_currency(item['harga_satuan'])} = {format_currency(item['subtotal'])}\n\n" detail_text += f"───────────────────────────────────────\n" detail_text += f"Subtotal : {format_currency(t['subtotal'])}\n" detail_text += f"Diskon : {format_currency(t['diskon'])}\n" if t.get('promo_code'): detail_text += f"Promo Code : {t['promo_code']}\n" detail_text += f"───────────────────────────────────────\n" detail_text += f"TOTAL : {format_currency(t['total'])}\n" if t.get('payment_method') == 'Cash': detail_text += f"Bayar : {format_currency(t.get('paid_amount', 0))}\n" detail_text += f"Kembalian : {format_currency(t.get('change', 0))}\n" detail_text += f"═══════════════════════════════════════\n" self.riwayat_detail_text.config(state='normal') self.riwayat_detail_text.delete('1.0', tk.END) self.riwayat_detail_text.insert('1.0', detail_text) self.riwayat_detail_text.config(state='disabled') # ================================ # TAB: MEJA # ================================ def build_meja_tab(self, parent): for w in parent.winfo_children(): w.destroy() header = ttk.Frame(parent) header.pack(fill='x', padx=10, pady=8) ttk.Label(header, text="🪑 Manajemen Meja", font=("Arial", 14, "bold")).pack(side='left') ttk.Button(header, text="🔄 Refresh", command=lambda: self.build_meja_tab(parent)).pack(side='right', padx=5) # Summary summary_frame = ttk.LabelFrame(parent, text="📊 Status Meja", padding=10) summary_frame.pack(fill='x', padx=10, pady=5) summary_inner = ttk.Frame(summary_frame) summary_inner.pack() kosong = len([m for m in data_meja.values() if m == "Kosong"]) terisi = len([m for m in data_meja.values() if m == "Terisi"]) ttk.Label(summary_inner, text="✅ Meja Kosong:", font=("Arial", 10)).grid(row=0, column=0, sticky='w', padx=10, pady=3) ttk.Label(summary_inner, text=str(kosong), font=("Arial", 10, "bold"), foreground='green').grid(row=0, column=1, sticky='w', padx=10, pady=3) ttk.Label(summary_inner, text="🔴 Meja Terisi:", font=("Arial", 10)).grid(row=1, column=0, sticky='w', padx=10, pady=3) ttk.Label(summary_inner, text=str(terisi), font=("Arial", 10, "bold"), foreground='red').grid(row=1, column=1, sticky='w', padx=10, pady=3) # Grid Meja meja_frame = ttk.LabelFrame(parent, text="🗺️ Layout Meja", padding=15) meja_frame.pack(fill='both', expand=True, padx=10, pady=5) grid_frame = tk.Frame(meja_frame, bg='white') grid_frame.pack(expand=True) # Buat grid 2x5 untuk 10 meja for i in range(1, 11): status = data_meja.get(i, "Kosong") if status == "Kosong": bg_color = "#D4EDDA" fg_color = "#155724" icon = "✅" else: bg_color = "#F8D7DA" fg_color = "#721C24" icon = "🔴" # Hitung posisi grid row = (i - 1) // 5 col = (i - 1) % 5 # Frame untuk setiap meja meja_card = tk.Frame(grid_frame, bg=bg_color, relief='solid', bd=2, width=120, height=100) meja_card.grid(row=row, column=col, padx=10, pady=10) meja_card.pack_propagate(False) # Konten meja tk.Label(meja_card, text=icon, font=("Arial", 20), bg=bg_color).pack(pady=(10, 5)) tk.Label(meja_card, text=f"Meja {i}", font=("Arial", 11, "bold"), bg=bg_color, fg=fg_color).pack() tk.Label(meja_card, text=status, font=("Arial", 9), bg=bg_color, fg=fg_color).pack(pady=(2, 5)) # Button aksi if status == "Terisi": btn = tk.Button(meja_card, text="🧹 Kosongkan", command=lambda m=i: self.kosongkan_meja(m, parent), font=("Arial", 8), bg='#FFC107', fg='black', relief='flat', padx=8, pady=3, cursor='hand2') btn.pack(pady=3) else: btn = tk.Button(meja_card, text="🔒 Tandai Terisi", command=lambda m=i: self.tandai_terisi_meja(m, parent), font=("Arial", 8), bg='#007BFF', fg='white', relief='flat', padx=8, pady=3, cursor='hand2') btn.pack(pady=3) # Info footer info_frame = ttk.Frame(parent) info_frame.pack(fill='x', padx=10, pady=(5, 10)) ttk.Label(info_frame, text="💡 Tips: Meja otomatis terisi saat pembeli checkout, dan kosong saat pembayaran selesai", font=("Arial", 9), foreground='gray').pack() def kosongkan_meja(self, nomor_meja, parent): confirm = messagebox.askyesno("Konfirmasi", f"Kosongkan Meja {nomor_meja}?\n\n" f"Pastikan tamu sudah selesai dan pembayaran telah lunas.") if not confirm: return data_meja[nomor_meja] = "Kosong" save_meja() add_notification("info", f"Meja {nomor_meja} dikosongkan oleh {self.current_user['username']}") messagebox.showinfo("Success", f"Meja {nomor_meja} berhasil dikosongkan") self.build_meja_tab(parent) def tandai_terisi_meja(self, nomor_meja, parent): confirm = messagebox.askyesno("Konfirmasi", f"Tandai Meja {nomor_meja} terisi?\n\n" f"Gunakan fitur ini jika ada tamu tanpa order sistem.") if not confirm: return data_meja[nomor_meja] = "Terisi" save_meja() add_notification("info", f"Meja {nomor_meja} ditandai terisi oleh {self.current_user['username']}") messagebox.showinfo("Success", f"Meja {nomor_meja} berhasil ditandai terisi") self.build_meja_tab(parent) # ================================ # TAB: USER MANAGEMENT # ================================ def build_user_tab(self, parent): for w in parent.winfo_children(): w.destroy() header = ttk.Frame(parent) header.pack(fill='x', padx=10, pady=8) ttk.Label(header, text="👥 Kelola User", font=("Arial", 14, "bold")).pack(side='left') ttk.Button(header, text="➕ Tambah User", command=self.add_user_dialog).pack(side='right', padx=5) ttk.Button(header, text="🔄 Refresh", command=lambda: self.build_user_tab(parent)).pack(side='right', padx=5) tree_frame = ttk.Frame(parent) tree_frame.pack(fill='both', expand=True, padx=10, pady=5) tree_scroll = ttk.Scrollbar(tree_frame, orient='vertical') tree_scroll.pack(side='right', fill='y') cols = ("ID", "Username", "Role", "Status") self.user_tree = ttk.Treeview( tree_frame, columns=cols, show='headings', height=15, yscrollcommand=tree_scroll.set ) tree_scroll.config(command=self.user_tree.yview) for c in cols: self.user_tree.heading(c, text=c) if c == "ID": self.user_tree.column(c, width=50) else: self.user_tree.column(c, width=150) self.user_tree.pack(side='left', fill='both', expand=True) for u in users: self.user_tree.insert("", tk.END, values=( u['id'], u['username'], u['role'], "✅ Aktif" )) action_frame = ttk.Frame(parent) action_frame.pack(fill='x', padx=10, pady=10) ttk.Button(action_frame, text="✏️ Edit", command=self.edit_user_dialog, width=20).pack(side='left', padx=5) ttk.Button(action_frame, text="🔒 Reset Password", command=self.reset_user_password, width=20).pack(side='left', padx=5) ttk.Button(action_frame, text="🗑️ Hapus", command=self.delete_user, width=20).pack(side='left', padx=5) def add_user_dialog(self): dialog = tk.Toplevel(self.root) dialog.title("➕ Tambah User Baru") dialog.geometry("400x300") dialog.resizable(False, False) frame = ttk.Frame(dialog, padding=20) frame.pack(fill='both', expand=True) ttk.Label(frame, text="Username:").grid(row=0, column=0, sticky='w', pady=10) username_entry = ttk.Entry(frame, width=25) username_entry.grid(row=0, column=1, pady=10) ttk.Label(frame, text="Password:").grid(row=1, column=0, sticky='w', pady=10) password_entry = ttk.Entry(frame, show="●", width=25) password_entry.grid(row=1, column=1, pady=10) ttk.Label(frame, text="Role:").grid(row=2, column=0, sticky='w', pady=10) role_var = tk.StringVar(value="pembeli") role_cb = ttk.Combobox(frame, textvariable=role_var, values=["admin", "kasir", "waiter", "pembeli", "owner"], state="readonly", width=23) role_cb.grid(row=2, column=1, pady=10) def save_user(): username = username_entry.get().strip() password = password_entry.get().strip() role = role_var.get() if not username or not password: messagebox.showwarning("Input Error", "Username dan password harus diisi") return for u in users: if u['username'] == username: messagebox.showwarning("Sudah Ada", f"Username '{username}' sudah digunakan") return new_id = max([int(u['id']) for u in users], default=0) + 1 users.append({ 'id': str(new_id), 'username': username, 'password': password, 'role': role }) save_users() add_notification("success", f"User '{username}' berhasil ditambahkan") messagebox.showinfo("Success", f"User '{username}' berhasil ditambahkan") dialog.destroy() self.build_user_tab(self.user_tree.master.master) ttk.Button(frame, text="💾 Simpan", command=save_user).grid(row=3, column=0, columnspan=2, pady=20) def edit_user_dialog(self): sel = self.user_tree.selection() if not sel: messagebox.showwarning("Pilih User", "Pilih user yang akan diedit") return item = self.user_tree.item(sel)['values'] user_id = str(item[0]) user = None for u in users: if u['id'] == user_id: user = u break if not user: return dialog = tk.Toplevel(self.root) dialog.title(f"✏️ Edit User: {user['username']}") dialog.geometry("400x250") dialog.resizable(False, False) frame = ttk.Frame(dialog, padding=20) frame.pack(fill='both', expand=True) ttk.Label(frame, text="Username:").grid(row=0, column=0, sticky='w', pady=10) username_entry = ttk.Entry(frame, width=25) username_entry.insert(0, user['username']) username_entry.grid(row=0, column=1, pady=10) ttk.Label(frame, text="Role:").grid(row=1, column=0, sticky='w', pady=10) role_var = tk.StringVar(value=user['role']) role_cb = ttk.Combobox(frame, textvariable=role_var, values=["admin", "kasir", "waiter", "pembeli", "owner"], state="readonly", width=23) role_cb.grid(row=1, column=1, pady=10) def update_user(): new_username = username_entry.get().strip() if not new_username: messagebox.showwarning("Input Error", "Username harus diisi") return for u in users: if u['username'] == new_username and u['id'] != user_id: messagebox.showwarning("Sudah Ada", f"Username '{new_username}' sudah digunakan") return user['username'] = new_username user['role'] = role_var.get() save_users() add_notification("info", f"User '{new_username}' berhasil diupdate") messagebox.showinfo("Success", "User berhasil diupdate") dialog.destroy() self.build_user_tab(self.user_tree.master.master) ttk.Button(frame, text="💾 Update", command=update_user).grid(row=2, column=0, columnspan=2, pady=20) def reset_user_password(self): sel = self.user_tree.selection() if not sel: messagebox.showwarning("Pilih User", "Pilih user untuk reset password") return item = self.user_tree.item(sel)['values'] user_id = str(item[0]) user = None for u in users: if u['id'] == user_id: user = u break if not user: return new_password = simpledialog.askstring("Reset Password", f"Password baru untuk '{user['username']}':", show='●') if new_password: user['password'] = new_password save_users() add_notification("warning", f"Password user '{user['username']}' direset") messagebox.showinfo("Success", f"Password '{user['username']}' berhasil direset") def delete_user(self): sel = self.user_tree.selection() if not sel: messagebox.showwarning("Pilih User", "Pilih user yang akan dihapus") return item = self.user_tree.item(sel)['values'] user_id = str(item[0]) user = None for u in users: if u['id'] == user_id: user = u break if not user: return if user['id'] == str(self.current_user['id']): messagebox.showwarning("Error", "Tidak bisa menghapus user yang sedang login") return confirm = messagebox.askyesno("Konfirmasi", f"Hapus user '{user['username']}'?") if not confirm: return users.remove(user) save_users() add_notification("warning", f"User '{user['username']}' dihapus") messagebox.showinfo("Success", f"User '{user['username']}' berhasil dihapus") self.build_user_tab(self.user_tree.master.master) # ================================ # TAB: WAITER PESANAN (FIXED) # ================================ def build_waiter_pesanan_tab(self, parent): for w in parent.winfo_children(): w.destroy() header = ttk.Frame(parent) header.pack(fill='x', padx=10, pady=8) ttk.Label(header, text="🍽️ Daftar Pesanan", font=("Arial", 14, "bold")).pack(side='left') ttk.Button(header, text="🔄 Refresh", command=lambda: self.build_waiter_pesanan_tab(parent)).pack(side='right', padx=5) info_frame = ttk.Frame(parent) info_frame.pack(fill='x', padx=10, pady=5) pending_count = len([t for t in transaksi if t['status'] == 'Baru']) proses_count = len([t for t in transaksi if t['status'] == 'Diproses']) ttk.Label(info_frame, text=f"📋 Pesanan Baru: {pending_count}", font=("Arial", 10), foreground='red').pack(side='left', padx=20) ttk.Label(info_frame, text=f"⏳ Sedang Diproses: {proses_count}", font=("Arial", 10), foreground='orange').pack(side='left', padx=20) tree_frame = ttk.Frame(parent) tree_frame.pack(fill='both', expand=True, padx=10, pady=5) tree_scroll = ttk.Scrollbar(tree_frame, orient='vertical') tree_scroll.pack(side='right', fill='y') cols = ("ID", "Tanggal", "User", "Meja", "Total", "Status") self.pesanan_tree = ttk.Treeview( tree_frame, columns=cols, show='headings', height=15, yscrollcommand=tree_scroll.set ) tree_scroll.config(command=self.pesanan_tree.yview) for c in cols: self.pesanan_tree.heading(c, text=c) if c == "ID": self.pesanan_tree.column(c, width=50) elif c == "Meja": self.pesanan_tree.column(c, width=60) elif c == "Total": self.pesanan_tree.column(c, width=100) else: self.pesanan_tree.column(c, width=100) self.pesanan_tree.pack(side='left', fill='both', expand=True) for t in transaksi: if t.get('payment_status') == 'Berhasil': continue self.pesanan_tree.insert("", tk.END, values=( t['id'], t['tanggal'], t['user'], t.get('meja', '-'), format_currency(t['total']), t['status'] )) action_frame = ttk.Frame(parent) action_frame.pack(fill='x', padx=10, pady=10) ttk.Button(action_frame, text="✅ Proses Pesanan", command=self.proses_pesanan, width=20).pack(side='left', padx=5) ttk.Button(action_frame, text="🍽️ Pesanan Siap", command=self.siap_pesanan, width=20).pack(side='left', padx=5) ttk.Button(action_frame, text="📄 Lihat Detail", command=self.detail_pesanan, width=20).pack(side='left', padx=5) def proses_pesanan(self): sel = self.pesanan_tree.selection() if not sel: messagebox.showwarning("Pilih Pesanan", "Pilih pesanan yang akan diproses") return item = self.pesanan_tree.item(sel)['values'] transaksi_id = item[0] for t in transaksi: if t['id'] == transaksi_id: if t['status'] != 'Baru': messagebox.showinfo("Info", "Pesanan sudah diproses") return t['status'] = 'Diproses' save_transaksi() add_notification("info", f"Pesanan #{t['id']} sedang diproses") messagebox.showinfo("Success", f"Pesanan #{transaksi_id} sedang diproses") self.build_waiter_pesanan_tab(self.pesanan_tree.master.master) return def siap_pesanan(self): sel = self.pesanan_tree.selection() if not sel: messagebox.showwarning("Pilih Pesanan", "Pilih pesanan yang sudah siap") return item = self.pesanan_tree.item(sel)['values'] transaksi_id = item[0] for t in transaksi: if t['id'] == transaksi_id: if t['status'] == 'Baru': messagebox.showwarning("Warning", "Proses pesanan terlebih dahulu") return if t['status'] == 'Siap': messagebox.showinfo("Info", "Pesanan sudah ditandai siap") return t['status'] = 'Siap' save_transaksi() add_notification("success", f"Pesanan #{t['id']} siap disajikan") messagebox.showinfo("Success", f"Pesanan #{transaksi_id} siap disajikan!") self.build_waiter_pesanan_tab(self.pesanan_tree.master.master) return def detail_pesanan(self): sel = self.pesanan_tree.selection() if not sel: messagebox.showwarning("Pilih Pesanan", "Pilih pesanan untuk melihat detail") return item = self.pesanan_tree.item(sel)['values'] transaksi_id = item[0] for t in transaksi: if t['id'] == transaksi_id: detail = f"═══════════════════════════════\n" detail += f"DETAIL PESANAN #{t['id']}\n" detail += f"═══════════════════════════════\n\n" detail += f"Meja: {t.get('meja', '-')}\n" detail += f"User: {t['user']}\n" detail += f"Status: {t['status']}\n\n" detail += f"ITEM PESANAN:\n" detail += f"───────────────────────────────\n" for it in t['items']: detail += f"• {it['nama']} x{it['qty']}\n" detail += f"───────────────────────────────\n" detail += f"Total: {format_currency(t['total'])}\n" detail += f"═══════════════════════════════\n" messagebox.showinfo(f"Detail Pesanan #{transaksi_id}", detail) return # ================================ # TAB: KELOLA MENU (Admin) # ================================ def build_menu_manage_tab(self, parent): for w in parent.winfo_children(): w.destroy() header = ttk.Frame(parent) header.pack(fill='x', padx=10, pady=8) ttk.Label(header, text="⚙️ Kelola Menu", font=("Arial", 14, "bold")).pack(side='left') ttk.Button(header, text="➕ Tambah Menu", command=self.add_menu_dialog).pack(side='right', padx=5) ttk.Button(header, text="🔄 Refresh", command=lambda: self.build_menu_manage_tab(parent)).pack(side='right', padx=5) search_frame = ttk.LabelFrame(parent, text="🔍 Filter Menu", padding=10) search_frame.pack(fill='x', padx=10, pady=5) search_inner = ttk.Frame(search_frame) search_inner.pack() ttk.Label(search_inner, text="Kategori:").grid(row=0, column=0, padx=5) self.menu_manage_filter_var = tk.StringVar(value="Semua") categories = ["Semua"] + sorted(set(m['kategori'] for m in menu)) ttk.Combobox(search_inner, textvariable=self.menu_manage_filter_var, values=categories, state="readonly", width=15).grid(row=0, column=1, padx=5) ttk.Button(search_inner, text="🔍 Filter", command=lambda: self.refresh_menu_manage_tree()).grid(row=0, column=2, padx=5) tree_frame = ttk.Frame(parent) tree_frame.pack(fill='both', expand=True, padx=10, pady=5) tree_scroll = ttk.Scrollbar(tree_frame, orient='vertical') tree_scroll.pack(side='right', fill='y') cols = ("ID", "Nama", "Harga", "Kategori", "Stok", "Promo", "Diskon%") self.menu_manage_tree = ttk.Treeview( tree_frame, columns=cols, show='headings', height=12, yscrollcommand=tree_scroll.set ) tree_scroll.config(command=self.menu_manage_tree.yview) for c in cols: self.menu_manage_tree.heading(c, text=c) if c == "ID": self.menu_manage_tree.column(c, width=40) elif c == "Nama": self.menu_manage_tree.column(c, width=150) elif c == "Diskon%": self.menu_manage_tree.column(c, width=60) else: self.menu_manage_tree.column(c, width=100) self.menu_manage_tree.pack(side='left', fill='both', expand=True) self.refresh_menu_manage_tree() action_frame = ttk.Frame(parent) action_frame.pack(fill='x', padx=10, pady=10) ttk.Button(action_frame, text="✏️ Edit Menu", command=self.edit_menu_dialog, width=20).pack(side='left', padx=5) ttk.Button(action_frame, text="📸 Upload Foto", command=self.upload_menu_photo, width=20).pack(side='left', padx=5) ttk.Button(action_frame, text="🗑️ Hapus Menu", command=self.delete_menu, width=20).pack(side='left', padx=5) def refresh_menu_manage_tree(self): for r in self.menu_manage_tree.get_children(): self.menu_manage_tree.delete(r) filter_cat = self.menu_manage_filter_var.get() for m in menu: if filter_cat != "Semua" and m['kategori'] != filter_cat: continue self.menu_manage_tree.insert("", tk.END, values=( m['id'], m['nama'], format_currency(m['harga']), m['kategori'], m['stok'], m.get('promo', '-'), f"{m.get('item_discount_pct', 0)}%" )) def add_menu_dialog(self): dialog = tk.Toplevel(self.root) dialog.title("➕ Tambah Menu Baru") dialog.geometry("450x500") dialog.resizable(False, False) frame = ttk.Frame(dialog, padding=20) frame.pack(fill='both', expand=True) ttk.Label(frame, text="Nama Menu:").grid(row=0, column=0, sticky='w', pady=8) nama_entry = ttk.Entry(frame, width=30) nama_entry.grid(row=0, column=1, pady=8) ttk.Label(frame, text="Harga:").grid(row=1, column=0, sticky='w', pady=8) harga_entry = ttk.Entry(frame, width=30) harga_entry.grid(row=1, column=1, pady=8) ttk.Label(frame, text="Kategori:").grid(row=2, column=0, sticky='w', pady=8) kategori_var = tk.StringVar(value="Minuman") ttk.Combobox(frame, textvariable=kategori_var, values=["Minuman", "Makanan", "Snack", "Dessert"], width=28).grid(row=2, column=1, pady=8) ttk.Label(frame, text="Stok Awal:").grid(row=3, column=0, sticky='w', pady=8) stok_entry = ttk.Entry(frame, width=30) stok_entry.insert(0, "10") stok_entry.grid(row=3, column=1, pady=8) ttk.Label(frame, text="Promo Text:").grid(row=4, column=0, sticky='w', pady=8) promo_entry = ttk.Entry(frame, width=30) promo_entry.grid(row=4, column=1, pady=8) ttk.Label(frame, text="Diskon %:").grid(row=5, column=0, sticky='w', pady=8) discount_entry = ttk.Entry(frame, width=30) discount_entry.insert(0, "0") discount_entry.grid(row=5, column=1, pady=8) def save_menu(): nama = nama_entry.get().strip() try: harga = int(harga_entry.get()) stok = int(stok_entry.get()) discount = float(discount_entry.get()) except: messagebox.showerror("Input Error", "Harga, Stok, dan Diskon harus angka") return if not nama: messagebox.showwarning("Input Error", "Nama menu harus diisi") return new_id = max([m['id'] for m in menu], default=0) + 1 menu.append({ 'id': new_id, 'nama': nama, 'harga': harga, 'kategori': kategori_var.get(), 'stok': stok, 'foto': '', 'promo': promo_entry.get().strip(), 'item_discount_pct': discount }) save_menu() add_notification("success", f"Menu '{nama}' berhasil ditambahkan") messagebox.showinfo("Success", f"Menu '{nama}' berhasil ditambahkan!") dialog.destroy() self.build_menu_manage_tab(self.menu_manage_tree.master.master) ttk.Button(frame, text="💾 Simpan", command=save_menu).grid(row=6, column=0, columnspan=2, pady=20) def edit_menu_dialog(self): sel = self.menu_manage_tree.selection() if not sel: messagebox.showwarning("Pilih Menu", "Pilih menu yang akan diedit") return item = self.menu_manage_tree.item(sel)['values'] menu_id = item[0] menu_item = find_menu_by_id(menu_id) if not menu_item: return dialog = tk.Toplevel(self.root) dialog.title(f"✏️ Edit Menu: {menu_item['nama']}") dialog.geometry("450x500") dialog.resizable(False, False) frame = ttk.Frame(dialog, padding=20) frame.pack(fill='both', expand=True) ttk.Label(frame, text="Nama Menu:").grid(row=0, column=0, sticky='w', pady=8) nama_entry = ttk.Entry(frame, width=30) nama_entry.insert(0, menu_item['nama']) nama_entry.grid(row=0, column=1, pady=8) ttk.Label(frame, text="Harga:").grid(row=1, column=0, sticky='w', pady=8) harga_entry = ttk.Entry(frame, width=30) harga_entry.insert(0, str(menu_item['harga'])) harga_entry.grid(row=1, column=1, pady=8) ttk.Label(frame, text="Kategori:").grid(row=2, column=0, sticky='w', pady=8) kategori_var = tk.StringVar(value=menu_item['kategori']) ttk.Combobox(frame, textvariable=kategori_var, values=["Minuman", "Makanan", "Snack", "Dessert"], width=28).grid(row=2, column=1, pady=8) ttk.Label(frame, text="Stok:").grid(row=3, column=0, sticky='w', pady=8) stok_entry = ttk.Entry(frame, width=30) stok_entry.insert(0, str(menu_item['stok'])) stok_entry.grid(row=3, column=1, pady=8) ttk.Label(frame, text="Promo Text:").grid(row=4, column=0, sticky='w', pady=8) promo_entry = ttk.Entry(frame, width=30) promo_entry.insert(0, menu_item.get('promo', '')) promo_entry.grid(row=4, column=1, pady=8) ttk.Label(frame, text="Diskon %:").grid(row=5, column=0, sticky='w', pady=8) discount_entry = ttk.Entry(frame, width=30) discount_entry.insert(0, str(menu_item.get('item_discount_pct', 0))) discount_entry.grid(row=5, column=1, pady=8) def update_menu(): nama = nama_entry.get().strip() try: harga = int(harga_entry.get()) stok = int(stok_entry.get()) discount = float(discount_entry.get()) except: messagebox.showerror("Input Error", "Harga, Stok, dan Diskon harus angka") return if not nama: messagebox.showwarning("Input Error", "Nama menu harus diisi") return menu_item['nama'] = nama menu_item['harga'] = harga menu_item['kategori'] = kategori_var.get() menu_item['stok'] = stok menu_item['promo'] = promo_entry.get().strip() menu_item['item_discount_pct'] = discount save_menu() add_notification("info", f"Menu '{nama}' berhasil diupdate") messagebox.showinfo("Success", "Menu berhasil diupdate!") dialog.destroy() self.build_menu_manage_tab(self.menu_manage_tree.master.master) ttk.Button(frame, text="💾 Update", command=update_menu).grid(row=6, column=0, columnspan=2, pady=20) def upload_menu_photo(self): sel = self.menu_manage_tree.selection() if not sel: messagebox.showwarning("Pilih Menu", "Pilih menu untuk upload foto") return item = self.menu_manage_tree.item(sel)['values'] menu_id = item[0] menu_item = find_menu_by_id(menu_id) if not menu_item: return file_path = filedialog.askopenfilename( title="Pilih Foto Menu", filetypes=[("Image files", "*.jpg *.jpeg *.png *.gif *.bmp")] ) if file_path: new_path = copy_image_to_project(file_path) if new_path: menu_item['foto'] = new_path save_menu() messagebox.showinfo("Success", f"Foto berhasil diupload untuk '{menu_item['nama']}'") self.build_menu_manage_tab(self.menu_manage_tree.master.master) else: messagebox.showerror("Error", "Gagal mengcopy foto") def delete_menu(self): sel = self.menu_manage_tree.selection() if not sel: messagebox.showwarning("Pilih Menu", "Pilih menu yang akan dihapus") return item = self.menu_manage_tree.item(sel)['values'] menu_id = item[0] menu_item = find_menu_by_id(menu_id) if not menu_item: return confirm = messagebox.askyesno("Konfirmasi", f"Hapus menu '{menu_item['nama']}'?") if not confirm: return menu.remove(menu_item) save_menu() add_notification("warning", f"Menu '{menu_item['nama']}' dihapus") messagebox.showinfo("Success", f"Menu '{menu_item['nama']}' berhasil dihapus") self.build_menu_manage_tab(self.menu_manage_tree.master.master) # ================================ # TAB: STOK BAHAN (Admin) # ================================ def build_bahan_tab(self, parent): for w in parent.winfo_children(): w.destroy() header = ttk.Frame(parent) header.pack(fill='x', padx=10, pady=8) ttk.Label(header, text="📦 Kelola Stok Bahan", font=("Arial", 14, "bold")).pack(side='left') ttk.Button(header, text="➕ Tambah Bahan", command=self.add_bahan_dialog).pack(side='right', padx=5) ttk.Button(header, text="🔄 Refresh", command=lambda: self.build_bahan_tab(parent)).pack(side='right', padx=5) # Warning frame untuk stok rendah low_stock = [name for name, qty in bahan.items() if qty < 10] if low_stock: warning_frame = ttk.Frame(parent) warning_frame.pack(fill='x', padx=10, pady=5) tk.Label(warning_frame, text=f"⚠️ PERINGATAN: {len(low_stock)} bahan dengan stok < 10!", font=("Arial", 10, "bold"), bg="#FFF3CD", fg="#856404", padx=10, pady=8).pack(fill='x') # Container dengan 2 kolom container = ttk.Frame(parent) container.pack(fill='both', expand=True, padx=10, pady=5) # LEFT: Daftar Bahan left_frame = ttk.LabelFrame(container, text="📋 Daftar Bahan Baku", padding=10) left_frame.pack(side='left', fill='both', expand=True, padx=(0, 5)) tree_scroll = ttk.Scrollbar(left_frame, orient='vertical') tree_scroll.pack(side='right', fill='y') cols = ("Nama Bahan", "Jumlah", "Status") self.bahan_tree = ttk.Treeview( left_frame, columns=cols, show='headings', height=15, yscrollcommand=tree_scroll.set ) tree_scroll.config(command=self.bahan_tree.yview) self.bahan_tree.heading("Nama Bahan", text="Nama Bahan") self.bahan_tree.heading("Jumlah", text="Jumlah") self.bahan_tree.heading("Status", text="Status") self.bahan_tree.column("Nama Bahan", width=150) self.bahan_tree.column("Jumlah", width=80) self.bahan_tree.column("Status", width=100) self.bahan_tree.pack(side='left', fill='both', expand=True) for nama, qty in sorted(bahan.items()): if qty < 10: status = "🔴 Rendah" elif qty < 20: status = "🟡 Sedang" else: status = "🟢 Aman" self.bahan_tree.insert("", tk.END, values=(nama, qty, status)) action_frame = ttk.Frame(left_frame) action_frame.pack(fill='x', pady=(10, 0)) ttk.Button(action_frame, text="➕ Tambah Stok", command=self.add_stock_dialog, width=18).pack(side='left', padx=3) ttk.Button(action_frame, text="➖ Kurangi Stok", command=self.reduce_stock_dialog, width=18).pack(side='left', padx=3) ttk.Button(action_frame, text="🗑️ Hapus Bahan", command=self.delete_bahan, width=18).pack(side='left', padx=3) # RIGHT: Resep Menu right_frame = ttk.LabelFrame(container, text="📝 Resep Menu", padding=10) right_frame.pack(side='right', fill='both', expand=True, padx=(5, 0)) ttk.Label(right_frame, text="Pilih Menu:", font=("Arial", 9, "bold")).pack(anchor='w', pady=5) menu_names = [f"{m['id']} - {m['nama']}" for m in menu] self.resep_menu_var = tk.StringVar() menu_combo = ttk.Combobox(right_frame, textvariable=self.resep_menu_var, values=menu_names, state="readonly", width=30) menu_combo.pack(fill='x', pady=5) menu_combo.bind("<>", lambda e: self.show_resep()) self.resep_text = tk.Text(right_frame, height=12, font=("Arial", 10), wrap='word', state='disabled') self.resep_text.pack(fill='both', expand=True, pady=5) ttk.Button(right_frame, text="✏️ Edit Resep", command=self.edit_resep_dialog).pack(fill='x', pady=5) def show_resep(self): selected = self.resep_menu_var.get() if not selected: return try: menu_id = int(selected.split(' - ')[0]) except: return menu_item = find_menu_by_id(menu_id) if not menu_item: return self.resep_text.config(state='normal') self.resep_text.delete('1.0', tk.END) text = f"═══════════════════════════════\n" text += f"RESEP: {menu_item['nama']}\n" text += f"═══════════════════════════════\n\n" if menu_id in resep and resep[menu_id]: text += "Bahan yang dibutuhkan:\n\n" for bahan_nama, qty in resep[menu_id].items(): stok_tersedia = bahan.get(bahan_nama, 0) text += f"• {bahan_nama}: {qty} unit\n" text += f" (Stok tersedia: {stok_tersedia})\n\n" else: text += "❌ Belum ada resep untuk menu ini.\n\n" text += "Klik 'Edit Resep' untuk menambahkan." self.resep_text.insert('1.0', text) self.resep_text.config(state='disabled') def add_bahan_dialog(self): dialog = tk.Toplevel(self.root) dialog.title("➕ Tambah Bahan Baru") dialog.geometry("400x250") dialog.resizable(False, False) frame = ttk.Frame(dialog, padding=20) frame.pack(fill='both', expand=True) ttk.Label(frame, text="Nama Bahan:").grid(row=0, column=0, sticky='w', pady=10) nama_entry = ttk.Entry(frame, width=25) nama_entry.grid(row=0, column=1, pady=10) ttk.Label(frame, text="Jumlah Awal:").grid(row=1, column=0, sticky='w', pady=10) jumlah_entry = ttk.Entry(frame, width=25) jumlah_entry.insert(0, "50") jumlah_entry.grid(row=1, column=1, pady=10) def save_bahan(): nama = nama_entry.get().strip().lower() try: jumlah = int(jumlah_entry.get()) except: messagebox.showerror("Input Error", "Jumlah harus angka") return if not nama: messagebox.showwarning("Input Error", "Nama bahan harus diisi") return if nama in bahan: messagebox.showwarning("Sudah Ada", f"Bahan '{nama}' sudah ada") return bahan[nama] = jumlah save_bahan() add_notification("success", f"Bahan '{nama}' berhasil ditambahkan") messagebox.showinfo("Success", f"Bahan '{nama}' berhasil ditambahkan!") dialog.destroy() self.build_bahan_tab(self.bahan_tree.master.master.master) ttk.Button(frame, text="💾 Simpan", command=save_bahan).grid(row=2, column=0, columnspan=2, pady=20) def add_stock_dialog(self): sel = self.bahan_tree.selection() if not sel: messagebox.showwarning("Pilih Bahan", "Pilih bahan untuk menambah stok") return item = self.bahan_tree.item(sel)['values'] nama_bahan = item[0] qty = simpledialog.askinteger("Tambah Stok", f"Tambah berapa unit untuk '{nama_bahan}'?", minvalue=1) if qty: bahan[nama_bahan] += qty save_bahan() add_notification("info", f"Stok {nama_bahan} ditambah {qty} unit") messagebox.showinfo("Success", f"Stok '{nama_bahan}' berhasil ditambahkan!") self.build_bahan_tab(self.bahan_tree.master.master.master) def reduce_stock_dialog(self): sel = self.bahan_tree.selection() if not sel: messagebox.showwarning("Pilih Bahan", "Pilih bahan untuk mengurangi stok") return item = self.bahan_tree.item(sel)['values'] nama_bahan = item[0] stok_sekarang = bahan.get(nama_bahan, 0) qty = simpledialog.askinteger("Kurangi Stok", f"Kurangi berapa unit dari '{nama_bahan}'?\n(Stok sekarang: {stok_sekarang})", minvalue=1, maxvalue=stok_sekarang) if qty: bahan[nama_bahan] -= qty if bahan[nama_bahan] < 0: bahan[nama_bahan] = 0 save_bahan() add_notification("warning", f"Stok {nama_bahan} dikurangi {qty} unit") messagebox.showinfo("Success", f"Stok '{nama_bahan}' berhasil dikurangi!") self.build_bahan_tab(self.bahan_tree.master.master.master) def delete_bahan(self): sel = self.bahan_tree.selection() if not sel: messagebox.showwarning("Pilih Bahan", "Pilih bahan yang akan dihapus") return item = self.bahan_tree.item(sel)['values'] nama_bahan = item[0] # Check if used in recipe used_in = [] for menu_id, ingredients in resep.items(): if nama_bahan in ingredients: menu_item = find_menu_by_id(menu_id) if menu_item: used_in.append(menu_item['nama']) if used_in: messagebox.showwarning("Tidak Bisa Dihapus", f"Bahan '{nama_bahan}' digunakan di resep:\n" + "\n".join(used_in)) return confirm = messagebox.askyesno("Konfirmasi", f"Hapus bahan '{nama_bahan}'?") if not confirm: return del bahan[nama_bahan] save_bahan() add_notification("warning", f"Bahan '{nama_bahan}' dihapus") messagebox.showinfo("Success", f"Bahan '{nama_bahan}' berhasil dihapus") self.build_bahan_tab(self.bahan_tree.master.master.master) def edit_resep_dialog(self): selected = self.resep_menu_var.get() if not selected: messagebox.showwarning("Pilih Menu", "Pilih menu terlebih dahulu") return try: menu_id = int(selected.split(' - ')[0]) except: return menu_item = find_menu_by_id(menu_id) if not menu_item: return dialog = tk.Toplevel(self.root) dialog.title(f"✏️ Edit Resep: {menu_item['nama']}") dialog.geometry("500x500") dialog.resizable(False, False) frame = ttk.Frame(dialog, padding=20) frame.pack(fill='both', expand=True) ttk.Label(frame, text=f"Resep untuk: {menu_item['nama']}", font=("Arial", 12, "bold")).pack(pady=10) # Frame untuk resep entries resep_frame = ttk.Frame(frame) resep_frame.pack(fill='both', expand=True, pady=10) # Canvas + Scrollbar untuk banyak bahan canvas = tk.Canvas(resep_frame, height=300) scrollbar = ttk.Scrollbar(resep_frame, orient="vertical", command=canvas.yview) scrollable_frame = ttk.Frame(canvas) scrollable_frame.bind( "", lambda e: canvas.configure(scrollregion=canvas.bbox("all")) ) canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") # Get current recipe current_resep = resep.get(menu_id, {}) # Buat entry untuk setiap bahan yang ada entries = {} row = 0 for bahan_nama in sorted(bahan.keys()): ttk.Label(scrollable_frame, text=f"{bahan_nama}:").grid(row=row, column=0, sticky='w', padx=5, pady=5) qty_entry = ttk.Entry(scrollable_frame, width=10) qty_entry.insert(0, str(current_resep.get(bahan_nama, 0))) qty_entry.grid(row=row, column=1, padx=5, pady=5) ttk.Label(scrollable_frame, text="unit").grid(row=row, column=2, sticky='w', padx=5, pady=5) entries[bahan_nama] = qty_entry row += 1 def save_resep(): new_resep = {} for bahan_nama, entry in entries.items(): try: qty = int(entry.get()) if qty > 0: new_resep[bahan_nama] = qty except: pass if not new_resep: confirm = messagebox.askyesno("Konfirmasi", "Resep kosong. Hapus resep untuk menu ini?") if confirm and menu_id in resep: del resep[menu_id] else: resep[menu_id] = new_resep save_resep() add_notification("info", f"Resep '{menu_item['nama']}' berhasil diupdate") messagebox.showinfo("Success", "Resep berhasil disimpan!") dialog.destroy() self.show_resep() ttk.Button(frame, text="💾 Simpan Resep", command=save_resep).pack(pady=10) # ================================ # TAB: PROMO (Admin) # ================================ def build_promo_tab(self, parent): for w in parent.winfo_children(): w.destroy() header = ttk.Frame(parent) header.pack(fill='x', padx=10, pady=8) ttk.Label(header, text="🎁 Kelola Kode Promo", font=("Arial", 14, "bold")).pack(side='left') ttk.Button(header, text="➕ Tambah Promo", command=self.add_promo_dialog).pack(side='right', padx=5) ttk.Button(header, text="🔄 Refresh", command=lambda: self.build_promo_tab(parent)).pack(side='right', padx=5) info_frame = ttk.Frame(parent) info_frame.pack(fill='x', padx=10, pady=5) tk.Label(info_frame, text=f"📊 Total Kode Promo Aktif: {len(promo_codes)}", font=("Arial", 10, "bold"), bg="#E3F2FD", fg="#1565C0", padx=15, pady=8).pack(fill='x') # Promo Code List list_frame = ttk.LabelFrame(parent, text="📋 Daftar Kode Promo", padding=10) list_frame.pack(fill='both', expand=True, padx=10, pady=5) tree_scroll = ttk.Scrollbar(list_frame, orient='vertical') tree_scroll.pack(side='right', fill='y') cols = ("Kode Promo", "Diskon %", "Status") self.promo_tree = ttk.Treeview( list_frame, columns=cols, show='headings', height=12, yscrollcommand=tree_scroll.set ) tree_scroll.config(command=self.promo_tree.yview) self.promo_tree.heading("Kode Promo", text="Kode Promo") self.promo_tree.heading("Diskon %", text="Diskon %") self.promo_tree.heading("Status", text="Status") self.promo_tree.column("Kode Promo", width=200) self.promo_tree.column("Diskon %", width=100) self.promo_tree.column("Status", width=150) self.promo_tree.pack(side='left', fill='both', expand=True) for code, discount in sorted(promo_codes.items()): self.promo_tree.insert("", tk.END, values=( code, f"{discount}%", "✅ Aktif" )) # Action buttons action_frame = ttk.Frame(list_frame) action_frame.pack(fill='x', pady=(10, 0)) ttk.Button(action_frame, text="✏️ Edit Promo", command=self.edit_promo_dialog, width=20).pack(side='left', padx=5) ttk.Button(action_frame, text="🗑️ Hapus Promo", command=self.delete_promo, width=20).pack(side='left', padx=5) ttk.Button(action_frame, text="📋 Salin Kode", command=self.copy_promo_code, width=20).pack(side='left', padx=5) # Info box info_box = ttk.LabelFrame(parent, text="ℹ️ Informasi", padding=10) info_box.pack(fill='x', padx=10, pady=5) info_text = """ - Kode promo dapat digunakan oleh pembeli saat checkout - Diskon promo diterapkan setelah diskon item - Kode promo tidak case-sensitive (CAFE10 = cafe10) - Gunakan kode yang mudah diingat untuk pelanggan """ ttk.Label(info_box, text=info_text, font=("Arial", 9), foreground='#666666', justify='left').pack(anchor='w') def add_promo_dialog(self): dialog = tk.Toplevel(self.root) dialog.title("➕ Tambah Kode Promo Baru") dialog.geometry("450x300") dialog.resizable(False, False) frame = ttk.Frame(dialog, padding=20) frame.pack(fill='both', expand=True) ttk.Label(frame, text="Kode Promo:", font=("Arial", 10, "bold")).grid(row=0, column=0, sticky='w', pady=10) ttk.Label(frame, text="(Huruf kapital, tanpa spasi)").grid(row=1, column=0, sticky='w', pady=(0, 10)) code_entry = ttk.Entry(frame, width=30, font=("Arial", 11)) code_entry.grid(row=0, column=1, rowspan=2, pady=10, padx=10) ttk.Label(frame, text="Diskon (%):", font=("Arial", 10, "bold")).grid(row=2, column=0, sticky='w', pady=10) discount_entry = ttk.Entry(frame, width=30) discount_entry.grid(row=2, column=1, pady=10, padx=10) ttk.Label(frame, text="Contoh: CAFE10, DISKON20, WELCOME15", font=("Arial", 9), foreground='gray').grid(row=3, column=0, columnspan=2, pady=5) def save_promo(): code = code_entry.get().strip().upper() try: discount = int(discount_entry.get()) except: messagebox.showerror("Input Error", "Diskon harus angka") return if not code: messagebox.showwarning("Input Error", "Kode promo harus diisi") return if ' ' in code: messagebox.showwarning("Input Error", "Kode promo tidak boleh mengandung spasi") return if discount < 1 or discount > 100: messagebox.showwarning("Input Error", "Diskon harus antara 1-100%") return if code in promo_codes: messagebox.showwarning("Sudah Ada", f"Kode promo '{code}' sudah ada") return promo_codes[code] = discount save_promo_codes() add_notification("success", f"Kode promo '{code}' berhasil ditambahkan") messagebox.showinfo("Success", f"✅ Kode promo '{code}' berhasil dibuat!\n\nDiskon: {discount}%") dialog.destroy() self.build_promo_tab(self.promo_tree.master.master) ttk.Button(frame, text="💾 Simpan Promo", command=save_promo).grid(row=4, column=0, columnspan=2, pady=20) def edit_promo_dialog(self): sel = self.promo_tree.selection() if not sel: messagebox.showwarning("Pilih Promo", "Pilih kode promo yang akan diedit") return item = self.promo_tree.item(sel)['values'] code = item[0] current_discount = promo_codes.get(code, 0) dialog = tk.Toplevel(self.root) dialog.title(f"✏️ Edit Kode Promo: {code}") dialog.geometry("450x250") dialog.resizable(False, False) frame = ttk.Frame(dialog, padding=20) frame.pack(fill='both', expand=True) ttk.Label(frame, text="Kode Promo:", font=("Arial", 10, "bold")).grid(row=0, column=0, sticky='w', pady=10) code_label = ttk.Label(frame, text=code, font=("Arial", 12, "bold"), foreground='#1565C0') code_label.grid(row=0, column=1, pady=10, padx=10, sticky='w') ttk.Label(frame, text="Diskon Baru (%):", font=("Arial", 10, "bold")).grid(row=1, column=0, sticky='w', pady=10) discount_entry = ttk.Entry(frame, width=30) discount_entry.insert(0, str(current_discount)) discount_entry.grid(row=1, column=1, pady=10, padx=10) def update_promo(): try: discount = int(discount_entry.get()) except: messagebox.showerror("Input Error", "Diskon harus angka") return if discount < 1 or discount > 100: messagebox.showwarning("Input Error", "Diskon harus antara 1-100%") return promo_codes[code] = discount save_promo_codes() add_notification("info", f"Kode promo '{code}' diupdate menjadi {discount}%") messagebox.showinfo("Success", f"✅ Kode promo '{code}' berhasil diupdate!") dialog.destroy() self.build_promo_tab(self.promo_tree.master.master) ttk.Button(frame, text="💾 Update", command=update_promo).grid(row=2, column=0, columnspan=2, pady=20) def delete_promo(self): sel = self.promo_tree.selection() if not sel: messagebox.showwarning("Pilih Promo", "Pilih kode promo yang akan dihapus") return item = self.promo_tree.item(sel)['values'] code = item[0] confirm = messagebox.askyesno("Konfirmasi", f"Hapus kode promo '{code}'?") if not confirm: return del promo_codes[code] save_promo_codes() add_notification("warning", f"Kode promo '{code}' dihapus") messagebox.showinfo("Success", f"Kode promo '{code}' berhasil dihapus") self.build_promo_tab(self.promo_tree.master.master) def copy_promo_code(self): sel = self.promo_tree.selection() if not sel: messagebox.showwarning("Pilih Promo", "Pilih kode promo yang akan disalin") return item = self.promo_tree.item(sel)['values'] code = item[0] self.root.clipboard_clear() self.root.clipboard_append(code) messagebox.showinfo("Tersalin", f"✅ Kode promo '{code}' berhasil disalin ke clipboard!") # ================================ # TAB: LAPORAN (Owner/Admin) # ================================ def build_laporan_tab(self, parent): for w in parent.winfo_children(): w.destroy() header = ttk.Frame(parent) header.pack(fill='x', padx=10, pady=8) ttk.Label(header, text="📊 Laporan & Analisis", font=("Arial", 14, "bold")).pack(side='left') ttk.Button(header, text="🔄 Refresh", command=lambda: self.build_laporan_tab(parent)).pack(side='right', padx=5) ttk.Button(header, text="📥 Export Excel", command=self.export_laporan_excel).pack(side='right', padx=5) # Period selector period_frame = ttk.LabelFrame(parent, text="📅 Pilih Periode Laporan", padding=10) period_frame.pack(fill='x', padx=10, pady=5) period_inner = ttk.Frame(period_frame) period_inner.pack() ttk.Label(period_inner, text="Dari:").grid(row=0, column=0, padx=5) self.laporan_start_date = tk.StringVar(value=str(datetime.date.today())) ttk.Entry(period_inner, textvariable=self.laporan_start_date, width=15).grid(row=0, column=1, padx=5) ttk.Label(period_inner, text="Sampai:").grid(row=0, column=2, padx=5) self.laporan_end_date = tk.StringVar(value=str(datetime.date.today())) ttk.Entry(period_inner, textvariable=self.laporan_end_date, width=15).grid(row=0, column=3, padx=5) ttk.Button(period_inner, text="📊 Tampilkan", command=self.refresh_laporan).grid(row=0, column=4, padx=10) # Quick filters quick_frame = ttk.Frame(period_frame) quick_frame.pack(pady=5) ttk.Button(quick_frame, text="📅 Hari Ini", command=lambda: self.set_period_today()).pack(side='left', padx=3) ttk.Button(quick_frame, text="📆 Minggu Ini", command=lambda: self.set_period_week()).pack(side='left', padx=3) ttk.Button(quick_frame, text="📊 Bulan Ini", command=lambda: self.set_period_month()).pack(side='left', padx=3) # Main container with tabs notebook = ttk.Notebook(parent) notebook.pack(fill='both', expand=True, padx=10, pady=5) # Tab 1: Dashboard tab_dashboard = ttk.Frame(notebook) notebook.add(tab_dashboard, text="📊 Dashboard") self.build_dashboard_tab(tab_dashboard) # Tab 2: Detail Transaksi tab_detail = ttk.Frame(notebook) notebook.add(tab_detail, text="📋 Detail Transaksi") self.build_detail_transaksi_tab(tab_detail) # Tab 3: Menu Analytics tab_menu = ttk.Frame(notebook) notebook.add(tab_menu, text="🍽️ Analisis Menu") self.build_menu_analytics_tab(tab_menu) # Tab 4: Payment Analytics tab_payment = ttk.Frame(notebook) notebook.add(tab_payment, text="💳 Analisis Pembayaran") self.build_payment_analytics_tab(tab_payment) def set_period_today(self): today = str(datetime.date.today()) self.laporan_start_date.set(today) self.laporan_end_date.set(today) self.refresh_laporan() def set_period_week(self): today = datetime.date.today() start = today - datetime.timedelta(days=today.weekday()) self.laporan_start_date.set(str(start)) self.laporan_end_date.set(str(today)) self.refresh_laporan() def set_period_month(self): today = datetime.date.today() start = today.replace(day=1) self.laporan_start_date.set(str(start)) self.laporan_end_date.set(str(today)) self.refresh_laporan() def refresh_laporan(self): # Rebuild all tabs notebook = None for widget in self.root.winfo_children(): if isinstance(widget, ttk.Notebook): for tab in widget.winfo_children(): if isinstance(tab, ttk.Notebook): notebook = tab break if notebook: for i, tab in enumerate(notebook.winfo_children()): if i == 0: self.build_dashboard_tab(tab) elif i == 1: self.build_detail_transaksi_tab(tab) elif i == 2: self.build_menu_analytics_tab(tab) elif i == 3: self.build_payment_analytics_tab(tab) def get_filtered_transactions(self): start_date = self.laporan_start_date.get() end_date = self.laporan_end_date.get() return [t for t in transaksi if start_date <= t['tanggal'] <= end_date and t.get('payment_status') == 'Berhasil'] def build_dashboard_tab(self, parent): for w in parent.winfo_children(): w.destroy() trans = self.get_filtered_transactions() # Summary Cards summary_frame = ttk.Frame(parent) summary_frame.pack(fill='x', padx=10, pady=10) # Card 1: Total Pendapatan card1 = tk.Frame(summary_frame, bg='#4CAF50', relief='solid', bd=1) card1.pack(side='left', fill='both', expand=True, padx=5) total_income = sum(t['total'] for t in trans) tk.Label(card1, text="💰", font=("Arial", 32), bg='#4CAF50', fg='white').pack(pady=(15, 5)) tk.Label(card1, text="Total Pendapatan", font=("Arial", 11), bg='#4CAF50', fg='white').pack() tk.Label(card1, text=format_currency(total_income), font=("Arial", 18, "bold"), bg='#4CAF50', fg='white').pack(pady=(5, 15)) # Card 2: Total Transaksi card2 = tk.Frame(summary_frame, bg='#2196F3', relief='solid', bd=1) card2.pack(side='left', fill='both', expand=True, padx=5) tk.Label(card2, text="📋", font=("Arial", 32), bg='#2196F3', fg='white').pack(pady=(15, 5)) tk.Label(card2, text="Total Transaksi", font=("Arial", 11), bg='#2196F3', fg='white').pack() tk.Label(card2, text=str(len(trans)), font=("Arial", 18, "bold"), bg='#2196F3', fg='white').pack(pady=(5, 15)) # Card 3: Rata-rata card3 = tk.Frame(summary_frame, bg='#FF9800', relief='solid', bd=1) card3.pack(side='left', fill='both', expand=True, padx=5) avg_trans = total_income // len(trans) if trans else 0 tk.Label(card3, text="📊", font=("Arial", 32), bg='#FF9800', fg='white').pack(pady=(15, 5)) tk.Label(card3, text="Rata-rata/Transaksi", font=("Arial", 11), bg='#FF9800', fg='white').pack() tk.Label(card3, text=format_currency(avg_trans), font=("Arial", 18, "bold"), bg='#FF9800', fg='white').pack(pady=(5, 15)) # Card 4: Menu Terjual card4 = tk.Frame(summary_frame, bg='#9C27B0', relief='solid', bd=1) card4.pack(side='left', fill='both', expand=True, padx=5) total_items = sum(sum(item['qty'] for item in t['items']) for t in trans) tk.Label(card4, text="🍽️", font=("Arial", 32), bg='#9C27B0', fg='white').pack(pady=(15, 5)) tk.Label(card4, text="Total Item Terjual", font=("Arial", 11), bg='#9C27B0', fg='white').pack() tk.Label(card4, text=str(total_items), font=("Arial", 18, "bold"), bg='#9C27B0', fg='white').pack(pady=(5, 15)) # Chart Section chart_frame = ttk.LabelFrame(parent, text="📈 Grafik Penjualan", padding=10) chart_frame.pack(fill='both', expand=True, padx=10, pady=5) if MATPLOTLIB_AVAILABLE and trans: self.create_sales_chart(chart_frame, trans) else: tk.Label(chart_frame, text="📊 Chart membutuhkan matplotlib\nInstall: pip install matplotlib", font=("Arial", 12), fg='gray').pack(pady=50) # Top Menu Section top_frame = ttk.LabelFrame(parent, text="🏆 Top 5 Menu Terlaris", padding=10) top_frame.pack(fill='x', padx=10, pady=5) menu_sales = defaultdict(int) for t in trans: for item in t['items']: menu_sales[item['nama']] += item['qty'] top_menu = sorted(menu_sales.items(), key=lambda x: x[1], reverse=True)[:5] if top_menu: for i, (nama, qty) in enumerate(top_menu, 1): rank_frame = tk.Frame(top_frame, bg='white', relief='solid', bd=1) rank_frame.pack(fill='x', pady=3) medal = ['🥇', '🥈', '🥉', '4️⃣', '5️⃣'][i-1] tk.Label(rank_frame, text=f"{medal} {nama}", font=("Arial", 11, "bold"), bg='white', anchor='w').pack(side='left', padx=10, pady=8) tk.Label(rank_frame, text=f"{qty} terjual", font=("Arial", 10), bg='white', fg='green', anchor='e').pack(side='right', padx=10, pady=8) else: tk.Label(top_frame, text="Belum ada data", fg='gray').pack(pady=10) def create_sales_chart(self, parent, transactions): """Buat multiple charts untuk visualisasi data penjualan""" if not MATPLOTLIB_AVAILABLE or not transactions: tk.Label(parent, text="📊 Chart data tidak tersedia", fg='gray').pack(pady=20) return today = str(datetime.date.today()) fig = Figure(figsize=(16, 10), dpi=80) # Chart 1: Daily Sales (Bar Chart) ax1 = fig.add_subplot(3, 3, 1) daily_sales = defaultdict(int) for t in transactions: daily_sales[t['tanggal']] += t['total'] dates = sorted(daily_sales.keys()) sales = [daily_sales[d] for d in dates] ax1.bar(dates, sales, color='#4CAF50', alpha=0.8) ax1.set_title('📊 Penjualan Harian', fontweight='bold', fontsize=10) ax1.set_ylabel('Pendapatan (Rp)', fontsize=9) ax1.tick_params(axis='x', rotation=45) ax1.grid(True, alpha=0.3, axis='y') # Chart 2: Payment Methods (Pie Chart) ax2 = fig.add_subplot(3, 3, 2) payment_methods = defaultdict(int) for t in transactions: method = t.get('payment_method', 'Unknown') payment_methods[method] += t['total'] colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA726'] ax2.pie(payment_methods.values(), labels=payment_methods.keys(), autopct='%1.1f%%', colors=colors[:len(payment_methods)], startangle=90) ax2.set_title('💳 Metode Pembayaran (Total)', fontweight='bold', fontsize=10) # Chart 3: Top 5 Menu (Bar Chart Horizontal) ax3 = fig.add_subplot(3, 3, 3) menu_sales = defaultdict(int) for t in transactions: for item in t['items']: menu_sales[item['nama']] += item['qty'] top_menus = dict(sorted(menu_sales.items(), key=lambda x: x[1], reverse=True)[:5]) if top_menus: ax3.barh(list(top_menus.keys()), list(top_menus.values()), color='#FFA726', alpha=0.8) ax3.set_title('🍽️ Menu Top 5', fontweight='bold', fontsize=10) ax3.set_xlabel('Jumlah Terjual', fontsize=9) # Chart 4: Cumulative Revenue ax4 = fig.add_subplot(3, 3, 4) cumulative = 0 cum_values = [] for d in dates: cumulative += daily_sales[d] cum_values.append(cumulative) ax4.plot(dates, cum_values, marker='o', linewidth=2, markersize=6, color='#2196F3') ax4.fill_between(range(len(dates)), cum_values, alpha=0.3, color='#2196F3') ax4.set_title('📈 Kumulatif Pendapatan', fontweight='bold', fontsize=10) ax4.set_ylabel('Total (Rp)', fontsize=9) ax4.tick_params(axis='x', rotation=45) ax4.grid(True, alpha=0.3) # Chart 5: Transaction Count per Day ax5 = fig.add_subplot(3, 3, 5) daily_count = defaultdict(int) for t in transactions: daily_count[t['tanggal']] += 1 counts = [daily_count[d] for d in dates] ax5.bar(dates, counts, color='#9C27B0', alpha=0.8) ax5.set_title('📊 Jumlah Transaksi per Hari', fontweight='bold', fontsize=10) ax5.set_ylabel('Jumlah Transaksi', fontsize=9) ax5.tick_params(axis='x', rotation=45) ax5.grid(True, alpha=0.3, axis='y') # Chart 6: Average Transaction Value ax6 = fig.add_subplot(3, 3, 6) daily_avg = [] for d in dates: trans_on_day = [t['total'] for t in transactions if t['tanggal'] == d] avg = sum(trans_on_day) / len(trans_on_day) if trans_on_day else 0 daily_avg.append(avg) ax6.plot(dates, daily_avg, marker='s', linewidth=2, markersize=5, color='#4CAF50') ax6.set_title('💵 Rata-rata Transaksi', fontweight='bold', fontsize=10) ax6.set_ylabel('Nilai Rata-rata (Rp)', fontsize=9) ax6.tick_params(axis='x', rotation=45) ax6.grid(True, alpha=0.3) # Chart 7: Hari Ini - Pendapatan QRIS vs CASH (Pie Chart) ax7 = fig.add_subplot(3, 3, 7) today_trans = [t for t in transactions if t['tanggal'] == today] qris_cash = defaultdict(int) for t in today_trans: method = t.get('payment_method', 'Unknown') if method: method_lower = method.lower() if method_lower == 'qris': qris_cash['QRIS'] += t['total'] elif method_lower == 'cash': qris_cash['CASH'] += t['total'] else: qris_cash[method] += t['total'] if qris_cash and sum(qris_cash.values()) > 0: colors_qris = ['#FF6B6B', '#4CAF50', '#2196F3', '#FFA726'] labels_pie = [f"{k}: Rp {int(v):,}" for k, v in qris_cash.items()] ax7.pie(qris_cash.values(), labels=labels_pie, autopct='%1.1f%%', colors=colors_qris[:len(qris_cash)], startangle=90) ax7.set_title(f'💳 Pendapatan Hari Ini ({today})', fontweight='bold', fontsize=10) else: ax7.text(0.5, 0.5, 'Tidak ada transaksi hari ini', ha='center', va='center', fontsize=10) ax7.set_title(f'💳 Pendapatan Hari Ini ({today})', fontweight='bold', fontsize=10) # Chart 8: Hari Ini - Jumlah Transaksi QRIS vs CASH (Bar Chart) ax8 = fig.add_subplot(3, 3, 8) qris_cash_count = defaultdict(int) for t in today_trans: method = t.get('payment_method', 'Unknown') if method: method_lower = method.lower() if method_lower == 'qris': qris_cash_count['QRIS'] += 1 elif method_lower == 'cash': qris_cash_count['CASH'] += 1 else: qris_cash_count[method] += 1 if qris_cash_count and len(today_trans) > 0: methods = list(qris_cash_count.keys()) counts_qris = list(qris_cash_count.values()) colors_bar = ['#FF6B6B', '#4CAF50', '#2196F3', '#FFA726'] ax8.bar(methods, counts_qris, color=colors_bar[:len(methods)], alpha=0.8) ax8.set_title('📊 Jumlah Transaksi Hari Ini', fontweight='bold', fontsize=10) ax8.set_ylabel('Jumlah Transaksi', fontsize=9) ax8.grid(True, alpha=0.3, axis='y') else: ax8.text(0.5, 0.5, 'Tidak ada transaksi hari ini', ha='center', va='center', fontsize=10) ax8.set_title('📊 Jumlah Transaksi Hari Ini', fontweight='bold', fontsize=10) # Chart 9: Hari Ini - Rata-rata per Metode (Bar Chart) ax9 = fig.add_subplot(3, 3, 9) qris_cash_avg = defaultdict(lambda: {'total': 0, 'count': 0}) for t in today_trans: method = t.get('payment_method', 'Unknown') if method: method_lower = method.lower() if method_lower == 'qris': qris_cash_avg['QRIS']['total'] += t['total'] qris_cash_avg['QRIS']['count'] += 1 elif method_lower == 'cash': qris_cash_avg['CASH']['total'] += t['total'] qris_cash_avg['CASH']['count'] += 1 else: qris_cash_avg[method]['total'] += t['total'] qris_cash_avg[method]['count'] += 1 if qris_cash_avg and len(today_trans) > 0: methods_avg = list(qris_cash_avg.keys()) avg_values = [qris_cash_avg[m]['total'] / qris_cash_avg[m]['count'] if qris_cash_avg[m]['count'] > 0 else 0 for m in methods_avg] colors_bar = ['#FF6B6B', '#4CAF50', '#2196F3', '#FFA726'] ax9.bar(methods_avg, avg_values, color=colors_bar[:len(methods_avg)], alpha=0.8) ax9.set_title('💵 Rata-rata per Metode Hari Ini', fontweight='bold', fontsize=10) ax9.set_ylabel('Nilai Rata-rata (Rp)', fontsize=9) ax9.grid(True, alpha=0.3, axis='y') else: ax9.text(0.5, 0.5, 'Tidak ada transaksi hari ini', ha='center', va='center', fontsize=10) ax9.set_title('💵 Rata-rata per Metode Hari Ini', fontweight='bold', fontsize=10) fig.tight_layout() canvas = FigureCanvasTkAgg(fig, master=parent) canvas.draw() canvas.get_tk_widget().pack(fill='both', expand=True) def build_detail_transaksi_tab(self, parent): for w in parent.winfo_children(): w.destroy() trans = self.get_filtered_transactions() info_frame = ttk.Frame(parent) info_frame.pack(fill='x', padx=10, pady=5) ttk.Label(info_frame, text=f"📋 Menampilkan {len(trans)} transaksi", font=("Arial", 10, "bold")).pack(side='left') tree_frame = ttk.Frame(parent) tree_frame.pack(fill='both', expand=True, padx=10, pady=5) tree_scroll_y = ttk.Scrollbar(tree_frame, orient='vertical') tree_scroll_y.pack(side='right', fill='y') tree_scroll_x = ttk.Scrollbar(tree_frame, orient='horizontal') tree_scroll_x.pack(side='bottom', fill='x') cols = ("ID", "Tanggal", "Waktu", "User", "Meja", "Items", "Subtotal", "Diskon", "Total", "Metode", "Promo") detail_tree = ttk.Treeview( tree_frame, columns=cols, show='headings', height=15, yscrollcommand=tree_scroll_y.set, xscrollcommand=tree_scroll_x.set ) tree_scroll_y.config(command=detail_tree.yview) tree_scroll_x.config(command=detail_tree.xview) for c in cols: detail_tree.heading(c, text=c) if c in ["ID", "Meja"]: detail_tree.column(c, width=50) elif c in ["Subtotal", "Diskon", "Total"]: detail_tree.column(c, width=100) elif c == "Items": detail_tree.column(c, width=200) else: detail_tree.column(c, width=100) detail_tree.pack(side='left', fill='both', expand=True) for t in reversed(trans): items_str = ", ".join([f"{it['nama']}({it['qty']})" for it in t['items']]) detail_tree.insert("", tk.END, values=( t['id'], t['tanggal'], t['waktu'].strftime('%H:%M'), t['user'], t.get('meja', '-'), items_str, format_currency(t['subtotal']), format_currency(t['diskon']), format_currency(t['total']), t.get('payment_method', '-'), t.get('promo_code', '-') )) def build_menu_analytics_tab(self, parent): for w in parent.winfo_children(): w.destroy() trans = self.get_filtered_transactions() # Calculate menu statistics menu_stats = defaultdict(lambda: {'qty': 0, 'revenue': 0}) for t in trans: for item in t['items']: menu_stats[item['nama']]['qty'] += item['qty'] menu_stats[item['nama']]['revenue'] += item['subtotal'] # Sort by quantity sorted_menu = sorted(menu_stats.items(), key=lambda x: x[1]['qty'], reverse=True) # Summary summary_frame = ttk.LabelFrame(parent, text="📊 Ringkasan Menu", padding=10) summary_frame.pack(fill='x', padx=10, pady=5) summary_inner = ttk.Frame(summary_frame) summary_inner.pack() total_menu_sold = sum(s['qty'] for s in menu_stats.values()) total_menu_revenue = sum(s['revenue'] for s in menu_stats.values()) ttk.Label(summary_inner, text="Total Menu Berbeda Terjual:").grid(row=0, column=0, sticky='w', padx=10, pady=3) ttk.Label(summary_inner, text=str(len(menu_stats)), font=("Arial", 10, "bold")).grid(row=0, column=1, sticky='w', padx=10, pady=3) ttk.Label(summary_inner, text="Total Item Terjual:").grid(row=1, column=0, sticky='w', padx=10, pady=3) ttk.Label(summary_inner, text=str(total_menu_sold), font=("Arial", 10, "bold")).grid(row=1, column=1, sticky='w', padx=10, pady=3) ttk.Label(summary_inner, text="Total Pendapatan dari Menu:").grid(row=2, column=0, sticky='w', padx=10, pady=3) ttk.Label(summary_inner, text=format_currency(total_menu_revenue), font=("Arial", 10, "bold"), foreground='green').grid(row=2, column=1, sticky='w', padx=10, pady=3) # Chart if MATPLOTLIB_AVAILABLE and sorted_menu: chart_frame = ttk.LabelFrame(parent, text="📊 Top 10 Menu", padding=10) chart_frame.pack(fill='both', expand=True, padx=10, pady=5) top_10 = sorted_menu[:10] names = [m[0] for m in top_10] qtys = [m[1]['qty'] for m in top_10] fig = Figure(figsize=(8, 5), dpi=80) ax = fig.add_subplot(111) bars = ax.bar(names, qtys, color='#4CAF50') ax.set_ylabel('Jumlah Terjual', fontsize=10) ax.set_title('Top 10 Menu Terlaris', fontsize=12, fontweight='bold') ax.grid(axis='y', alpha=0.3) ax.tick_params(axis='x', rotation=45) # Add value labels for bar in bars: height = bar.get_height() ax.text(bar.get_x() + bar.get_width()/2., height, f'{int(height)}', ha='center', va='bottom', fontsize=9) fig.tight_layout() canvas = FigureCanvasTkAgg(fig, chart_frame) canvas.draw() canvas.get_tk_widget().pack(fill='both', expand=True) # Detail table table_frame = ttk.LabelFrame(parent, text="📋 Detail Semua Menu", padding=10) table_frame.pack(fill='both', expand=True, padx=10, pady=5) tree_scroll = ttk.Scrollbar(table_frame, orient='vertical') tree_scroll.pack(side='right', fill='y') cols = ("Rank", "Nama Menu", "Qty Terjual", "Pendapatan", "% dari Total") menu_tree = ttk.Treeview( table_frame, columns=cols, show='headings', height=10, yscrollcommand=tree_scroll.set ) tree_scroll.config(command=menu_tree.yview) for c in cols: menu_tree.heading(c, text=c) if c == "Rank": menu_tree.column(c, width=50) elif c == "Nama Menu": menu_tree.column(c, width=200) else: menu_tree.column(c, width=120) menu_tree.pack(side='left', fill='both', expand=True) for rank, (nama, stats) in enumerate(sorted_menu, 1): pct = (stats['revenue'] / total_menu_revenue * 100) if total_menu_revenue > 0 else 0 menu_tree.insert("", tk.END, values=( rank, nama, stats['qty'], format_currency(stats['revenue']), f"{pct:.1f}%" )) def build_payment_analytics_tab(self, parent): for w in parent.winfo_children(): w.destroy() trans = self.get_filtered_transactions() # Calculate payment statistics payment_stats = defaultdict(lambda: {'count': 0, 'total': 0}) for t in trans: method = t.get('payment_method', 'Unknown') payment_stats[method]['count'] += 1 payment_stats[method]['total'] += t['total'] # Summary cards summary_frame = ttk.Frame(parent) summary_frame.pack(fill='x', padx=10, pady=10) for method, stats in sorted(payment_stats.items()): card = tk.Frame(summary_frame, bg='#2196F3', relief='solid', bd=1) card.pack(side='left', fill='both', expand=True, padx=5) icon_map = {'Cash': '💵', 'QRIS': '📱', 'GoPay': '💳', 'OVO': '💳', 'DANA': '💳', 'ShopeePay': '💳'} icon = icon_map.get(method, '💳') tk.Label(card, text=icon, font=("Arial", 28), bg='#2196F3', fg='white').pack(pady=(10, 5)) tk.Label(card, text=method, font=("Arial", 11, "bold"), bg='#2196F3', fg='white').pack() tk.Label(card, text=f"{stats['count']} transaksi", font=("Arial", 9), bg='#2196F3', fg='white').pack(pady=(2, 0)) tk.Label(card, text=format_currency(stats['total']), font=("Arial", 14, "bold"), bg='#2196F3', fg='white').pack(pady=(5, 10)) # Pie chart if MATPLOTLIB_AVAILABLE and payment_stats: chart_frame = ttk.LabelFrame(parent, text="📊 Distribusi Metode Pembayaran", padding=10) chart_frame.pack(fill='both', expand=True, padx=10, pady=5) methods = list(payment_stats.keys()) counts = [payment_stats[m]['count'] for m in methods] fig = Figure(figsize=(10, 5), dpi=80) # Pie chart for count ax1 = fig.add_subplot(121) colors = ['#4CAF50', '#2196F3', '#FF9800', '#9C27B0', '#F44336'] wedges, texts, autotexts = ax1.pie(counts, labels=methods, autopct='%1.1f%%', colors=colors[:len(methods)], startangle=90) for text in texts: text.set_fontsize(10) for autotext in autotexts: autotext.set_color('white') autotext.set_fontsize(9) autotext.set_fontweight('bold') ax1.set_title('Berdasarkan Jumlah Transaksi', fontsize=11, fontweight='bold') # Bar chart for revenue ax2 = fig.add_subplot(122) revenues = [payment_stats[m]['total'] for m in methods] bars = ax2.bar(methods, revenues, color=colors[:len(methods)]) ax2.set_ylabel('Pendapatan (Rp)', fontsize=10) ax2.set_title('Berdasarkan Pendapatan', fontsize=11, fontweight='bold') ax2.tick_params(axis='x', rotation=45) ax2.yaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(lambda x, p: f'{int(x/1000)}k')) ax2.grid(axis='y', alpha=0.3) # Add value labels on bars for bar in bars: height = bar.get_height() ax2.text(bar.get_x() + bar.get_width()/2., height, f'{int(height/1000)}k', ha='center', va='bottom', fontsize=9) fig.tight_layout() canvas = FigureCanvasTkAgg(fig, chart_frame) canvas.draw() canvas.get_tk_widget().pack(fill='both', expand=True) # Detail table table_frame = ttk.LabelFrame(parent, text="📋 Detail Pembayaran", padding=10) table_frame.pack(fill='x', padx=10, pady=5) cols = ("Metode", "Jumlah Trans", "Total Pendapatan", "Rata-rata", "% dari Total") payment_tree = ttk.Treeview( table_frame, columns=cols, show='headings', height=6 ) for c in cols: payment_tree.heading(c, text=c) payment_tree.column(c, width=150) payment_tree.pack(fill='x') total_revenue = sum(s['total'] for s in payment_stats.values()) for method, stats in sorted(payment_stats.items(), key=lambda x: x[1]['total'], reverse=True): avg = stats['total'] // stats['count'] if stats['count'] > 0 else 0 pct = (stats['total'] / total_revenue * 100) if total_revenue > 0 else 0 payment_tree.insert("", tk.END, values=( method, stats['count'], format_currency(stats['total']), format_currency(avg), f"{pct:.1f}%" )) def export_laporan_excel(self): try: trans = self.get_filtered_transactions() if not trans: messagebox.showwarning("Tidak Ada Data", "Tidak ada transaksi untuk periode ini") return filename = filedialog.asksaveasfilename( defaultextension=".csv", filetypes=[("CSV files", "*.csv"), ("All files", "*.*")], initialfile=f"laporan_{self.laporan_start_date.get()}_to_{self.laporan_end_date.get()}.csv" ) if filename: with open(filename, 'w', newline='', encoding='utf-8') as f: fieldnames = ["ID", "Tanggal", "Waktu", "User", "Meja", "Items", "Subtotal", "Diskon", "Total", "Metode", "Promo"] writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() for t in trans: items_str = "; ".join([f"{it['nama']} x{it['qty']}" for it in t['items']]) writer.writerow({ "ID": t['id'], "Tanggal": t['tanggal'], "Waktu": t['waktu'].strftime('%H:%M:%S'), "User": t['user'], "Meja": t.get('meja', '-'), "Items": items_str, "Subtotal": t['subtotal'], "Diskon": t['diskon'], "Total": t['total'], "Metode": t.get('payment_method', '-'), "Promo": t.get('promo_code', '-') }) messagebox.showinfo("Success", f"✅ Laporan berhasil di-export ke:\n{filename}") except Exception as e: messagebox.showerror("Export Error", f"Gagal export laporan:\n{str(e)}") # ================================ # NOTIFIKASI # ================================ def open_notifikasi_window(self): notif_win = tk.Toplevel(self.root) notif_win.title("🔔 Notifikasi") notif_win.geometry("600x500") header = ttk.Frame(notif_win) header.pack(fill='x', padx=10, pady=10) ttk.Label(header, text="🔔 Notifikasi", font=("Arial", 14, "bold")).pack(side='left') ttk.Button(header, text="✅ Tandai Semua Dibaca", command=lambda: self.mark_all_read(notif_win)).pack(side='right', padx=5) ttk.Button(header, text="🗑️ Hapus Semua", command=lambda: self.clear_all_notifications(notif_win)).pack(side='right', padx=5) list_frame = ttk.Frame(notif_win) list_frame.pack(fill='both', expand=True, padx=10, pady=5) canvas = tk.Canvas(list_frame, bg='white') scrollbar = ttk.Scrollbar(list_frame, orient='vertical', command=canvas.yview) notif_container = ttk.Frame(canvas) canvas.configure(yscrollcommand=scrollbar.set) scrollbar.pack(side='right', fill='y') canvas.pack(side='left', fill='both', expand=True) canvas.create_window((0, 0), window=notif_container, anchor='nw') notif_container.bind('', lambda e: canvas.configure(scrollregion=canvas.bbox('all'))) if not notifikasi: ttk.Label(notif_container, text="📭 Tidak ada notifikasi", font=("Arial", 12), foreground='gray').pack(pady=50) else: for n in reversed(notifikasi): self.create_notif_card(notif_container, n, notif_win) def create_notif_card(self, parent, notif, window): bg_color = "#F8F9FA" if notif['read'] else "#E3F2FD" card = tk.Frame(parent, bg=bg_color, relief='solid', bd=1) card.pack(fill='x', padx=5, pady=5) inner = tk.Frame(card, bg=bg_color) inner.pack(fill='x', padx=10, pady=8) icon_map = { 'success': '✅', 'info': 'ℹ️', 'warning': '⚠️', 'error': '❌' } icon = icon_map.get(notif['type'], '📌') tk.Label(inner, text=icon, font=("Arial", 16), bg=bg_color).pack(side='left', padx=(0, 10)) text_frame = tk.Frame(inner, bg=bg_color) text_frame.pack(side='left', fill='x', expand=True) tk.Label(text_frame, text=notif['message'], font=("Arial", 10), bg=bg_color, anchor='w', wraplength=450).pack(fill='x') tk.Label(text_frame, text=notif['timestamp'], font=("Arial", 8), bg=bg_color, fg='gray', anchor='w').pack(fill='x') if not notif['read']: tk.Button(inner, text="✓", font=("Arial", 10), bg='#4CAF50', fg='white', relief='flat', padx=10, pady=2, command=lambda: self.mark_as_read(notif['id'], window)).pack(side='right') def mark_as_read(self, notif_id, window): for n in notifikasi: if n['id'] == notif_id: n['read'] = True break save_notifikasi() self.open_notifikasi_window() window.destroy() self.build_main_ui() def mark_all_read(self, window): for n in notifikasi: n['read'] = True save_notifikasi() messagebox.showinfo("Success", "Semua notifikasi ditandai sudah dibaca") self.open_notifikasi_window() window.destroy() self.build_main_ui() def clear_all_notifications(self, window): confirm = messagebox.askyesno("Konfirmasi", "Hapus semua notifikasi?") if confirm: notifikasi.clear() save_notifikasi() messagebox.showinfo("Success", "Semua notifikasi dihapus") window.destroy() self.build_main_ui() # ================================ # MAIN # ================================ if __name__ == "__main__": root = tk.Tk() app = CafeApp(root) root.mainloop()