Manajemen Meja

This commit is contained in:
Bluwww 2025-12-13 22:23:06 +07:00
parent 17dd7a3d0c
commit ddac4df06b
5 changed files with 482 additions and 73 deletions

View File

@ -5,3 +5,8 @@ id,transaksi_id,menu_id,qty,harga_satuan,subtotal_item
4,5,2,1,25000.0,25000.0
5,5,4,1,25000.0,25000.0
6,5,6,1,19000.0,19000.0
7,6,2,1,25000.0,25000.0
8,6,4,1,25000.0,25000.0
9,6,3,1,30000.0,30000.0
10,7,4,2,25000.0,50000.0
11,7,3,2,30000.0,60000.0

1 id transaksi_id menu_id qty harga_satuan subtotal_item
5 4 5 2 1 25000.0 25000.0
6 5 5 4 1 25000.0 25000.0
7 6 5 6 1 19000.0 19000.0
8 7 6 2 1 25000.0 25000.0
9 8 6 4 1 25000.0 25000.0
10 9 6 3 1 30000.0 30000.0
11 10 7 4 2 25000.0 50000.0
12 11 7 3 2 30000.0 60000.0

View File

@ -3,3 +3,6 @@ user_id,menu_id,order_count,last_ordered
1,2,1,2025-12-13 20:12:35
1,4,1,2025-12-13 20:12:35
1,6,1,2025-12-13 20:12:35
4,2,1,2025-12-13 22:18:23
4,4,2,2025-12-13 22:22:02
4,3,2,2025-12-13 22:22:02

1 user_id menu_id order_count last_ordered
3 1 2 1 2025-12-13 20:12:35
4 1 4 1 2025-12-13 20:12:35
5 1 6 1 2025-12-13 20:12:35
6 4 2 1 2025-12-13 22:18:23
7 4 4 2 2025-12-13 22:22:02
8 4 3 2 2025-12-13 22:22:02

401
main.py
View File

