diff --git a/detail_transaksi.csv b/detail_transaksi.csv index 0b1d68b..1456333 100644 --- a/detail_transaksi.csv +++ b/detail_transaksi.csv @@ -1 +1 @@ -id,transaksi_id,menu_id,qty,harga_satuan,subtotal_item \ No newline at end of file +id,transaksi_id,menu_id,qty,harga_satuan,subtotal_item diff --git a/main.py b/main.py index 9c97e1c..5e9ade0 100644 --- a/main.py +++ b/main.py @@ -9,7 +9,7 @@ import os import csv import tkinter as tk -from tkinter import ttk, messagebox, filedialog +from tkinter import ttk, messagebox, filedialog, simpledialog from PIL import Image, ImageTk USERS_CSV = "users.csv" @@ -365,6 +365,187 @@ def promo_get(code): return None +# Wilayah dikuasai Transaksi + + +def transaksi_add(user_id, nomor_meja, cart_items, promo_code=None): + """Simpan transaksi baru dengan status 'pending'""" + from datetime import datetime + + if not cart_items: + return False, "Keranjang kosong" + + # Hitung diskon dan total + calc = apply_discounts_and_promo(cart_items, promo_code) + + # Buat transaksi + rows = read_all(TRANSAKSI_CSV) + transaksi_id = next_int_id(rows, "id") + tanggal = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + transaksi_row = { + "id": transaksi_id, + "user_id": str(user_id), + "nomor_meja": str(nomor_meja), + "total": str(calc['total']), + "status": "pending", + "promo_code": calc.get('promo_code') or "", + "subtotal": str(calc['subtotal']), + "item_discount": str(calc['item_discount']), + "promo_discount": str(calc['promo_discount']), + "tanggal": tanggal + } + rows.append(transaksi_row) + write_all(TRANSAKSI_CSV, ["id", "user_id", "nomor_meja", "total", "status", "promo_code", "subtotal", "item_discount", "promo_discount", "tanggal"], rows) + + # Simpan detail transaksi + detail_rows = read_all(DETAIL_TRANSAKSI_CSV) + for item in cart_items: + detail_id = next_int_id(detail_rows, "id") + menu_data = menu_get(item['menu_id']) + if not menu_data: + continue + _, nama, kategori, harga, stok, foto, tersedia, item_disc = menu_data + + qty = int(item['qty']) + subtotal_item = harga * qty + + detail_rows.append({ + "id": detail_id, + "transaksi_id": transaksi_id, + "menu_id": str(item['menu_id']), + "qty": str(qty), + "harga_satuan": str(harga), + "subtotal_item": str(subtotal_item) + }) + + # Kurangi stok + success, msg = menu_decrease_stock(item['menu_id'], qty) + if not success: + return False, f"Gagal mengurangi stok menu ID {item['menu_id']}: {msg}" + + write_all(DETAIL_TRANSAKSI_CSV, ["id", "transaksi_id", "menu_id", "qty", "harga_satuan", "subtotal_item"], detail_rows) + + return True, transaksi_id + + +def transaksi_list(status=None, user_id=None): + """Ambil daftar transaksi, bisa filter by status atau user_id""" + rows = read_all(TRANSAKSI_CSV) + out = [] + for r in rows: + if status and r.get("status") != status: + continue + if user_id and r.get("user_id") != str(user_id): + continue + + try: + tid = int(r.get("id") or 0) + except: + tid = r.get("id") + try: + uid = int(r.get("user_id") or 0) + except: + uid = r.get("user_id") + try: + meja = int(r.get("nomor_meja") or 0) + except: + meja = r.get("nomor_meja") + try: + total = float(r.get("total") or 0.0) + except: + total = 0.0 + + out.append((tid, uid, meja, total, r.get("status"), r.get("promo_code"), r.get("tanggal"))) + + out.sort(key=lambda x: int(x[0]), reverse=True) + return out + + +def transaksi_get(transaksi_id): + """Ambil detail transaksi by ID""" + rows = read_all(TRANSAKSI_CSV) + for r in rows: + if r.get("id") == str(transaksi_id): + try: + tid = int(r.get("id") or 0) + except: + tid = r.get("id") + try: + uid = int(r.get("user_id") or 0) + except: + uid = r.get("user_id") + try: + meja = int(r.get("nomor_meja") or 0) + except: + meja = r.get("nomor_meja") + try: + total = float(r.get("total") or 0.0) + except: + total = 0.0 + try: + subtotal = float(r.get("subtotal") or 0.0) + except: + subtotal = 0.0 + try: + item_disc = float(r.get("item_discount") or 0.0) + except: + item_disc = 0.0 + try: + promo_disc = float(r.get("promo_discount") or 0.0) + except: + promo_disc = 0.0 + + return (tid, uid, meja, total, r.get("status"), r.get("promo_code"), subtotal, item_disc, promo_disc, r.get("tanggal")) + return None + + +def transaksi_update_status(transaksi_id, new_status): + """Update status transaksi""" + rows = read_all(TRANSAKSI_CSV) + found = False + for r in rows: + if r.get("id") == str(transaksi_id): + r["status"] = new_status + found = True + break + + if found: + write_all(TRANSAKSI_CSV, ["id", "user_id", "nomor_meja", "total", "status", "promo_code", "subtotal", "item_discount", "promo_discount", "tanggal"], rows) + return True + return False + + +def detail_transaksi_list(transaksi_id): + """Ambil semua detail item dari transaksi tertentu""" + rows = read_all(DETAIL_TRANSAKSI_CSV) + out = [] + for r in rows: + if r.get("transaksi_id") == str(transaksi_id): + try: + did = int(r.get("id") or 0) + except: + did = r.get("id") + try: + mid = int(r.get("menu_id") or 0) + except: + mid = r.get("menu_id") + try: + qty = int(r.get("qty") or 0) + except: + qty = 0 + try: + harga = float(r.get("harga_satuan") or 0.0) + except: + harga = 0.0 + try: + subtotal = float(r.get("subtotal_item") or 0.0) + except: + subtotal = 0.0 + + out.append((did, mid, qty, harga, subtotal)) + + return out @@ -463,6 +644,8 @@ class App: self.root.geometry("1000x650") self.root.resizable(False, False) self.login_frame() + style = ttk.Style() + style.configure("Accent.TButton", font=("Arial", 11, "bold")) def login_frame(self): for w in self.root.winfo_children(): @@ -514,7 +697,9 @@ class App: self.tab_menu_manage = ttk.Frame(main) self.tab_menu_view = ttk.Frame(main) self.tab_promo = ttk.Frame(main) + self.tab_order = ttk.Frame(main) + main.add(self.tab_order, text="Order Menu") main.add(self.tab_menu_view, text="Menu - View") if self.session['role'] == 'admin': main.add(self.tab_menu_manage, text="Menu - Manage") @@ -523,6 +708,7 @@ class App: pass self.build_menu_view_tab(self.tab_menu_view) + self.build_order_tab(self.tab_order) if self.session['role'] == 'admin': self.build_menu_manage_tab(self.tab_menu_manage) self.build_promo_tab(self.tab_promo) @@ -866,7 +1052,359 @@ class App: promo_delete(code) messagebox.showinfo("Dihapus","Promo terhapus") self.reload_promo_table() + + def build_order_tab(self, parent): + """Tab untuk order menu dengan tampilan card seperti GrabFood/Gojek""" + for w in parent.winfo_children(): + w.destroy() + + # Split jadi 2 panel: kiri = menu cards, kanan = cart + 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='both', padx=6, pady=6) + + # === PANEL KIRI: Daftar Menu dengan Card === + ttk.Label(left, text="Daftar Menu", font=("Arial", 13, "bold")).pack(pady=6) + + # Filter search + search_frame = ttk.Frame(left) + search_frame.pack(fill='x', pady=4) + ttk.Label(search_frame, text="Cari:").pack(side='left', padx=3) + self.order_search_var = tk.StringVar() + ttk.Entry(search_frame, textvariable=self.order_search_var, width=25).pack(side='left', padx=3) + ttk.Button(search_frame, text="Cari", command=self.reload_order_menu_cards).pack(side='left', padx=3) + ttk.Button(search_frame, text="Reset", command=self.reset_order_search).pack(side='left', padx=3) + + # Scrollable frame untuk cards + canvas = tk.Canvas(left, bg='white') + scrollbar = ttk.Scrollbar(left, orient="vertical", command=canvas.yview) + self.menu_cards_frame = ttk.Frame(canvas) + + self.menu_cards_frame.bind( + "", + lambda e: canvas.configure(scrollregion=canvas.bbox("all")) + ) + + canvas.create_window((0, 0), window=self.menu_cards_frame, anchor="nw") + canvas.configure(yscrollcommand=scrollbar.set) + + canvas.pack(side="left", fill="both", expand=True) + scrollbar.pack(side="right", fill="y") + + # Bind mouse wheel untuk scroll + def _on_mousewheel(event): + canvas.yview_scroll(int(-1*(event.delta/120)), "units") + canvas.bind_all("", _on_mousewheel) + + # === PANEL KANAN: Keranjang === + ttk.Label(right, text="Keranjang Belanja", font=("Arial", 13, "bold")).pack(pady=6) + + # Treeview cart + cart_cols = ("Menu", "Qty", "Harga", "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) + if c == "Menu": + self.cart_tree.column(c, width=150) + else: + self.cart_tree.column(c, width=70) + self.cart_tree.pack(fill='both', expand=True) + + # Tombol hapus item atau kosongkan cart + cart_btn_frame = ttk.Frame(right) + cart_btn_frame.pack(pady=4) + ttk.Button(cart_btn_frame, text="Hapus Item", command=self.remove_cart_item).pack(side='left', padx=3) + ttk.Button(cart_btn_frame, text="Kosongkan", command=self.clear_cart).pack(side='left', padx=3) + + # Info total + self.cart_subtotal_label = ttk.Label(right, text="Subtotal: Rp 0", font=("Arial", 10)) + self.cart_subtotal_label.pack(pady=2) + self.cart_discount_label = ttk.Label(right, text="Diskon Item: Rp 0", font=("Arial", 10)) + self.cart_discount_label.pack(pady=2) + self.cart_promo_label = ttk.Label(right, text="Diskon Promo: Rp 0", font=("Arial", 10)) + self.cart_promo_label.pack(pady=2) + self.cart_total_label = ttk.Label(right, text="TOTAL: Rp 0", font=("Arial", 12, "bold")) + self.cart_total_label.pack(pady=4) + + # Input nomor meja dan promo + checkout_frame = ttk.Frame(right) + checkout_frame.pack(pady=6) + ttk.Label(checkout_frame, text="No. Meja:").grid(row=0, column=0, sticky='e', padx=3, pady=3) + self.order_meja_var = tk.StringVar() + ttk.Entry(checkout_frame, textvariable=self.order_meja_var, width=15).grid(row=0, column=1, pady=3) + + ttk.Label(checkout_frame, text="Kode Promo:").grid(row=1, column=0, sticky='e', padx=3, pady=3) + self.order_promo_var = tk.StringVar() + ttk.Entry(checkout_frame, textvariable=self.order_promo_var, width=15).grid(row=1, column=1, pady=3) + ttk.Button(checkout_frame, text="Terapkan", command=self.update_cart_display).grid(row=1, column=2, padx=3) + + # Tombol checkout + ttk.Button(right, text="🛒 CHECKOUT", command=self.checkout_order, style="Accent.TButton").pack(pady=8) + + # Init cart data + self.cart_items = [] # List of dict: {'menu_id': int, 'qty': int} + + # Load menu cards + self.reload_order_menu_cards() + def reload_order_menu_cards(self): + """Load menu dalam bentuk cards dengan gambar + tombol +/-""" + # Clear existing cards + for widget in self.menu_cards_frame.winfo_children(): + widget.destroy() + + # Get menu data + search = self.order_search_var.get().strip() or None + results = menu_list(search_text=search, available_only=True) + + # Buat dict untuk qty di cart + cart_dict = {} + for cart_item in self.cart_items: + cart_dict[cart_item['menu_id']] = cart_item['qty'] + + # Render cards dalam grid (2 kolom) + row = 0 + col = 0 + for menu_data in results: + mid, nama, kategori, harga, stok, foto, tersedia, item_disc = menu_data + + # Create card frame + card = tk.Frame(self.menu_cards_frame, relief='ridge', borderwidth=2, bg='white', padx=10, pady=10) + card.grid(row=row, column=col, padx=8, pady=8, sticky='nsew') + + # Gambar + if foto and os.path.exists(foto): + try: + img = Image.open(foto) + img = img.resize((150, 100), Image.Resampling.LANCZOS) + photo = ImageTk.PhotoImage(img) + + # Simpan reference agar tidak di-garbage collect + img_label = tk.Label(card, image=photo, bg='white') + img_label.image = photo + img_label.pack() + except: + tk.Label(card, text="[No Image]", bg='lightgray', width=20, height=6).pack() + else: + tk.Label(card, text="[No Image]", bg='lightgray', width=20, height=6).pack() + + # Nama menu + tk.Label(card, text=nama, font=("Arial", 11, "bold"), bg='white', wraplength=150).pack(pady=(5, 2)) + + # Kategori + tk.Label(card, text=kategori, font=("Arial", 9), fg='gray', bg='white').pack() + + # Harga + harga_text = f"Rp {harga:,.0f}" + if item_disc > 0: + harga_text += f" (-{item_disc}%)" + tk.Label(card, text=harga_text, font=("Arial", 10, "bold"), fg='green', bg='white').pack(pady=2) + + # Stok info + tk.Label(card, text=f"Stok: {stok}", font=("Arial", 8), fg='blue', bg='white').pack(pady=2) + + # Tombol +/- atau + saja + qty_in_cart = cart_dict.get(mid, 0) + + btn_frame = tk.Frame(card, bg='white') + btn_frame.pack(pady=5) + + if qty_in_cart > 0: + # Tampilkan - [qty] + + tk.Button( + btn_frame, + text="➖", + font=("Arial", 12, "bold"), + bg='#FF5722', + fg='white', + width=3, + command=lambda m=mid: self.decrease_from_card(m) + ).pack(side='left', padx=2) + + tk.Label( + btn_frame, + text=str(qty_in_cart), + font=("Arial", 12, "bold"), + bg='white', + width=3 + ).pack(side='left', padx=5) + + tk.Button( + btn_frame, + text="➕", + font=("Arial", 12, "bold"), + bg='#4CAF50', + fg='white', + width=3, + command=lambda m=mid, s=stok: self.increase_from_card(m, s) + ).pack(side='left', padx=2) + else: + # Tampilkan tombol + aja + tk.Button( + btn_frame, + text="➕ Tambah", + font=("Arial", 10, "bold"), + bg='#4CAF50', + fg='white', + width=12, + command=lambda m=mid, s=stok: self.increase_from_card(m, s) + ).pack() + + # Next column + col += 1 + if col >= 2: # 2 cards per row + col = 0 + row += 1 + + def reset_order_search(self): + self.order_search_var.set("") + self.reload_order_menu_cards() + + def increase_from_card(self, menu_id, stok): + """Tambah qty dari tombol + di card""" + # Cek qty saat ini + current_qty = 0 + cart_item_found = None + for cart_item in self.cart_items: + if cart_item['menu_id'] == menu_id: + current_qty = cart_item['qty'] + cart_item_found = cart_item + break + + # Cek stok + if current_qty >= stok: + messagebox.showwarning("Stok Habis", f"Stok hanya tersisa {stok}") + return + + # Tambah qty + if cart_item_found: + cart_item_found['qty'] += 1 + else: + self.cart_items.append({'menu_id': menu_id, 'qty': 1}) + + # Update tampilan + self.reload_order_menu_cards() + self.update_cart_display() + + def decrease_from_card(self, menu_id): + """Kurangi qty dari tombol - di card""" + for i, cart_item in enumerate(self.cart_items): + if cart_item['menu_id'] == menu_id: + cart_item['qty'] -= 1 + + # Kalau qty jadi 0, hapus dari cart + if cart_item['qty'] <= 0: + del self.cart_items[i] + + # Update tampilan + self.reload_order_menu_cards() + self.update_cart_display() + return + + def update_cart_display(self): + """Update tampilan keranjang dan hitung total""" + + # Clear tree + for r in self.cart_tree.get_children(): + self.cart_tree.delete(r) + + # Tampilkan item + for cart_item in self.cart_items: + menu_data = menu_get(cart_item['menu_id']) + if not menu_data: + print(f"WARNING: Menu ID {cart_item['menu_id']} tidak ditemukan!") + continue + + _, nama, kategori, harga, stok, foto, tersedia, item_disc = menu_data + qty = cart_item['qty'] + subtotal = harga * qty + + # DEBUG + print(f"Menambahkan ke tree: {nama} x{qty} = {subtotal}") + + self.cart_tree.insert("", tk.END, values=(nama, qty, f"{harga:,.0f}", f"{subtotal:,.0f}")) + + # Hitung total dengan diskon + promo_code = self.order_promo_var.get().strip() or None + + if self.cart_items: # PENTING: Hanya hitung kalau ada item + calc = apply_discounts_and_promo(self.cart_items, promo_code) + + self.cart_subtotal_label.config(text=f"Subtotal: Rp {calc['subtotal']:,.0f}") + self.cart_discount_label.config(text=f"Diskon Item: Rp {calc['item_discount']:,.0f}") + self.cart_promo_label.config(text=f"Diskon Promo: Rp {calc['promo_discount']:,.0f}") + self.cart_total_label.config(text=f"TOTAL: Rp {calc['total']:,.0f}") + else: + # Reset label kalau cart kosong + self.cart_subtotal_label.config(text="Subtotal: Rp 0") + self.cart_discount_label.config(text="Diskon Item: Rp 0") + self.cart_promo_label.config(text="Diskon Promo: Rp 0") + self.cart_total_label.config(text="TOTAL: Rp 0") + + + def remove_cart_item(self): + """Hapus item dari cart""" + sel = self.cart_tree.selection() + if not sel: + messagebox.showwarning("Pilih Item", "Pilih item di keranjang") + return + + idx = self.cart_tree.index(sel) + if idx >= len(self.cart_items): + return + + del self.cart_items[idx] + self.update_cart_display() + + def clear_cart(self): + """Kosongkan keranjang""" + if not self.cart_items: + return + + if messagebox.askyesno("Konfirmasi", "Kosongkan keranjang?"): + self.cart_items = [] + self.update_cart_display() + + def checkout_order(self): + """Simpan pesanan ke database""" + if not self.cart_items: + messagebox.showwarning("Keranjang Kosong", "Tambahkan item terlebih dahulu") + return + + nomor_meja = self.order_meja_var.get().strip() + if not nomor_meja: + messagebox.showwarning("Input Error", "Masukkan nomor meja") + return + + try: + nomor_meja = int(nomor_meja) + except: + messagebox.showerror("Input Error", "Nomor meja harus angka") + return + + promo_code = self.order_promo_var.get().strip() or None + + # Validasi promo + if promo_code: + promo_data = promo_get(promo_code) + if not promo_data: + messagebox.showwarning("Promo Invalid", "Kode promo tidak ditemukan") + return + + # Simpan transaksi + success, result = transaksi_add(self.session['id'], nomor_meja, self.cart_items, promo_code) + + if success: + messagebox.showinfo("Sukses", f"Pesanan berhasil! ID Transaksi: {result}\nStatus: Pending") + # Reset + self.cart_items = [] + self.order_meja_var.set("") + self.order_promo_var.set("") + self.update_cart_display() + self.reload_order_menu() # Refresh stok + else: + messagebox.showerror("Error", f"Gagal menyimpan pesanan: {result}") diff --git a/menu.csv b/menu.csv index e4f28ee..30cfbe4 100644 --- a/menu.csv +++ b/menu.csv @@ -1,5 +1,5 @@ id,nama,kategori,harga,stok,foto,tersedia,item_discount_pct 1,Americano,Minuman,20000,10,,1,0 -2,Latte,Minuman,25000,5,,1,10 -3,Banana Cake,Dessert,30000,2,,1,0 +2,Latte,Minuman,25000,2,,1,10 +3,Banana Cake,Dessert,30000,1,,1,0 4,Nasi Goreng,Makanan,35000,0,,0,0 diff --git a/transaksi.csv b/transaksi.csv index 6ae0c5a..a66928d 100644 --- a/transaksi.csv +++ b/transaksi.csv @@ -1 +1 @@ -id,user_id,nomor_meja,total,status,promo_code,subtotal,item_discount,promo_discount,tanggal \ No newline at end of file +id,user_id,nomor_meja,total,status,promo_code,subtotal,item_discount,promo_discount,tanggal