Sistem Order Pembeli

This commit is contained in:
Jevinca Marvella 2025-12-08 16:25:46 +07:00
parent 1f904a2ba8
commit 5f8f54e8d2
4 changed files with 543 additions and 5 deletions

540
main.py
View File

@ -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)
@ -867,6 +1053,358 @@ class App:
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(
"<Configure>",
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("<MouseWheel>", _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}")

View File

@ -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

1 id nama kategori harga stok foto tersedia item_discount_pct
2 1 Americano Minuman 20000 10 1 0
3 2 Latte Minuman 25000 5 2 1 10
4 3 Banana Cake Dessert 30000 2 1 1 0
5 4 Nasi Goreng Makanan 35000 0 0 0