From ddac4df06bd1360caec28ff950808ef67c25dbdd Mon Sep 17 00:00:00 2001 From: Bluwww Date: Sat, 13 Dec 2025 22:23:06 +0700 Subject: [PATCH] Manajemen Meja --- detail_transaksi.csv | 5 + favorite.csv | 3 + main.py | 539 +++++++++++++++++++++++++++++++++++++------ menu.csv | 6 +- transaksi.csv | 2 + 5 files changed, 482 insertions(+), 73 deletions(-) diff --git a/detail_transaksi.csv b/detail_transaksi.csv index 267f171..c07b1e8 100644 --- a/detail_transaksi.csv +++ b/detail_transaksi.csv @@ -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 diff --git a/favorite.csv b/favorite.csv index 65fae17..8bc90f3 100644 --- a/favorite.csv +++ b/favorite.csv @@ -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 diff --git a/main.py b/main.py index ff40bea..8d050fb 100644 --- a/main.py +++ b/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) # ========================================== @@ -889,6 +897,9 @@ class App: # Laporan (akses pemilik) 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") @@ -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,83 +3016,88 @@ class App: def show_report_chart(self): - """Tampilkan grafik laporan menggunakan matplotlib""" - import matplotlib.pyplot as plt - from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg - from datetime import datetime - - # 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 = {} - total_per_day = {} - - 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 + """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 - # Parse total - total = float(total_str.replace('Rp ', '').replace(',', '').replace('.', '')) + # Get data from tree + if not self.report_tree.get_children(): + messagebox.showwarning("Tidak Ada Data", "Generate laporan terlebih dahulu") + return - # Count by metode - metode_counts[metode] = metode_counts.get(metode, 0) + 1 + # Collect data + metode_counts = {} + total_per_day = {} - # Sum by date - try: - date_obj = datetime.strptime(tanggal, "%Y-%m-%d %H:%M:%S") - date_key = date_obj.strftime("%Y-%m-%d") - total_per_day[date_key] = total_per_day.get(date_key, 0) + total - except: - pass - - # Create window - chart_window = tk.Toplevel(self.root) - chart_window.title("📊 Grafik Laporan Penjualan") - chart_window.geometry("900x600") - - # Create figure with 2 subplots - fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) - - # Chart 1: Pie chart metode pembayaran - if metode_counts: - labels = list(metode_counts.keys()) - sizes = list(metode_counts.values()) - colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8'] + 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 + + # Parse total + total = float(total_str.replace('Rp ', '').replace(',', '').replace('.', '')) + + # Count by metode + metode_counts[metode] = metode_counts.get(metode, 0) + 1 + + # Sum by date + try: + date_obj = datetime.strptime(tanggal, "%Y-%m-%d %H:%M:%S") + date_key = date_obj.strftime("%Y-%m-%d") + total_per_day[date_key] = total_per_day.get(date_key, 0) + total + except: + pass - ax1.pie(sizes, labels=labels, autopct='%1.1f%%', colors=colors, startangle=90) - ax1.set_title('Transaksi per Metode Pembayaran', fontsize=12, fontweight='bold') - - # Chart 2: Bar chart pendapatan per hari - if total_per_day: - dates = sorted(total_per_day.keys()) - totals = [total_per_day[d] for d in dates] + # Create window + chart_window = tk.Toplevel(self.root) + chart_window.title("📊 Grafik Laporan Penjualan") + chart_window.geometry("900x600") - date_labels = [ - datetime.strptime(d, "%Y-%m-%d").strftime("%d/%m") - for d in dates - ] + # Create figure with 2 subplots + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) - ax2.bar(date_labels, totals, color='#4ECDC4') - ax2.set_xlabel('Tanggal', fontweight='bold') - ax2.set_ylabel('Pendapatan (Rp)', fontweight='bold') - ax2.set_title('Pendapatan Harian', fontsize=12, fontweight='bold') - ax2.tick_params(axis='x', rotation=45) + # Chart 1: Pie chart metode pembayaran + if metode_counts: + labels = list(metode_counts.keys()) + sizes = list(metode_counts.values()) + colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8'] + + ax1.pie(sizes, labels=labels, autopct='%1.1f%%', colors=colors, startangle=90) + ax1.set_title('Transaksi per Metode Pembayaran', fontsize=12, fontweight='bold') - ax2.yaxis.set_major_formatter( - plt.FuncFormatter(lambda x, p: f'Rp {x/1000:.0f}K') - ) - - plt.tight_layout() - - # Embed in Tkinter - canvas = FigureCanvasTkAgg(fig, master=chart_window) - canvas.draw() - canvas.get_tk_widget().pack(fill='both', expand=True) - - ttk.Button(chart_window, text="Tutup", command=chart_window.destroy).pack(pady=10) + # Chart 2: Bar chart pendapatan per hari + if total_per_day: + dates = sorted(total_per_day.keys()) + totals = [total_per_day[d] for d in dates] + + date_labels = [ + datetime.strptime(d, "%Y-%m-%d").strftime("%d/%m") + for d in dates + ] + + ax2.bar(date_labels, totals, color='#4ECDC4') + ax2.set_xlabel('Tanggal', fontweight='bold') + ax2.set_ylabel('Pendapatan (Rp)', fontweight='bold') + ax2.set_title('Pendapatan Harian', fontsize=12, fontweight='bold') + ax2.tick_params(axis='x', rotation=45) + + ax2.yaxis.set_major_formatter( + plt.FuncFormatter(lambda x, p: f'Rp {x/1000:.0f}K') + ) + + plt.tight_layout() + + # Embed in Tkinter + canvas = FigureCanvasTkAgg(fig, master=chart_window) + canvas.draw() + canvas.get_tk_widget().pack(fill='both', expand=True) + + 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): @@ -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( + "", + 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("", _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 diff --git a/menu.csv b/menu.csv index d5ca631..148d1ae 100644 --- a/menu.csv +++ b/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 diff --git a/transaksi.csv b/transaksi.csv index 54b23ff..6978a5a 100644 --- a/transaksi.csv +++ b/transaksi.csv @@ -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