diff --git a/Main.py b/Main.py index 7aebd37..1571843 100644 --- a/Main.py +++ b/Main.py @@ -1,52 +1,549 @@ +""" +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 -# Try Pillow for image support; fallback to Tkinter PhotoImage +# Image support try: from PIL import Image, ImageTk PIL_AVAILABLE = True -except Exception: +except: PIL_AVAILABLE = False -# ------------------------- -# In-memory "database" -# ------------------------- -users = [ - {"username": "admin", "password": "123", "role": "admin"}, - {"username": "kasir", "password": "123", "role": "kasir"}, - {"username": "waiter", "password": "123", "role": "waiter"}, - {"username": "owner", "password": "123", "role": "owner"}, - {"username": "pembeli", "password": "123", "role": "pembeli"}, -] +# QRCode support +try: + import qrcode + from io import BytesIO + QRCODE_AVAILABLE = True +except: + QRCODE_AVAILABLE = False -menu = [ - {"id": 1, "nama": "Es Teh", "harga": 5000, "kategori": "Minuman", "stok": 10, "foto": None, "promo": None}, - {"id": 2, "nama": "Kopi Susu", "harga": 12000, "kategori": "Minuman", "stok": 8, "foto": None, "promo": "Diskon 10%"}, - {"id": 3, "nama": "Nasi Goreng", "harga": 15000, "kategori": "Makanan", "stok": 5, "foto": None, "promo": None}, -] +# 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 -bahan = { - "teh": 50, - "kopi": 20, - "susu": 15, - "beras": 10, - "bumbu": 10 -} +# ================================ +# KONFIGURASI +# ================================ -current_user = None -cart = [] -_image_refs = {} +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 +# ================================ -# ------------------------- -# Helpers -# ------------------------- 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 @@ -62,410 +559,4153 @@ def ensure_image(path, maxsize=(240, 160)): tkimg = tk.PhotoImage(file=path) _image_refs[key] = tkimg return tkimg - except Exception: + except: return None def reset_image_refs(): _image_refs.clear() -# ------------------------- -# Keranjang Functions -# ------------------------- -def _add_to_cart(self): - sel = self.tree.selection() - if not sel: - messagebox.showwarning("Pilih Menu", "Pilih menu dulu.") - return - mid = int(sel[0]) - it = find_menu_by_id(mid) - if not it: - return - try: - qty = int(simpledialog.askstring("Jumlah", f"Berapa {it['nama']} yang ingin ditambahkan?")) - except: - return - if qty <= 0: - messagebox.showerror("Error", "Jumlah harus > 0") - return - if qty > it.get("stok",0): - messagebox.showerror("Error", f"Stok tidak cukup ({it.get('stok',0)})") - return - for c in cart: - if c["menu_id"] == mid: - c["jumlah"] += qty - break - else: - cart.append({"menu_id": mid, "jumlah": qty}) - messagebox.showinfo("Keranjang", f"{it['nama']} x{qty} berhasil ditambahkan ke keranjang.") +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 _view_cart(self): - if not cart: - messagebox.showinfo("Keranjang", "Keranjang kosong.") - return - s = "" - total = 0 - for c in cart: - it = find_menu_by_id(c["menu_id"]) - harga = it["harga"] * c["jumlah"] - s += f"{it['nama']} x {c['jumlah']} = Rp{harga}\n" - total += harga - s += f"\nTotal: Rp{total}" - if messagebox.askyesno("Checkout", s + "\n\nCheckout sekarang?"): - for c in cart: - it = find_menu_by_id(c["menu_id"]) - it["stok"] -= c["jumlah"] - cart.clear() - self._refresh_menu() - messagebox.showinfo("Sukses", "Pesanan berhasil dibuat!") +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': '' + }) -# ------------------------- -# GUI App -# ------------------------- -class Anggota1App: +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("Cafe Ceria") - self.root.geometry("1000x650") - self.root.minsize(920, 600) - self._build_login() - - # ---------- login ---------- + 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 _build_login(self): + + def logout(self): + self.current_user = None + self.cart_items = [] + self.show_welcome_screen() + + # ================================ + # MAIN UI + # ================================ + + def build_main_ui(self): self._clear_root() - frame = ttk.Frame(self.root, padding=20) - frame.pack(expand=True) - ttk.Label(frame, text="LOGIN", font=("Segoe UI", 18)).grid(row=0, column=0, columnspan=2, pady=(0,10)) - ttk.Label(frame, text="Username:").grid(row=1, column=0, sticky="e", padx=5, pady=5) - self.e_user = ttk.Entry(frame) - self.e_user.grid(row=1, column=1, padx=5, pady=5) - ttk.Label(frame, text="Password:").grid(row=2, column=0, sticky="e", padx=5, pady=5) - self.e_pass = ttk.Entry(frame, show="*") - self.e_pass.grid(row=2, column=1, padx=5, pady=5) - btn = ttk.Button(frame, text="Login", command=self._handle_login) - btn.grid(row=3, column=0, columnspan=2, pady=12) - ttk.Label(frame, text="(users: admin/kasir/waiter/owner/pembeli ; pass: 123)").grid(row=4, column=0, columnspan=2, pady=(8,0)) - - def _handle_login(self): - global current_user - u = self.e_user.get().strip() - p = self.e_pass.get().strip() - for usr in users: - if usr["username"] == u and usr["password"] == p: - current_user = usr - messagebox.showinfo("Login", f"Berhasil login sebagai {u} ({usr['role']})") - self._build_main_ui() - return - messagebox.showerror("Login Gagal", "Username atau password salah.") - - # ---------- main UI ---------- - def _build_main_ui(self): - self._clear_root() - - topbar = ttk.Frame(self.root) - topbar.pack(fill="x", padx=6, pady=6) - ttk.Label(topbar, text=f"User: {current_user['username']} ({current_user['role']})", font=("Segoe UI", 10)).pack(side="left") - ttk.Button(topbar, text="Logout", command=self._logout).pack(side="right") - - main = ttk.Frame(self.root) - main.pack(fill="both", expand=True, padx=8, pady=4) - - ctrl = ttk.Frame(main, width=320, padding=8) - ctrl.pack(side="left", fill="y") - ttk.Label(ctrl, text="Search / Cari:").pack(anchor="w") - self.search_var = tk.StringVar() - sframe = ttk.Frame(ctrl) - sframe.pack(fill="x", pady=4) - self.search_entry = ttk.Entry(sframe, textvariable=self.search_var) - self.search_entry.pack(side="left", fill="x", expand=True) - ttk.Button(sframe, text="Search", command=self._search_menu).pack(side="left", padx=4) - ttk.Button(sframe, text="Refresh", command=self._refresh_menu).pack(side="left") - ttk.Label(ctrl, text="Filter Kategori:").pack(anchor="w", pady=(10,0)) - self.filter_var = tk.StringVar() - values = ["All"] + sorted({it.get("kategori","Undefined") for it in menu}) - self.filter_cb = ttk.Combobox(ctrl, values=values, state="readonly", textvariable=self.filter_var) - self.filter_cb.set("All") - self.filter_cb.pack(fill="x", pady=4) - ttk.Button(ctrl, text="Apply Filter", command=self._apply_filter).pack(fill="x", pady=(0,6)) - ttk.Label(ctrl, text="Sort:").pack(anchor="w", pady=(6,0)) - self.sort_var = tk.StringVar() - self.sort_cb = ttk.Combobox(ctrl, values=["Default","Harga - Rendah→Tinggi","Harga - Tinggi→Rendah","Stok - Rendah→Tinggi","Stok - Tinggi→Rendah"], state="readonly", textvariable=self.sort_var) - self.sort_cb.set("Default") - self.sort_cb.pack(fill="x", pady=4) - ttk.Button(ctrl, text="Apply Sort", command=self._apply_sort).pack(fill="x", pady=(0,6)) - - ttk.Separator(ctrl).pack(fill="x", pady=8) - self.btn_add = ttk.Button(ctrl, text="Tambah Menu (+Foto)", command=self._gui_add_menu) - self.btn_edit = ttk.Button(ctrl, text="Edit Menu (+Ganti Foto)", command=self._gui_edit_menu) - self.btn_delete = ttk.Button(ctrl, text="Hapus Menu", command=self._gui_delete_menu) - self.btn_refresh = ttk.Button(ctrl, text="Refresh List", command=self._refresh_menu) - self.btn_add.pack(fill="x", pady=4) - self.btn_edit.pack(fill="x", pady=4) - self.btn_delete.pack(fill="x", pady=4) - self.btn_refresh.pack(fill="x", pady=6) - if current_user["role"] != "admin": - self.btn_add.state(["disabled"]) - self.btn_edit.state(["disabled"]) - self.btn_delete.state(["disabled"]) - - ttk.Label(ctrl, text="Stok Bahan:", font=("Segoe UI", 10, "bold")).pack(anchor="w", pady=(10,0)) - self.bahan_listbox = tk.Listbox(ctrl, height=8) - self.bahan_listbox.pack(fill="both", expand=False, pady=(4,6)) - self._refresh_bahan_listbox() - self.btn_edit_bahan = ttk.Button(ctrl, text="Tambah/Edit Stok Bahan", command=self._gui_edit_bahan) - self.btn_edit_bahan.pack(fill="x") - if current_user["role"] != "admin": - self.btn_edit_bahan.state(["disabled"]) - - right = ttk.Frame(main, padding=8) - right.pack(side="right", fill="both", expand=True) - ttk.Label(right, text="Daftar Menu", font=("Segoe UI", 12)).pack(anchor="w") - self.tree = ttk.Treeview(right, columns=("harga","kategori","stok"), show="headings", selectmode="browse") - self.tree.heading("harga", text="Harga") - self.tree.heading("kategori", text="Kategori") - self.tree.heading("stok", text="Stok") - self.tree.column("harga", width=120, anchor="center") - self.tree.column("kategori", width=120, anchor="center") - self.tree.column("stok", width=80, anchor="center") - self.tree.pack(fill="both", expand=True, pady=(6,8)) - self.tree.bind("<>", self._on_tree_select) - - detail_frame = ttk.Frame(right) - detail_frame.pack(fill="x", pady=4) - self.img_label = ttk.Label(detail_frame) - self.img_label.pack(side="left", padx=6) - info_frame = ttk.Frame(detail_frame) - info_frame.pack(side="left", fill="both", expand=True, padx=6) - self.detail_text = tk.Text(info_frame, height=8, state="disabled") - self.detail_text.pack(fill="both", expand=True) - - # --- Tambahkan tombol keranjang untuk pembeli --- - if current_user["role"] == "pembeli": - ttk.Button(right, text="Tambah ke Keranjang", command=lambda: _add_to_cart(self)).pack(fill="x", pady=4) - ttk.Button(right, text="Lihat Keranjang / Checkout", command=lambda: _view_cart(self)).pack(fill="x", pady=4) - - self._populate_tree(menu) - - # ---------- Data ops ---------- - def _populate_tree(self, items): - for r in self.tree.get_children(): - self.tree.delete(r) - for it in items: - self.tree.insert("", "end", iid=str(it["id"]), values=(f"Rp{it['harga']}", it.get("kategori","-"), it.get("stok",0)), text=it["nama"]) - self._clear_details() - - def _refresh_menu(self): - self.search_var.set("") - self.filter_cb['values'] = ["All"] + sorted({it.get("kategori","Undefined") for it in menu}) - self.filter_cb.set("All") - self.sort_cb.set("Default") - self._populate_tree(menu) - reset_image_refs() - self._refresh_bahan_listbox() - - def _apply_filter(self): - cat = self.filter_var.get() if self.filter_var.get() else "All" - if cat == "All": - filtered = list(menu) - else: - filtered = [it for it in menu if it.get("kategori","") == cat] - self._populate_tree(filtered) - - def _search_menu(self): - q = self.search_var.get().strip().lower() - if not q: - self._populate_tree(menu) - return - res = [] - for it in menu: - if q in it["nama"].lower() or q in str(it["harga"]) or q in it.get("kategori","").lower(): - res.append(it) - self._populate_tree(res) - - def _apply_sort(self): - key = self.sort_var.get() - arr = list(menu) - if key == "Harga - Rendah→Tinggi": - arr.sort(key=lambda x: x["harga"]) - elif key == "Harga - Tinggi→Rendah": - arr.sort(key=lambda x: -x["harga"]) - elif key == "Stok - Rendah→Tinggi": - arr.sort(key=lambda x: x.get("stok",0)) - elif key == "Stok - Tinggi→Rendah": - arr.sort(key=lambda x: -x.get("stok",0)) - self._populate_tree(arr) - - def _clear_details(self): - self.detail_text.config(state="normal") - self.detail_text.delete("1.0", tk.END) - self.detail_text.config(state="disabled") - self.img_label.config(image="", text="(No Image)") - self.img_label.image = None - - def _on_tree_select(self, evt): - sel = self.tree.selection() - if not sel: - return - mid = int(sel[0]) - it = find_menu_by_id(mid) - if not it: return - self.detail_text.config(state="normal") - self.detail_text.delete("1.0", tk.END) - s = f"ID: {it['id']}\nNama: {it['nama']}\nHarga: Rp{it['harga']}\nKategori: {it.get('kategori','-')}\nStok: {it.get('stok',0)}\nFoto: {os.path.basename(it['foto']) if it.get('foto') else 'Tidak ada'}\n" - self.detail_text.insert(tk.END, s) - self.detail_text.config(state="disabled") - img = ensure_image(it.get('foto')) - if img: - self.img_label.config(image=img, text="") - self.img_label.image = img - else: - self.img_label.config(image="", text="(No Image)") - self.img_label.image = None - - # ---------- CRUD Menu ---------- - def _gui_add_menu(self): - if current_user["role"] != "admin": - messagebox.showerror("Akses", "Hanya admin bisa menambah menu.") - return - dialog = AddEditDialog(self.root, title="Tambah Menu") - self.root.wait_window(dialog.top) - if dialog.result is None: - return - nama, harga, stok, kategori, foto_path = dialog.result - try: - harga = int(harga); stok = int(stok) - except: - messagebox.showerror("Error", "Harga & Stok harus angka.") - return - new_id = menu[-1]["id"] + 1 if menu else 1 - menu.append({"id": new_id, "nama": nama, "harga": harga, "kategori": kategori, "stok": stok, "foto": foto_path}) - messagebox.showinfo("Sukses", "Menu ditambahkan.") - self._refresh_menu() - - def _gui_edit_menu(self): - if current_user["role"] != "admin": - messagebox.showerror("Akses", "Hanya admin bisa edit menu.") - return - sel = self.tree.selection() - if not sel: - messagebox.showwarning("Pilih", "Pilih menu untuk diedit.") - return - mid = int(sel[0]) - it = find_menu_by_id(mid) - if not it: - messagebox.showerror("Error", "Menu tidak ditemukan.") - return - dialog = AddEditDialog(self.root, title="Edit Menu", existing=it) - self.root.wait_window(dialog.top) - if dialog.result is None: - return - nama, harga, stok, kategori, foto_path = dialog.result - try: - harga = int(harga); stok = int(stok) - except: - messagebox.showerror("Error", "Harga & Stok harus angka.") - return - it.update({"nama": nama, "harga": harga, "kategori": kategori, "stok": stok, "foto": foto_path}) - messagebox.showinfo("Sukses", "Menu diperbarui.") - self._refresh_menu() - - def _gui_delete_menu(self): - if current_user["role"] != "admin": - messagebox.showerror("Akses", "Hanya admin bisa hapus menu.") - return - sel = self.tree.selection() - if not sel: - messagebox.showwarning("Pilih", "Pilih menu untuk dihapus.") - return - mid = int(sel[0]) - it = find_menu_by_id(mid) - if not it: - messagebox.showerror("Error", "Menu tidak ditemukan.") - return - if messagebox.askyesno("Hapus", f"Yakin ingin hapus {it['nama']}?"): - menu.remove(it) - self._refresh_menu() - - # ---------- Bahan ---------- - def _refresh_bahan_listbox(self): - self.bahan_listbox.delete(0, tk.END) - for k,v in bahan.items(): - self.bahan_listbox.insert(tk.END, f"{k}: {v}") - - def _gui_edit_bahan(self): - if current_user["role"] != "admin": - messagebox.showerror("Akses", "Hanya admin bisa edit stok bahan.") - return - k = simpledialog.askstring("Bahan", "Nama bahan:") - if not k: - return - try: - v = int(simpledialog.askstring("Stok", "Jumlah:")) - except: - return - bahan[k] = v - self._refresh_bahan_listbox() - - def _logout(self): - global current_user - current_user = None - self._build_login() - -# ---------- Dialog ---------- -class AddEditDialog: - def __init__(self, parent, title="Tambah/Edit", existing=None): - self.top = tk.Toplevel(parent) - self.top.title(title) - self.result = None - self.existing = existing - ttk.Label(self.top, text="Nama:").grid(row=0, column=0, sticky="e", padx=5, pady=4) - self.e_nama = ttk.Entry(self.top) - self.e_nama.grid(row=0, column=1, padx=5, pady=4) - ttk.Label(self.top, text="Harga:").grid(row=1, column=0, sticky="e", padx=5, pady=4) - self.e_harga = ttk.Entry(self.top) - self.e_harga.grid(row=1, column=1, padx=5, pady=4) - ttk.Label(self.top, text="Stok:").grid(row=2, column=0, sticky="e", padx=5, pady=4) - self.e_stok = ttk.Entry(self.top) - self.e_stok.grid(row=2, column=1, padx=5, pady=4) - ttk.Label(self.top, text="Kategori:").grid(row=3, column=0, sticky="e", padx=5, pady=4) - self.e_kategori = ttk.Entry(self.top) - self.e_kategori.grid(row=3, column=1, padx=5, pady=4) - ttk.Label(self.top, text="Foto:").grid(row=4, column=0, sticky="e", padx=5, pady=4) - self.foto_path = tk.StringVar() - self.e_foto = ttk.Entry(self.top, textvariable=self.foto_path) - self.e_foto.grid(row=4, column=1, padx=5, pady=4) - ttk.Button(self.top, text="Browse", command=self._browse_file).grid(row=4, column=2, padx=4, pady=4) - ttk.Button(self.top, text="OK", command=self._ok).grid(row=5, column=0, pady=8) - ttk.Button(self.top, text="Cancel", command=self.top.destroy).grid(row=5, column=1) - if existing: - self.e_nama.insert(0, existing["nama"]) - self.e_harga.insert(0, str(existing["harga"])) - self.e_stok.insert(0, str(existing.get("stok",0))) - self.e_kategori.insert(0, existing.get("kategori","")) - self.foto_path.set(existing.get("foto","")) - - def _browse_file(self): - path = filedialog.askopenfilename(filetypes=[("Image files","*.png *.jpg *.jpeg *.gif")]) - if path: - self.foto_path.set(path) - - def _ok(self): - self.result = ( - self.e_nama.get(), - self.e_harga.get(), - self.e_stok.get(), - self.e_kategori.get(), - self.foto_path.get() + 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")) ) - self.top.destroy() + + 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 +# ================================ -# ------------------------- -# Main -# ------------------------- if __name__ == "__main__": root = tk.Tk() - app = Anggota1App(root) - root.mainloop() + app = CafeApp(root) + root.mainloop() \ No newline at end of file diff --git a/data/bahan.csv b/data/bahan.csv new file mode 100644 index 0000000..e02ca3c --- /dev/null +++ b/data/bahan.csv @@ -0,0 +1,8 @@ +nama,jumlah +kopi,50 +susu,30 +teh,40 +nasi,20 +mie,15 +telur,25 +bumbu,30 diff --git a/data/detail_transaksi.csv b/data/detail_transaksi.csv new file mode 100644 index 0000000..82c96eb --- /dev/null +++ b/data/detail_transaksi.csv @@ -0,0 +1 @@ +id,transaksi_id,menu_id,nama_menu,jumlah,harga_satuan,subtotal,diskon diff --git a/data/favorites.csv b/data/favorites.csv new file mode 100644 index 0000000..fbd9e29 --- /dev/null +++ b/data/favorites.csv @@ -0,0 +1 @@ +username,menu_ids diff --git a/data/meja.csv b/data/meja.csv new file mode 100644 index 0000000..95e0389 --- /dev/null +++ b/data/meja.csv @@ -0,0 +1,11 @@ +nomor,status +1,Kosong +2,Kosong +3,Kosong +4,Kosong +5,Kosong +6,Kosong +7,Kosong +8,Kosong +9,Kosong +10,Kosong diff --git a/data/menu.csv b/data/menu.csv new file mode 100644 index 0000000..1322aee --- /dev/null +++ b/data/menu.csv @@ -0,0 +1,11 @@ +id,nama,harga,kategori,stok,foto,promo,item_discount_pct +1,Americano,20000,Minuman,50,,,0 +2,Latte,25000,Minuman,45,,Diskon 10%,10 +3,Nasi Goreng,35000,Makanan,30,,,0 +4,Mie Goreng,30000,Makanan,35,,,0 +5,Es Teh,5000,Minuman,100,,,0 +6,Kopi Susu,15000,Minuman,60,,Diskon 5%,5 +7,Cappuccino,28000,Minuman,40,,,0 +8,Roti Bakar,15000,Makanan,25,,Diskon 15%,15 +9,Pasta Carbonara,45000,Makanan,20,,,0 +10,Smoothie Buah,22000,Minuman,35,,,0 diff --git a/data/notifikasi.csv b/data/notifikasi.csv new file mode 100644 index 0000000..c734ca4 --- /dev/null +++ b/data/notifikasi.csv @@ -0,0 +1 @@ +id,timestamp,type,message,read diff --git a/data/pembayaran.csv b/data/pembayaran.csv new file mode 100644 index 0000000..7e9eca3 --- /dev/null +++ b/data/pembayaran.csv @@ -0,0 +1 @@ +id,transaksi_id,metode,jumlah,status,tanggal diff --git a/data/promo_codes.csv b/data/promo_codes.csv new file mode 100644 index 0000000..8128eeb --- /dev/null +++ b/data/promo_codes.csv @@ -0,0 +1,5 @@ +code,discount_percent +CAFE10,10 +CAFE20,20 +WELCOME,15 +SPECIAL25,25 diff --git a/data/resep.csv b/data/resep.csv new file mode 100644 index 0000000..f250a52 --- /dev/null +++ b/data/resep.csv @@ -0,0 +1,13 @@ +menu_id,bahan,jumlah +1,kopi,1 +2,kopi,1 +2,susu,1 +3,nasi,1 +3,telur,1 +3,bumbu,1 +4,mie,1 +4,telur,1 +4,bumbu,1 +5,teh,1 +6,kopi,1 +6,susu,1 diff --git a/data/transaksi.csv b/data/transaksi.csv new file mode 100644 index 0000000..8e6245c --- /dev/null +++ b/data/transaksi.csv @@ -0,0 +1 @@ +id,tanggal,waktu,user,items,subtotal,diskon,total,meja,status,payment_status,payment_method,paid_amount,change,promo_code diff --git a/data/users.csv b/data/users.csv new file mode 100644 index 0000000..c36286d --- /dev/null +++ b/data/users.csv @@ -0,0 +1,6 @@ +id,username,password,role +1,admin,admin123,admin +2,kasir,kasir123,kasir +3,waiter,waiter123,waiter +4,pembeli,user123,pembeli +5,owner,owner123,owner diff --git a/images/WhatsApp Image 2025-12-14 at 22.08.43 (1)_20251214_221702.jpeg b/images/WhatsApp Image 2025-12-14 at 22.08.43 (1)_20251214_221702.jpeg new file mode 100644 index 0000000..35687cc Binary files /dev/null and b/images/WhatsApp Image 2025-12-14 at 22.08.43 (1)_20251214_221702.jpeg differ diff --git a/images/WhatsApp Image 2025-12-14 at 22.08.43_20251214_221654.jpeg b/images/WhatsApp Image 2025-12-14 at 22.08.43_20251214_221654.jpeg new file mode 100644 index 0000000..d4380f0 Binary files /dev/null and b/images/WhatsApp Image 2025-12-14 at 22.08.43_20251214_221654.jpeg differ diff --git a/images/cappucino.jpeg b/images/cappucino.jpeg new file mode 100644 index 0000000..4046123 Binary files /dev/null and b/images/cappucino.jpeg differ diff --git a/images/es teh.jpeg b/images/es teh.jpeg new file mode 100644 index 0000000..c693e6f Binary files /dev/null and b/images/es teh.jpeg differ diff --git a/images/kopi susu.jpeg b/images/kopi susu.jpeg new file mode 100644 index 0000000..fa84009 Binary files /dev/null and b/images/kopi susu.jpeg differ diff --git a/images/mie goreng.jpeg b/images/mie goreng.jpeg new file mode 100644 index 0000000..43b97d8 Binary files /dev/null and b/images/mie goreng.jpeg differ diff --git a/images/nasi goreng.jpeg b/images/nasi goreng.jpeg new file mode 100644 index 0000000..1dd5c7f Binary files /dev/null and b/images/nasi goreng.jpeg differ diff --git a/images/pasta.jpeg b/images/pasta.jpeg new file mode 100644 index 0000000..97b6478 Binary files /dev/null and b/images/pasta.jpeg differ diff --git a/images/roti bakar.jpeg b/images/roti bakar.jpeg new file mode 100644 index 0000000..9b1e746 Binary files /dev/null and b/images/roti bakar.jpeg differ diff --git a/images/smothie buah.jpeg b/images/smothie buah.jpeg new file mode 100644 index 0000000..96f92bc Binary files /dev/null and b/images/smothie buah.jpeg differ