@ -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 (
" <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 } \n Status: 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 } " )