@ -818,6 +818,7 @@ class App:
def logout(self):
self.session = None
self.img_cache.clear()
self.notification_running = False
self.login_frame()
def dashboard_frame(self):
@ -860,6 +861,9 @@ class App:
if self.session['role'] == 'waiter':
main.add(self.tab_waiter, text="🍽️ Kelola Pesanan")
self.tab_meja = ttk.Frame(main)
main.add(self.tab_meja, text="🪑 Kelola Meja")
# ==========================================
# ROLE: KASIR (Order + Transaksi SAJA)
# ==========================================
@ -867,6 +871,9 @@ class App:
self.tab_payment = ttk.Frame(main)
main.add(self.tab_payment, text="💰 Transaksi")
self.tab_meja = ttk.Frame(main)
main.add(self.tab_meja, text="🪑 Kelola Meja")
# ==========================================
# ROLE: PEMILIK (Laporan SAJA)
# ==========================================
@ -874,6 +881,7 @@ class App:
self.tab_report = ttk.Frame(main)
main.add(self.tab_report, text="📊 Laporan")
# ==========================================
# ROLE: ADMIN (Kelola Semua)
# ==========================================
@ -890,6 +898,9 @@ class App:
self.tab_report = ttk.Frame(main)
main.add(self.tab_report, text="📊 Laporan")
self.tab_meja = ttk.Frame(main)
main.add(self.tab_meja, text="🪑 Kelola Meja")
# Kelola Menu & Promo (KHUSUS ADMIN)
main.add(self.tab_menu_manage, text="⚙️ Kelola Menu")
main.add(self.tab_promo, text="🎁 Kelola Promo")
@ -914,11 +925,13 @@ class App:
# Waiter
if self.session['role'] == 'waiter':
self.build_waiter_tab(self.tab_waiter)
self.build_meja_tab(self.tab_meja)
# Kasir (Order + Transaksi SAJA)
if self.session['role'] == 'kasir':
self.build_order_tab(self.tab_order)
self.build_payment_tab(self.tab_payment)
self.build_meja_tab(self.tab_meja)
# Pemilik (Laporan SAJA)
if self.session['role'] == 'pemilik':
@ -933,6 +946,7 @@ class App:
self.build_menu_manage_tab(self.tab_menu_manage)
self.build_promo_tab(self.tab_promo)
self.build_user_manage_tab(self.tab_user_manage) # TAMBAHAN
self.build_meja_tab(self.tab_meja)
def build_menu_view_tab(self, parent):
@ -1929,6 +1943,7 @@ class App:
success, result = transaksi_add(self.session['id'], nomor_meja, self.cart_items, promo_code)
if success:
meja_update_status(nomor_meja, "terisi", result)
messagebox.showinfo("Sukses", f"Pesanan berhasil! ID Transaksi: {result}\nStatus: Pending")
# Reset
self.cart_items = []
@ -3001,7 +3016,8 @@ class App:
def show_report_chart(self):
"""Tampilkan grafik laporan menggunakan matplotlib"""
"""Tampilkan grafik laporan (dengan fallback jika matplotlib tidak ada)"""
try:
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from datetime import datetime
@ -3079,6 +3095,10 @@ class App:
ttk.Button(chart_window, text="Tutup", command=chart_window.destroy).pack(pady=10)
except ImportError:
# Fallback: Tampilkan grafik ASCII sederhana
self.show_text_chart()
def reload_payment_orders(self):
"""Load transaksi dengan status 'selesai' yang belum dibayar"""
@ -3489,6 +3509,385 @@ class App:
except Exception as e:
messagebox.showerror("Error", f"Gagal menyimpan struk: {e}")
def check_new_orders(self):
"""Cek pesanan baru yang perlu perhatian waiter/kasir"""
if self.session['role'] not in ['waiter', 'kasir', 'admin']:
return 0
# Hitung pesanan pending untuk waiter
if self.session['role'] in ['waiter', 'admin']:
pending_orders = transaksi_list(status='pending')
return len(pending_orders)
# Hitung pesanan selesai untuk kasir
if self.session['role'] in ['kasir', 'admin']:
selesai_orders = transaksi_list(status='selesai')
# Filter yang belum dibayar
unpaid_count = 0
for order in selesai_orders:
tid = order[0]
payment = pembayaran_get_by_transaksi(tid)
if not payment:
unpaid_count += 1
return unpaid_count
return 0
def start_notification_check(self):
"""Start auto-refresh untuk notifikasi (setiap 10 detik)"""
if not hasattr(self, 'notification_running'):
self.notification_running = True
self.update_notification_badge()
def update_notification_badge(self):
"""Update badge notifikasi"""
if not self.notification_running:
return
try:
count = self.check_new_orders()
# Update badge di tab yang sesuai
if self.session['role'] in ['waiter', 'admin']:
if hasattr(self, 'tab_waiter'):
for tab_id in range(self.root.nametowidget('.!notebook').index('end')):
tab_text = self.root.nametowidget('.!notebook').tab(tab_id, 'text')
if 'Kelola Pesanan' in tab_text:
if count > 0:
self.root.nametowidget('.!notebook').tab(tab_id, text=f"🍽️ Kelola Pesanan ({count})")
else:
self.root.nametowidget('.!notebook').tab(tab_id, text="🍽️ Kelola Pesanan")
break
if self.session['role'] in ['kasir', 'admin']:
if hasattr(self, 'tab_payment'):
for tab_id in range(self.root.nametowidget('.!notebook').index('end')):
tab_text = self.root.nametowidget('.!notebook').tab(tab_id, 'text')
if 'Transaksi' in tab_text:
if count > 0:
self.root.nametowidget('.!notebook').tab(tab_id, text=f"💰 Transaksi ({count})")
else:
self.root.nametowidget('.!notebook').tab(tab_id, text="💰 Transaksi")
break
except:
pass
# Schedule next check (10 seconds)
if self.notification_running:
self.root.after(10000, self.update_notification_badge)
def show_text_chart(self):
"""Tampilkan grafik ASCII sebagai fallback jika matplotlib tidak ada"""
# Get data from tree
if not self.report_tree.get_children():
messagebox.showwarning("Tidak Ada Data", "Generate laporan terlebih dahulu")
return
# Collect data
metode_counts = {}
for item_id in self.report_tree.get_children():
values = self.report_tree.item(item_id)['values']
tid, tanggal, meja, total_str, metode, status = values
# Count by metode
metode_counts[metode] = metode_counts.get(metode, 0) + 1
# Create ASCII bar chart
chart_text = "=" * 60 + "\n"
chart_text += "GRAFIK TRANSAKSI PER METODE PEMBAYARAN\n"
chart_text += "=" * 60 + "\n\n"
if metode_counts:
max_count = max(metode_counts.values())
for metode, count in sorted(metode_counts.items()):
# Calculate bar length (max 40 chars)
bar_length = int((count / max_count) * 40) if max_count > 0 else 0
bar = "" * bar_length
percentage = (count / sum(metode_counts.values())) * 100 if sum(metode_counts.values()) > 0 else 0
chart_text += f"{metode.ljust(15)} | {bar} {count} ({percentage:.1f}%)\n"
chart_text += "\n" + "=" * 60 + "\n"
chart_text += f"Total Transaksi: {sum(metode_counts.values())}\n"
chart_text += "=" * 60
# Show in window
w = tk.Toplevel(self.root)
w.title("📊 Grafik Laporan (Text Mode)")
w.geometry("700x500")
frm = ttk.Frame(w, padding=15)
frm.pack(fill='both', expand=True)
ttk.Label(frm, text=" Matplotlib tidak terinstall - Mode Text Chart", font=("Arial", 10), foreground='orange').pack(pady=10)
text = tk.Text(frm, width=80, height=25, font=("Courier New", 10))
text_scroll = ttk.Scrollbar(frm, orient='vertical', command=text.yview)
text.configure(yscrollcommand=text_scroll.set)
text.insert('1.0', chart_text)
text.config(state='disabled')
text.pack(side='left', fill='both', expand=True)
text_scroll.pack(side='right', fill='y')
ttk.Button(w, text="Tutup", command=w.destroy).pack(pady=10)
# MEJA
def build_meja_tab(self, parent):
"""Tab untuk kelola status meja (admin/kasir/waiter)"""
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="🪑 Manajemen Meja Cafe", font=("Arial", 14, "bold")).pack(side='left')
ttk.Button(header, text="🔄 Refresh", command=self.reload_meja_status).pack(side='right', padx=6)
# Info panel
info_frame = ttk.LabelFrame(parent, text=" Info Meja", padding=10)
info_frame.pack(fill='x', padx=10, pady=6)
info_inner = ttk.Frame(info_frame)
info_inner.pack()
ttk.Label(info_inner, text="🟢 Kosong:", font=("Arial", 10)).grid(row=0, column=0, padx=15, pady=3)
self.meja_kosong_label = ttk.Label(info_inner, text="0", font=("Arial", 10, "bold"), foreground='green')
self.meja_kosong_label.grid(row=0, column=1, padx=5, pady=3)
ttk.Label(info_inner, text="🔴 Terisi:", font=("Arial", 10)).grid(row=0, column=2, padx=15, pady=3)
self.meja_terisi_label = ttk.Label(info_inner, text="0", font=("Arial", 10, "bold"), foreground='red')
self.meja_terisi_label.grid(row=0, column=3, padx=5, pady=3)
# Container untuk card meja
canvas_frame = ttk.Frame(parent)
canvas_frame.pack(fill='both', expand=True, padx=10, pady=6)
# Canvas dengan scrollbar
canvas = tk.Canvas(canvas_frame, bg='#f5f5f5', highlightthickness=0)
scrollbar = ttk.Scrollbar(canvas_frame, orient="vertical", command=canvas.yview)
self.meja_cards_frame = ttk.Frame(canvas)
self.meja_cards_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=self.meja_cards_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# Mouse wheel scroll
def _on_mousewheel(event):
canvas.yview_scroll(int(-1*(event.delta/120)), "units")
canvas.bind_all("<MouseWheel>", _on_mousewheel)
# Load meja cards
self.reload_meja_status()
def reload_meja_status(self):
"""Load semua meja dalam bentuk cards"""
# Clear existing cards
for widget in self.meja_cards_frame.winfo_children():
widget.destroy()
# Get all meja data
meja_list = read_all(MEJA_CSV)
# Hitung statistik
kosong_count = sum(1 for m in meja_list if m.get('status') == 'kosong')
terisi_count = sum(1 for m in meja_list if m.get('status') == 'terisi')
# Update info labels
self.meja_kosong_label.config(text=str(kosong_count))
self.meja_terisi_label.config(text=str(terisi_count))
# Render meja cards (5 kolom)
row = 0
col = 0
for meja in sorted(meja_list, key=lambda x: int(x.get('nomor_meja', 0))):
nomor = meja.get('nomor_meja')
status = meja.get('status', 'kosong')
transaksi_id = meja.get('transaksi_id', '')
# Determine color
if status == 'kosong':
bg_color = '#C8E6C9' # Light green
status_color = '#4CAF50'
status_text = '🟢 KOSONG'
else:
bg_color = '#FFCDD2' # Light red
status_color = '#F44336'
status_text = '🔴 TERISI'
# Create card
card = tk.Frame(
self.meja_cards_frame,
relief='solid',
borderwidth=2,
bg=bg_color,
padx=15,
pady=15
)
card.grid(row=row, column=col, padx=8, pady=8, sticky='nsew')
# Nomor meja (besar)
tk.Label(
card,
text=f"MEJA {nomor}",
font=("Arial", 16, "bold"),
bg=bg_color
).pack(pady=(0, 5))
# Status
tk.Label(
card,
text=status_text,
font=("Arial", 10, "bold"),
fg=status_color,
bg=bg_color
).pack(pady=5)
# Info transaksi (jika terisi)
if status == 'terisi' and transaksi_id:
tk.Label(
card,
text=f"Transaksi: #{transaksi_id}",
font=("Arial", 8),
bg=bg_color
).pack(pady=2)
# Get detail transaksi
transaksi_data = transaksi_get(int(transaksi_id))
if transaksi_data:
tid, uid, meja_num, total, status_trx, promo, subtotal, item_disc, promo_disc, tanggal = transaksi_data
tk.Label(
card,
text=f"Total: Rp {total:,.0f}",
font=("Arial", 8, "bold"),
bg=bg_color,
fg='#1976D2'
).pack(pady=2)
tk.Label(
card,
text=f"Status: {status_trx.upper()}",
font=("Arial", 7),
bg=bg_color
).pack(pady=2)
# Tombol aksi
btn_frame = tk.Frame(card, bg=bg_color)
btn_frame.pack(pady=(10, 0))
if status == 'terisi':
# Tombol Tutup Meja (hanya jika sudah dibayar)
if transaksi_id:
transaksi_data = transaksi_get(int(transaksi_id))
if transaksi_data and transaksi_data[4] == 'dibayar':
tk.Button(
btn_frame,
text="✅ Tutup Meja",
font=("Arial", 9, "bold"),
bg='#4CAF50',
fg='white',
width=12,
borderwidth=0,
cursor='hand2',
command=lambda n=nomor: self.tutup_meja(n)
).pack()
else:
tk.Label(
btn_frame,
text="⏳ Menunggu Pembayaran",
font=("Arial", 8),
bg=bg_color,
fg='orange'
).pack()
else:
# Meja kosong - tampilkan info saja
tk.Label(
btn_frame,
text="Siap digunakan",
font=("Arial", 8),
bg=bg_color,
fg='gray'
).pack()
# Next column
col += 1
if col >= 5: # 5 meja per row
col = 0
row += 1
# Configure grid weights
for i in range(5):
self.meja_cards_frame.columnconfigure(i, weight=1)
def tutup_meja(self, nomor_meja):
"""Tutup meja dan reset status"""
# Konfirmasi
if not messagebox.askyesno("Konfirmasi", f"Tutup meja {nomor_meja}?\n\nPastikan pelanggan sudah selesai dan transaksi sudah dibayar."):
return
# Tutup meja
success = meja_tutup(nomor_meja)
if success:
messagebox.showinfo("✅ Berhasil", f"Meja {nomor_meja} berhasil ditutup dan siap digunakan lagi")
self.reload_meja_status()
else:
messagebox.showerror("❌ Gagal", f"Gagal menutup meja {nomor_meja}")
# ========================================
# FUNGSI TAMBAHAN UNTUK BACKEND
# (sudah ada tapi ditambahkan untuk kelengkapan)
# ========================================
def meja_list_all():
"""Ambil semua data meja"""
rows = read_all(MEJA_CSV)
out = []
for r in rows:
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
# Done

