Project.Python/Main.py
2025-12-16 21:28:52 +07:00

4711 lines
190 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
SISTEM MANAGEMENT CAFE - ULTIMATE EDITION (FIXED & COMPLETE)
==============================================================
✅ SEMUA FITUR SUDAH DIIMPLEMENTASI & TERINTEGRASI
✅ SEMUA BUG SUDAH DIPERBAIKI
✅ PEMBELI SUDAH BISA MEMESAN
✅ KASIR SUDAH BISA TERIMA PEMBAYARAN (Cash/QRIS/E-Wallet)
""Akun Default:
- admin / admin123
- kasir / kasir123
- waiter / waiter123
- pembeli / user123
- owner / owner123
"""
import os
import csv
import shutil
import datetime
import tkinter as tk
from tkinter import ttk, messagebox, simpledialog, filedialog
from collections import defaultdict
import hashlib
# Image support
try:
from PIL import Image, ImageTk
PIL_AVAILABLE = True
except:
PIL_AVAILABLE = False
# QRCode support
try:
import qrcode
from io import BytesIO
QRCODE_AVAILABLE = True
except:
QRCODE_AVAILABLE = False
# Matplotlib support
try:
import matplotlib
matplotlib.use('TkAgg')
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
MATPLOTLIB_AVAILABLE = True
except:
MATPLOTLIB_AVAILABLE = False
# ================================
# KONFIGURASI
# ================================
DATA_DIR = "data"
IMAGES_DIR = "images"
CSV_USERS = os.path.join(DATA_DIR, "users.csv")
CSV_MENU = os.path.join(DATA_DIR, "menu.csv")
CSV_BAHAN = os.path.join(DATA_DIR, "bahan.csv")
CSV_RESEP = os.path.join(DATA_DIR, "resep.csv")
CSV_TRANSAKSI = os.path.join(DATA_DIR, "transaksi.csv")
CSV_DETAIL_TRANSAKSI = os.path.join(DATA_DIR, "detail_transaksi.csv")
CSV_MEJA = os.path.join(DATA_DIR, "meja.csv")
CSV_FAVORITES = os.path.join(DATA_DIR, "favorites.csv")
CSV_NOTIFIKASI = os.path.join(DATA_DIR, "notifikasi.csv")
CSV_PROMO_CODES = os.path.join(DATA_DIR, "promo_codes.csv")
CSV_PEMBAYARAN = os.path.join(DATA_DIR, "pembayaran.csv")
os.makedirs(DATA_DIR, exist_ok=True)
os.makedirs(IMAGES_DIR, exist_ok=True)
# ================================
# GLOBAL DATABASE
# ================================
users = []
menu = []
bahan = {}
resep = {}
transaksi = []
detail_transaksi = []
data_meja = {}
favorites = {}
notifikasi = []
promo_codes = {}
pembayaran = []
_image_refs = {}
# ================================
# CSV UTILITY
# ================================
def ensure_file(path, fieldnames):
if not os.path.exists(path):
with open(path, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
def read_csv(path):
if not os.path.exists(path):
return []
try:
with open(path, newline="", encoding="utf-8") as f:
return list(csv.DictReader(f))
except:
return []
def write_csv(path, fieldnames, rows):
try:
with open(path, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
except Exception as e:
print(f"Error writing {path}: {e}")
# ================================
# INIT DATA
# ================================
def init_default_data():
global users, menu, bahan, resep, data_meja, promo_codes
if not users:
users.extend([
{"id": "1", "username": "admin", "password": "admin123", "role": "admin"},
{"id": "2", "username": "kasir", "password": "kasir123", "role": "kasir"},
{"id": "3", "username": "waiter", "password": "waiter123", "role": "waiter"},
{"id": "4", "username": "pembeli", "password": "user123", "role": "pembeli"},
{"id": "5", "username": "owner", "password": "owner123", "role": "owner"},
])
if not menu:
menu.extend([
{"id": 1, "nama": "Americano", "harga": 20000, "kategori": "Minuman", "stok": 50, "foto": "", "promo": "", "item_discount_pct": 0},
{"id": 2, "nama": "Latte", "harga": 25000, "kategori": "Minuman", "stok": 45, "foto": "", "promo": "Diskon 10%", "item_discount_pct": 10},
{"id": 3, "nama": "Nasi Goreng", "harga": 35000, "kategori": "Makanan", "stok": 30, "foto": "", "promo": "", "item_discount_pct": 0},
{"id": 4, "nama": "Mie Goreng", "harga": 30000, "kategori": "Makanan", "stok": 35, "foto": "", "promo": "", "item_discount_pct": 0},
{"id": 5, "nama": "Es Teh", "harga": 5000, "kategori": "Minuman", "stok": 100, "foto": "", "promo": "", "item_discount_pct": 0},
{"id": 6, "nama": "Kopi Susu", "harga": 15000, "kategori": "Minuman", "stok": 60, "foto": "", "promo": "Diskon 5%", "item_discount_pct": 5},
{"id": 7, "nama": "Cappuccino", "harga": 28000, "kategori": "Minuman", "stok": 40, "foto": "", "promo": "", "item_discount_pct": 0},
{"id": 8, "nama": "Roti Bakar", "harga": 15000, "kategori": "Makanan", "stok": 25, "foto": "", "promo": "Diskon 15%", "item_discount_pct": 15},
{"id": 9, "nama": "Pasta Carbonara", "harga": 45000, "kategori": "Makanan", "stok": 20, "foto": "", "promo": "", "item_discount_pct": 0},
{"id": 10, "nama": "Smoothie Buah", "harga": 22000, "kategori": "Minuman", "stok": 35, "foto": "", "promo": "", "item_discount_pct": 0},
])
if not bahan:
bahan.update({
"kopi": 50, "susu": 30, "teh": 40, "nasi": 20,
"mie": 15, "telur": 25, "bumbu": 30
})
if not resep:
resep.update({
1: {"kopi": 1},
2: {"kopi": 1, "susu": 1},
3: {"nasi": 1, "telur": 1, "bumbu": 1},
4: {"mie": 1, "telur": 1, "bumbu": 1},
5: {"teh": 1},
6: {"kopi": 1, "susu": 1},
})
if not data_meja:
data_meja.update({i: "Kosong" for i in range(1, 11)})
if not promo_codes:
promo_codes.update({
"CAFE10": 10,
"CAFE20": 20,
"WELCOME": 15,
"SPECIAL25": 25
})
# ================================
# LOAD/SAVE FUNCTIONS
# ================================
def save_users():
write_csv(CSV_USERS, ["id", "username", "password", "role"], users)
def load_users():
global users
users = read_csv(CSV_USERS)
if not users:
init_default_data()
save_users()
def save_menu():
rows = []
for m in menu:
rows.append({
"id": str(m["id"]),
"nama": m["nama"],
"harga": str(m["harga"]),
"kategori": m["kategori"],
"stok": str(m["stok"]),
"foto": m.get("foto", ""),
"promo": m.get("promo", ""),
"item_discount_pct": str(m.get("item_discount_pct", 0))
})
write_csv(CSV_MENU, ["id", "nama", "harga", "kategori", "stok", "foto", "promo", "item_discount_pct"], rows)
def load_menu():
global menu
rows = read_csv(CSV_MENU)
menu = []
for r in rows:
try:
menu.append({
"id": int(r["id"]),
"nama": r["nama"],
"harga": int(float(r["harga"])),
"kategori": r["kategori"],
"stok": int(float(r["stok"])),
"foto": r.get("foto", ""),
"promo": r.get("promo", ""),
"item_discount_pct": float(r.get("item_discount_pct", 0))
})
except:
pass
if not menu:
init_default_data()
save_menu()
def save_bahan():
rows = [{"nama": k, "jumlah": str(v)} for k, v in bahan.items()]
write_csv(CSV_BAHAN, ["nama", "jumlah"], rows)
def load_bahan():
global bahan
rows = read_csv(CSV_BAHAN)
bahan = {}
for r in rows:
try:
bahan[r["nama"]] = int(float(r["jumlah"]))
except:
pass
if not bahan:
init_default_data()
save_bahan()
def save_resep():
rows = []
for menu_id, ingredients in resep.items():
for bahan_nama, qty in ingredients.items():
rows.append({"menu_id": str(menu_id), "bahan": bahan_nama, "jumlah": str(qty)})
write_csv(CSV_RESEP, ["menu_id", "bahan", "jumlah"], rows)
def load_resep():
global resep
rows = read_csv(CSV_RESEP)
resep = {}
for r in rows:
try:
mid = int(r["menu_id"])
if mid not in resep:
resep[mid] = {}
resep[mid][r["bahan"]] = int(float(r["jumlah"]))
except:
pass
if not resep:
init_default_data()
save_resep()
def save_transaksi():
rows = []
for t in transaksi:
items_str = ";".join([f"{it['nama']}x{it['qty']}@{it['harga_satuan']}" for it in t['items']])
rows.append({
"id": str(t['id']),
"tanggal": t['tanggal'],
"waktu": t['waktu'].strftime('%Y-%m-%d %H:%M:%S'),
"user": t['user'],
"items": items_str,
"subtotal": str(t['subtotal']),
"diskon": str(t['diskon']),
"total": str(t['total']),
"meja": str(t.get('meja', '')),
"status": t['status'],
"payment_status": t.get('payment_status', 'Pending'),
"payment_method": t.get('payment_method', ''),
"paid_amount": str(t.get('paid_amount', 0)),
"change": str(t.get('change', 0)),
"promo_code": t.get('promo_code', '')
})
write_csv(CSV_TRANSAKSI, ["id", "tanggal", "waktu", "user", "items", "subtotal", "diskon", "total", "meja", "status", "payment_status", "payment_method", "paid_amount", "change", "promo_code"], rows)
def load_transaksi():
global transaksi
rows = read_csv(CSV_TRANSAKSI)
transaksi = []
for r in rows:
try:
items = []
if r['items']:
for item_str in r['items'].split(';'):
parts = item_str.split('x')
nama = parts[0]
qty_price = parts[1].split('@')
items.append({
'nama': nama,
'qty': int(qty_price[0]),
'harga_satuan': int(qty_price[1]),
'subtotal': int(qty_price[0]) * int(qty_price[1])
})
transaksi.append({
'id': int(r['id']),
'tanggal': r['tanggal'],
'waktu': datetime.datetime.strptime(r['waktu'], '%Y-%m-%d %H:%M:%S'),
'user': r['user'],
'items': items,
'subtotal': int(float(r['subtotal'])),
'diskon': int(float(r['diskon'])),
'total': int(float(r['total'])),
'meja': int(r['meja']) if r['meja'] else None,
'status': r['status'],
'payment_status': r.get('payment_status', 'Pending'),
'payment_method': r.get('payment_method', ''),
'paid_amount': int(float(r.get('paid_amount', 0))),
'change': int(float(r.get('change', 0))),
'promo_code': r.get('promo_code', '')
})
except:
pass
def save_detail_transaksi():
rows = []
for d in detail_transaksi:
rows.append({
"id": str(d['id']),
"transaksi_id": str(d['transaksi_id']),
"menu_id": str(d['menu_id']),
"nama_menu": d['nama_menu'],
"jumlah": str(d['jumlah']),
"harga_satuan": str(d['harga_satuan']),
"subtotal": str(d['subtotal']),
"diskon": str(d['diskon'])
})
write_csv(CSV_DETAIL_TRANSAKSI, ["id", "transaksi_id", "menu_id", "nama_menu", "jumlah", "harga_satuan", "subtotal", "diskon"], rows)
def load_detail_transaksi():
global detail_transaksi
rows = read_csv(CSV_DETAIL_TRANSAKSI)
detail_transaksi = []
for r in rows:
try:
detail_transaksi.append({
'id': int(r['id']),
'transaksi_id': int(r['transaksi_id']),
'menu_id': int(r['menu_id']),
'nama_menu': r['nama_menu'],
'jumlah': int(r['jumlah']),
'harga_satuan': int(float(r['harga_satuan'])),
'subtotal': int(float(r['subtotal'])),
'diskon': int(float(r['diskon']))
})
except:
pass
def save_meja():
rows = [{"nomor": str(k), "status": v} for k, v in data_meja.items()]
write_csv(CSV_MEJA, ["nomor", "status"], rows)
def load_meja():
global data_meja
rows = read_csv(CSV_MEJA)
data_meja = {}
for r in rows:
try:
data_meja[int(r['nomor'])] = r['status']
except:
pass
if not data_meja:
init_default_data()
save_meja()
def save_favorites():
rows = []
for username, menu_ids in favorites.items():
rows.append({"username": username, "menu_ids": ",".join(map(str, menu_ids))})
write_csv(CSV_FAVORITES, ["username", "menu_ids"], rows)
def load_favorites():
global favorites
rows = read_csv(CSV_FAVORITES)
favorites = {}
for r in rows:
try:
if r['menu_ids']:
favorites[r['username']] = [int(x) for x in r['menu_ids'].split(',')]
else:
favorites[r['username']] = []
except:
favorites[r['username']] = []
def save_notifikasi():
write_csv(CSV_NOTIFIKASI, ["id", "timestamp", "type", "message", "read"], notifikasi)
def load_notifikasi():
global notifikasi
rows = read_csv(CSV_NOTIFIKASI)
notifikasi = []
for r in rows:
try:
notifikasi.append({
'id': int(r['id']),
'timestamp': r['timestamp'],
'type': r['type'],
'message': r['message'],
'read': r['read'] == 'True'
})
except:
pass
def save_promo_codes():
rows = [{"code": code, "discount_percent": str(discount)} for code, discount in promo_codes.items()]
write_csv(CSV_PROMO_CODES, ["code", "discount_percent"], rows)
def load_promo_codes():
global promo_codes
rows = read_csv(CSV_PROMO_CODES)
promo_codes = {}
for r in rows:
try:
promo_codes[r['code']] = int(r['discount_percent'])
except:
pass
if not promo_codes:
init_default_data()
save_promo_codes()
def save_pembayaran():
rows = []
for p in pembayaran:
rows.append({
"id": str(p['id']),
"transaksi_id": str(p['transaksi_id']),
"metode": p['metode'],
"jumlah": str(p['jumlah']),
"status": p['status'],
"tanggal": p['tanggal']
})
write_csv(CSV_PEMBAYARAN, ["id", "transaksi_id", "metode", "jumlah", "status", "tanggal"], rows)
def load_pembayaran():
global pembayaran
rows = read_csv(CSV_PEMBAYARAN)
pembayaran = []
for r in rows:
try:
pembayaran.append({
'id': int(r['id']),
'transaksi_id': int(r['transaksi_id']),
'metode': r['metode'],
'jumlah': int(float(r['jumlah'])),
'status': r['status'],
'tanggal': r['tanggal']
})
except:
pass
def load_all_data():
ensure_file(CSV_USERS, ["id", "username", "password", "role"])
ensure_file(CSV_MENU, ["id", "nama", "harga", "kategori", "stok", "foto", "promo", "item_discount_pct"])
ensure_file(CSV_BAHAN, ["nama", "jumlah"])
ensure_file(CSV_RESEP, ["menu_id", "bahan", "jumlah"])
ensure_file(CSV_TRANSAKSI, ["id", "tanggal", "waktu", "user", "items", "subtotal", "diskon", "total", "meja", "status", "payment_status", "payment_method", "paid_amount", "change", "promo_code"])
ensure_file(CSV_DETAIL_TRANSAKSI, ["id", "transaksi_id", "menu_id", "nama_menu", "jumlah", "harga_satuan", "subtotal", "diskon"])
ensure_file(CSV_MEJA, ["nomor", "status"])
ensure_file(CSV_FAVORITES, ["username", "menu_ids"])
ensure_file(CSV_NOTIFIKASI, ["id", "timestamp", "type", "message", "read"])
ensure_file(CSV_PROMO_CODES, ["code", "discount_percent"])
ensure_file(CSV_PEMBAYARAN, ["id", "transaksi_id", "metode", "jumlah", "status", "tanggal"])
load_users()
load_menu()
load_bahan()
load_resep()
load_transaksi()
load_detail_transaksi()
load_meja()
load_favorites()
load_notifikasi()
load_promo_codes()
load_pembayaran()
add_sample_transactions()
# ================================
# HELPER FUNCTIONS
# ================================
def find_menu_by_id(mid):
for it in menu:
if it["id"] == mid:
return it
return None
def format_currency(n):
try:
return f"Rp{int(n):,}".replace(",", ".")
except:
return f"Rp{n}"
def calculate_discount(harga, promo_text):
if not promo_text:
return 0
try:
import re
match = re.search(r'(\d+)%', promo_text)
if match:
percent = int(match.group(1))
return int(harga * percent / 100)
except:
pass
return 0
def add_notification(ntype, message):
global notifikasi
new_id = max([n['id'] for n in notifikasi], default=0) + 1
notifikasi.append({
'id': new_id,
'timestamp': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'type': ntype,
'message': message,
'read': False
})
save_notifikasi()
def copy_image_to_project(src_path):
if not src_path or not os.path.exists(src_path):
return ""
try:
filename = os.path.basename(src_path)
name, ext = os.path.splitext(filename)
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
new_filename = f"{name}_{timestamp}{ext}"
dest_path = os.path.join(IMAGES_DIR, new_filename)
shutil.copy2(src_path, dest_path)
return dest_path
except Exception as e:
print(f"Error copying image: {e}")
return ""
def ensure_image(path, maxsize=(240, 160)):
if not path or not os.path.exists(path):
return None
key = (path, maxsize)
if key in _image_refs:
return _image_refs[key]
try:
if PIL_AVAILABLE:
img = Image.open(path)
img.thumbnail(maxsize)
tkimg = ImageTk.PhotoImage(img)
else:
tkimg = tk.PhotoImage(file=path)
_image_refs[key] = tkimg
return tkimg
except:
return None
def reset_image_refs():
_image_refs.clear()
def get_daily_report(date_str):
trans = [t for t in transaksi if t['tanggal'] == date_str and t.get('payment_status') == 'Berhasil']
total_omzet = sum(t['total'] for t in trans)
total_transaksi = len(trans)
menu_count = defaultdict(int)
for t in trans:
for item in t['items']:
menu_count[item['nama']] += item['qty']
return {
'date': date_str,
'total_omzet': total_omzet,
'total_transaksi': total_transaksi,
'menu_terlaris': dict(menu_count)
}
def add_sample_transactions():
"""Tambahkan data transaksi sample untuk demo"""
global transaksi, detail_transaksi
if len(transaksi) < 5:
today = str(datetime.date.today())
yesterday = str(datetime.date.today() - datetime.timedelta(days=1))
two_days_ago = str(datetime.date.today() - datetime.timedelta(days=2))
sample_data = [
{'tanggal': today, 'items': [{'nama': 'Americano', 'qty': 2, 'harga_satuan': 20000}, {'nama': 'Es Teh', 'qty': 1, 'harga_satuan': 5000}], 'total': 45000, 'meja': 1},
{'tanggal': today, 'items': [{'nama': 'Latte', 'qty': 3, 'harga_satuan': 25000}, {'nama': 'Roti Bakar', 'qty': 2, 'harga_satuan': 15000}], 'total': 105000, 'meja': 2},
{'tanggal': today, 'items': [{'nama': 'Nasi Goreng', 'qty': 2, 'harga_satuan': 35000}, {'nama': 'Kopi Susu', 'qty': 2, 'harga_satuan': 15000}], 'total': 100000, 'meja': 3},
{'tanggal': today, 'items': [{'nama': 'Mie Goreng', 'qty': 1, 'harga_satuan': 30000}, {'nama': 'Cappuccino', 'qty': 1, 'harga_satuan': 28000}], 'total': 58000, 'meja': 4},
{'tanggal': yesterday, 'items': [{'nama': 'Pasta Carbonara', 'qty': 2, 'harga_satuan': 45000}, {'nama': 'Smoothie Buah', 'qty': 3, 'harga_satuan': 22000}], 'total': 156000, 'meja': 5},
{'tanggal': yesterday, 'items': [{'nama': 'Americano', 'qty': 4, 'harga_satuan': 20000}, {'nama': 'Es Teh', 'qty': 2, 'harga_satuan': 5000}], 'total': 90000, 'meja': 6},
{'tanggal': two_days_ago, 'items': [{'nama': 'Latte', 'qty': 2, 'harga_satuan': 25000}, {'nama': 'Nasi Goreng', 'qty': 1, 'harga_satuan': 35000}], 'total': 85000, 'meja': 7},
]
for idx, data in enumerate(sample_data, start=1):
trans_id = idx
transaksi.append({
'id': trans_id,
'tanggal': data['tanggal'],
'waktu': datetime.datetime.now() - datetime.timedelta(hours=idx),
'user': 'waiter',
'items': data['items'],
'subtotal': data['total'],
'diskon': 0,
'total': data['total'],
'meja': data['meja'],
'status': 'Selesai',
'payment_status': 'Berhasil',
'payment_method': 'cash' if idx % 2 == 0 else 'qris',
'paid_amount': data['total'],
'change': 0,
'promo_code': ''
})
def get_payment_summary(start_date, end_date):
trans = [t for t in transaksi
if start_date <= t['tanggal'] <= end_date
and t.get('payment_status') == 'Berhasil']
total = sum(t['total'] for t in trans)
count = len(trans)
method_breakdown = defaultdict(int)
for t in trans:
method = t.get('payment_method', 'Unknown')
method_breakdown[method] += 1
return {
'total_income': total,
'total_count': count,
'avg': total / count if count > 0 else 0,
'method_breakdown': dict(method_breakdown)
}
# ================================
# MAIN APP CLASS
# ================================
class CafeApp:
def __init__(self, root):
self.root = root
self.root.title("Sistem Management Cafe - Ultimate Edition (FIXED)")
self.root.geometry("1200x700")
self.root.minsize(1000, 600)
# Session
self.current_user = None
self.cart_items = []
# Load data
load_all_data()
# Style
self.setup_styles()
# Start
self.show_welcome_screen()
def setup_styles(self):
style = ttk.Style()
style.theme_use('clam')
style.configure("Accent.TButton",
background="#8B7355",
foreground="white",
font=("Arial", 10, "bold"))
style.map("Accent.TButton",
background=[('active', '#6B5335')])
# ================================
# WELCOME & LOGIN
# ================================
def show_welcome_screen(self):
self._clear_root()
self.root.configure(bg="#F5F5F0")
main_frame = tk.Frame(self.root, bg="#F5F5F0")
main_frame.place(relx=0.5, rely=0.5, anchor="center")
title_frame = tk.Frame(main_frame, bg="#F5F5F0")
title_frame.pack(pady=(0, 30))
tk.Label(title_frame, text="", font=("Segoe UI", 64), bg="#F5F5F0", fg="#8B7355").pack()
tk.Label(title_frame, text="CAFE MANAGEMENT", font=("Segoe UI", 32, "bold"),
bg="#F5F5F0", fg="#5C4033").pack(pady=(10, 0))
tk.Label(title_frame, text="Ultimate Edition - FIXED VERSION", font=("Segoe UI", 14),
bg="#F5F5F0", fg="#999999").pack()
btn_frame = tk.Frame(main_frame, bg="#F5F5F0")
btn_frame.pack(pady=40)
guest_btn = tk.Button(btn_frame, text="🌐 Browse Sebagai Guest",
command=self.enter_as_guest,
font=("Segoe UI", 12, "bold"),
bg="#27AE60", fg="white",
relief="flat", bd=0,
padx=40, pady=15, cursor="hand2")
guest_btn.pack(pady=10)
guest_btn.bind("<Enter>", lambda e: guest_btn.configure(bg="#229954"))
guest_btn.bind("<Leave>", 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("<Enter>", lambda e: login_btn.configure(bg="#6B5335"))
login_btn.bind("<Leave>", 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("<Enter>", lambda e: login_btn.configure(bg="#6B5335"))
login_btn.bind("<Leave>", lambda e: login_btn.configure(bg="#8B7355"))
self.e_pass.bind("<Return>", lambda e: self.handle_login())
back_btn = tk.Button(main_frame, text="← Kembali",
command=self.show_welcome_screen,
font=("Segoe UI", 9), bg="#E8E0D5", fg="#5C4033",
relief="flat", bd=0, padx=20, pady=8, cursor="hand2")
back_btn.pack(pady=(15, 0))
tk.Label(main_frame, text="Default: admin/admin123, kasir/kasir123, pembeli/user123",
font=("Segoe UI", 9), bg="#F5F5F0", fg="#999999").pack(pady=(10, 0))
def handle_login(self):
u = self.e_user.get().strip()
p = self.e_pass.get().strip()
for usr in users:
if usr["username"] == u and usr["password"] == p:
self.current_user = {
'id': int(usr['id']),
'username': usr['username'],
'role': usr['role']
}
messagebox.showinfo("Login", f"Berhasil login sebagai {u} ({usr['role']})")
self.build_main_ui()
return
messagebox.showerror("Login Gagal", "Username atau password salah.")
def _clear_root(self):
for w in self.root.winfo_children():
w.destroy()
def logout(self):
self.current_user = None
self.cart_items = []
self.show_welcome_screen()
# ================================
# MAIN UI
# ================================
def build_main_ui(self):
self._clear_root()
self.root.configure(bg="#F5F5F0")
# Top bar
topbar = tk.Frame(self.root, bg="#5C4033", height=60)
topbar.pack(fill="x")
topbar.pack_propagate(False)
left_header = tk.Frame(topbar, bg="#5C4033")
left_header.pack(side="left", padx=20, pady=10)
tk.Label(left_header, text="☕ CAFE MANAGEMENT", font=("Segoe UI", 14, "bold"),
bg="#5C4033", fg="white").pack(side="left")
role_text = self.current_user['role'].upper() if self.current_user['role'] != 'guest' else 'GUEST MODE'
tk.Label(left_header, text=f" | {self.current_user['username']} ({role_text})",
font=("Segoe UI", 10), bg="#5C4033", fg="#D4AF77").pack(side="left")
right_header = tk.Frame(topbar, bg="#5C4033")
right_header.pack(side="right", padx=20, pady=10)
# Notifikasi button (untuk staff saja)
if self.current_user['role'] != 'guest':
unread = len([n for n in notifikasi if not n['read']])
notif_text = f"🔔 ({unread})" if unread > 0 else "🔔"
notif_btn = tk.Button(right_header, text=notif_text,
command=self.open_notifikasi_window,
font=("Arial", 9), bg="#6B5335", fg="white",
relief="flat", bd=0, padx=15, pady=8, cursor="hand2")
notif_btn.pack(side="right", padx=3)
# Logout button
logout_btn = tk.Button(right_header, text="🚪 Logout", command=self.logout,
font=("Arial", 9), bg="#A0522D", fg="white",
relief="flat", bd=0, padx=15, pady=8, cursor="hand2")
logout_btn.pack(side="right", padx=3)
# Main content
main = ttk.Notebook(self.root)
main.pack(fill='both', expand=True, padx=10, pady=10)
# Build tabs based on role
if self.current_user['role'] == 'guest':
self.build_guest_tabs(main)
elif self.current_user['role'] == 'pembeli':
self.build_pembeli_tabs(main)
elif self.current_user['role'] == 'kasir':
self.build_kasir_tabs(main)
elif self.current_user['role'] == 'waiter':
self.build_waiter_tabs(main)
elif self.current_user['role'] == 'owner':
self.build_owner_tabs(main)
elif self.current_user['role'] == 'admin':
self.build_admin_tabs(main)
# ================================
# TAB BUILDERS
# ================================
def build_guest_tabs(self, notebook):
tab_menu = ttk.Frame(notebook)
notebook.add(tab_menu, text="📖 Lihat Menu")
self.build_menu_browse_tab(tab_menu, guest_mode=True)
def build_pembeli_tabs(self, notebook):
tab_menu = ttk.Frame(notebook)
tab_order = ttk.Frame(notebook)
tab_fav = ttk.Frame(notebook)
notebook.add(tab_menu, text="📖 Menu")
notebook.add(tab_order, text="🛒 Order")
notebook.add(tab_fav, text="⭐ Favorit")
self.build_menu_browse_tab(tab_menu)
self.build_order_tab(tab_order)
self.build_favorite_tab(tab_fav)
def build_kasir_tabs(self, notebook):
tab_payment = ttk.Frame(notebook)
tab_riwayat = ttk.Frame(notebook)
notebook.add(tab_payment, text="💰 Pembayaran")
notebook.add(tab_riwayat, text="📜 Riwayat")
self.build_payment_tab(tab_payment)
self.build_riwayat_tab(tab_riwayat)
def build_waiter_tabs(self, notebook):
tab_pesanan = ttk.Frame(notebook)
tab_meja = ttk.Frame(notebook)
notebook.add(tab_pesanan, text="🍽️ Pesanan")
notebook.add(tab_meja, text="🪑 Meja")
self.build_waiter_pesanan_tab(tab_pesanan)
self.build_meja_tab(tab_meja)
def build_owner_tabs(self, notebook):
tab_laporan = ttk.Frame(notebook)
tab_riwayat = ttk.Frame(notebook)
notebook.add(tab_laporan, text="📊 Laporan")
notebook.add(tab_riwayat, text="📜 Riwayat")
self.build_laporan_tab(tab_laporan)
self.build_riwayat_tab(tab_riwayat)
def build_admin_tabs(self, notebook):
tab_menu_manage = ttk.Frame(notebook)
tab_bahan = ttk.Frame(notebook)
tab_promo = ttk.Frame(notebook)
tab_user = ttk.Frame(notebook)
tab_laporan = ttk.Frame(notebook)
notebook.add(tab_menu_manage, text="⚙️ Kelola Menu")
notebook.add(tab_bahan, text="📦 Stok Bahan")
notebook.add(tab_promo, text="🎁 Promo")
notebook.add(tab_user, text="👥 User")
notebook.add(tab_laporan, text="📊 Laporan")
self.build_menu_manage_tab(tab_menu_manage)
self.build_bahan_tab(tab_bahan)
self.build_promo_tab(tab_promo)
self.build_user_tab(tab_user)
self.build_laporan_tab(tab_laporan)
# ================================
# TAB: MENU BROWSE
# ================================
def build_menu_browse_tab(self, parent, guest_mode=False):
for w in parent.winfo_children():
w.destroy()
header = tk.Frame(parent, bg="#F5F5F0")
header.pack(fill="x", padx=15, pady=10)
tk.Label(header, text="🍽️ Menu Cafe", font=("Arial", 18, "bold"),
bg="#F5F5F0", fg="#5C4033").pack(side="left")
if guest_mode:
info = tk.Label(header,
text=" Mode Guest - Login untuk memesan",
font=("Arial", 10), bg="#FFF3CD", fg="#856404",
padx=10, pady=5)
info.pack(side="right")
search_frame = tk.Frame(parent, bg="white", relief="solid", bd=1)
search_frame.pack(fill="x", padx=15, pady=5)
inner_search = tk.Frame(search_frame, bg="white")
inner_search.pack(padx=10, pady=8)
tk.Label(inner_search, text="🔍 Cari:", font=("Arial", 9),
bg="white").pack(side="left", padx=5)
self.menu_search_var = tk.StringVar()
tk.Entry(inner_search, textvariable=self.menu_search_var,
font=("Arial", 10), width=25).pack(side="left", padx=5)
tk.Button(inner_search, text="Cari", command=self.refresh_menu_browse,
font=("Arial", 9), bg="#8B7355", fg="white",
relief="flat", padx=15, pady=5).pack(side="left", padx=5)
tk.Label(inner_search, text="📂 Kategori:", font=("Arial", 9),
bg="white").pack(side="left", padx=(15, 5))
self.menu_filter_var = tk.StringVar(value="Semua")
categories = ["Semua"] + sorted(set(m['kategori'] for m in menu))
filter_cb = ttk.Combobox(inner_search, textvariable=self.menu_filter_var,
values=categories, state="readonly", width=15)
filter_cb.pack(side="left", padx=5)
filter_cb.bind("<<ComboboxSelected>>", 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>", configure_scroll)
def configure_canvas(event):
canvas.itemconfig(canvas_window, width=event.width)
canvas.bind("<Configure>", configure_canvas)
def on_mousewheel(event):
canvas.yview_scroll(-1 * int(event.delta / 120), "units")
canvas.bind_all("<MouseWheel>", 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(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=self.menu_cards_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
def _on_mousewheel(event):
canvas.yview_scroll(int(-1*(event.delta/120)), "units")
canvas.bind_all("<MouseWheel>", _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("<<TreeviewSelect>>", 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(
"<Configure>",
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("<MouseWheel>", _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("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
canvas.bind("<Configure>", lambda e: canvas.itemconfig(canvas_window, width=e.width))
canvas.bind_all("<MouseWheel>", 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("<Double-1>", 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("<<ComboboxSelected>>", 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(
"<Configure>",
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('<Configure>', lambda e: canvas.configure(scrollregion=canvas.bbox('all')))
if not notifikasi:
ttk.Label(notif_container, text="📭 Tidak ada notifikasi",
font=("Arial", 12), foreground='gray').pack(pady=50)
else:
for n in reversed(notifikasi):
self.create_notif_card(notif_container, n, notif_win)
def create_notif_card(self, parent, notif, window):
bg_color = "#F8F9FA" if notif['read'] else "#E3F2FD"
card = tk.Frame(parent, bg=bg_color, relief='solid', bd=1)
card.pack(fill='x', padx=5, pady=5)
inner = tk.Frame(card, bg=bg_color)
inner.pack(fill='x', padx=10, pady=8)
icon_map = {
'success': '',
'info': '',
'warning': '⚠️',
'error': ''
}
icon = icon_map.get(notif['type'], '📌')
tk.Label(inner, text=icon, font=("Arial", 16), bg=bg_color).pack(side='left', padx=(0, 10))
text_frame = tk.Frame(inner, bg=bg_color)
text_frame.pack(side='left', fill='x', expand=True)
tk.Label(text_frame, text=notif['message'], font=("Arial", 10),
bg=bg_color, anchor='w', wraplength=450).pack(fill='x')
tk.Label(text_frame, text=notif['timestamp'], font=("Arial", 8),
bg=bg_color, fg='gray', anchor='w').pack(fill='x')
if not notif['read']:
tk.Button(inner, text="", font=("Arial", 10), bg='#4CAF50', fg='white',
relief='flat', padx=10, pady=2,
command=lambda: self.mark_as_read(notif['id'], window)).pack(side='right')
def mark_as_read(self, notif_id, window):
for n in notifikasi:
if n['id'] == notif_id:
n['read'] = True
break
save_notifikasi()
self.open_notifikasi_window()
window.destroy()
self.build_main_ui()
def mark_all_read(self, window):
for n in notifikasi:
n['read'] = True
save_notifikasi()
messagebox.showinfo("Success", "Semua notifikasi ditandai sudah dibaca")
self.open_notifikasi_window()
window.destroy()
self.build_main_ui()
def clear_all_notifications(self, window):
confirm = messagebox.askyesno("Konfirmasi", "Hapus semua notifikasi?")
if confirm:
notifikasi.clear()
save_notifikasi()
messagebox.showinfo("Success", "Semua notifikasi dihapus")
window.destroy()
self.build_main_ui()
# ================================
# MAIN
# ================================
if __name__ == "__main__":
root = tk.Tk()
app = CafeApp(root)
root.mainloop()