2025-12-13 17:45:53 +07:00

2944 lines
112 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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.

"""akun default buat login :
- admin / admin123 (role admin)
- kasir / kasir123 (role kasir)
- waiter / waiter123 (role waiter)
- user / user123 (role pembeli)
- owner / owner123 (role pemilik)
"""
import os
import csv
import tkinter as tk
from tkinter import ttk, messagebox, filedialog, simpledialog
from PIL import Image, ImageTk
USERS_CSV = "users.csv"
MENU_CSV = "menu.csv"
PROMO_CSV = "promo.csv"
TRANSAKSI_CSV = "transaksi.csv"
DETAIL_TRANSAKSI_CSV = "detail_transaksi.csv"
FAVORITE_CSV = "favorite.csv"
MEJA_CSV = "meja.csv"
PEMBAYARAN_CSV = "pembayaran.csv"
IMG_PREVIEW_SIZE = (120, 80)
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_all(path):
if not os.path.exists(path):
return []
with open(path, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
return list(reader)
def write_all(path, fieldnames, rows):
with open(path, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
def next_int_id(rows, id_field="id"):
max_id = 0
for r in rows:
try:
v = int(r.get(id_field, 0) or 0)
if v > max_id:
max_id = v
except:
continue
return str(max_id + 1)
def init_db_csv():
ensure_file(USERS_CSV, ["id", "username", "password", "role"])
ensure_file(MENU_CSV, ["id", "nama", "kategori", "harga", "stok", "foto", "tersedia", "item_discount_pct"])
ensure_file(PROMO_CSV, ["code", "type", "value", "min_total"])
ensure_file(TRANSAKSI_CSV, ["id", "user_id", "nomor_meja", "total", "status", "promo_code", "subtotal", "item_discount", "promo_discount", "tanggal"])
ensure_file(DETAIL_TRANSAKSI_CSV, ["id", "transaksi_id", "menu_id", "qty", "harga_satuan", "subtotal_item"])
ensure_file(FAVORITE_CSV, ["user_id", "menu_id", "order_count", "last_ordered"])
ensure_file(MEJA_CSV, ["nomor_meja", "status", "transaksi_id"])
ensure_file(PEMBAYARAN_CSV, ["id", "transaksi_id", "metode_pembayaran", "jumlah_bayar", "status_pembayaran", "tanggal_bayar", "struk"])
seed_defaults()
# buat masukin data/sample ke database csv
def seed_defaults():
users = read_all(USERS_CSV)
if not users:
defaults = [
('admin','admin123','admin'),
('kasir','kasir123','kasir'),
('waiter','waiter123','waiter'),
('user','user123','pembeli'),
('owner','owner123','pemilik'),
]
rows = []
for i,(u,p,r) in enumerate(defaults, start=1):
rows.append({"id": str(i), "username": u, "password": p, "role": r})
write_all(USERS_CSV, ["id","username","password","role"], rows)
menu_rows = read_all(MENU_CSV)
if not menu_rows:
sample = [
('Americano','Minuman',20000,10,None,1,0),
('Latte','Minuman',25000,5,None,1,10),
('Banana Cake','Dessert',30000,2,None,1,0),
('Nasi Goreng','Makanan',35000,0,None,0,0),
]
rows = []
for i,(name,kategori,harga,stok,foto,tersedia,disc) in enumerate(sample, start=1):
rows.append({
"id": str(i),
"nama": name,
"kategori": kategori,
"harga": str(harga),
"stok": str(stok),
"foto": foto or "",
"tersedia": str(tersedia),
"item_discount_pct": str(disc)
})
write_all(MENU_CSV, ["id","nama","kategori","harga","stok","foto","tersedia","item_discount_pct"], rows)
promo_rows = read_all(PROMO_CSV)
if not promo_rows:
promos = [
('PARDEDE','percent',10,0),
('BOTAK','fixed',5000,20000),
]
rows = []
for code,ptype,val,min_total in promos:
rows.append({
"code": code,
"type": ptype,
"value": str(val),
"min_total": str(min_total)
})
write_all(PROMO_CSV, ["code","type","value","min_total"], rows)
# Seed meja (10 meja default)
meja_rows = read_all(MEJA_CSV)
if not meja_rows:
rows = []
for i in range(1, 11): # Meja 1-10
rows.append({
"nomor_meja": str(i),
"status": "kosong",
"transaksi_id": ""
})
write_all(MEJA_CSV, ["nomor_meja", "status", "transaksi_id"], rows)
def authenticate(username, password):
rows = read_all(USERS_CSV)
for r in rows:
if r.get("username") == username and r.get("password") == password:
return {'id': int(r.get("id")), 'username': r.get("username"), 'role': r.get("role")}
return None
# Wilayah dikuasai Menu
def menu_add(nama, kategori, harga, stok, foto, item_discount_pct=0):
rows = read_all(MENU_CSV)
new_id = next_int_id(rows, "id")
tersedia = "1" if int(stok) > 0 else "0"
rows.append({
"id": new_id,
"nama": nama,
"kategori": kategori,
"harga": str(float(harga)),
"stok": str(int(stok)),
"foto": foto or "",
"tersedia": tersedia,
"item_discount_pct": str(float(item_discount_pct))
})
write_all(MENU_CSV, ["id","nama","kategori","harga","stok","foto","tersedia","item_discount_pct"], rows)
def menu_update(menu_id, nama, kategori, harga, stok, foto, item_discount_pct=0):
rows = read_all(MENU_CSV)
found = False
for r in rows:
if r.get("id") == str(menu_id):
r["nama"] = nama
r["kategori"] = kategori
r["harga"] = str(float(harga))
r["stok"] = str(int(stok))
r["foto"] = foto or ""
r["tersedia"] = "1" if int(stok) > 0 else "0"
r["item_discount_pct"] = str(float(item_discount_pct))
found = True
break
if found:
write_all(MENU_CSV, ["id","nama","kategori","harga","stok","foto","tersedia","item_discount_pct"], rows)
else:
raise ValueError("Menu id tidak ditemukan")
def menu_delete(menu_id):
rows = read_all(MENU_CSV)
newrows = [r for r in rows if r.get("id") != str(menu_id)]
write_all(MENU_CSV, ["id","nama","kategori","harga","stok","foto","tersedia","item_discount_pct"], newrows)
def menu_list(kategori=None, available_only=False, search_text=None):
rows = read_all(MENU_CSV)
out = []
for r in rows:
if kategori and r.get("kategori") != kategori:
continue
if available_only and r.get("tersedia") != "1":
continue
if search_text:
s = search_text.lower()
if s not in (r.get("nama","").lower() or "") and s not in (r.get("kategori","").lower() or ""):
continue
try:
mid = int(r.get("id") or 0)
except:
mid = r.get("id")
try:
harga = float(r.get("harga") or 0.0)
except:
harga = 0.0
try:
stok = int(float(r.get("stok") or 0))
except:
stok = 0
foto = r.get("foto") or None
tersedia = 1 if r.get("tersedia") == "1" else 0
try:
item_disc = float(r.get("item_discount_pct") or 0.0)
except:
item_disc = 0.0
out.append((mid, r.get("nama"), r.get("kategori"), harga, stok, foto, tersedia, item_disc))
out.sort(key=lambda x: int(x[0]))
return out
def menu_get(menu_id):
rows = read_all(MENU_CSV)
for r in rows:
if r.get("id") == str(menu_id):
try:
mid = int(r.get("id") or 0)
except:
mid = r.get("id")
try:
harga = float(r.get("harga") or 0.0)
except:
harga = 0.0
try:
stok = int(float(r.get("stok") or 0))
except:
stok = 0
foto = r.get("foto") or None
tersedia = 1 if r.get("tersedia") == "1" else 0
try:
item_disc = float(r.get("item_discount_pct") or 0.0)
except:
item_disc = 0.0
return (mid, r.get("nama"), r.get("kategori"), harga, stok, foto, tersedia, item_disc)
return None
def menu_decrease_stock(menu_id, qty):
rows = read_all(MENU_CSV)
found = False
for r in rows:
if r.get("id") == str(menu_id):
found = True
try:
stok = int(float(r.get("stok") or 0))
except:
stok = 0
if stok < qty:
return False, "Stok tidak cukup"
newstok = stok - qty
r["stok"] = str(newstok)
r["tersedia"] = "1" if newstok > 0 else "0"
break
if not found:
return False, "Menu tidak ditemukan"
write_all(MENU_CSV, ["id","nama","kategori","harga","stok","foto","tersedia","item_discount_pct"], rows)
return True, newstok
# Reza balap liar
# wilayah dikuasai promo
def promo_add(code, ptype, value, min_total=0):
rows = read_all(PROMO_CSV)
for r in rows:
if r.get("code") == code:
raise ValueError("Kode promo sudah ada")
rows.append({
"code": code,
"type": ptype,
"value": str(float(value)),
"min_total": str(float(min_total))
})
write_all(PROMO_CSV, ["code","type","value","min_total"], rows)
def promo_update(code, ptype, value, min_total=0):
rows = read_all(PROMO_CSV)
found = False
for r in rows:
if r.get("code") == code:
r["type"] = ptype
r["value"] = str(float(value))
r["min_total"] = str(float(min_total))
found = True
break
if not found:
raise ValueError("Promo tidak ditemukan")
write_all(PROMO_CSV, ["code","type","value","min_total"], rows)
def promo_delete(code):
rows = read_all(PROMO_CSV)
newrows = [r for r in rows if r.get("code") != code]
write_all(PROMO_CSV, ["code","type","value","min_total"], newrows)
def promo_list():
rows = read_all(PROMO_CSV)
out = []
for r in rows:
try:
val = float(r.get("value") or 0.0)
except:
val = 0.0
try:
mt = float(r.get("min_total") or 0.0)
except:
mt = 0.0
out.append((r.get("code"), r.get("type"), val, mt))
out.sort(key=lambda x: x[0] or "")
return out
def promo_get(code):
rows = read_all(PROMO_CSV)
for r in rows:
if r.get("code") == code:
try:
val = float(r.get("value") or 0.0)
except:
val = 0.0
try:
mt = float(r.get("min_total") or 0.0)
except:
mt = 0.0
return (r.get("code"), r.get("type"), val, mt)
return None
# Wilayah dikuasai Transaksi
def transaksi_add(user_id, nomor_meja, cart_items, promo_code=None):
"""Simpan transaksi baru dengan status 'pending'"""
from datetime import datetime
if not cart_items:
return False, "Keranjang kosong"
# Hitung diskon dan total
calc = apply_discounts_and_promo(cart_items, promo_code)
# Buat transaksi
rows = read_all(TRANSAKSI_CSV)
transaksi_id = next_int_id(rows, "id")
tanggal = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
transaksi_row = {
"id": transaksi_id,
"user_id": str(user_id),
"nomor_meja": str(nomor_meja),
"total": str(calc['total']),
"status": "pending",
"promo_code": calc.get('promo_code') or "",
"subtotal": str(calc['subtotal']),
"item_discount": str(calc['item_discount']),
"promo_discount": str(calc['promo_discount']),
"tanggal": tanggal
}
rows.append(transaksi_row)
write_all(TRANSAKSI_CSV, ["id", "user_id", "nomor_meja", "total", "status", "promo_code", "subtotal", "item_discount", "promo_discount", "tanggal"], rows)
# Simpan detail transaksi
detail_rows = read_all(DETAIL_TRANSAKSI_CSV)
for item in cart_items:
detail_id = next_int_id(detail_rows, "id")
menu_data = menu_get(item['menu_id'])
if not menu_data:
continue
_, nama, kategori, harga, stok, foto, tersedia, item_disc = menu_data
qty = int(item['qty'])
subtotal_item = harga * qty
detail_rows.append({
"id": detail_id,
"transaksi_id": transaksi_id,
"menu_id": str(item['menu_id']),
"qty": str(qty),
"harga_satuan": str(harga),
"subtotal_item": str(subtotal_item)
})
# Kurangi stok
success, msg = menu_decrease_stock(item['menu_id'], qty)
if not success:
return False, f"Gagal mengurangi stok menu ID {item['menu_id']}: {msg}"
# Update favorite count
favorite_update(user_id, item['menu_id'])
write_all(DETAIL_TRANSAKSI_CSV, ["id", "transaksi_id", "menu_id", "qty", "harga_satuan", "subtotal_item"], detail_rows)
return True, transaksi_id
def transaksi_list(status=None, user_id=None):
"""Ambil daftar transaksi, bisa filter by status atau user_id"""
rows = read_all(TRANSAKSI_CSV)
out = []
for r in rows:
if status and r.get("status") != status:
continue
if user_id and r.get("user_id") != str(user_id):
continue
try:
tid = int(r.get("id") or 0)
except:
tid = r.get("id")
try:
uid = int(r.get("user_id") or 0)
except:
uid = r.get("user_id")
try:
meja = int(r.get("nomor_meja") or 0)
except:
meja = r.get("nomor_meja")
try:
total = float(r.get("total") or 0.0)
except:
total = 0.0
out.append((tid, uid, meja, total, r.get("status"), r.get("promo_code"), r.get("tanggal")))
out.sort(key=lambda x: int(x[0]), reverse=True)
return out
def transaksi_get(transaksi_id):
"""Ambil detail transaksi by ID"""
rows = read_all(TRANSAKSI_CSV)
for r in rows:
if r.get("id") == str(transaksi_id):
try:
tid = int(r.get("id") or 0)
except:
tid = r.get("id")
try:
uid = int(r.get("user_id") or 0)
except:
uid = r.get("user_id")
try:
meja = int(r.get("nomor_meja") or 0)
except:
meja = r.get("nomor_meja")
try:
total = float(r.get("total") or 0.0)
except:
total = 0.0
try:
subtotal = float(r.get("subtotal") or 0.0)
except:
subtotal = 0.0
try:
item_disc = float(r.get("item_discount") or 0.0)
except:
item_disc = 0.0
try:
promo_disc = float(r.get("promo_discount") or 0.0)
except:
promo_disc = 0.0
return (tid, uid, meja, total, r.get("status"), r.get("promo_code"), subtotal, item_disc, promo_disc, r.get("tanggal"))
return None
def transaksi_update_status(transaksi_id, new_status):
"""Update status transaksi"""
rows = read_all(TRANSAKSI_CSV)
found = False
for r in rows:
if r.get("id") == str(transaksi_id):
r["status"] = new_status
found = True
break
if found:
write_all(TRANSAKSI_CSV, ["id", "user_id", "nomor_meja", "total", "status", "promo_code", "subtotal", "item_discount", "promo_discount", "tanggal"], rows)
return True
return False
def detail_transaksi_list(transaksi_id):
"""Ambil semua detail item dari transaksi tertentu"""
rows = read_all(DETAIL_TRANSAKSI_CSV)
out = []
for r in rows:
if r.get("transaksi_id") == str(transaksi_id):
try:
did = int(r.get("id") or 0)
except:
did = r.get("id")
try:
mid = int(r.get("menu_id") or 0)
except:
mid = r.get("menu_id")
try:
qty = int(r.get("qty") or 0)
except:
qty = 0
try:
harga = float(r.get("harga_satuan") or 0.0)
except:
harga = 0.0
try:
subtotal = float(r.get("subtotal_item") or 0.0)
except:
subtotal = 0.0
out.append((did, mid, qty, harga, subtotal))
return out
# 19 juta lapangan badmin
# Buat logika diskon + promok
def apply_discounts_and_promo(cart_items, promo_code=None):
subtotal = 0.0
item_discount_total = 0.0
menu_rows = read_all(MENU_CSV)
menu_dict = {r["id"]: r for r in menu_rows}
for it in cart_items:
mid = str(it.get('menu_id'))
r = menu_dict.get(mid)
if not r:
continue
try:
price = float(r.get("harga") or 0.0)
except:
price = 0.0
try:
item_disc_pct = float(r.get("item_discount_pct") or 0.0)
except:
item_disc_pct = 0.0
qty = int(it.get('qty', 1))
line = price * qty
subtotal += line
if item_disc_pct and item_disc_pct > 0:
item_discount_total += (price * qty) * (item_disc_pct / 100.0)
promo_discount = 0.0
promo_applied = None
if promo_code:
p = promo_get(promo_code)
if p:
_, ptype, val, min_total = p
if subtotal >= (min_total or 0.0):
if ptype == 'percent':
promo_discount = (subtotal - item_discount_total) * (val / 100.0)
else:
promo_discount = val
promo_applied = promo_code
total = subtotal - item_discount_total - promo_discount
if total < 0:
total = 0.0
return {
'subtotal': round(subtotal, 2),
'item_discount': round(item_discount_total, 2),
'promo_code': promo_applied,
'promo_discount': round(promo_discount, 2),
'total': round(total, 2)
}
# === FUNGSI FAVORITE ===
def favorite_update(user_id, menu_id):
"""Update atau tambah favorite count untuk user tertentu"""
from datetime import datetime
rows = read_all(FAVORITE_CSV)
found = False
for r in rows:
if r.get("user_id") == str(user_id) and r.get("menu_id") == str(menu_id):
try:
count = int(r.get("order_count") or 0)
except:
count = 0
r["order_count"] = str(count + 1)
r["last_ordered"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
found = True
break
if not found:
rows.append({
"user_id": str(user_id),
"menu_id": str(menu_id),
"order_count": "1",
"last_ordered": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
})
write_all(FAVORITE_CSV, ["user_id", "menu_id", "order_count", "last_ordered"], rows)
def favorite_list(user_id, limit=5):
"""Ambil menu favorit user, sorted by order_count descending"""
rows = read_all(FAVORITE_CSV)
out = []
for r in rows:
if r.get("user_id") == str(user_id):
try:
mid = int(r.get("menu_id") or 0)
except:
mid = r.get("menu_id")
try:
count = int(r.get("order_count") or 0)
except:
count = 0
last_ordered = r.get("last_ordered")
out.append((mid, count, last_ordered))
out.sort(key=lambda x: x[1], reverse=True)
if limit:
out = out[:limit]
return out
# === FUNGSI PEMBAYARAN ===
def pembayaran_add(transaksi_id, metode_pembayaran, jumlah_bayar, status_pembayaran='sukses', struk=''):
"""Simpan data pembayaran baru"""
from datetime import datetime
rows = read_all(PEMBAYARAN_CSV)
new_id = next_int_id(rows, "id")
tanggal_bayar = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
rows.append({
"id": new_id,
"transaksi_id": str(transaksi_id),
"metode_pembayaran": metode_pembayaran,
"jumlah_bayar": str(float(jumlah_bayar)),
"status_pembayaran": status_pembayaran,
"tanggal_bayar": tanggal_bayar,
"struk": struk
})
write_all(PEMBAYARAN_CSV, ["id", "transaksi_id", "metode_pembayaran", "jumlah_bayar", "status_pembayaran", "tanggal_bayar", "struk"], rows)
return new_id
def pembayaran_get_by_transaksi(transaksi_id):
"""Ambil data pembayaran berdasarkan transaksi_id"""
rows = read_all(PEMBAYARAN_CSV)
for r in rows:
if r.get("transaksi_id") == str(transaksi_id):
try:
pid = int(r.get("id") or 0)
except:
pid = r.get("id")
try:
jumlah = float(r.get("jumlah_bayar") or 0.0)
except:
jumlah = 0.0
return (pid, r.get("metode_pembayaran"), jumlah, r.get("status_pembayaran"), r.get("tanggal_bayar"), r.get("struk"))
return None
# === FUNGSI MEJA ===
def meja_update_status(nomor_meja, new_status, transaksi_id=""):
"""Update status meja (kosong/terisi) dan link ke transaksi"""
rows = read_all(MEJA_CSV)
found = False
for r in rows:
if r.get("nomor_meja") == str(nomor_meja):
r["status"] = new_status
r["transaksi_id"] = str(transaksi_id) if transaksi_id else ""
found = True
break
if found:
write_all(MEJA_CSV, ["nomor_meja", "status", "transaksi_id"], rows)
return True
return False
def meja_tutup(nomor_meja):
"""Tutup meja (set ke kosong)"""
return meja_update_status(nomor_meja, "kosong", "")
# Wilayah dikuasai UI
class App:
def __init__(self, root):
self.root = root
self.root.title("Cafe Totoro Mania")
self.session = None
self.img_cache = {}
self.setup_ui()
def setup_ui(self):
self.root.geometry("1000x650")
self.root.resizable(False, False)
self.login_frame()
style = ttk.Style()
style.configure("Accent.TButton", font=("Arial", 11, "bold"))
style.configure("TButton", padding=6) # Padding tombol lebih besar
def login_frame(self):
for w in self.root.winfo_children():
w.destroy()
frame = ttk.Frame(self.root, padding=20)
frame.pack(expand=True)
ttk.Label(frame, text="Login kak", font=("Arial", 24)).grid(row=0, column=0, columnspan=2, pady=10)
ttk.Label(frame, text="Username:").grid(row=1, column=0, sticky='e', pady=5)
self.username_var = tk.StringVar()
ttk.Entry(frame, textvariable=self.username_var, width=30).grid(row=1, column=1, pady=5)
ttk.Label(frame, text="Password:").grid(row=2, column=0, sticky='e', pady=5)
self.password_var = tk.StringVar()
ttk.Entry(frame, textvariable=self.password_var, show="*", width=30).grid(row=2, column=1, pady=5)
ttk.Button(frame, text="Login", command=self.handle_login).grid(row=3, column=0, columnspan=2, pady=12)
def handle_login(self):
u = self.username_var.get().strip()
p = self.password_var.get().strip()
if not u or not p:
messagebox.showwarning("Input", "Masukkan username & password")
return
user = authenticate(u,p)
if not user:
messagebox.showerror("Gagal", "Username atau password salah")
return
self.session = user
messagebox.showinfo("Sukses", f"Login berhasil sebagai {user['role']}")
self.dashboard_frame()
def logout(self):
self.session = None
self.img_cache.clear()
self.login_frame()
def dashboard_frame(self):
for w in self.root.winfo_children():
w.destroy()
top = ttk.Frame(self.root)
top.pack(fill='x')
ttk.Label(top, text=f"User: {self.session['username']} | Role: {self.session['role']}",
font=("Arial", 12)).pack(side='left', padx=10, pady=6)
ttk.Button(top, text="Logout", command=self.logout).pack(side='right', padx=10)
main = ttk.Notebook(self.root)
main.pack(fill='both', expand=True, padx=10, pady=8)
self.tab_menu_manage = ttk.Frame(main)
self.tab_menu_view = ttk.Frame(main)
self.tab_promo = ttk.Frame(main)
self.tab_order = ttk.Frame(main)
self.tab_waiter = ttk.Frame(main)
self.tab_favorite = ttk.Frame(main)
# Tab untuk semua role
main.add(self.tab_menu_view, text="Menu - View")
# Tab khusus pembeli (bisa order)
if self.session['role'] in ['pembeli', 'admin', 'user']:
main.add(self.tab_order, text="Order Menu")
if self.session['role'] in ['pembeli', 'user']:
main.add(self.tab_favorite, text="Favorit Saya")
# Tab khusus waiter
if self.session['role'] in ['waiter', 'admin']:
main.add(self.tab_waiter, text="Waiter - Pesanan")
# Tab pembayaran untuk kasir & admin
if self.session['role'] in ['kasir', 'admin']:
self.tab_payment = ttk.Frame(main)
main.add(self.tab_payment, text="💰 Pembayaran")
# Tab khusus admin
if self.session['role'] == 'admin':
main.add(self.tab_menu_manage, text="Menu - Manage")
main.add(self.tab_promo, text="Promo - Manage")
else:
pass
self.build_menu_view_tab(self.tab_menu_view)
if self.session['role'] in ['pembeli', 'admin', 'user']:
self.build_order_tab(self.tab_order)
if self.session['role'] in ['pembeli', 'user']:
self.build_favorite_tab(self.tab_favorite)
if self.session['role'] in ['waiter', 'admin']:
self.build_waiter_tab(self.tab_waiter)
if self.session['role'] in ['kasir', 'admin']:
self.build_payment_tab(self.tab_payment)
if self.session['role'] == 'admin':
self.build_menu_manage_tab(self.tab_menu_manage)
self.build_promo_tab(self.tab_promo)
def build_menu_view_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='y', padx=6, pady=6)
filter_frame = ttk.Frame(left)
filter_frame.pack(fill='x', pady=6)
ttk.Label(filter_frame, text="Cari / Nama atau Kategori:").pack(side='left', padx=3)
self.view_search_var = tk.StringVar()
ttk.Entry(filter_frame, textvariable=self.view_search_var, width=30).pack(side='left', padx=3)
ttk.Button(filter_frame, text="Cari", command=self.reload_view_table).pack(side='left', padx=3)
ttk.Button(filter_frame, text="Reset", command=self.reset_view_filters).pack(side='left', padx=3)
ttk.Button(filter_frame, text="Hanya Tersedia", command=lambda: self.reload_view_table(available_only=True)).pack(side='left', padx=6)
cols = ("ID","Nama","Kategori","Harga","Stok","Tersedia","ItemDisc%")
self.view_tree = ttk.Treeview(left, columns=cols, show='headings', height=18)
for c in cols:
self.view_tree.heading(c, text=c)
self.view_tree.column(c, width=90 if c!="Nama" else 200)
self.view_tree.pack(fill='both', expand=True)
self.view_tree.bind("<<TreeviewSelect>>", self.on_view_select)
ttk.Label(right, text="Preview Item", font=("Arial", 12, "bold")).pack(pady=6)
self.preview_label = ttk.Label(right, text="Pilih menu di kiri")
self.preview_label.pack()
self.preview_img_label = ttk.Label(right)
self.preview_img_label.pack(pady=6)
self.preview_detail = tk.Text(right, width=45, height=12)
self.preview_detail.pack()
self.reload_view_table()
def reload_view_table(self, available_only=False):
s = self.view_search_var.get().strip() if hasattr(self, 'view_search_var') else ""
results = menu_list(search_text=s or None, available_only=available_only)
for r in self.view_tree.get_children():
self.view_tree.delete(r)
for row in results:
mid,nama,kategori,harga,stok,foto,tersedia,item_disc = row
self.view_tree.insert("", tk.END, values=(mid,nama,kategori,harga,stok, "Yes" if tersedia else "No", item_disc))
def reset_view_filters(self):
self.view_search_var.set("")
self.reload_view_table()
def on_view_select(self, event):
sel = self.view_tree.selection()
if not sel:
return
item = self.view_tree.item(sel)['values']
menu_id = item[0]
data = menu_get(menu_id)
if not data:
return
mid,nama,kategori,harga,stok,foto,tersedia,item_disc = data
self.preview_detail.delete('1.0', tk.END)
txt = f"ID: {mid}\nNama: {nama}\nKategori: {kategori}\nHarga: {harga}\nStok: {stok}\nTersedia: {'Yes' if tersedia else 'No'}\nItem Discount: {item_disc}%\nFoto path: {foto}\n"
self.preview_detail.insert(tk.END, txt)
if foto and os.path.exists(foto):
try:
img = Image.open(foto)
img.thumbnail(IMG_PREVIEW_SIZE)
tkimg = ImageTk.PhotoImage(img)
self.img_cache['preview'] = tkimg
self.preview_img_label.config(image=tkimg)
except Exception as e:
self.preview_img_label.config(image='')
else:
self.preview_img_label.config(image='')
def build_menu_manage_tab(self, parent):
for w in parent.winfo_children():
w.destroy()
topfrm = ttk.Frame(parent)
topfrm.pack(fill='x', padx=6, pady=6)
ttk.Label(topfrm, text="Kelola Menu", font=("Arial", 14, "bold")).pack(side='left')
ttk.Button(topfrm, text="Tambah Menu", command=self.open_add_menu_window).pack(side='right', padx=6)
cols = ("ID","Nama","Kategori","Harga","Stok","Tersedia","ItemDisc%")
self.manage_tree = ttk.Treeview(parent, columns=cols, show='headings', height=18)
for c in cols:
self.manage_tree.heading(c, text=c)
self.manage_tree.column(c, width=100 if c!="Nama" else 220)
self.manage_tree.pack(fill='both', padx=6, pady=6)
btnfrm = ttk.Frame(parent)
btnfrm.pack(pady=6)
ttk.Button(btnfrm, text="Edit Terpilih", command=self.open_edit_menu_window).pack(side='left', padx=6)
ttk.Button(btnfrm, text="Hapus Terpilih", command=self.delete_selected_menu).pack(side='left', padx=6)
ttk.Button(btnfrm, text="Reload", command=self.reload_manage_table).pack(side='left', padx=6)
self.reload_manage_table()
def reload_manage_table(self):
for r in self.manage_tree.get_children():
self.manage_tree.delete(r)
rows = menu_list()
for row in rows:
mid,nama,kategori,harga,stok,foto,tersedia,item_disc = row
self.manage_tree.insert("", tk.END, values=(mid,nama,kategori,harga,stok,"Yes" if tersedia else "No", item_disc))
def open_add_menu_window(self):
w = tk.Toplevel(self.root)
w.title("Tambah Menu")
frm = ttk.Frame(w,padding=10)
frm.pack()
labels = ["Nama","Kategori","Harga","Stok","Foto path","Item Discount (%)"]
vars = {}
for i,lab in enumerate(labels):
ttk.Label(frm, text=lab).grid(row=i, column=0, sticky='e', pady=4)
vars[lab] = tk.StringVar()
ttk.Entry(frm, textvariable=vars[lab], width=40).grid(row=i, column=1, pady=4)
ttk.Button(frm, text="Pilih Foto", command=lambda: self.select_file(vars["Foto path"])).grid(row=4, column=2, padx=6)
def save():
try:
nama = vars["Nama"].get().strip()
kategori = vars["Kategori"].get().strip()
harga = float(vars["Harga"].get())
stok = int(vars["Stok"].get())
foto = vars["Foto path"].get().strip() or None
item_disc = float(vars["Item Discount (%)"].get() or 0)
except Exception as e:
messagebox.showerror("Input error", "Periksa kembali input (Harga/Stok harus angka)")
return
menu_add(nama,kategori,harga,stok,foto,item_disc)
messagebox.showinfo("Sukses","Menu ditambahkan")
w.destroy()
self.reload_manage_table()
self.reload_view_table()
ttk.Button(frm, text="Simpan", command=save).grid(row=len(labels), column=1, pady=8)
def open_edit_menu_window(self):
sel = self.manage_tree.selection()
if not sel:
messagebox.showwarning("Pilih", "Pilih menu terlebih dahulu")
return
item = self.manage_tree.item(sel)['values']
menu_id = item[0]
data = menu_get(menu_id)
if not data:
messagebox.showerror("Error", "Data menu tidak ditemukan")
return
mid,nama,kategori,harga,stok,foto,tersedia,item_disc = data
w = tk.Toplevel(self.root)
w.title("Edit Menu")
frm = ttk.Frame(w,padding=10)
frm.pack()
labels = ["Nama","Kategori","Harga","Stok","Foto path","Item Discount (%)"]
vars = {}
defaults = [nama,kategori,str(harga),str(stok),foto or "",str(item_disc or 0)]
for i,lab in enumerate(labels):
ttk.Label(frm, text=lab).grid(row=i, column=0, sticky='e', pady=4)
vars[lab] = tk.StringVar(value=defaults[i])
ttk.Entry(frm, textvariable=vars[lab], width=40).grid(row=i, column=1, pady=4)
ttk.Button(frm, text="Pilih Foto", command=lambda: self.select_file(vars["Foto path"])).grid(row=4, column=2, padx=6)
def save():
try:
nama = vars["Nama"].get().strip()
kategori = vars["Kategori"].get().strip()
harga = float(vars["Harga"].get())
stok = int(vars["Stok"].get())
foto = vars["Foto path"].get().strip() or None
item_disc = float(vars["Item Discount (%)"].get() or 0)
except:
messagebox.showerror("Input error", "Periksa input")
return
menu_update(menu_id, nama, kategori, harga, stok, foto, item_disc)
messagebox.showinfo("Sukses","Menu diperbarui")
w.destroy()
self.reload_manage_table()
self.reload_view_table()
ttk.Button(frm, text="Update", command=save).grid(row=len(labels), column=1, pady=8)
def delete_selected_menu(self):
sel = self.manage_tree.selection()
if not sel:
messagebox.showwarning("Pilih", "Pilih menu untuk dihapus")
return
item = self.manage_tree.item(sel)['values']
menu_id = item[0]
if messagebox.askyesno("Konfirmasi", "Hapus menu terpilih?"):
menu_delete(menu_id)
messagebox.showinfo("Dihapus", "Menu berhasil dihapus")
self.reload_manage_table()
self.reload_view_table()
def select_file(self, var):
p = filedialog.askopenfilename(title="Pilih file gambar",
filetypes=[("Image files","*.png;*.jpg;*.jpeg;*.gif;*.bmp"),("All files","*.*")])
if p:
var.set(p)
def build_promo_tab(self, parent):
for w in parent.winfo_children():
w.destroy()
top = ttk.Frame(parent)
top.pack(fill='x', pady=6)
ttk.Label(top, text="Promo Codes", font=("Arial", 14, "bold")).pack(side='left', padx=6)
ttk.Button(top, text="Tambah Promo", command=self.open_add_promo).pack(side='right', padx=6)
cols = ("Code","Type","Value","MinTotal")
self.promo_tree = ttk.Treeview(parent, columns=cols, show='headings', height=12)
for c in cols:
self.promo_tree.heading(c, text=c)
self.promo_tree.column(c, width=120)
self.promo_tree.pack(fill='x', padx=6, pady=6)
btnfrm = ttk.Frame(parent)
btnfrm.pack(pady=6)
ttk.Button(btnfrm, text="Edit Promo", command=self.open_edit_promo).pack(side='left', padx=6)
ttk.Button(btnfrm, text="Hapus Promo", command=self.delete_selected_promo).pack(side='left', padx=6)
ttk.Button(btnfrm, text="Reload", command=self.reload_promo_table).pack(side='left', padx=6)
self.reload_promo_table()
def reload_promo_table(self):
for r in self.promo_tree.get_children():
self.promo_tree.delete(r)
for p in promo_list():
self.promo_tree.insert("", tk.END, values=p)
def open_add_promo(self):
w = tk.Toplevel(self.root)
w.title("Tambah Promo")
w.geometry("350x230")
w.transient(self.root)
w.grab_set()
frm = ttk.Frame(w, padding=15)
frm.pack(fill="both", expand=True)
vars = {
'code': tk.StringVar(),
'type': tk.StringVar(value='percent'),
'value': tk.StringVar(),
'min_total': tk.StringVar(value='0')
}
ttk.Label(frm, text="Code:").grid(row=0, column=0, sticky='e', pady=5)
ttk.Entry(frm, textvariable=vars['code'], width=20).grid(row=0, column=1)
ttk.Label(frm, text="Type (percent/fixed):").grid(row=1, column=0, sticky='e', pady=5)
ttk.Entry(frm, textvariable=vars['type'], width=20).grid(row=1, column=1)
ttk.Label(frm, text="Value:").grid(row=2, column=0, sticky='e', pady=5)
ttk.Entry(frm, textvariable=vars['value'], width=20).grid(row=2, column=1)
ttk.Label(frm, text="Min Total:").grid(row=3, column=0, sticky='e', pady=5)
ttk.Entry(frm, textvariable=vars['min_total'], width=20).grid(row=3, column=1)
def save():
try:
code = vars['code'].get().strip().upper()
ptype = vars['type'].get().strip()
val = float(vars['value'].get())
mt = float(vars['min_total'].get() or 0)
if ptype not in ('percent', 'fixed'):
raise ValueError("type harus 'percent' atau 'fixed'")
except Exception as e:
messagebox.showerror("Error", f"Input salah: {e}")
return
try:
promo_add(code, ptype, val, mt)
messagebox.showinfo("Sukses", "Promo ditambahkan")
w.destroy()
self.reload_promo_table()
except Exception as e:
messagebox.showerror("Error", f"Kode promo sudah ada atau error: {e}")
ttk.Button(frm, text="Simpan", command=save).grid(row=4, column=1, pady=12)
def open_edit_promo(self):
sel = self.promo_tree.selection()
if not sel:
messagebox.showwarning("Pilih", "Pilih promo untuk diedit")
return
code = self.promo_tree.item(sel)['values'][0]
row = promo_get(code)
if not row:
messagebox.showerror("Error", "Promo tidak ditemukan")
return
code, ptype, val, min_total = row
w = tk.Toplevel(self.root)
w.title("Edit Promo")
w.geometry("350x230")
w.transient(self.root)
w.grab_set()
frm = ttk.Frame(w, padding=15)
frm.pack(fill="both", expand=True)
vars = {
'type': tk.StringVar(value=ptype),
'value': tk.StringVar(value=str(val)),
'min_total': tk.StringVar(value=str(min_total))
}
ttk.Label(frm, text=f"Code: {code}", font=("Arial", 10, "bold")).grid(row=0, column=0, columnspan=2, pady=5)
ttk.Label(frm, text="Type (percent/fixed):").grid(row=1, column=0, sticky='e', pady=5)
ttk.Entry(frm, textvariable=vars['type'], width=20).grid(row=1, column=1)
ttk.Label(frm, text="Value:").grid(row=2, column=0, sticky='e', pady=5)
ttk.Entry(frm, textvariable=vars['value'], width=20).grid(row=2, column=1)
ttk.Label(frm, text="Min Total:").grid(row=3, column=0, sticky='e', pady=5)
ttk.Entry(frm, textvariable=vars['min_total'], width=20).grid(row=3, column=1)
def save():
try:
ptype = vars['type'].get().strip()
val = float(vars['value'].get())
mt = float(vars['min_total'].get() or 0)
if ptype not in ('percent', 'fixed'):
raise ValueError("type harus 'percent' atau 'fixed'")
except Exception as e:
messagebox.showerror("Error", f"Input salah: {e}")
return
promo_update(code, ptype, val, mt)
messagebox.showinfo("Sukses", "Promo diperbarui")
w.destroy()
self.reload_promo_table()
ttk.Button(frm, text="Update", command=save).grid(row=4, column=1, pady=12)
def delete_selected_promo(self):
sel = self.promo_tree.selection()
if not sel:
messagebox.showwarning("Pilih", "Pilih promo")
return
code = self.promo_tree.item(sel)['values'][0]
if messagebox.askyesno("Konfirmasi", f"Hapus promo {code}?"):
promo_delete(code)
messagebox.showinfo("Dihapus","Promo terhapus")
self.reload_promo_table()
def build_order_tab(self, parent):
"""Tab untuk order menu dengan tampilan card seperti GrabFood/Gojek"""
for w in parent.winfo_children():
w.destroy()
# Split jadi 2 panel: kiri = menu cards, kanan = cart
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)
# === PANEL KIRI: Daftar Menu dengan Card ===
ttk.Label(left, text="Daftar Menu", font=("Arial", 12, "bold")).pack(pady=4)
# Filter search
search_frame = ttk.Frame(left)
search_frame.pack(fill='x', pady=4)
ttk.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)
# Scrollable frame untuk cards (DENGAN BACKGROUND PUTIH)
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")
# Bind mouse wheel untuk scroll
def _on_mousewheel(event):
canvas.yview_scroll(int(-1*(event.delta/120)), "units")
canvas.bind_all("<MouseWheel>", _on_mousewheel)
# === PANEL KANAN: Keranjang ===
ttk.Label(right, text="Keranjang Belanja", font=("Arial", 12, "bold")).pack(pady=4)
# Treeview cart
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)
# Tombol hapus item atau kosongkan cart
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)
# Info total
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)
# Input nomor meja dan promo (LAYOUT RAPI)
checkout_frame = ttk.Frame(right)
checkout_frame.pack(fill='x', pady=6, padx=10)
# Row 0: No. Meja
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()
ttk.Entry(checkout_frame, textvariable=self.order_meja_var, width=20).grid(row=0, column=1, columnspan=2, pady=3, sticky='ew')
# Row 1: Kode Promo
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')
# Configure columns
checkout_frame.columnconfigure(1, weight=1)
# Tombol checkout (PASTI KELIHATAN)
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()
# Init cart data
self.cart_items = []
# Load menu cards
self.reload_order_menu_cards()
def reload_order_menu_cards(self):
"""Load menu dalam bentuk cards dengan gambar + tombol +/-"""
# Clear existing cards
for widget in self.menu_cards_frame.winfo_children():
widget.destroy()
# Get menu data
search = self.order_search_var.get().strip() or None
results = menu_list(search_text=search, available_only=True)
# Buat dict untuk qty di cart
cart_dict = {}
for cart_item in self.cart_items:
cart_dict[cart_item['menu_id']] = cart_item['qty']
# Render cards dalam grid (2 kolom)
row = 0
col = 0
for menu_data in results:
mid, nama, kategori, harga, stok, foto, tersedia, item_disc = menu_data
# Create card frame (DENGAN BACKGROUND PUTIH + BORDER)
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')
# Gambar
if foto and os.path.exists(foto):
try:
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()
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()
# Nama menu
tk.Label(card, text=nama, font=("Arial", 11, "bold"), bg='white', wraplength=150).pack(pady=(5, 2))
# Kategori
tk.Label(card, text=kategori, font=("Arial", 9), fg='gray', bg='white').pack()
# Harga
harga_text = f"Rp {harga:,.0f}"
if item_disc > 0:
harga_text += f" (-{item_disc}%)"
tk.Label(card, text=harga_text, font=("Arial", 10, "bold"), fg='#4CAF50', bg='white').pack(pady=2)
else:
tk.Label(card, text=harga_text, font=("Arial", 10, "bold"), fg='green', bg='white').pack(pady=2)
# Stok info
tk.Label(card, text=f"Stok: {stok}", font=("Arial", 8), fg='blue', bg='white').pack(pady=2)
# Tombol +/- atau + saja
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:
# Tampilkan - [qty] +
tk.Button(
btn_frame,
text="",
font=("Arial", 12, "bold"),
bg='#FF5722',
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='#4CAF50',
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:
# Tampilkan tombol + aja
tk.Button(
btn_frame,
text=" Tambah",
font=("Arial", 10, "bold"),
bg='#4CAF50',
fg='white',
width=12,
borderwidth=0,
cursor='hand2',
command=lambda m=mid, s=stok: self.increase_from_card(m, s)
).pack()
# Next column
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):
"""Tambah qty dari tombol + di card"""
# Cek qty saat ini
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
# Cek stok
if current_qty >= stok:
messagebox.showwarning("Stok Habis", f"Stok hanya tersisa {stok}")
return
# Tambah qty
if cart_item_found:
cart_item_found['qty'] += 1
else:
self.cart_items.append({'menu_id': menu_id, 'qty': 1})
# Update tampilan
self.reload_order_menu_cards()
self.update_cart_display()
def decrease_from_card(self, menu_id):
"""Kurangi qty dari tombol - di card"""
for i, cart_item in enumerate(self.cart_items):
if cart_item['menu_id'] == menu_id:
cart_item['qty'] -= 1
# Kalau qty jadi 0, hapus dari cart
if cart_item['qty'] <= 0:
del self.cart_items[i]
# Update tampilan
self.reload_order_menu_cards()
self.update_cart_display()
return
# Wilayah dikuasai Favorite
def build_favorite_tab(self, parent):
"""Tab untuk melihat menu favorit dan history pesanan"""
for w in parent.winfo_children():
w.destroy()
# Header
header = ttk.Frame(parent)
header.pack(fill='x', padx=10, pady=8)
ttk.Label(header, text="🌟 Menu Favorit & History", font=("Arial", 14, "bold")).pack(side='left')
ttk.Button(header, text="🔄 Refresh", command=self.reload_favorite_tab).pack(side='right', padx=6)
# Split 2 panel: kiri = favorit, kanan = history
left = ttk.LabelFrame(parent, text="⭐ Menu Favorit Saya (Top 5)", padding=10)
left.pack(side='left', fill='both', expand=True, padx=10, pady=6)
right = ttk.LabelFrame(parent, text="📜 History Pesanan Terakhir", padding=10)
right.pack(side='right', fill='both', expand=True, padx=10, pady=6)
# === PANEL KIRI: Menu Favorit ===
# Treeview favorit
fav_cols = ("Rank", "Menu", "Kategori", "Harga", "Dipesan", "Terakhir")
self.favorite_tree = ttk.Treeview(left, columns=fav_cols, show='headings', height=12)
self.favorite_tree.heading("Rank", text="#")
self.favorite_tree.heading("Menu", text="Menu")
self.favorite_tree.heading("Kategori", text="Kategori")
self.favorite_tree.heading("Harga", text="Harga")
self.favorite_tree.heading("Dipesan", text="Dipesan")
self.favorite_tree.heading("Terakhir", text="Terakhir")
self.favorite_tree.column("Rank", width=40)
self.favorite_tree.column("Menu", width=150)
self.favorite_tree.column("Kategori", width=90)
self.favorite_tree.column("Harga", width=80)
self.favorite_tree.column("Dipesan", width=70)
self.favorite_tree.column("Terakhir", width=140)
self.favorite_tree.pack(fill='both', expand=True, pady=6)
# Tombol quick order
fav_btn_frame = ttk.Frame(left)
fav_btn_frame.pack(pady=6)
ttk.Label(fav_btn_frame, text="Quick Order:").pack(side='left', padx=6)
ttk.Button(fav_btn_frame, text="🛒 Pesan Lagi", command=self.quick_order_favorite).pack(side='left', padx=3)
# === PANEL KANAN: History Transaksi ===
# Treeview history
hist_cols = ("ID", "Tanggal", "Meja", "Total", "Status")
self.history_tree = ttk.Treeview(right, columns=hist_cols, show='headings', height=12)
self.history_tree.heading("ID", text="ID")
self.history_tree.heading("Tanggal", text="Tanggal")
self.history_tree.heading("Meja", text="Meja")
self.history_tree.heading("Total", text="Total")
self.history_tree.heading("Status", text="Status")
self.history_tree.column("ID", width=40)
self.history_tree.column("Tanggal", width=140)
self.history_tree.column("Meja", width=60)
self.history_tree.column("Total", width=100)
self.history_tree.column("Status", width=90)
self.history_tree.pack(fill='both', expand=True, pady=6)
# Bind event untuk lihat detail
self.history_tree.bind("<<TreeviewSelect>>", self.on_history_select)
# Detail history
detail_frame = ttk.Frame(right)
detail_frame.pack(fill='x', pady=6)
self.history_detail_text = tk.Text(detail_frame, height=8, font=("Courier New", 8), wrap='word')
hist_scroll = ttk.Scrollbar(detail_frame, orient='vertical', command=self.history_detail_text.yview)
self.history_detail_text.configure(yscrollcommand=hist_scroll.set)
self.history_detail_text.pack(side='left', fill='both', expand=True)
hist_scroll.pack(side='right', fill='y')
# Tombol history action
hist_btn_frame = ttk.Frame(right)
hist_btn_frame.pack(pady=6)
ttk.Button(hist_btn_frame, text="🔁 Pesan Ulang", command=self.reorder_from_history).pack(side='left', padx=3)
# Load data
self.reload_favorite_tab()
def reload_favorite_tab(self):
"""Load data favorit dan history"""
# Clear trees
for r in self.favorite_tree.get_children():
self.favorite_tree.delete(r)
for r in self.history_tree.get_children():
self.history_tree.delete(r)
# Load favorit
favorites = favorite_list(self.session['id'], limit=5)
rank = 1
for fav in favorites:
menu_id, count, last_ordered = fav
menu_data = menu_get(menu_id)
if not menu_data:
continue
_, nama, kategori, harga, stok, foto, tersedia, item_disc = menu_data
self.favorite_tree.insert("", tk.END, values=(
rank,
nama,
kategori,
f"Rp {harga:,.0f}",
f"{count}x",
last_ordered
))
rank += 1
# Load history transaksi
history = transaksi_list(user_id=self.session['id'])
for h in history:
tid, uid, meja, total, status, promo_code, tanggal = h
self.history_tree.insert("", tk.END, values=(
tid,
tanggal,
meja,
f"Rp {total:,.0f}",
status
))
def on_history_select(self, event):
"""Tampilkan detail history saat dipilih"""
sel = self.history_tree.selection()
if not sel:
return
item = self.history_tree.item(sel)['values']
transaksi_id = item[0]
# Get detail transaksi
transaksi_data = transaksi_get(transaksi_id)
if not transaksi_data:
return
tid, uid, meja, total, status, promo_code, subtotal, item_disc, promo_disc, tanggal = transaksi_data
detail_items = detail_transaksi_list(transaksi_id)
# Format detail
detail_text = f"TRANSAKSI #{tid} - {status.upper()}\n"
detail_text += f"{'='*40}\n"
detail_text += f"Tanggal: {tanggal}\n"
detail_text += f"Meja: {meja}\n\n"
detail_text += f"Item Pesanan:\n"
detail_text += f"{'-'*40}\n"
for detail in detail_items:
did, mid, qty, harga, subtotal_item = detail
menu_data = menu_get(mid)
if menu_data:
_, nama, kategori, _, _, _, _, _ = menu_data
detail_text += f"{nama}\n"
detail_text += f" {qty} x Rp {harga:,.0f} = Rp {subtotal_item:,.0f}\n"
detail_text += f"{'-'*40}\n"
detail_text += f"Subtotal: Rp {subtotal:,.0f}\n"
detail_text += f"Diskon: Rp {item_disc + promo_disc:,.0f}\n"
detail_text += f"TOTAL: Rp {total:,.0f}\n"
self.history_detail_text.delete('1.0', tk.END)
self.history_detail_text.insert('1.0', detail_text)
def quick_order_favorite(self):
"""Pesan ulang dari menu favorit yang dipilih"""
sel = self.favorite_tree.selection()
if not sel:
messagebox.showwarning("Pilih Menu", "Pilih menu favorit terlebih dahulu")
return
item = self.favorite_tree.item(sel)['values']
menu_name = item[1]
# Cari menu_id dari nama
all_menus = menu_list()
menu_id = None
for m in all_menus:
if m[1] == menu_name:
menu_id = m[0]
break
if not menu_id:
messagebox.showerror("Error", "Menu tidak ditemukan")
return
# Tambah ke cart (qty 1)
menu_data = menu_get(menu_id)
if not menu_data:
return
_, nama, kategori, harga, stok, foto, tersedia, item_disc = menu_data
if stok < 1:
messagebox.showwarning("Stok Habis", f"Stok {nama} habis")
return
# Cek apakah sudah ada di cart
found = False
for cart_item in self.cart_items:
if cart_item['menu_id'] == menu_id:
if cart_item['qty'] < stok:
cart_item['qty'] += 1
found = True
else:
messagebox.showwarning("Stok Habis", f"Stok {nama} hanya {stok}")
return
break
if not found:
self.cart_items.append({'menu_id': menu_id, 'qty': 1})
messagebox.showinfo("Ditambahkan", f"{nama} ditambahkan ke keranjang!\n\nSilakan ke tab 'Order Menu' untuk checkout.")
def reorder_from_history(self):
"""Pesan ulang semua item dari history yang dipilih"""
sel = self.history_tree.selection()
if not sel:
messagebox.showwarning("Pilih History", "Pilih history pesanan terlebih dahulu")
return
item = self.history_tree.item(sel)['values']
transaksi_id = item[0]
# Get detail items
detail_items = detail_transaksi_list(transaksi_id)
if not detail_items:
messagebox.showerror("Error", "Tidak ada detail pesanan")
return
# Tambah semua item ke cart
added_count = 0
for detail in detail_items:
did, mid, qty, harga, subtotal_item = detail
# Cek stok
menu_data = menu_get(mid)
if not menu_data:
continue
_, nama, kategori, harga_now, stok, foto, tersedia, item_disc = menu_data
if stok < qty:
messagebox.showwarning("Stok Kurang", f"Stok {nama} hanya {stok}, pesanan asli {qty}")
qty = stok
if qty <= 0:
continue
# Tambah ke cart
found = False
for cart_item in self.cart_items:
if cart_item['menu_id'] == mid:
new_qty = cart_item['qty'] + qty
if new_qty <= stok:
cart_item['qty'] = new_qty
found = True
added_count += 1
break
if not found:
self.cart_items.append({'menu_id': mid, 'qty': qty})
added_count += 1
if added_count > 0:
messagebox.showinfo("Berhasil", f"{added_count} item ditambahkan ke keranjang!\n\nSilakan ke tab 'Order Menu' untuk checkout.")
else:
messagebox.showwarning("Gagal", "Tidak ada item yang bisa ditambahkan (stok habis)")
def update_cart_display(self):
"""Update tampilan keranjang dan hitung total"""
# Clear tree
for r in self.cart_tree.get_children():
self.cart_tree.delete(r)
# Tampilkan item
for cart_item in self.cart_items:
menu_data = menu_get(cart_item['menu_id'])
if not menu_data:
print(f"WARNING: Menu ID {cart_item['menu_id']} tidak ditemukan!")
continue
_, nama, kategori, harga, stok, foto, tersedia, item_disc = menu_data
qty = cart_item['qty']
subtotal = harga * qty
# DEBUG
print(f"Menambahkan ke tree: {nama} x{qty} = {subtotal}")
self.cart_tree.insert("", tk.END, values=(nama, qty, f"{harga:,.0f}", f"{subtotal:,.0f}"))
# Hitung total dengan diskon
promo_code = self.order_promo_var.get().strip() or None
if self.cart_items: # PENTING: Hanya hitung kalau ada item
calc = apply_discounts_and_promo(self.cart_items, promo_code)
self.cart_subtotal_label.config(text=f"Subtotal: Rp {calc['subtotal']:,.0f}")
self.cart_discount_label.config(text=f"Diskon Item: Rp {calc['item_discount']:,.0f}")
self.cart_promo_label.config(text=f"Diskon Promo: Rp {calc['promo_discount']:,.0f}")
self.cart_total_label.config(text=f"TOTAL: Rp {calc['total']:,.0f}")
else:
# Reset label kalau cart kosong
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")
def remove_cart_item(self):
"""Hapus item dari cart"""
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.update_cart_display()
def clear_cart(self):
"""Kosongkan keranjang"""
if not self.cart_items:
return
if messagebox.askyesno("Konfirmasi", "Kosongkan keranjang?"):
self.cart_items = []
self.update_cart_display()
def checkout_order(self):
"""Simpan pesanan ke database"""
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
promo_code = self.order_promo_var.get().strip() or None
# Validasi promo
if promo_code:
promo_data = promo_get(promo_code)
if not promo_data:
messagebox.showwarning("Promo Invalid", "Kode promo tidak ditemukan")
return
# Simpan transaksi
success, result = transaksi_add(self.session['id'], nomor_meja, self.cart_items, promo_code)
if success:
messagebox.showinfo("Sukses", f"Pesanan berhasil! ID Transaksi: {result}\nStatus: Pending")
# Reset
self.cart_items = []
self.order_meja_var.set("")
self.order_promo_var.set("")
self.update_cart_display()
self.reload_order_menu_cards() # Refresh stok
else:
messagebox.showerror("Error", f"Gagal menyimpan pesanan: {result}")
# Wilayah dikuasai Waiter
def build_waiter_tab(self, parent):
"""Tab untuk waiter mengelola pesanan"""
for w in parent.winfo_children():
w.destroy()
# Header
header = ttk.Frame(parent)
header.pack(fill='x', padx=10, pady=6)
ttk.Label(header, text="Dashboard Waiter - Kelola Pesanan", font=("Arial", 13, "bold")).pack(side='left')
ttk.Button(header, text="🔄 Refresh", command=self.reload_waiter_orders).pack(side='right', padx=6)
# Filter status
filter_frame = ttk.Frame(parent)
filter_frame.pack(fill='x', padx=10, pady=4)
ttk.Label(filter_frame, text="Filter Status:").pack(side='left', padx=6)
ttk.Button(filter_frame, text="Semua", command=lambda: self.reload_waiter_orders(None), width=8).pack(side='left', padx=2)
ttk.Button(filter_frame, text="Pending", command=lambda: self.reload_waiter_orders('pending'), width=8).pack(side='left', padx=2)
ttk.Button(filter_frame, text="Menunggu", command=lambda: self.reload_waiter_orders('menunggu'), width=10).pack(side='left', padx=2)
ttk.Button(filter_frame, text="Diproses", command=lambda: self.reload_waiter_orders('diproses'), width=8).pack(side='left', padx=2)
ttk.Button(filter_frame, text="Selesai", command=lambda: self.reload_waiter_orders('selesai'), width=8).pack(side='left', padx=2)
# Treeview pesanan (LEBIH PENDEK)
tree_frame = ttk.Frame(parent)
tree_frame.pack(fill='x', padx=10, pady=4)
cols = ("ID", "User ID", "No.Meja", "Total", "Status", "Promo", "Tanggal")
self.waiter_tree = ttk.Treeview(tree_frame, columns=cols, show='headings', height=8)
self.waiter_tree.heading("ID", text="ID")
self.waiter_tree.heading("User ID", text="User ID")
self.waiter_tree.heading("No.Meja", text="No.Meja")
self.waiter_tree.heading("Total", text="Total")
self.waiter_tree.heading("Status", text="Status")
self.waiter_tree.heading("Promo", text="Promo")
self.waiter_tree.heading("Tanggal", text="Tanggal")
self.waiter_tree.column("ID", width=40)
self.waiter_tree.column("User ID", width=60)
self.waiter_tree.column("No.Meja", width=70)
self.waiter_tree.column("Total", width=100)
self.waiter_tree.column("Status", width=90)
self.waiter_tree.column("Promo", width=80)
self.waiter_tree.column("Tanggal", width=140)
# Scrollbar untuk tree
tree_scroll = ttk.Scrollbar(tree_frame, orient='vertical', command=self.waiter_tree.yview)
self.waiter_tree.configure(yscrollcommand=tree_scroll.set)
self.waiter_tree.pack(side='left', fill='both', expand=True)
tree_scroll.pack(side='right', fill='y')
# Bind event saat pilih pesanan
self.waiter_tree.bind("<<TreeviewSelect>>", self.on_waiter_select)
# Detail pesanan (LEBIH PENDEK - height=6 aja)
detail_frame = ttk.LabelFrame(parent, text="📋 Detail Pesanan", padding=8)
detail_frame.pack(fill='x', padx=10, pady=4)
# Frame untuk text + scrollbar
text_frame = ttk.Frame(detail_frame)
text_frame.pack(fill='both', expand=True)
self.waiter_detail_text = tk.Text(text_frame, height=6, width=100, font=("Courier New", 8), wrap='word')
detail_scroll = ttk.Scrollbar(text_frame, orient='vertical', command=self.waiter_detail_text.yview)
self.waiter_detail_text.configure(yscrollcommand=detail_scroll.set)
self.waiter_detail_text.pack(side='left', fill='both', expand=True)
detail_scroll.pack(side='right', fill='y')
# TOMBOL AKSI - PASTI KELIHATAN (TIDAK PAKAI expand=True)
action_frame = ttk.LabelFrame(parent, text="🎯 Ubah Status Pesanan", padding=12)
action_frame.pack(fill='x', padx=10, pady=8)
# Grid 2x2 untuk 4 tombol
ttk.Button(
action_frame,
text="✅ Terima (Pending → Menunggu)",
command=lambda: self.update_order_status('menunggu'),
width=35
).grid(row=0, column=0, padx=4, pady=4, sticky='ew')
ttk.Button(
action_frame,
text="🍳 Proses (Menunggu → Diproses)",
command=lambda: self.update_order_status('diproses'),
width=35
).grid(row=0, column=1, padx=4, pady=4, sticky='ew')
ttk.Button(
action_frame,
text="🍽️ Selesai (Diproses → Selesai)",
command=lambda: self.update_order_status('selesai'),
width=35
).grid(row=1, column=0, padx=4, pady=4, sticky='ew')
ttk.Button(
action_frame,
text="💰 Dibayar (Selesai → Dibayar)",
command=lambda: self.update_order_status('dibayar'),
width=35
).grid(row=1, column=1, padx=4, pady=4, sticky='ew')
# Buat kolom responsive
action_frame.columnconfigure(0, weight=1)
action_frame.columnconfigure(1, weight=1)
# Load data
self.reload_waiter_orders()
def reload_waiter_orders(self, status_filter=None):
"""Load pesanan untuk waiter"""
# Clear tree
for r in self.waiter_tree.get_children():
self.waiter_tree.delete(r)
# Get transaksi
if status_filter:
orders = transaksi_list(status=status_filter)
else:
# Tampilkan semua kecuali yang sudah dibayar
all_orders = transaksi_list()
orders = [o for o in all_orders if o[4] != 'dibayar']
# Insert ke tree
for order in orders:
tid, uid, meja, total, status, promo_code, tanggal = order
promo_display = promo_code if promo_code else "-"
self.waiter_tree.insert("", tk.END, values=(
tid, uid, meja, f"Rp {total:,.0f}", status, promo_display, tanggal
))
def on_waiter_select(self, event):
"""Tampilkan detail pesanan saat dipilih"""
sel = self.waiter_tree.selection()
if not sel:
return
item = self.waiter_tree.item(sel)['values']
transaksi_id = item[0]
# Get detail transaksi
transaksi_data = transaksi_get(transaksi_id)
if not transaksi_data:
return
tid, uid, meja, total, status, promo_code, subtotal, item_disc, promo_disc, tanggal = transaksi_data
# Get detail items
detail_items = detail_transaksi_list(transaksi_id)
# Format detail
detail_text = f"═══════════════════════════════════════════════\n"
detail_text += f"TRANSAKSI ID: {tid}\n"
detail_text += f"═══════════════════════════════════════════════\n\n"
detail_text += f"User ID : {uid}\n"
detail_text += f"Nomor Meja : {meja}\n"
detail_text += f"Status : {status.upper()}\n"
detail_text += f"Tanggal : {tanggal}\n"
detail_text += f"Kode Promo : {promo_code if promo_code else '-'}\n\n"
detail_text += f"───────────────────────────────────────────────\n"
detail_text += f"ITEM PESANAN:\n"
detail_text += f"───────────────────────────────────────────────\n"
for detail in detail_items:
did, mid, qty, harga, subtotal_item = detail
# Get nama menu
menu_data = menu_get(mid)
if menu_data:
_, nama, kategori, _, _, _, _, _ = menu_data
detail_text += f"{nama} ({kategori})\n"
detail_text += f" {qty} x Rp {harga:,.0f} = Rp {subtotal_item:,.0f}\n\n"
detail_text += f"───────────────────────────────────────────────\n"
detail_text += f"Subtotal : Rp {subtotal:,.0f}\n"
detail_text += f"Diskon Item : Rp {item_disc:,.0f}\n"
detail_text += f"Diskon Promo : Rp {promo_disc:,.0f}\n"
detail_text += f"───────────────────────────────────────────────\n"
detail_text += f"TOTAL BAYAR : Rp {total:,.0f}\n"
detail_text += f"═══════════════════════════════════════════════\n"
# Tampilkan di text widget
self.waiter_detail_text.delete('1.0', tk.END)
self.waiter_detail_text.insert('1.0', detail_text)
def update_order_status(self, new_status):
"""Update status pesanan yang dipilih"""
sel = self.waiter_tree.selection()
if not sel:
messagebox.showwarning("Pilih Pesanan", "Pilih pesanan terlebih dahulu")
return
item = self.waiter_tree.item(sel)['values']
transaksi_id = item[0]
current_status = item[4]
# Validasi flow status yang BENAR
valid_transitions = {
'pending': ['menunggu'],
'menunggu': ['diproses'],
'diproses': ['selesai'],
'selesai': ['dibayar']
}
# Cek apakah current status valid
if current_status not in valid_transitions:
messagebox.showerror("Error", f"Status '{current_status}' sudah final, tidak bisa diubah lagi")
return
# Cek apakah new_status valid untuk current_status
if new_status not in valid_transitions[current_status]:
expected = ', '.join(valid_transitions[current_status])
messagebox.showwarning("Status Tidak Valid",
f"❌ Tidak bisa ubah dari '{current_status}' ke '{new_status}'.\n\n"
f"✅ Status yang benar setelah '{current_status}' adalah:\n"
f"{expected}")
return
# Konfirmasi
if not messagebox.askyesno("Konfirmasi",
f"Ubah status pesanan #{transaksi_id}\n\n"
f"Dari: {current_status.upper()}\n"
f"Ke: {new_status.upper()}\n\n"
f"Lanjutkan?"):
return
# Update status
success = transaksi_update_status(transaksi_id, new_status)
if success:
messagebox.showinfo("✅ Berhasil", f"Status berhasil diubah menjadi '{new_status.upper()}'")
self.reload_waiter_orders()
# Auto select row yang sama
for item_id in self.waiter_tree.get_children():
if self.waiter_tree.item(item_id)['values'][0] == transaksi_id:
self.waiter_tree.selection_set(item_id)
self.waiter_tree.see(item_id)
# Trigger event untuk reload detail
self.on_waiter_select(None)
break
else:
messagebox.showerror("❌ Gagal", "Gagal mengubah status pesanan")
# Wilayah dikuasai Favorite
def favorite_update(user_id, menu_id):
"""Update atau tambah favorite count untuk user tertentu"""
from datetime import datetime
rows = read_all(FAVORITE_CSV)
found = False
for r in rows:
if r.get("user_id") == str(user_id) and r.get("menu_id") == str(menu_id):
# Update count
try:
count = int(r.get("order_count") or 0)
except:
count = 0
r["order_count"] = str(count + 1)
r["last_ordered"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
found = True
break
if not found:
# Tambah baru
rows.append({
"user_id": str(user_id),
"menu_id": str(menu_id),
"order_count": "1",
"last_ordered": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
})
write_all(FAVORITE_CSV, ["user_id", "menu_id", "order_count", "last_ordered"], rows)
def favorite_list(user_id, limit=5):
"""Ambil menu favorit user, sorted by order_count descending"""
rows = read_all(FAVORITE_CSV)
out = []
for r in rows:
if r.get("user_id") == str(user_id):
try:
mid = int(r.get("menu_id") or 0)
except:
mid = r.get("menu_id")
try:
count = int(r.get("order_count") or 0)
except:
count = 0
last_ordered = r.get("last_ordered")
out.append((mid, count, last_ordered))
# Sort by count descending
out.sort(key=lambda x: x[1], reverse=True)
# Limit results
if limit:
out = out[:limit]
return out
def favorite_all(limit=10):
"""Ambil menu paling populer dari semua user"""
rows = read_all(FAVORITE_CSV)
menu_counts = {}
for r in rows:
menu_id = r.get("menu_id")
try:
count = int(r.get("order_count") or 0)
except:
count = 0
if menu_id in menu_counts:
menu_counts[menu_id] += count
else:
menu_counts[menu_id] = count
# Convert to list dan sort
out = []
for menu_id, total_count in menu_counts.items():
try:
mid = int(menu_id)
except:
mid = menu_id
out.append((mid, total_count))
out.sort(key=lambda x: x[1], reverse=True)
if limit:
out = out[:limit]
return out
# WILAYAH DIKUASAI PEMBAYARAN & MEJA
# === FUNGSI PEMBAYARAN ===
def pembayaran_add(transaksi_id, metode_pembayaran, jumlah_bayar, status_pembayaran='sukses', struk=''):
"""Simpan data pembayaran baru"""
from datetime import datetime
rows = read_all(PEMBAYARAN_CSV)
new_id = next_int_id(rows, "id")
tanggal_bayar = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
rows.append({
"id": new_id,
"transaksi_id": str(transaksi_id),
"metode_pembayaran": metode_pembayaran,
"jumlah_bayar": str(float(jumlah_bayar)),
"status_pembayaran": status_pembayaran,
"tanggal_bayar": tanggal_bayar,
"struk": struk
})
write_all(PEMBAYARAN_CSV, ["id", "transaksi_id", "metode_pembayaran", "jumlah_bayar", "status_pembayaran", "tanggal_bayar", "struk"], rows)
return new_id
def pembayaran_get_by_transaksi(transaksi_id):
"""Ambil data pembayaran berdasarkan transaksi_id"""
rows = read_all(PEMBAYARAN_CSV)
for r in rows:
if r.get("transaksi_id") == str(transaksi_id):
try:
pid = int(r.get("id") or 0)
except:
pid = r.get("id")
try:
jumlah = float(r.get("jumlah_bayar") or 0.0)
except:
jumlah = 0.0
return (pid, r.get("metode_pembayaran"), jumlah, r.get("status_pembayaran"), r.get("tanggal_bayar"), r.get("struk"))
return None
def pembayaran_list(status=None, metode=None):
"""Ambil semua pembayaran dengan filter opsional"""
rows = read_all(PEMBAYARAN_CSV)
out = []
for r in rows:
if status and r.get("status_pembayaran") != status:
continue
if metode and r.get("metode_pembayaran") != metode:
continue
try:
pid = int(r.get("id") or 0)
except:
pid = r.get("id")
try:
tid = int(r.get("transaksi_id") or 0)
except:
tid = r.get("transaksi_id")
try:
jumlah = float(r.get("jumlah_bayar") or 0.0)
except:
jumlah = 0.0
out.append((pid, tid, r.get("metode_pembayaran"), jumlah, r.get("status_pembayaran"), r.get("tanggal_bayar")))
out.sort(key=lambda x: int(x[0]), reverse=True)
return out
# === FUNGSI MEJA ===
def meja_list(status=None):
"""Ambil daftar meja dengan filter status opsional"""
rows = read_all(MEJA_CSV)
out = []
for r in rows:
if status and r.get("status") != status:
continue
try:
nomor = int(r.get("nomor_meja") or 0)
except:
nomor = r.get("nomor_meja")
transaksi_id = r.get("transaksi_id") or ""
out.append((nomor, r.get("status"), transaksi_id))
out.sort(key=lambda x: int(x[0]) if isinstance(x[0], int) else 0)
return out
def meja_get(nomor_meja):
"""Ambil data meja berdasarkan nomor"""
rows = read_all(MEJA_CSV)
for r in rows:
if r.get("nomor_meja") == str(nomor_meja):
try:
nomor = int(r.get("nomor_meja") or 0)
except:
nomor = r.get("nomor_meja")
transaksi_id = r.get("transaksi_id") or ""
return (nomor, r.get("status"), transaksi_id)
return None
def meja_update_status(nomor_meja, new_status, transaksi_id=""):
"""Update status meja (kosong/terisi) dan link ke transaksi"""
rows = read_all(MEJA_CSV)
found = False
for r in rows:
if r.get("nomor_meja") == str(nomor_meja):
r["status"] = new_status
r["transaksi_id"] = str(transaksi_id) if transaksi_id else ""
found = True
break
if found:
write_all(MEJA_CSV, ["nomor_meja", "status", "transaksi_id"], rows)
return True
return False
def meja_tutup(nomor_meja):
"""Tutup meja (set ke kosong)"""
return meja_update_status(nomor_meja, "kosong", "")
def meja_buka(nomor_meja, transaksi_id):
"""Buka meja (set ke terisi dengan transaksi_id)"""
return meja_update_status(nomor_meja, "terisi", transaksi_id)
def build_payment_tab(self, parent):
"""Tab pembayaran untuk kasir & admin"""
for w in parent.winfo_children():
w.destroy()
# Header
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)
# Container utama
main_container = ttk.Frame(parent)
main_container.pack(fill='both', expand=True, padx=10, pady=6)
# === PANEL KIRI: Daftar Transaksi ===
left = ttk.LabelFrame(main_container, text="📋 Transaksi Siap Dibayar (Status: Selesai)", padding=10)
left.pack(side='left', fill='both', expand=True, padx=(0, 5))
# Treeview transaksi dengan scrollbar
tree_frame = ttk.Frame(left)
tree_frame.pack(fill='both', expand=True)
tree_scroll = ttk.Scrollbar(tree_frame, orient='vertical')
tree_scroll.pack(side='right', fill='y')
cols = ("ID", "Meja", "Total", "Status", "Tanggal")
self.payment_tree = ttk.Treeview(tree_frame, columns=cols, show='headings', height=10, 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 transaksi
detail_frame = ttk.Frame(left)
detail_frame.pack(fill='x', pady=(10, 0))
ttk.Label(detail_frame, text="Detail Transaksi:", font=("Arial", 9, "bold")).pack(anchor='w')
self.payment_detail_text = tk.Text(detail_frame, height=6, font=("Courier New", 8), wrap='word')
detail_scroll = ttk.Scrollbar(detail_frame, orient='vertical', command=self.payment_detail_text.yview)
self.payment_detail_text.configure(yscrollcommand=detail_scroll.set)
self.payment_detail_text.pack(side='left', fill='both', expand=True)
detail_scroll.pack(side='right', fill='y')
# === PANEL KANAN: Form Pembayaran ===
right_outer = ttk.LabelFrame(main_container, text="💳 Form Pembayaran", padding=5)
right_outer.pack(side='right', fill='both', expand=True, padx=(5, 0))
# Frame wrapper untuk canvas
wrapper = ttk.Frame(right_outer)
wrapper.pack(fill='both', expand=True)
# Scrollbar
scrollbar = ttk.Scrollbar(wrapper, orient='vertical')
scrollbar.pack(side='right', fill='y')
# Canvas dengan HEIGHT FIXED
canvas = tk.Canvas(wrapper, yscrollcommand=scrollbar.set, highlightthickness=0, height=450)
canvas.pack(side='left', fill='both', expand=True)
scrollbar.config(command=canvas.yview)
# Frame konten
right = ttk.Frame(canvas)
canvas_window = canvas.create_window((0, 0), window=right, anchor='nw')
# Update scroll region
def configure_scroll(event):
canvas.configure(scrollregion=canvas.bbox('all'))
right.bind('<Configure>', configure_scroll)
# Update width
def configure_canvas(event):
canvas.itemconfig(canvas_window, width=event.width)
canvas.bind('<Configure>', configure_canvas)
# Mouse wheel
def on_mousewheel(event):
canvas.yview_scroll(-1 * int(event.delta / 120), "units")
canvas.bind_all('<MouseWheel>', on_mousewheel)
# === ISI FORM ===
# Info transaksi
info_frame = ttk.Frame(right)
info_frame.pack(fill='x', pady=15, padx=15)
self.selected_transaksi_label = ttk.Label(info_frame, text="Belum ada transaksi dipilih", font=("Arial", 10, "bold"), foreground='red')
self.selected_transaksi_label.pack()
self.selected_total_label = ttk.Label(info_frame, text="Total: Rp 0", font=("Arial", 12, "bold"), foreground='green')
self.selected_total_label.pack(pady=4)
ttk.Separator(right, orient='horizontal').pack(fill='x', pady=15, padx=15)
# Metode pembayaran
method_frame = ttk.Frame(right)
method_frame.pack(fill='x', pady=10, padx=15)
ttk.Label(method_frame, text="💳 Pilih Metode Pembayaran:", font=("Arial", 10, "bold")).pack(anchor='w', pady=6)
self.payment_method_var = tk.StringVar(value='cash')
ttk.Radiobutton(method_frame, text="💵 Cash", variable=self.payment_method_var, value='cash', command=self.on_payment_method_change).pack(anchor='w', pady=3)
ttk.Radiobutton(method_frame, text="📱 QRIS", variable=self.payment_method_var, value='qris', command=self.on_payment_method_change).pack(anchor='w', pady=3)
ttk.Radiobutton(method_frame, text="💳 E-Wallet (GoPay/OVO/Dana)", variable=self.payment_method_var, value='ewallet', command=self.on_payment_method_change).pack(anchor='w', pady=3)
ttk.Separator(right, orient='horizontal').pack(fill='x', pady=15, padx=15)
# Frame input dinamis
self.payment_input_frame = ttk.Frame(right)
self.payment_input_frame.pack(fill='x', pady=10, padx=15)
self.build_cash_input()
ttk.Separator(right, orient='horizontal').pack(fill='x', pady=15, padx=15)
# Tombol
btn_frame = ttk.Frame(right)
btn_frame.pack(fill='x', pady=20, padx=15)
ttk.Button(btn_frame, text="✅ PROSES PEMBAYARAN", command=self.process_payment, style="Accent.TButton").pack(ipadx=30, ipady=10)
# PENTING: Tambah banyak space di bawah biar scrollbar muncul
for i in range(20):
ttk.Label(right, text="").pack()
# Load data
self.reload_payment_orders()
def reload_payment_orders(self):
"""Load transaksi dengan status 'selesai' yang belum dibayar"""
# Clear tree
for r in self.payment_tree.get_children():
self.payment_tree.delete(r)
# Get transaksi selesai
orders = transaksi_list(status='selesai')
# Filter yang belum ada pembayaran
for order in orders:
tid, uid, meja, total, status, promo_code, tanggal = order
# Cek apakah sudah ada pembayaran
payment_data = pembayaran_get_by_transaksi(tid)
if payment_data:
continue # Skip jika sudah dibayar
self.payment_tree.insert("", tk.END, values=(
tid, meja, f"Rp {total:,.0f}", status, tanggal
))
def on_payment_select(self, event):
"""Tampilkan detail transaksi saat dipilih"""
sel = self.payment_tree.selection()
if not sel:
return
item = self.payment_tree.item(sel)['values']
transaksi_id = item[0]
# Get detail transaksi
transaksi_data = transaksi_get(transaksi_id)
if not transaksi_data:
return
tid, uid, meja, total, status, promo_code, subtotal, item_disc, promo_disc, tanggal = transaksi_data
detail_items = detail_transaksi_list(transaksi_id)
# Update label
self.selected_transaksi_label.config(text=f"Transaksi #{tid} - Meja {meja}", foreground='blue')
self.selected_total_label.config(text=f"Total: Rp {total:,.0f}")
# Format detail
detail_text = f"═══════════════════════════════════════════════\n"
detail_text += f"TRANSAKSI #{tid}\n"
detail_text += f"═══════════════════════════════════════════════\n\n"
detail_text += f"Meja : {meja}\n"
detail_text += f"Tanggal : {tanggal}\n"
detail_text += f"Status : {status.upper()}\n"
detail_text += f"Promo : {promo_code if promo_code else '-'}\n\n"
detail_text += f"───────────────────────────────────────────────\n"
detail_text += f"ITEM PESANAN:\n"
detail_text += f"───────────────────────────────────────────────\n"
for detail in detail_items:
did, mid, qty, harga, subtotal_item = detail
menu_data = menu_get(mid)
if menu_data:
_, nama, kategori, _, _, _, _, _ = menu_data
detail_text += f"{nama}\n"
detail_text += f" {qty} x Rp {harga:,.0f} = Rp {subtotal_item:,.0f}\n\n"
detail_text += f"───────────────────────────────────────────────\n"
detail_text += f"Subtotal : Rp {subtotal:,.0f}\n"
detail_text += f"Diskon : Rp {item_disc + promo_disc:,.0f}\n"
detail_text += f"───────────────────────────────────────────────\n"
detail_text += f"TOTAL BAYAR : Rp {total:,.0f}\n"
detail_text += f"═══════════════════════════════════════════════\n"
self.payment_detail_text.delete('1.0', tk.END)
self.payment_detail_text.insert('1.0', detail_text)
def on_payment_method_change(self):
"""Ganti input form sesuai metode pembayaran"""
method = self.payment_method_var.get()
# Clear frame
for w in self.payment_input_frame.winfo_children():
w.destroy()
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):
"""Form input untuk pembayaran cash"""
ttk.Label(self.payment_input_frame, text="💵 Pembayaran Cash", font=("Arial", 10, "bold")).pack(pady=6)
input_frame = ttk.Frame(self.payment_input_frame)
input_frame.pack(fill='x', pady=6)
ttk.Label(input_frame, text="Jumlah Bayar:").grid(row=0, column=0, sticky='w', pady=4)
self.cash_amount_var = tk.StringVar()
ttk.Entry(input_frame, textvariable=self.cash_amount_var, width=20).grid(row=0, column=1, pady=4, sticky='ew')
input_frame.columnconfigure(1, weight=1)
# Label kembalian
self.cash_change_label = ttk.Label(self.payment_input_frame, text="Kembalian: Rp 0", font=("Arial", 10), foreground='green')
self.cash_change_label.pack(pady=6)
# Bind event untuk hitung kembalian real-time
self.cash_amount_var.trace('w', self.calculate_cash_change)
def calculate_cash_change(self, *args):
"""Hitung kembalian cash secara real-time"""
sel = self.payment_tree.selection()
if not sel:
return
item = self.payment_tree.item(sel)['values']
transaksi_id = item[0]
transaksi_data = transaksi_get(transaksi_id)
if not transaksi_data:
return
total = transaksi_data[3]
try:
cash_input = float(self.cash_amount_var.get() or 0)
change = cash_input - total
if change < 0:
self.cash_change_label.config(text=f"Kembalian: Kurang Rp {abs(change):,.0f}", foreground='red')
else:
self.cash_change_label.config(text=f"Kembalian: Rp {change:,.0f}", foreground='green')
except:
self.cash_change_label.config(text="Kembalian: Rp 0", foreground='gray')
def build_qris_input(self):
"""Form input untuk pembayaran QRIS (simulasi QR Code)"""
ttk.Label(self.payment_input_frame, text="📱 Pembayaran QRIS", font=("Arial", 10, "bold")).pack(pady=6)
ttk.Label(self.payment_input_frame, text="Scan QR Code di bawah untuk bayar:", font=("Arial", 9)).pack(pady=4)
# Generate QR Code
sel = self.payment_tree.selection()
if sel:
item = self.payment_tree.item(sel)['values']
transaksi_id = item[0]
transaksi_data = transaksi_get(transaksi_id)
if transaksi_data:
total = transaksi_data[3]
# QR Code data (simulasi)
qr_data = f"QRIS-CAFE-TOTORO-TRX{transaksi_id}-TOTAL{int(total)}"
try:
import qrcode
qr = qrcode.QRCode(version=1, box_size=5, border=2)
qr.add_data(qr_data)
qr.make(fit=True)
qr_img = qr.make_image(fill_color="black", back_color="white")
qr_img = qr_img.resize((200, 200), Image.Resampling.LANCZOS)
qr_photo = ImageTk.PhotoImage(qr_img)
self.img_cache['qris'] = qr_photo
qr_label = ttk.Label(self.payment_input_frame, image=qr_photo)
qr_label.pack(pady=10)
except ImportError:
ttk.Label(self.payment_input_frame, text="[QR Code - Install qrcode library]", background='#e0e0e0', font=("Arial", 9)).pack(pady=10)
ttk.Label(self.payment_input_frame, text="⚠️ Ini adalah SIMULASI QRIS", font=("Arial", 8), foreground='orange').pack(pady=6)
ttk.Label(self.payment_input_frame, text="Klik 'PROSES PEMBAYARAN' untuk konfirmasi", font=("Arial", 8)).pack()
def build_ewallet_input(self):
"""Form input untuk pembayaran E-Wallet (simulasi)"""
ttk.Label(self.payment_input_frame, text="💳 Pembayaran E-Wallet", font=("Arial", 10, "bold")).pack(pady=6)
ttk.Label(self.payment_input_frame, text="Pilih E-Wallet:", font=("Arial", 9)).pack(pady=4)
self.ewallet_type_var = tk.StringVar(value='gopay')
ewallet_frame = ttk.Frame(self.payment_input_frame)
ewallet_frame.pack(pady=6)
ttk.Radiobutton(ewallet_frame, text="GoPay", variable=self.ewallet_type_var, value='gopay').pack(anchor='w', pady=2)
ttk.Radiobutton(ewallet_frame, text="OVO", variable=self.ewallet_type_var, value='ovo').pack(anchor='w', pady=2)
ttk.Radiobutton(ewallet_frame, text="Dana", variable=self.ewallet_type_var, value='dana').pack(anchor='w', pady=2)
ttk.Radiobutton(ewallet_frame, text="ShopeePay", variable=self.ewallet_type_var, value='shopeepay').pack(anchor='w', pady=2)
ttk.Label(self.payment_input_frame, text="⚠️ Ini adalah SIMULASI E-Wallet", font=("Arial", 8), foreground='orange').pack(pady=6)
ttk.Label(self.payment_input_frame, text="Klik 'PROSES PEMBAYARAN' untuk konfirmasi", font=("Arial", 8)).pack()
def process_payment(self):
"""Proses pembayaran sesuai metode yang dipilih"""
# Cek apakah ada transaksi terpilih
sel = self.payment_tree.selection()
if not sel:
messagebox.showwarning("Pilih Transaksi", "Pilih transaksi yang akan dibayar")
return
item = self.payment_tree.item(sel)['values']
transaksi_id = item[0]
transaksi_data = transaksi_get(transaksi_id)
if not transaksi_data:
messagebox.showerror("Error", "Data transaksi tidak ditemukan")
return
tid, uid, meja, total, status, promo_code, subtotal, item_disc, promo_disc, tanggal = transaksi_data
# Get metode pembayaran
method = self.payment_method_var.get()
# Validasi & proses sesuai metode
if method == 'cash':
success, message = self.process_cash_payment(tid, total)
elif method == 'qris':
success, message = self.process_qris_payment(tid, total)
elif method == 'ewallet':
success, message = self.process_ewallet_payment(tid, total)
else:
messagebox.showerror("Error", "Metode pembayaran tidak valid")
return
if success:
# Generate struk
struk = self.generate_struk(tid)
# Update status transaksi jadi 'dibayar'
transaksi_update_status(tid, 'dibayar')
# Tutup meja
meja_tutup(meja)
# Tampilkan struk
self.show_struk(struk)
# Reload data
self.reload_payment_orders()
# Reset selection
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)
else:
messagebox.showerror("Pembayaran Gagal", message)
def process_cash_payment(self, transaksi_id, total):
"""Proses pembayaran cash"""
try:
cash_input = float(self.cash_amount_var.get() or 0)
except:
return False, "Jumlah bayar tidak valid"
if cash_input < total:
return False, f"Uang kurang! Total: Rp {total:,.0f}, Bayar: Rp {cash_input:,.0f}"
# Simpan pembayaran
pembayaran_add(transaksi_id, 'cash', cash_input, 'sukses', '')
return True, "Pembayaran cash berhasil"
def process_qris_payment(self, transaksi_id, total):
"""Proses pembayaran QRIS (simulasi)"""
# Konfirmasi
confirm = messagebox.askyesno("Konfirmasi QRIS",
f"Pembayaran QRIS sebesar Rp {total:,.0f}\n\n"
f"⚠️ SIMULASI: Anggap customer sudah scan QR Code\n\n"
f"Lanjutkan pembayaran?")
if not confirm:
return False, "Pembayaran dibatalkan"
# Simpan pembayaran
pembayaran_add(transaksi_id, 'qris', total, 'sukses', '')
return True, "Pembayaran QRIS berhasil"
def process_ewallet_payment(self, transaksi_id, total):
"""Proses pembayaran E-Wallet (simulasi)"""
ewallet_type = self.ewallet_type_var.get()
ewallet_name = ewallet_type.upper()
# Konfirmasi
confirm = messagebox.askyesno("Konfirmasi E-Wallet",
f"Pembayaran {ewallet_name} sebesar Rp {total:,.0f}\n\n"
f"⚠️ SIMULASI: Anggap customer sudah konfirmasi di app\n\n"
f"Lanjutkan pembayaran?")
if not confirm:
return False, "Pembayaran dibatalkan"
# Simpan pembayaran
pembayaran_add(transaksi_id, f'ewallet-{ewallet_type}', total, 'sukses', '')
return True, f"Pembayaran {ewallet_name} berhasil"
def generate_struk(self, transaksi_id):
"""Generate struk transaksi"""
transaksi_data = transaksi_get(transaksi_id)
if not transaksi_data:
return ""
tid, uid, meja, total, status, promo_code, subtotal, item_disc, promo_disc, tanggal = transaksi_data
detail_items = detail_transaksi_list(transaksi_id)
payment_data = pembayaran_get_by_transaksi(transaksi_id)
struk = "═════════════════════════════════════════\n"
struk += " CAFE TOTORO MANIA\n"
struk += " Jl. Raya Kampus No. 123, Surabaya\n"
struk += " Telp: 031-123456\n"
struk += "═════════════════════════════════════════\n\n"
struk += f"No. Transaksi : {tid}\n"
struk += f"Tanggal : {tanggal}\n"
struk += f"Meja : {meja}\n"
struk += f"Kasir : {self.session['username']}\n"
if payment_data:
metode = payment_data[1]
struk += f"Pembayaran : {metode.upper()}\n"
struk += "─────────────────────────────────────────\n"
struk += "ITEM PESANAN:\n"
struk += "─────────────────────────────────────────\n"
for detail in detail_items:
did, mid, qty, harga, subtotal_item = detail
menu_data = menu_get(mid)
if menu_data:
_, nama, kategori, _, _, _, _, _ = menu_data
struk += f"{nama}\n"
struk += f" {qty} x Rp {harga:,.0f}".ljust(30)
struk += f"Rp {subtotal_item:,.0f}\n"
struk += "─────────────────────────────────────────\n"
struk += f"Subtotal : Rp {subtotal:,.0f}\n"
if item_disc > 0:
struk += f"Diskon Item : Rp {item_disc:,.0f}\n"
if promo_disc > 0:
struk += f"Diskon Promo : Rp {promo_disc:,.0f}\n"
struk += "─────────────────────────────────────────\n"
struk += f"TOTAL BAYAR : Rp {total:,.0f}\n"
# Jika cash, tampilkan bayar & kembalian
if payment_data and payment_data[1] == 'cash':
jumlah_bayar = payment_data[2]
kembalian = jumlah_bayar - total
struk += f"Bayar : Rp {jumlah_bayar:,.0f}\n"
struk += f"Kembalian : Rp {kembalian:,.0f}\n"
struk += "═════════════════════════════════════════\n"
struk += " TERIMA KASIH ATAS KUNJUNGAN ANDA\n"
struk += " SAMPAI JUMPA LAGI!\n"
struk += "═════════════════════════════════════════\n"
return struk
def show_struk(self, struk):
"""Tampilkan struk dalam popup window"""
w = tk.Toplevel(self.root)
w.title("✅ Pembayaran Berhasil - Struk Transaksi")
w.geometry("500x600")
w.transient(self.root)
w.grab_set()
# Frame untuk struk
frm = ttk.Frame(w, padding=15)
frm.pack(fill='both', expand=True)
ttk.Label(frm, text="✅ PEMBAYARAN BERHASIL!", font=("Arial", 14, "bold"), foreground='green').pack(pady=10)
# Text widget untuk struk
struk_text = tk.Text(frm, width=50, height=28, font=("Courier New", 9), wrap='word')
struk_scroll = ttk.Scrollbar(frm, orient='vertical', command=struk_text.yview)
struk_text.configure(yscrollcommand=struk_scroll.set)
struk_text.insert('1.0', struk)
struk_text.config(state='disabled') # Read-only
struk_text.pack(side='left', fill='both', expand=True)
struk_scroll.pack(side='right', fill='y')
# Tombol
btn_frame = ttk.Frame(w)
btn_frame.pack(pady=10)
ttk.Button(btn_frame, text="🖨️ Print (Simulasi)", command=lambda: self.print_struk_simulation(struk)).pack(side='left', padx=6)
ttk.Button(btn_frame, text="✅ Tutup", command=w.destroy).pack(side='left', padx=6)
def print_struk_simulation(self, struk):
"""Simulasi print struk (save to file)"""
from datetime import datetime
filename = f"struk_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
try:
with open(filename, 'w', encoding='utf-8') as f:
f.write(struk)
messagebox.showinfo("Print Simulasi", f"Struk berhasil disimpan ke file:\n{filename}")
except Exception as e:
messagebox.showerror("Error", f"Gagal menyimpan struk: {e}")
# Done
if __name__ == "__main__":
init_db_csv()
root = tk.Tk()
app = App(root)
root.mainloop()