From 207f2eecedb69ef9ef834ad8cba359a2cd9b3bff Mon Sep 17 00:00:00 2001 From: Jevinca Marvella Date: Sat, 13 Dec 2025 19:09:07 +0700 Subject: [PATCH] Revisi+Tambah fitur --- main.py | 630 ++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 594 insertions(+), 36 deletions(-) diff --git a/main.py b/main.py index b164fe0..47459ac 100644 --- a/main.py +++ b/main.py @@ -825,8 +825,11 @@ class App: 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.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) @@ -836,49 +839,104 @@ class App: self.tab_order = ttk.Frame(main) self.tab_waiter = ttk.Frame(main) self.tab_favorite = ttk.Frame(main) + + # ======================================== + # KONDISI TAB BERDASARKAN ROLE + # ======================================== - # Tab untuk semua role - main.add(self.tab_menu_view, text="Menu - View") - - # Tab khusus pembeli (bisa order) - if self.session['role'] in ['pembeli', 'admin', 'user']: - main.add(self.tab_order, text="Order Menu") - + # Tab untuk SEMUA role + main.add(self.tab_menu_view, text="📖 Menu - View") + + # ========================================== + # ROLE: PEMBELI / USER + # ========================================== if self.session['role'] in ['pembeli', 'user']: - main.add(self.tab_favorite, text="Favorit Saya") + main.add(self.tab_order, text="🛒 Order Menu") + main.add(self.tab_favorite, text="⭐ Favorit Saya") - # Tab khusus waiter - if self.session['role'] in ['waiter', 'admin']: - main.add(self.tab_waiter, text="Waiter - Pesanan") + # ========================================== + # ROLE: WAITER + # ========================================== + if self.session['role'] == 'waiter': + main.add(self.tab_waiter, text="🍽️ Kelola Pesanan") - # Tab pembayaran untuk kasir & admin - if self.session['role'] in ['kasir', 'admin']: + # ========================================== + # ROLE: KASIR (Order + Transaksi SAJA) + # ========================================== + if self.session['role'] == 'kasir': + main.add(self.tab_order, text="🛒 Order Menu") self.tab_payment = ttk.Frame(main) - main.add(self.tab_payment, text="💰 Pembayaran") - - # Tab khusus admin - if self.session['role'] == 'admin': - main.add(self.tab_menu_manage, text="Menu - Manage") - main.add(self.tab_promo, text="Promo - Manage") - else: - pass + main.add(self.tab_payment, text="💰 Transaksi") - self.build_menu_view_tab(self.tab_menu_view) - - if self.session['role'] in ['pembeli', 'admin', 'user']: - self.build_order_tab(self.tab_order) - if self.session['role'] in ['pembeli', 'user']: - self.build_favorite_tab(self.tab_favorite) - - if self.session['role'] in ['waiter', 'admin']: - self.build_waiter_tab(self.tab_waiter) - - if self.session['role'] in ['kasir', 'admin']: - self.build_payment_tab(self.tab_payment) - + # ========================================== + # ROLE: PEMILIK (Laporan SAJA) + # ========================================== + if self.session['role'] == 'pemilik': + self.tab_report = ttk.Frame(main) + main.add(self.tab_report, text="📊 Laporan") + + # ========================================== + # ROLE: ADMIN (Kelola Semua) + # ========================================== if self.session['role'] == 'admin': + # Admin bisa order juga (untuk testing) + main.add(self.tab_order, text="🛒 Order Menu") + + # Kelola pesanan (akses waiter) + main.add(self.tab_waiter, text="🍽️ Kelola Pesanan") + + # Transaksi (akses kasir) + self.tab_payment = ttk.Frame(main) + main.add(self.tab_payment, text="💰 Transaksi") + + # Laporan (akses pemilik) + self.tab_report = ttk.Frame(main) + main.add(self.tab_report, text="📊 Laporan") + + # Kelola Menu & Promo (KHUSUS ADMIN) + main.add(self.tab_menu_manage, text="⚙️ Kelola Menu") + main.add(self.tab_promo, text="🎁 Kelola Promo") + + # Kelola User (TAMBAHAN NANTI) + self.tab_user_manage = ttk.Frame(main) + main.add(self.tab_user_manage, text="👥 Kelola User") + + + # ======================================== + # BUILD TAB BERDASARKAN ROLE (REVISI) + # ======================================== + + # Menu View - untuk SEMUA role + self.build_menu_view_tab(self.tab_menu_view) + + # Pembeli/User + if self.session['role'] in ['pembeli', 'user']: + self.build_order_tab(self.tab_order) + self.build_favorite_tab(self.tab_favorite) + + # Waiter + if self.session['role'] == 'waiter': + self.build_waiter_tab(self.tab_waiter) + + # Kasir (Order + Transaksi SAJA) + if self.session['role'] == 'kasir': + self.build_order_tab(self.tab_order) + self.build_payment_tab(self.tab_payment) + + # Pemilik (Laporan SAJA) + if self.session['role'] == 'pemilik': + self.build_report_tab(self.tab_report) + + # Admin (Semua Akses) + if self.session['role'] == 'admin': + self.build_order_tab(self.tab_order) + self.build_waiter_tab(self.tab_waiter) + self.build_payment_tab(self.tab_payment) + self.build_report_tab(self.tab_report) 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 + def build_menu_view_tab(self, parent): for w in parent.winfo_children(): @@ -2525,6 +2583,506 @@ class App: # Load data self.reload_payment_orders() + def build_report_tab(self, parent): + """Tab laporan penjualan untuk admin/pemilik""" + 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="📊 Laporan Penjualan", font=("Arial", 14, "bold")).pack(side='left') + ttk.Button(header, text="🔄 Refresh", command=self.reload_report).pack(side='right', padx=6) + + # Filter frame + filter_frame = ttk.LabelFrame(parent, text="🔍 Filter Laporan", padding=10) + filter_frame.pack(fill='x', padx=10, pady=6) + + # Row 0: Periode + ttk.Label(filter_frame, text="Periode:").grid(row=0, column=0, sticky='w', padx=5, pady=4) + self.report_period_var = tk.StringVar(value='harian') + + period_frame = ttk.Frame(filter_frame) + period_frame.grid(row=0, column=1, sticky='w', padx=5, pady=4) + + ttk.Radiobutton(period_frame, text="Hari Ini", variable=self.report_period_var, value='harian').pack(side='left', padx=5) + ttk.Radiobutton(period_frame, text="Minggu Ini", variable=self.report_period_var, value='mingguan').pack(side='left', padx=5) + ttk.Radiobutton(period_frame, text="Bulan Ini", variable=self.report_period_var, value='bulanan').pack(side='left', padx=5) + + # Row 1: Metode Pembayaran + ttk.Label(filter_frame, text="Metode:").grid(row=1, column=0, sticky='w', padx=5, pady=4) + self.report_method_var = tk.StringVar(value='semua') + + method_combo = ttk.Combobox(filter_frame, textvariable=self.report_method_var, width=20, state='readonly') + method_combo['values'] = ('Semua', 'Cash', 'QRIS', 'E-Wallet') + method_combo.grid(row=1, column=1, sticky='w', padx=5, pady=4) + + # Tombol generate + ttk.Button( + filter_frame, + text="📊 Generate Laporan", + command=self.generate_report, + style="Accent.TButton" + ).grid(row=2, column=0, columnspan=2, pady=10) + + # Summary frame + summary_frame = ttk.LabelFrame(parent, text="📈 Ringkasan", padding=10) + summary_frame.pack(fill='x', padx=10, pady=6) + + summary_inner = ttk.Frame(summary_frame) + summary_inner.pack() + + # Labels untuk ringkasan + ttk.Label(summary_inner, text="Total Transaksi:").grid(row=0, column=0, sticky='w', padx=10, pady=3) + self.report_total_trx_label = ttk.Label(summary_inner, text="0", font=("Arial", 10, "bold")) + self.report_total_trx_label.grid(row=0, column=1, sticky='w', padx=10, pady=3) + + ttk.Label(summary_inner, text="Total Pendapatan:").grid(row=1, column=0, sticky='w', padx=10, pady=3) + self.report_total_income_label = ttk.Label( + summary_inner, + text="Rp 0", + font=("Arial", 10, "bold"), + foreground='green' + ) + self.report_total_income_label.grid(row=1, column=1, sticky='w', padx=10, pady=3) + + ttk.Label(summary_inner, text="Rata-rata/Transaksi:").grid(row=2, column=0, sticky='w', padx=10, pady=3) + self.report_avg_label = ttk.Label(summary_inner, text="Rp 0", font=("Arial", 10, "bold")) + self.report_avg_label.grid(row=2, column=1, sticky='w', padx=10, pady=3) + + # Detail tabel + detail_frame = ttk.LabelFrame(parent, text="📋 Detail Transaksi", padding=10) + detail_frame.pack(fill='both', expand=True, padx=10, pady=6) + + # Treeview + tree_frame = ttk.Frame(detail_frame) + tree_frame.pack(fill='both', expand=True) + + tree_scroll = ttk.Scrollbar(tree_frame, orient='vertical') + tree_scroll.pack(side='right', fill='y') + + cols = ("ID", "Tanggal", "Meja", "Total", "Metode", "Status") + self.report_tree = ttk.Treeview( + tree_frame, + columns=cols, + show='headings', + height=10, + yscrollcommand=tree_scroll.set + ) + + tree_scroll.config(command=self.report_tree.yview) + + self.report_tree.heading("ID", text="ID") + self.report_tree.heading("Tanggal", text="Tanggal") + self.report_tree.heading("Meja", text="Meja") + self.report_tree.heading("Total", text="Total") + self.report_tree.heading("Metode", text="Metode") + self.report_tree.heading("Status", text="Status") + + self.report_tree.column("ID", width=50) + self.report_tree.column("Tanggal", width=140) + self.report_tree.column("Meja", width=60) + self.report_tree.column("Total", width=100) + self.report_tree.column("Metode", width=120) + self.report_tree.column("Status", width=80) + + self.report_tree.pack(side='left', fill='both', expand=True) + + # Tombol grafik + btn_frame = ttk.Frame(parent) + btn_frame.pack(pady=10) + ttk.Button(btn_frame, text="📊 Tampilkan Grafik", command=self.show_report_chart).pack() + + def build_user_manage_tab(self, parent): + """Tab kelola user (admin bisa tambah/edit kasir)""" + 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="👥 Kelola User & Kasir", font=("Arial", 14, "bold")).pack(side='left') + ttk.Button(header, text="➕ Tambah User", command=self.open_add_user_window).pack(side='right', padx=6) + + # Treeview user + tree_frame = ttk.Frame(parent) + tree_frame.pack(fill='both', expand=True, padx=10, pady=6) + + tree_scroll = ttk.Scrollbar(tree_frame, orient='vertical') + tree_scroll.pack(side='right', fill='y') + + cols = ("ID", "Username", "Role") + self.user_tree = ttk.Treeview(tree_frame, columns=cols, show='headings', height=15, yscrollcommand=tree_scroll.set) + + tree_scroll.config(command=self.user_tree.yview) + + self.user_tree.heading("ID", text="ID") + self.user_tree.heading("Username", text="Username") + self.user_tree.heading("Role", text="Role") + + self.user_tree.column("ID", width=80) + self.user_tree.column("Username", width=200) + self.user_tree.column("Role", width=150) + + self.user_tree.pack(side='left', fill='both', expand=True) + + # Tombol aksi + btn_frame = ttk.Frame(parent) + btn_frame.pack(pady=10) + + ttk.Button(btn_frame, text="✏️ Edit User", command=self.open_edit_user_window).pack(side='left', padx=5) + ttk.Button(btn_frame, text="🗑️ Hapus User", command=self.delete_selected_user).pack(side='left', padx=5) + ttk.Button(btn_frame, text="🔄 Refresh", command=self.reload_user_table).pack(side='left', padx=5) + + # Load data + self.reload_user_table() + + def reload_user_table(self): + """Reload tabel user""" + # Clear tree + for r in self.user_tree.get_children(): + self.user_tree.delete(r) + + # Get users + users = read_all(USERS_CSV) + + for user in users: + uid = user.get('id') + username = user.get('username') + role = user.get('role') + + self.user_tree.insert("", tk.END, values=(uid, username, role)) + + def open_add_user_window(self): + """Popup untuk tambah user baru""" + w = tk.Toplevel(self.root) + w.title("➕ Tambah User Baru") + w.geometry("400x280") + w.transient(self.root) + w.grab_set() + + frm = ttk.Frame(w, padding=15) + frm.pack(fill='both', expand=True) + + ttk.Label(frm, text="Tambah User Baru", font=("Arial", 12, "bold")).pack(pady=10) + + # Username + ttk.Label(frm, text="Username:").pack(anchor='w', pady=(10, 2)) + username_var = tk.StringVar() + ttk.Entry(frm, textvariable=username_var, width=30).pack(fill='x', pady=(0, 10)) + + # Password + ttk.Label(frm, text="Password:").pack(anchor='w', pady=2) + password_var = tk.StringVar() + ttk.Entry(frm, textvariable=password_var, show="*", width=30).pack(fill='x', pady=(0, 10)) + + # Role + ttk.Label(frm, text="Role:").pack(anchor='w', pady=2) + role_var = tk.StringVar(value='kasir') + + role_frame = ttk.Frame(frm) + role_frame.pack(fill='x', pady=(0, 10)) + + ttk.Radiobutton(role_frame, text="Kasir", variable=role_var, value='kasir').pack(side='left', padx=5) + ttk.Radiobutton(role_frame, text="Waiter", variable=role_var, value='waiter').pack(side='left', padx=5) + ttk.Radiobutton(role_frame, text="Pembeli", variable=role_var, value='pembeli').pack(side='left', padx=5) + + def save_user(): + username = username_var.get().strip() + password = password_var.get().strip() + role = role_var.get() + + if not username or not password: + messagebox.showerror("Error", "Username dan password harus diisi!") + return + + # Cek username sudah ada + users = read_all(USERS_CSV) + for u in users: + if u.get('username') == username: + messagebox.showerror("Error", f"Username '{username}' sudah digunakan!") + return + + # Tambah user + new_id = next_int_id(users, 'id') + users.append({ + 'id': new_id, + 'username': username, + 'password': password, + 'role': role + }) + + write_all(USERS_CSV, ["id", "username", "password", "role"], users) + + messagebox.showinfo("Sukses", f"User '{username}' berhasil ditambahkan!") + w.destroy() + self.reload_user_table() + + ttk.Button(frm, text="💾 Simpan", command=save_user, style="Accent.TButton").pack(pady=15) + + def open_edit_user_window(self): + """Popup untuk edit user""" + sel = self.user_tree.selection() + if not sel: + messagebox.showwarning("Pilih User", "Pilih user yang akan diedit") + return + + item = self.user_tree.item(sel)['values'] + user_id = item[0] + + # Get user data + users = read_all(USERS_CSV) + user_data = None + for u in users: + if u.get('id') == str(user_id): + user_data = u + break + + if not user_data: + messagebox.showerror("Error", "User tidak ditemukan") + return + + w = tk.Toplevel(self.root) + w.title("✏️ Edit User") + w.geometry("400x280") + w.transient(self.root) + w.grab_set() + + frm = ttk.Frame(w, padding=15) + frm.pack(fill='both', expand=True) + + ttk.Label(frm, text=f"Edit User: {user_data.get('username')}", font=("Arial", 12, "bold")).pack(pady=10) + + # Password baru + ttk.Label(frm, text="Password Baru (kosongkan jika tidak diubah):").pack(anchor='w', pady=2) + password_var = tk.StringVar() + ttk.Entry(frm, textvariable=password_var, show="*", width=30).pack(fill='x', pady=(0, 10)) + + # Role + ttk.Label(frm, text="Role:").pack(anchor='w', pady=2) + role_var = tk.StringVar(value=user_data.get('role')) + + role_frame = ttk.Frame(frm) + role_frame.pack(fill='x', pady=(0, 10)) + + ttk.Radiobutton(role_frame, text="Kasir", variable=role_var, value='kasir').pack(side='left', padx=5) + ttk.Radiobutton(role_frame, text="Waiter", variable=role_var, value='waiter').pack(side='left', padx=5) + ttk.Radiobutton(role_frame, text="Pembeli", variable=role_var, value='pembeli').pack(side='left', padx=5) + ttk.Radiobutton(role_frame, text="Admin", variable=role_var, value='admin').pack(side='left', padx=5) + + def update_user(): + new_password = password_var.get().strip() + new_role = role_var.get() + + # Update user + for u in users: + if u.get('id') == str(user_id): + if new_password: + u['password'] = new_password + u['role'] = new_role + break + + write_all(USERS_CSV, ["id", "username", "password", "role"], users) + + messagebox.showinfo("Sukses", "User berhasil diupdate!") + w.destroy() + self.reload_user_table() + + ttk.Button(frm, text="💾 Update", command=update_user, style="Accent.TButton").pack(pady=15) + + def delete_selected_user(self): + """Hapus user yang dipilih""" + sel = self.user_tree.selection() + if not sel: + messagebox.showwarning("Pilih User", "Pilih user yang akan dihapus") + return + + item = self.user_tree.item(sel)['values'] + user_id = item[0] + username = item[1] + + # Cek jangan hapus admin + if username == 'admin': + messagebox.showerror("Error", "Tidak bisa menghapus user admin!") + return + + # Konfirmasi + if not messagebox.askyesno("Konfirmasi", f"Hapus user '{username}'?"): + return + + # Hapus + users = read_all(USERS_CSV) + users = [u for u in users if u.get('id') != str(user_id)] + + write_all(USERS_CSV, ["id", "username", "password", "role"], users) + + messagebox.showinfo("Sukses", f"User '{username}' berhasil dihapus!") + self.reload_user_table() + + + def reload_report(self): + """Reload data laporan""" + self.generate_report() + + + def generate_report(self): + """Generate laporan berdasarkan filter""" + from datetime import datetime, timedelta + + # Clear tree + for r in self.report_tree.get_children(): + self.report_tree.delete(r) + + # Get filter + period = self.report_period_var.get() + method_filter = self.report_method_var.get().lower() + + # Hitung tanggal range + today = datetime.now() + + if period == 'harian': + start_date = today.replace(hour=0, minute=0, second=0, microsecond=0) + elif period == 'mingguan': + start_date = today - timedelta(days=today.weekday()) + start_date = start_date.replace(hour=0, minute=0, second=0, microsecond=0) + else: # bulanan + start_date = today.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + + # Get data transaksi yang sudah dibayar + all_transaksi = transaksi_list(status='dibayar') + + filtered_transaksi = [] + total_income = 0 + + for trx in all_transaksi: + tid, uid, meja, total, status, promo_code, tanggal = trx + + # Parse tanggal + try: + trx_date = datetime.strptime(tanggal, "%Y-%m-%d %H:%M:%S") + except: + continue + + # Filter by date range + if trx_date < start_date: + continue + + # Get payment info + payment_data = pembayaran_get_by_transaksi(tid) + if not payment_data: + continue + + pid, metode, jumlah, status_bayar, tanggal_bayar, struk = payment_data + + # Filter by method + if method_filter != 'semua': + if method_filter == 'cash' and metode != 'cash': + continue + elif method_filter == 'qris' and metode != 'qris': + continue + elif method_filter == 'e-wallet' and not metode.startswith('ewallet'): + continue + + # Add to filtered + filtered_transaksi.append((tid, tanggal, meja, total, metode, status)) + total_income += total + + # Insert to tree + self.report_tree.insert( + "", + tk.END, + values=(tid, tanggal, meja, f"Rp {total:,.0f}", metode.upper(), status) + ) + + # Update summary + count = len(filtered_transaksi) + avg = total_income / count if count > 0 else 0 + + self.report_total_trx_label.config(text=str(count)) + self.report_total_income_label.config(text=f"Rp {total_income:,.0f}") + self.report_avg_label.config(text=f"Rp {avg:,.0f}") + + + 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 + + # 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 + + # 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'] + + 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] + + 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) + + def reload_payment_orders(self): """Load transaksi dengan status 'selesai' yang belum dibayar""" # Clear tree