View File

@ -1,8 +1,8 @@
id,nama,kategori,harga,stok,foto,tersedia,item_discount_pct
1,Americano,Minuman,20000.0,7,img/americano.jpg,1,0.0
2,Latte,Minuman,25000.0,6,img/latte.jpg,1,10.0
3,Banana Cake,Dessert,30000.0,12,img/banana_cake.jpg,1,4.0
4,Nasi Goreng,Makanan,25000.0,13,img/nasi_goreng.jpg,1,0.0
2,Latte,Minuman,25000.0,5,img/latte.jpg,1,10.0
3,Banana Cake,Dessert,30000.0,9,img/banana_cake.jpg,1,4.0
4,Nasi Goreng,Makanan,25000.0,10,img/nasi_goreng.jpg,1,0.0
5,Nasi Kuning,Makanan,18000.0,7,img/Nasi-Kuning.jpg,1,5.0
6,Strawberry Milkshake,Minuman,19000.0,5,img/strawberry_milkshake.jpg,1,0.0
7,Coconut Matcha,Minuman,21000.0,14,img/coconut_matcha.jpg,1,5.0

1 id nama kategori harga stok foto tersedia item_discount_pct
2 1 Americano Minuman 20000.0 7 img/americano.jpg 1 0.0
3 2 Latte Minuman 25000.0 6 5 img/latte.jpg 1 10.0
4 3 Banana Cake Dessert 30000.0 12 9 img/banana_cake.jpg 1 4.0
5 4 Nasi Goreng Makanan 25000.0 13 10 img/nasi_goreng.jpg 1 0.0
6 5 Nasi Kuning Makanan 18000.0 7 img/Nasi-Kuning.jpg 1 5.0
7 6 Strawberry Milkshake Minuman 19000.0 5 img/strawberry_milkshake.jpg 1 0.0
8 7 Coconut Matcha Minuman 21000.0 14 img/coconut_matcha.jpg 1 5.0

