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

View File

@ -1 +1 @@
id,transaksi_id,menu_id,qty,harga_satuan,subtotal_item id,transaksi_id,menu_id,qty,harga_satuan,subtotal_item

1 id transaksi_id menu_id qty harga_satuan subtotal_item

540
main.py
View File

@ -9,7 +9,7 @@
import os import os
import csv import csv
import tkinter as tk import tkinter as tk
from tkinter import ttk, messagebox, filedialog from tkinter import ttk, messagebox, filedialog, simpledialog
from PIL import Image, ImageTk from PIL import Image, ImageTk
USERS_CSV = "users.csv" USERS_CSV = "users.csv"
@ -365,6 +365,187 @@ def promo_get(code):
return None 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.geometry("1000x650")
self.root.resizable(False, False) self.root.resizable(False, False)
self.login_frame() self.login_frame()
style = ttk.Style()
style.configure("Accent.TButton", font=("Arial", 11, "bold"))
def login_frame(self): def login_frame(self):
for w in self.root.winfo_children(): for w in self.root.winfo_children():
@ -514,7 +697,9 @@ class App:
self.tab_menu_manage = ttk.Frame(main) self.tab_menu_manage = ttk.Frame(main)
self.tab_menu_view = ttk.Frame(main) self.tab_menu_view = ttk.Frame(main)
self.tab_promo = 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") main.add(self.tab_menu_view, text="Menu - View")
if self.session['role'] == 'admin': if self.session['role'] == 'admin':
main.add(self.tab_menu_manage, text="Menu - Manage") main.add(self.tab_menu_manage, text="Menu - Manage")
@ -523,6 +708,7 @@ class App:
pass pass
self.build_menu_view_tab(self.tab_menu_view) self.build_menu_view_tab(self.tab_menu_view)
self.build_order_tab(self.tab_order)
if self.session['role'] == 'admin': if self.session['role'] == 'admin':
self.build_menu_manage_tab(self.tab_menu_manage) self.build_menu_manage_tab(self.tab_menu_manage)
self.build_promo_tab(self.tab_promo) self.build_promo_tab(self.tab_promo)
@ -866,7 +1052,359 @@ class App:
promo_delete(code) promo_delete(code)
messagebox.showinfo("Dihapus","Promo terhapus") messagebox.showinfo("Dihapus","Promo terhapus")
self.reload_promo_table() 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 id,nama,kategori,harga,stok,foto,tersedia,item_discount_pct
1,Americano,Minuman,20000,10,,1,0 1,Americano,Minuman,20000,10,,1,0
2,Latte,Minuman,25000,5,,1,10 2,Latte,Minuman,25000,2,,1,10
3,Banana Cake,Dessert,30000,2,,1,0 3,Banana Cake,Dessert,30000,1,,1,0
4,Nasi Goreng,Makanan,35000,0,,0,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

View File

@ -1 +1 @@
id,user_id,nomor_meja,total,status,promo_code,subtotal,item_discount,promo_discount,tanggal id,user_id,nomor_meja,total,status,promo_code,subtotal,item_discount,promo_discount,tanggal

1 id user_id nomor_meja total status promo_code subtotal item_discount promo_discount tanggal