Manajemen Meja
This commit is contained in:
parent
17dd7a3d0c
commit
ddac4df06b
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
401
main.py
401
main.py
@ -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
|
||||
|
||||
6
menu.csv
6
menu.csv
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user