View File

@ -4,3 +4,5 @@ id,user_id,nomor_meja,total,status,promo_code,subtotal,item_discount,promo_disco
3,4,3,20000.0,dibayar,,20000.0,0.0,0.0,2025-12-13 16:33:41
4,4,2,20000.0,dibayar,,20000.0,0.0,0.0,2025-12-13 17:22:52
5,1,2,59500.0,dibayar,CAFETOTORO,69000.0,2500.0,7000.0,2025-12-13 20:12:35
6,4,2,41965.0,diproses,MERDEKA,80000.0,3700.0,34335.0,2025-12-13 22:18:23
7,4,2,59180.0,dibayar,MERDEKA,110000.0,2400.0,48420.0,2025-12-13 22:22:02

1 id user_id nomor_meja total status promo_code subtotal item_discount promo_discount tanggal
4 3 4 3 20000.0 dibayar 20000.0 0.0 0.0 2025-12-13 16:33:41
5 4 4 2 20000.0 dibayar 20000.0 0.0 0.0 2025-12-13 17:22:52
6 5 1 2 59500.0 dibayar CAFETOTORO 69000.0 2500.0 7000.0 2025-12-13 20:12:35
7 6 4 2 41965.0 diproses MERDEKA 80000.0 3700.0 34335.0 2025-12-13 22:18:23
8 7 4 2 59180.0 dibayar MERDEKA 110000.0 2400.0 48420.0 2025-12-13 22:22:02