1636 lines
59 KiB
Python
1636 lines
59 KiB
Python
"""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
|
||
from PIL import Image, ImageTk
|
||
from datetime import datetime
|
||
|
||
USERS_CSV = "users.csv"
|
||
MENU_CSV = "menu.csv"
|
||
PROMO_CSV = "promo.csv"
|
||
TRANSAKSI_CSV = "transaksi.csv"
|
||
DETAIL_TRANSAKSI_CSV = "detail_transaksi.csv"
|
||
FAVORITES_CSV = "favorites.csv"
|
||
IMG_PREVIEW_SIZE = (120, 80)
|
||
|
||
COLORS = {
|
||
'primary': '#2C3E50', # Dark Blue
|
||
'secondary': '#E67E22', # Orange
|
||
'success': '#27AE60', # Green
|
||
'danger': '#E74C3C', # Red
|
||
'warning': '#F39C12', # Yellow
|
||
'info': '#3498DB', # Light Blue
|
||
'light': '#ECF0F1', # Light Gray
|
||
'dark': '#34495E', # Dark Gray
|
||
'bg_gradient_start': '#667eea', # Purple
|
||
'bg_gradient_end': '#764ba2', # Dark Purple
|
||
'cafe_brown': '#8B4513', # Saddle Brown
|
||
'cafe_cream': '#F5DEB3', # Wheat
|
||
'cafe_green': '#228B22', # Forest Green
|
||
}
|
||
|
||
|
||
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", "tanggal", "nomor_meja", "total", "status", "metode_pembayaran"])
|
||
ensure_file(DETAIL_TRANSAKSI_CSV, ["id", "transaksi_id", "menu_id", "nama_menu", "jumlah", "harga_satuan", "subtotal"])
|
||
ensure_file(FAVORITES_CSV, ["user_id", "menu_id", "count"])
|
||
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)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
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
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
# 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)
|
||
}
|
||
|
||
# TRANSAKSI FUNCTIONS (2nd Person : Jevvvv)
|
||
from datetime import datetime
|
||
|
||
def create_transaksi(user_id, nomor_meja, cart_items):
|
||
"""Membuat transaksi baru dengan status 'Menunggu'"""
|
||
rows = read_all(TRANSAKSI_CSV)
|
||
trans_id = next_int_id(rows, "id")
|
||
|
||
# Hitung total
|
||
total = 0.0
|
||
for item in cart_items:
|
||
total += item['subtotal']
|
||
|
||
trans = {
|
||
"id": trans_id,
|
||
"user_id": str(user_id),
|
||
"tanggal": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||
"nomor_meja": str(nomor_meja),
|
||
"total": str(total),
|
||
"status": "Menunggu", # Status: Menunggu, Diproses, Selesai
|
||
"metode_pembayaran": ""
|
||
}
|
||
rows.append(trans)
|
||
write_all(TRANSAKSI_CSV, ["id", "user_id", "tanggal", "nomor_meja", "total", "status", "metode_pembayaran"], rows)
|
||
|
||
# Simpan detail transaksi
|
||
detail_rows = read_all(DETAIL_TRANSAKSI_CSV)
|
||
for item in cart_items:
|
||
detail_id = next_int_id(detail_rows, "id")
|
||
detail = {
|
||
"id": detail_id,
|
||
"transaksi_id": trans_id,
|
||
"menu_id": str(item['menu_id']),
|
||
"nama_menu": item['nama'],
|
||
"jumlah": str(item['qty']),
|
||
"harga_satuan": str(item['harga']),
|
||
"subtotal": str(item['subtotal'])
|
||
}
|
||
detail_rows.append(detail)
|
||
write_all(DETAIL_TRANSAKSI_CSV, ["id", "transaksi_id", "menu_id", "nama_menu", "jumlah", "harga_satuan", "subtotal"], detail_rows)
|
||
|
||
# Kurangi stok
|
||
for item in cart_items:
|
||
menu_decrease_stock(item['menu_id'], item['qty'])
|
||
|
||
return trans_id
|
||
|
||
|
||
def get_all_transaksi(status=None):
|
||
"""Ambil semua transaksi, bisa filter by status"""
|
||
rows = read_all(TRANSAKSI_CSV)
|
||
result = []
|
||
for r in rows:
|
||
if status and r.get("status") != status:
|
||
continue
|
||
result.append({
|
||
'id': r.get('id'),
|
||
'user_id': r.get('user_id'),
|
||
'tanggal': r.get('tanggal'),
|
||
'nomor_meja': r.get('nomor_meja'),
|
||
'total': float(r.get('total', 0)),
|
||
'status': r.get('status'),
|
||
'metode_pembayaran': r.get('metode_pembayaran', '')
|
||
})
|
||
return result
|
||
|
||
|
||
def get_transaksi_detail(transaksi_id):
|
||
"""Ambil detail item dari transaksi"""
|
||
rows = read_all(DETAIL_TRANSAKSI_CSV)
|
||
result = []
|
||
for r in rows:
|
||
if r.get("transaksi_id") == str(transaksi_id):
|
||
result.append({
|
||
'id': r.get('id'),
|
||
'menu_id': r.get('menu_id'),
|
||
'nama_menu': r.get('nama_menu'),
|
||
'jumlah': int(r.get('jumlah', 0)),
|
||
'harga_satuan': float(r.get('harga_satuan', 0)),
|
||
'subtotal': float(r.get('subtotal', 0))
|
||
})
|
||
return result
|
||
|
||
|
||
def update_transaksi_status(transaksi_id, status):
|
||
"""Update status transaksi (Menunggu -> Diproses -> Selesai)"""
|
||
rows = read_all(TRANSAKSI_CSV)
|
||
for r in rows:
|
||
if r.get("id") == str(transaksi_id):
|
||
r["status"] = status
|
||
break
|
||
write_all(TRANSAKSI_CSV, ["id", "user_id", "tanggal", "nomor_meja", "total", "status", "metode_pembayaran"], rows)
|
||
|
||
|
||
# FAVORITES FUNCTIONS (2nd Person: JEVVVV)
|
||
def add_to_favorites(user_id, menu_id):
|
||
"""Tambah atau update counter menu favorit user"""
|
||
rows = read_all(FAVORITES_CSV)
|
||
found = False
|
||
for r in rows:
|
||
if r.get("user_id") == str(user_id) and r.get("menu_id") == str(menu_id):
|
||
r["count"] = str(int(r.get("count", 0)) + 1)
|
||
found = True
|
||
break
|
||
|
||
if not found:
|
||
rows.append({
|
||
"user_id": str(user_id),
|
||
"menu_id": str(menu_id),
|
||
"count": "1"
|
||
})
|
||
|
||
write_all(FAVORITES_CSV, ["user_id", "menu_id", "count"], rows)
|
||
|
||
|
||
def get_user_favorites(user_id, limit=5):
|
||
"""Ambil menu favorit user (paling sering dipesan)"""
|
||
rows = read_all(FAVORITES_CSV)
|
||
user_favs = []
|
||
for r in rows:
|
||
if r.get("user_id") == str(user_id):
|
||
user_favs.append({
|
||
'menu_id': int(r.get("menu_id")),
|
||
'count': int(r.get("count", 0))
|
||
})
|
||
|
||
# Sort by count descending
|
||
user_favs.sort(key=lambda x: x['count'], reverse=True)
|
||
|
||
# Ambil detail menu
|
||
result = []
|
||
for fav in user_favs[:limit]:
|
||
menu = menu_get(fav['menu_id'])
|
||
if menu:
|
||
result.append({
|
||
'menu': menu,
|
||
'count': fav['count']
|
||
})
|
||
|
||
return result
|
||
|
||
|
||
|
||
|
||
# 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.cart = []
|
||
self.setup_styles()
|
||
self.setup_ui()
|
||
|
||
def setup_styles(self):
|
||
style = ttk.Style()
|
||
style.theme_use('clam')
|
||
|
||
# Button styles
|
||
style.configure('Primary.TButton', background=COLORS['secondary'],
|
||
foreground='white', font=('Arial', 10, 'bold'))
|
||
style.map('Primary.TButton', background=[('active', COLORS['warning'])])
|
||
|
||
style.configure('Success.TButton', background=COLORS['success'],
|
||
foreground='white', font=('Arial', 10, 'bold'))
|
||
|
||
style.configure('Danger.TButton', background=COLORS['danger'],
|
||
foreground='white', font=('Arial', 10, 'bold'))
|
||
|
||
# Treeview style
|
||
style.configure('Treeview', background=COLORS['light'],
|
||
foreground=COLORS['dark'], font=('Arial', 9))
|
||
style.configure('Treeview.Heading', background=COLORS['primary'],
|
||
foreground='white', font=('Arial', 10, 'bold'))
|
||
style.map('Treeview', background=[('selected', COLORS['info'])])
|
||
|
||
def setup_ui(self):
|
||
self.root.geometry("1000x650")
|
||
self.root.resizable(False, False)
|
||
self.login_frame()
|
||
|
||
def login_frame(self):
|
||
for w in self.root.winfo_children():
|
||
w.destroy()
|
||
|
||
# Main container dengan background cafe
|
||
main_frame = tk.Frame(self.root, bg=COLORS['cafe_brown'])
|
||
main_frame.pack(fill='both', expand=True)
|
||
|
||
# Center frame
|
||
center = tk.Frame(main_frame, bg='white', relief='raised', bd=3)
|
||
center.place(relx=0.5, rely=0.5, anchor='center', width=500, height=550)
|
||
|
||
# Logo/Header Section
|
||
header_frame = tk.Frame(center, bg=COLORS['cafe_green'], height=120)
|
||
header_frame.pack(fill='x')
|
||
|
||
tk.Label(header_frame, text="☕", font=("Arial", 48),
|
||
bg=COLORS['cafe_green'], fg='white').pack(pady=5)
|
||
|
||
tk.Label(header_frame, text="CAFE TOTORO MANIA",
|
||
font=("Arial", 24, "bold"), bg=COLORS['cafe_green'],
|
||
fg='white').pack()
|
||
|
||
tk.Label(header_frame, text="~ Your Cozy Coffee Corner ~",
|
||
font=("Arial", 11, "italic"), bg=COLORS['cafe_green'],
|
||
fg=COLORS['cafe_cream']).pack(pady=2)
|
||
|
||
# Form Section
|
||
form_frame = tk.Frame(center, bg='white', padx=40, pady=30)
|
||
form_frame.pack(fill='both', expand=True)
|
||
|
||
tk.Label(form_frame, text="Selamat Datang! 👋",
|
||
font=("Arial", 18, "bold"), bg='white',
|
||
fg=COLORS['cafe_brown']).pack(pady=(0,10))
|
||
|
||
tk.Label(form_frame, text="Silakan login untuk melanjutkan",
|
||
font=("Arial", 10), bg='white',
|
||
fg=COLORS['dark']).pack(pady=(0,25))
|
||
|
||
# Username
|
||
tk.Label(form_frame, text="👤 Username", font=("Arial", 11, "bold"),
|
||
bg='white', fg=COLORS['dark']).pack(anchor='w', pady=(10,5))
|
||
|
||
self.username_var = tk.StringVar()
|
||
username_entry = tk.Entry(form_frame, textvariable=self.username_var,
|
||
font=("Arial", 12), relief='solid', bd=2)
|
||
username_entry.pack(fill='x', ipady=8)
|
||
|
||
# Password
|
||
tk.Label(form_frame, text="🔒 Password", font=("Arial", 11, "bold"),
|
||
bg='white', fg=COLORS['dark']).pack(anchor='w', pady=(15,5))
|
||
|
||
self.password_var = tk.StringVar()
|
||
password_entry = tk.Entry(form_frame, textvariable=self.password_var,
|
||
show="●", font=("Arial", 12), relief='solid', bd=2)
|
||
password_entry.pack(fill='x', ipady=8)
|
||
|
||
# Login Button
|
||
login_btn = tk.Button(form_frame, text="🔐 LOGIN", command=self.handle_login,
|
||
font=("Arial", 13, "bold"), bg=COLORS['cafe_green'],
|
||
fg='white', relief='flat', cursor='hand2', bd=0)
|
||
login_btn.pack(fill='x', pady=(25,10), ipady=10)
|
||
|
||
# Hover effect
|
||
login_btn.bind("<Enter>", lambda e: login_btn.config(bg=COLORS['success']))
|
||
login_btn.bind("<Leave>", lambda e: login_btn.config(bg=COLORS['cafe_green']))
|
||
|
||
# Info text
|
||
info_frame = tk.Frame(center, bg=COLORS['light'], height=60)
|
||
info_frame.pack(fill='x', side='bottom')
|
||
|
||
tk.Label(info_frame, text="💡 Tip: Gunakan user/user123 untuk pembeli",
|
||
font=("Arial", 9), bg=COLORS['light'],
|
||
fg=COLORS['dark']).pack(pady=15)
|
||
|
||
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.cart = []
|
||
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)
|
||
|
||
main.add(self.tab_menu_view, text="Menu - View")
|
||
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'] == '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()
|
||
|
||
|
||
# ========== TAB: ORDER SYSTEM (PEMBELI) ==========
|
||
def build_order_tab(self, parent):
|
||
"""Tab pemesanan untuk pembeli"""
|
||
for w in parent.winfo_children():
|
||
w.destroy()
|
||
|
||
# Container utama
|
||
container = ttk.Frame(parent)
|
||
container.pack(fill='both', expand=True, padx=10, pady=10)
|
||
|
||
# LEFT: Daftar Menu
|
||
left = ttk.Frame(container, relief='solid', borderwidth=1)
|
||
left.pack(side='left', fill='both', expand=True, padx=(0, 5))
|
||
|
||
ttk.Label(left, text="🍽️ Menu Tersedia", font=("Arial", 14, "bold")).pack(pady=8)
|
||
|
||
# Filter pencarian
|
||
filter_frm = ttk.Frame(left)
|
||
filter_frm.pack(fill='x', padx=10, pady=5)
|
||
ttk.Label(filter_frm, text="Cari Menu:").pack(side='left', padx=5)
|
||
self.order_search_var = tk.StringVar()
|
||
ttk.Entry(filter_frm, textvariable=self.order_search_var, width=20).pack(side='left', padx=5)
|
||
ttk.Button(filter_frm, text="🔍 Cari", command=self.reload_order_menu).pack(side='left', padx=3)
|
||
ttk.Button(filter_frm, text="🔄 Reset", command=self.reset_order_search).pack(side='left', padx=3)
|
||
|
||
# Treeview menu
|
||
cols = ("ID", "Nama", "Kategori", "Harga", "Stok")
|
||
self.order_menu_tree = ttk.Treeview(left, columns=cols, show='headings', height=15)
|
||
for c in cols:
|
||
self.order_menu_tree.heading(c, text=c)
|
||
w = 50 if c == "ID" else (180 if c == "Nama" else 100)
|
||
self.order_menu_tree.column(c, width=w)
|
||
self.order_menu_tree.pack(fill='both', expand=True, padx=10, pady=5)
|
||
|
||
# Tombol tambah ke keranjang
|
||
add_frame = ttk.Frame(left)
|
||
add_frame.pack(fill='x', padx=10, pady=8)
|
||
ttk.Label(add_frame, text="Jumlah:", font=("Arial", 10)).pack(side='left', padx=5)
|
||
self.order_qty_var = tk.StringVar(value="1")
|
||
ttk.Entry(add_frame, textvariable=self.order_qty_var, width=8).pack(side='left', padx=5)
|
||
ttk.Button(add_frame, text="➕ Tambah ke Keranjang", command=self.add_to_cart).pack(side='left', padx=10)
|
||
|
||
# RIGHT: Keranjang
|
||
right = ttk.Frame(container, relief='solid', borderwidth=1)
|
||
right.pack(side='right', fill='both', padx=(5, 0))
|
||
right.config(width=350)
|
||
|
||
ttk.Label(right, text="🛒 Keranjang Belanja", font=("Arial", 14, "bold")).pack(pady=8)
|
||
|
||
# Treeview keranjang
|
||
cart_cols = ("Menu", "Harga", "Qty", "Subtotal")
|
||
self.cart_tree = ttk.Treeview(right, columns=cart_cols, show='headings', height=12)
|
||
for c in cart_cols:
|
||
self.cart_tree.heading(c, text=c)
|
||
w = 120 if c == "Menu" else 70
|
||
self.cart_tree.column(c, width=w)
|
||
self.cart_tree.pack(fill='both', padx=10, pady=5)
|
||
|
||
# Tombol update & hapus
|
||
cart_btn_frm = ttk.Frame(right)
|
||
cart_btn_frm.pack(fill='x', padx=10, pady=5)
|
||
ttk.Button(cart_btn_frm, text="🗑️ Hapus Item", command=self.remove_from_cart).pack(side='left', padx=3)
|
||
ttk.Button(cart_btn_frm, text="📝 Update Qty", command=self.update_cart_qty).pack(side='left', padx=3)
|
||
|
||
# Total
|
||
self.cart_total_var = tk.StringVar(value="Total: Rp 0")
|
||
ttk.Label(right, textvariable=self.cart_total_var, font=("Arial", 12, "bold")).pack(pady=5)
|
||
|
||
# Form nomor meja
|
||
meja_frm = ttk.Frame(right)
|
||
meja_frm.pack(fill='x', padx=10, pady=8)
|
||
ttk.Label(meja_frm, text="Nomor Meja:", font=("Arial", 10)).pack(side='left', padx=5)
|
||
self.nomor_meja_var = tk.StringVar()
|
||
ttk.Entry(meja_frm, textvariable=self.nomor_meja_var, width=10).pack(side='left', padx=5)
|
||
|
||
# Tombol pesan
|
||
ttk.Button(right, text="📋 PESAN SEKARANG", command=self.submit_order,
|
||
style="Accent.TButton").pack(pady=10, padx=10, fill='x')
|
||
|
||
self.reload_order_menu()
|
||
|
||
def reload_order_menu(self):
|
||
"""Reload daftar menu untuk order"""
|
||
for item in self.order_menu_tree.get_children():
|
||
self.order_menu_tree.delete(item)
|
||
|
||
search = self.order_search_var.get().strip() if hasattr(self, 'order_search_var') else ""
|
||
menus = menu_list(available_only=True, search_text=search or None)
|
||
|
||
for m in menus:
|
||
mid, nama, kategori, harga, stok, foto, tersedia, disc = m
|
||
self.order_menu_tree.insert("", tk.END, values=(mid, nama, kategori, f"Rp {harga:,.0f}", stok))
|
||
|
||
def reset_order_search(self):
|
||
self.order_search_var.set("")
|
||
self.reload_order_menu()
|
||
|
||
def add_to_cart(self):
|
||
"""Tambah item ke keranjang"""
|
||
sel = self.order_menu_tree.selection()
|
||
if not sel:
|
||
messagebox.showwarning("Pilih Menu", "Pilih menu terlebih dahulu!")
|
||
return
|
||
|
||
try:
|
||
qty = int(self.order_qty_var.get())
|
||
if qty <= 0:
|
||
raise ValueError()
|
||
except:
|
||
messagebox.showerror("Error", "Jumlah harus angka positif!")
|
||
return
|
||
|
||
item_vals = self.order_menu_tree.item(sel)['values']
|
||
menu_id = item_vals[0]
|
||
nama = item_vals[1]
|
||
harga_str = item_vals[3].replace("Rp ", "").replace(",", "")
|
||
harga = float(harga_str)
|
||
stok = int(item_vals[4])
|
||
|
||
if qty > stok:
|
||
messagebox.showerror("Stok Habis", f"Stok hanya tersedia {stok}")
|
||
return
|
||
|
||
# Cek apakah sudah ada di cart
|
||
found = False
|
||
for cart_item in self.cart:
|
||
if cart_item['menu_id'] == menu_id:
|
||
cart_item['qty'] += qty
|
||
cart_item['subtotal'] = cart_item['harga'] * cart_item['qty']
|
||
found = True
|
||
break
|
||
|
||
if not found:
|
||
self.cart.append({
|
||
'menu_id': menu_id,
|
||
'nama': nama,
|
||
'harga': harga,
|
||
'qty': qty,
|
||
'subtotal': harga * qty
|
||
})
|
||
|
||
self.update_cart_display()
|
||
messagebox.showinfo("Berhasil", f"✅ {nama} x{qty} ditambahkan ke keranjang!")
|
||
|
||
def update_cart_display(self):
|
||
"""Update tampilan keranjang"""
|
||
for item in self.cart_tree.get_children():
|
||
self.cart_tree.delete(item)
|
||
|
||
total = 0.0
|
||
for item in self.cart:
|
||
self.cart_tree.insert("", tk.END, values=(
|
||
item['nama'],
|
||
f"Rp {item['harga']:,.0f}",
|
||
item['qty'],
|
||
f"Rp {item['subtotal']:,.0f}"
|
||
))
|
||
total += item['subtotal']
|
||
|
||
self.cart_total_var.set(f"Total: Rp {total:,.0f}")
|
||
|
||
def remove_from_cart(self):
|
||
"""Hapus item dari keranjang"""
|
||
sel = self.cart_tree.selection()
|
||
if not sel:
|
||
messagebox.showwarning("Pilih Item", "Pilih item yang akan dihapus!")
|
||
return
|
||
|
||
idx = self.cart_tree.index(sel)
|
||
del self.cart[idx]
|
||
self.update_cart_display()
|
||
messagebox.showinfo("Dihapus", "Item berhasil dihapus dari keranjang")
|
||
|
||
def update_cart_qty(self):
|
||
"""Update jumlah item di keranjang"""
|
||
sel = self.cart_tree.selection()
|
||
if not sel:
|
||
messagebox.showwarning("Pilih Item", "Pilih item yang akan diupdate!")
|
||
return
|
||
|
||
idx = self.cart_tree.index(sel)
|
||
item = self.cart[idx]
|
||
|
||
# Dialog input qty baru
|
||
new_qty = tk.simpledialog.askinteger("Update Jumlah",
|
||
f"Jumlah baru untuk {item['nama']}:",
|
||
initialvalue=item['qty'],
|
||
minvalue=1)
|
||
if new_qty:
|
||
item['qty'] = new_qty
|
||
item['subtotal'] = item['harga'] * new_qty
|
||
self.update_cart_display()
|
||
|
||
def submit_order(self):
|
||
"""Submit pesanan"""
|
||
if not self.cart:
|
||
messagebox.showwarning("Keranjang Kosong", "Tambahkan menu ke keranjang terlebih dahulu!")
|
||
return
|
||
|
||
nomor_meja = self.nomor_meja_var.get().strip()
|
||
if not nomor_meja:
|
||
messagebox.showwarning("Nomor Meja", "Masukkan nomor meja!")
|
||
return
|
||
|
||
try:
|
||
trans_id = create_transaksi(self.session['id'], nomor_meja, self.cart)
|
||
|
||
# Tambah ke favorites
|
||
for item in self.cart:
|
||
add_to_favorites(self.session['id'], item['menu_id'])
|
||
|
||
messagebox.showinfo("Berhasil",
|
||
f"✅ Pesanan berhasil dibuat!\n\n"
|
||
f"ID Transaksi: {trans_id}\n"
|
||
f"Nomor Meja: {nomor_meja}\n"
|
||
f"Total: Rp {sum(i['subtotal'] for i in self.cart):,.0f}\n\n"
|
||
f"Status: Menunggu\n"
|
||
f"Pesanan Anda akan segera diproses oleh waiter!")
|
||
|
||
# Reset
|
||
self.cart = []
|
||
self.nomor_meja_var.set("")
|
||
self.order_qty_var.set("1")
|
||
self.update_cart_display()
|
||
self.reload_order_menu()
|
||
|
||
except Exception as e:
|
||
messagebox.showerror("Error", f"Gagal membuat pesanan: {e}")
|
||
|
||
# ========== TAB: WAITER DASHBOARD ==========
|
||
def build_waiter_tab(self, parent):
|
||
"""Dashboard untuk waiter mengelola pesanan"""
|
||
for w in parent.winfo_children():
|
||
w.destroy()
|
||
|
||
ttk.Label(parent, text="📋 Dashboard Waiter - Kelola Pesanan",
|
||
font=("Arial", 16, "bold")).pack(pady=10)
|
||
|
||
# Filter status
|
||
filter_frm = ttk.Frame(parent)
|
||
filter_frm.pack(fill='x', padx=10, pady=5)
|
||
ttk.Label(filter_frm, text="Filter Status:").pack(side='left', padx=5)
|
||
ttk.Button(filter_frm, text="🕐 Menunggu",
|
||
command=lambda: self.reload_waiter_orders("Menunggu")).pack(side='left', padx=3)
|
||
ttk.Button(filter_frm, text="⏳ Diproses",
|
||
command=lambda: self.reload_waiter_orders("Diproses")).pack(side='left', padx=3)
|
||
ttk.Button(filter_frm, text="✅ Selesai",
|
||
command=lambda: self.reload_waiter_orders("Selesai")).pack(side='left', padx=3)
|
||
ttk.Button(filter_frm, text="📋 Semua",
|
||
command=lambda: self.reload_waiter_orders(None)).pack(side='left', padx=3)
|
||
ttk.Button(filter_frm, text="🔄 Refresh",
|
||
command=lambda: self.reload_waiter_orders()).pack(side='right', padx=10)
|
||
|
||
# Treeview pesanan
|
||
cols = ("ID", "Tanggal", "Meja", "Total", "Status")
|
||
self.waiter_tree = ttk.Treeview(parent, columns=cols, show='headings', height=10)
|
||
for c in cols:
|
||
self.waiter_tree.heading(c, text=c)
|
||
w = 50 if c == "ID" else (150 if c == "Tanggal" else 80)
|
||
self.waiter_tree.column(c, width=w)
|
||
self.waiter_tree.pack(fill='both', expand=True, padx=10, pady=10)
|
||
self.waiter_tree.bind("<<TreeviewSelect>>", self.on_waiter_select)
|
||
|
||
# Detail pesanan
|
||
detail_frm = ttk.LabelFrame(parent, text="📝 Detail Pesanan", padding=10)
|
||
detail_frm.pack(fill='both', padx=10, pady=5)
|
||
|
||
self.waiter_detail_text = tk.Text(detail_frm, height=8, width=80, state='disabled')
|
||
self.waiter_detail_text.pack(side='left', fill='both', expand=True)
|
||
|
||
# Tombol aksi
|
||
btn_frm = ttk.Frame(parent)
|
||
btn_frm.pack(fill='x', padx=10, pady=10)
|
||
ttk.Button(btn_frm, text="⏳ Proses Pesanan",
|
||
command=lambda: self.change_order_status("Diproses")).pack(side='left', padx=5)
|
||
ttk.Button(btn_frm, text="✅ Selesai Dilayani",
|
||
command=lambda: self.change_order_status("Selesai")).pack(side='left', padx=5)
|
||
|
||
self.reload_waiter_orders()
|
||
|
||
def reload_waiter_orders(self, status=None):
|
||
"""Reload daftar pesanan untuk waiter"""
|
||
for item in self.waiter_tree.get_children():
|
||
self.waiter_tree.delete(item)
|
||
|
||
orders = get_all_transaksi(status=status)
|
||
|
||
for order in orders:
|
||
status_icon = {"Menunggu": "🕐", "Diproses": "⏳", "Selesai": "✅"}.get(order['status'], "")
|
||
self.waiter_tree.insert("", tk.END, values=(
|
||
order['id'],
|
||
order['tanggal'],
|
||
order['nomor_meja'],
|
||
f"Rp {order['total']:,.0f}",
|
||
f"{status_icon} {order['status']}"
|
||
))
|
||
|
||
def on_waiter_select(self, event):
|
||
"""Ketika waiter pilih pesanan, tampilkan detail"""
|
||
sel = self.waiter_tree.selection()
|
||
if not sel:
|
||
return
|
||
|
||
trans_id = self.waiter_tree.item(sel)['values'][0]
|
||
details = get_transaksi_detail(trans_id)
|
||
|
||
self.waiter_detail_text.config(state='normal')
|
||
self.waiter_detail_text.delete('1.0', tk.END)
|
||
|
||
text = f"ID Transaksi: {trans_id}\n"
|
||
text += f"{'='*50}\n"
|
||
text += f"{'Menu':<25} {'Qty':<5} {'Harga':<12} {'Subtotal':<12}\n"
|
||
text += f"{'-'*50}\n"
|
||
|
||
for d in details:
|
||
text += f"{d['nama_menu']:<25} {d['jumlah']:<5} Rp {d['harga_satuan']:>8,.0f} Rp {d['subtotal']:>8,.0f}\n"
|
||
|
||
self.waiter_detail_text.insert('1.0', text)
|
||
self.waiter_detail_text.config(state='disabled')
|
||
|
||
def change_order_status(self, new_status):
|
||
"""Ubah status pesanan"""
|
||
sel = self.waiter_tree.selection()
|
||
if not sel:
|
||
messagebox.showwarning("Pilih Pesanan", "Pilih pesanan terlebih dahulu!")
|
||
return
|
||
|
||
trans_id = self.waiter_tree.item(sel)['values'][0]
|
||
current_status = self.waiter_tree.item(sel)['values'][4].split()[-1]
|
||
|
||
# Validasi flow status
|
||
if current_status == "Selesai":
|
||
messagebox.showinfo("Info", "Pesanan sudah selesai dilayani!")
|
||
return
|
||
|
||
# ========== LANJUTAN DARI ARTIFACTS SEBELUMNYA ==========
|
||
# Paste kode ini setelah fungsi change_order_status
|
||
|
||
if new_status == "Selesai" and current_status == "Menunggu":
|
||
if not messagebox.askyesno("Konfirmasi", "Pesanan belum diproses. Langsung selesai?"):
|
||
return
|
||
|
||
update_transaksi_status(trans_id, new_status)
|
||
messagebox.showinfo("Berhasil", f"Status pesanan diubah menjadi: {new_status}")
|
||
self.reload_waiter_orders()
|
||
|
||
# ========== TAB: FAVORITES (PEMBELI) ==========
|
||
def build_favorites_tab(self, parent):
|
||
"""Tab menu favorit untuk pembeli"""
|
||
for w in parent.winfo_children():
|
||
w.destroy()
|
||
|
||
ttk.Label(parent, text="⭐ Menu Favorit Saya",
|
||
font=("Arial", 16, "bold")).pack(pady=10)
|
||
|
||
ttk.Label(parent, text="Menu yang paling sering Anda pesan",
|
||
font=("Arial", 10)).pack(pady=5)
|
||
|
||
# Treeview favorites
|
||
cols = ("Nama Menu", "Kategori", "Harga", "Sering Dipesan")
|
||
self.fav_tree = ttk.Treeview(parent, columns=cols, show='headings', height=12)
|
||
for c in cols:
|
||
self.fav_tree.heading(c, text=c)
|
||
w = 200 if c == "Nama Menu" else 120
|
||
self.fav_tree.column(c, width=w)
|
||
self.fav_tree.pack(fill='both', expand=True, padx=10, pady=10)
|
||
|
||
# Tombol pesan cepat
|
||
btn_frm = ttk.Frame(parent)
|
||
btn_frm.pack(pady=10)
|
||
ttk.Label(btn_frm, text="Pesan Cepat - Jumlah:").pack(side='left', padx=5)
|
||
self.fav_qty_var = tk.StringVar(value="1")
|
||
ttk.Entry(btn_frm, textvariable=self.fav_qty_var, width=8).pack(side='left', padx=5)
|
||
ttk.Button(btn_frm, text="🚀 Pesan Langsung",
|
||
command=self.quick_order_from_fav).pack(side='left', padx=10)
|
||
ttk.Button(btn_frm, text="🔄 Refresh",
|
||
command=self.reload_favorites).pack(side='left', padx=5)
|
||
|
||
self.reload_favorites()
|
||
|
||
def reload_favorites(self):
|
||
"""Reload menu favorit user"""
|
||
for item in self.fav_tree.get_children():
|
||
self.fav_tree.delete(item)
|
||
|
||
favs = get_user_favorites(self.session['id'], limit=10)
|
||
|
||
if not favs:
|
||
self.fav_tree.insert("", tk.END, values=(
|
||
"Belum ada menu favorit", "-", "-", "0"
|
||
))
|
||
return
|
||
|
||
for fav in favs:
|
||
menu = fav['menu']
|
||
mid, nama, kategori, harga, stok, foto, tersedia, disc = menu
|
||
self.fav_tree.insert("", tk.END, values=(
|
||
nama,
|
||
kategori,
|
||
f"Rp {harga:,.0f}",
|
||
f"{fav['count']}x"
|
||
), tags=(str(mid),))
|
||
|
||
def quick_order_from_fav(self):
|
||
"""Pesan langsung dari menu favorit"""
|
||
sel = self.fav_tree.selection()
|
||
if not sel:
|
||
messagebox.showwarning("Pilih Menu", "Pilih menu favorit terlebih dahulu!")
|
||
return
|
||
|
||
item_vals = self.fav_tree.item(sel)['values']
|
||
if item_vals[0] == "Belum ada menu favorit":
|
||
return
|
||
|
||
# Ambil menu_id dari tags
|
||
menu_id = int(self.fav_tree.item(sel)['tags'][0])
|
||
menu = menu_get(menu_id)
|
||
|
||
if not menu:
|
||
messagebox.showerror("Error", "Menu tidak ditemukan!")
|
||
return
|
||
|
||
mid, nama, kategori, harga, stok, foto, tersedia, disc = menu
|
||
|
||
if not tersedia or stok == 0:
|
||
messagebox.showwarning("Stok Habis", f"{nama} sedang tidak tersedia!")
|
||
return
|
||
|
||
try:
|
||
qty = int(self.fav_qty_var.get())
|
||
if qty <= 0:
|
||
raise ValueError()
|
||
except:
|
||
messagebox.showerror("Error", "Jumlah harus angka positif!")
|
||
return
|
||
|
||
if qty > stok:
|
||
messagebox.showerror("Stok Tidak Cukup", f"Stok hanya tersedia {stok}")
|
||
return
|
||
|
||
# Tanya nomor meja
|
||
nomor_meja = tk.simpledialog.askstring("Nomor Meja", "Masukkan nomor meja Anda:")
|
||
if not nomor_meja:
|
||
return
|
||
|
||
# Buat transaksi langsung
|
||
cart_item = [{
|
||
'menu_id': mid,
|
||
'nama': nama,
|
||
'harga': harga,
|
||
'qty': qty,
|
||
'subtotal': harga * qty
|
||
}]
|
||
|
||
try:
|
||
trans_id = create_transaksi(self.session['id'], nomor_meja, cart_item)
|
||
add_to_favorites(self.session['id'], mid)
|
||
|
||
messagebox.showinfo("Berhasil",
|
||
f"✅ Pesanan Cepat Berhasil!\n\n"
|
||
f"{nama} x{qty}\n"
|
||
f"Total: Rp {harga * qty:,.0f}\n"
|
||
f"Meja: {nomor_meja}\n\n"
|
||
f"ID Transaksi: {trans_id}")
|
||
|
||
self.reload_favorites()
|
||
|
||
except Exception as e:
|
||
messagebox.showerror("Error", f"Gagal membuat pesanan: {e}")
|
||
|
||
# ========== UPDATE DASHBOARD FRAME ==========
|
||
# GANTI fungsi dashboard_frame yang lama dengan ini:
|
||
|
||
def dashboard_frame(self):
|
||
for w in self.root.winfo_children():
|
||
w.destroy()
|
||
|
||
# Header
|
||
top = ttk.Frame(self.root)
|
||
top.pack(fill='x')
|
||
ttk.Label(top, text=f"👤 User: {self.session['username']} | Role: {self.session['role'].upper()}",
|
||
font=("Arial", 12, "bold")).pack(side='left', padx=15, pady=8)
|
||
ttk.Button(top, text="🚪 Logout", command=self.logout).pack(side='right', padx=15)
|
||
|
||
# Notebook tabs
|
||
main = ttk.Notebook(self.root)
|
||
main.pack(fill='both', expand=True, padx=10, pady=10)
|
||
|
||
role = self.session['role']
|
||
|
||
# Setup tabs berdasarkan role
|
||
if role == 'pembeli':
|
||
self.tab_order = ttk.Frame(main)
|
||
self.tab_favorites = ttk.Frame(main)
|
||
main.add(self.tab_order, text="🛒 Pesan Menu")
|
||
main.add(self.tab_favorites, text="⭐ Menu Favorit")
|
||
self.build_order_tab(self.tab_order)
|
||
self.build_favorites_tab(self.tab_favorites)
|
||
|
||
elif role == 'waiter':
|
||
self.tab_waiter = ttk.Frame(main)
|
||
main.add(self.tab_waiter, text="📋 Dashboard Waiter")
|
||
self.build_waiter_tab(self.tab_waiter)
|
||
|
||
elif role == 'admin':
|
||
self.tab_menu_view = ttk.Frame(main)
|
||
self.tab_menu_manage = ttk.Frame(main)
|
||
self.tab_promo = ttk.Frame(main)
|
||
self.tab_waiter = ttk.Frame(main)
|
||
main.add(self.tab_menu_view, text="📖 Menu - View")
|
||
main.add(self.tab_menu_manage, text="⚙️ Menu - Manage")
|
||
main.add(self.tab_promo, text="🎁 Promo - Manage")
|
||
main.add(self.tab_waiter, text="📋 Dashboard Waiter")
|
||
self.build_menu_view_tab(self.tab_menu_view)
|
||
self.build_menu_manage_tab(self.tab_menu_manage)
|
||
self.build_promo_tab(self.tab_promo)
|
||
self.build_waiter_tab(self.tab_waiter)
|
||
|
||
else: # kasir, owner
|
||
self.tab_menu_view = ttk.Frame(main)
|
||
main.add(self.tab_menu_view, text="📖 Menu - View")
|
||
self.build_menu_view_tab(self.tab_menu_view)
|
||
|
||
|
||
|
||
|
||
|
||
# Done
|
||
|
||
|
||
if __name__ == "__main__":
|
||
init_db_csv()
|
||
root = tk.Tk()
|
||
app = App(root)
|
||
root.mainloop()
|