@ -818,6 +818,7 @@ class App:
def logout ( self ) :
self . session = None
self . img_cache . clear ( )
self . notification_running = False
self . login_frame ( )
def dashboard_frame ( self ) :
@ -860,6 +861,9 @@ class App:
if self . session [ ' role ' ] == ' waiter ' :
main . add ( self . tab_waiter , text = " 🍽️ Kelola Pesanan " )
self . tab_meja = ttk . Frame ( main )
main . add ( self . tab_meja , text = " 🪑 Kelola Meja " )
# ==========================================
# ROLE: KASIR (Order + Transaksi SAJA)
# ==========================================
@ -867,6 +871,9 @@ class App:
self . tab_payment = ttk . Frame ( main )
main . add ( self . tab_payment , text = " 💰 Transaksi " )
self . tab_meja = ttk . Frame ( main )
main . add ( self . tab_meja , text = " 🪑 Kelola Meja " )
# ==========================================
# ROLE: PEMILIK (Laporan SAJA)
# ==========================================
@ -874,6 +881,7 @@ class App:
self . tab_report = ttk . Frame ( main )
main . add ( self . tab_report , text = " 📊 Laporan " )
# ==========================================
# ROLE: ADMIN (Kelola Semua)
# ==========================================
@ -890,6 +898,9 @@ class App:
self . tab_report = ttk . Frame ( main )
main . add ( self . tab_report , text = " 📊 Laporan " )
self . tab_meja = ttk . Frame ( main )
main . add ( self . tab_meja , text = " 🪑 Kelola Meja " )
# Kelola Menu & Promo (KHUSUS ADMIN)
main . add ( self . tab_menu_manage , text = " ⚙️ Kelola Menu " )
main . add ( self . tab_promo , text = " 🎁 Kelola Promo " )
@ -914,11 +925,13 @@ class App:
# Waiter
if self . session [ ' role ' ] == ' waiter ' :
self . build_waiter_tab ( self . tab_waiter )
self . build_meja_tab ( self . tab_meja )
# Kasir (Order + Transaksi SAJA)
if self . session [ ' role ' ] == ' kasir ' :
self . build_order_tab ( self . tab_order )
self . build_payment_tab ( self . tab_payment )
self . build_meja_tab ( self . tab_meja )
# Pemilik (Laporan SAJA)
if self . session [ ' role ' ] == ' pemilik ' :
@ -933,6 +946,7 @@ class App:
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
self . build_meja_tab ( self . tab_meja )
def build_menu_view_tab ( self , parent ) :
@ -1929,6 +1943,7 @@ class App:
success , result = transaksi_add ( self . session [ ' id ' ] , nomor_meja , self . cart_items , promo_code )
if success :
meja_update_status ( nomor_meja , " terisi " , result )
messagebox . showinfo ( " Sukses " , f " Pesanan berhasil! ID Transaksi: { result } \n Status: Pending " )
# Reset
self . cart_items = [ ]
@ -3001,83 +3016,88 @@ class App:
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
""" Tampilkan grafik laporan (dengan fallback jika matplotlib tidak ada) """
try :
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
# 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 = { }
# 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
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 ( ' . ' , ' ' ) )
# Parse total
total = float ( total_str . replace ( ' Rp ' , ' ' ) . replace ( ' , ' , ' ' ) . replace ( ' . ' , ' ' ) )
# Count by metode
metode_counts [ metode ] = metode_counts . get ( metode , 0 ) + 1
# 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
# 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 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 ) )
# 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 ' ]
# 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 ' )
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 ]
# 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
]
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 . 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 ' )
)
ax2 . yaxis . set_major_formatter (
plt . FuncFormatter ( lambda x , p : f ' Rp { x / 1000 : .0f } K ' )
)
plt . tight_layout ( )
plt . tight_layout ( )
# Embed in Tkinter
canvas = FigureCanvasTkAgg ( fig , master = chart_window )
canvas . draw ( )
canvas . get_tk_widget ( ) . pack ( fill = ' both ' , expand = True )
# 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 )
ttk . Button ( chart_window , text = " Tutup " , command = chart_window . destroy ) . pack ( pady = 10 )
except ImportError :
# Fallback: Tampilkan grafik ASCII sederhana
self . show_text_chart ( )
def reload_payment_orders ( self ) :
@ -3489,6 +3509,385 @@ class App:
except Exception as e :
messagebox . showerror ( " Error " , f " Gagal menyimpan struk: { e } " )
def check_new_orders ( self ) :
""" Cek pesanan baru yang perlu perhatian waiter/kasir """
if self . session [ ' role ' ] not in [ ' waiter ' , ' kasir ' , ' admin ' ] :
return 0
# Hitung pesanan pending untuk waiter
if self . session [ ' role ' ] in [ ' waiter ' , ' admin ' ] :
pending_orders = transaksi_list ( status = ' pending ' )
return len ( pending_orders )
# Hitung pesanan selesai untuk kasir
if self . session [ ' role ' ] in [ ' kasir ' , ' admin ' ] :
selesai_orders = transaksi_list ( status = ' selesai ' )
# Filter yang belum dibayar
unpaid_count = 0
for order in selesai_orders :
tid = order [ 0 ]
payment = pembayaran_get_by_transaksi ( tid )
if not payment :
unpaid_count + = 1
return unpaid_count
return 0
def start_notification_check ( self ) :
""" Start auto-refresh untuk notifikasi (setiap 10 detik) """
if not hasattr ( self , ' notification_running ' ) :
self . notification_running = True
self . update_notification_badge ( )
def update_notification_badge ( self ) :
""" Update badge notifikasi """
if not self . notification_running :
return
try :
count = self . check_new_orders ( )
# Update badge di tab yang sesuai
if self . session [ ' role ' ] in [ ' waiter ' , ' admin ' ] :
if hasattr ( self , ' tab_waiter ' ) :
for tab_id in range ( self . root . nametowidget ( ' .!notebook ' ) . index ( ' end ' ) ) :
tab_text = self . root . nametowidget ( ' .!notebook ' ) . tab ( tab_id , ' text ' )
if ' Kelola Pesanan ' in tab_text :
if count > 0 :
self . root . nametowidget ( ' .!notebook ' ) . tab ( tab_id , text = f " 🍽️ Kelola Pesanan ( { count } ) " )
else :
self . root . nametowidget ( ' .!notebook ' ) . tab ( tab_id , text = " 🍽️ Kelola Pesanan " )
break
if self . session [ ' role ' ] in [ ' kasir ' , ' admin ' ] :
if hasattr ( self , ' tab_payment ' ) :
for tab_id in range ( self . root . nametowidget ( ' .!notebook ' ) . index ( ' end ' ) ) :
tab_text = self . root . nametowidget ( ' .!notebook ' ) . tab ( tab_id , ' text ' )
if ' Transaksi ' in tab_text :
if count > 0 :
self . root . nametowidget ( ' .!notebook ' ) . tab ( tab_id , text = f " 💰 Transaksi ( { count } ) " )
else :
self . root . nametowidget ( ' .!notebook ' ) . tab ( tab_id , text = " 💰 Transaksi " )
break
except :
pass
# Schedule next check (10 seconds)
if self . notification_running :
self . root . after ( 10000 , self . update_notification_badge )
def show_text_chart ( self ) :
""" Tampilkan grafik ASCII sebagai fallback jika matplotlib tidak ada """
# 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 = { }
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
# Count by metode
metode_counts [ metode ] = metode_counts . get ( metode , 0 ) + 1
# Create ASCII bar chart
chart_text = " = " * 60 + " \n "
chart_text + = " GRAFIK TRANSAKSI PER METODE PEMBAYARAN \n "
chart_text + = " = " * 60 + " \n \n "
if metode_counts :
max_count = max ( metode_counts . values ( ) )
for metode , count in sorted ( metode_counts . items ( ) ) :
# Calculate bar length (max 40 chars)
bar_length = int ( ( count / max_count ) * 40 ) if max_count > 0 else 0
bar = " █ " * bar_length
percentage = ( count / sum ( metode_counts . values ( ) ) ) * 100 if sum ( metode_counts . values ( ) ) > 0 else 0
chart_text + = f " { metode . ljust ( 15 ) } | { bar } { count } ( { percentage : .1f } %) \n "
chart_text + = " \n " + " = " * 60 + " \n "
chart_text + = f " Total Transaksi: { sum ( metode_counts . values ( ) ) } \n "
chart_text + = " = " * 60
# Show in window
w = tk . Toplevel ( self . root )
w . title ( " 📊 Grafik Laporan (Text Mode) " )
w . geometry ( " 700x500 " )
frm = ttk . Frame ( w , padding = 15 )
frm . pack ( fill = ' both ' , expand = True )
ttk . Label ( frm , text = " ℹ ️ Matplotlib tidak terinstall - Mode Text Chart" , font = ( " Arial " , 10 ) , foreground = ' orange ' ) . pack ( pady = 10 )
text = tk . Text ( frm , width = 80 , height = 25 , font = ( " Courier New " , 10 ) )
text_scroll = ttk . Scrollbar ( frm , orient = ' vertical ' , command = text . yview )
text . configure ( yscrollcommand = text_scroll . set )
text . insert ( ' 1.0 ' , chart_text )
text . config ( state = ' disabled ' )
text . pack ( side = ' left ' , fill = ' both ' , expand = True )
text_scroll . pack ( side = ' right ' , fill = ' y ' )
ttk . Button ( w , text = " Tutup " , command = w . destroy ) . pack ( pady = 10 )
# MEJA
def build_meja_tab ( self , parent ) :
""" Tab untuk kelola status meja (admin/kasir/waiter) """
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 = " 🪑 Manajemen Meja Cafe " , font = ( " Arial " , 14 , " bold " ) ) . pack ( side = ' left ' )
ttk . Button ( header , text = " 🔄 Refresh " , command = self . reload_meja_status ) . pack ( side = ' right ' , padx = 6 )
# Info panel
info_frame = ttk . LabelFrame ( parent , text = " ℹ ️ Info Meja" , padding = 10 )
info_frame . pack ( fill = ' x ' , padx = 10 , pady = 6 )
info_inner = ttk . Frame ( info_frame )
info_inner . pack ( )
ttk . Label ( info_inner , text = " 🟢 Kosong: " , font = ( " Arial " , 10 ) ) . grid ( row = 0 , column = 0 , padx = 15 , pady = 3 )
self . meja_kosong_label = ttk . Label ( info_inner , text = " 0 " , font = ( " Arial " , 10 , " bold " ) , foreground = ' green ' )
self . meja_kosong_label . grid ( row = 0 , column = 1 , padx = 5 , pady = 3 )
ttk . Label ( info_inner , text = " 🔴 Terisi: " , font = ( " Arial " , 10 ) ) . grid ( row = 0 , column = 2 , padx = 15 , pady = 3 )
self . meja_terisi_label = ttk . Label ( info_inner , text = " 0 " , font = ( " Arial " , 10 , " bold " ) , foreground = ' red ' )
self . meja_terisi_label . grid ( row = 0 , column = 3 , padx = 5 , pady = 3 )
# Container untuk card meja
canvas_frame = ttk . Frame ( parent )
canvas_frame . pack ( fill = ' both ' , expand = True , padx = 10 , pady = 6 )
# Canvas dengan scrollbar
canvas = tk . Canvas ( canvas_frame , bg = ' #f5f5f5 ' , highlightthickness = 0 )
scrollbar = ttk . Scrollbar ( canvas_frame , orient = " vertical " , command = canvas . yview )
self . meja_cards_frame = ttk . Frame ( canvas )
self . meja_cards_frame . bind (
" <Configure> " ,
lambda e : canvas . configure ( scrollregion = canvas . bbox ( " all " ) )
)
canvas . create_window ( ( 0 , 0 ) , window = self . meja_cards_frame , anchor = " nw " )
canvas . configure ( yscrollcommand = scrollbar . set )
canvas . pack ( side = " left " , fill = " both " , expand = True )
scrollbar . pack ( side = " right " , fill = " y " )
# Mouse wheel scroll
def _on_mousewheel ( event ) :
canvas . yview_scroll ( int ( - 1 * ( event . delta / 120 ) ) , " units " )
canvas . bind_all ( " <MouseWheel> " , _on_mousewheel )
# Load meja cards
self . reload_meja_status ( )
def reload_meja_status ( self ) :
""" Load semua meja dalam bentuk cards """
# Clear existing cards
for widget in self . meja_cards_frame . winfo_children ( ) :
widget . destroy ( )
# Get all meja data
meja_list = read_all ( MEJA_CSV )
# Hitung statistik
kosong_count = sum ( 1 for m in meja_list if m . get ( ' status ' ) == ' kosong ' )
terisi_count = sum ( 1 for m in meja_list if m . get ( ' status ' ) == ' terisi ' )
# Update info labels
self . meja_kosong_label . config ( text = str ( kosong_count ) )
self . meja_terisi_label . config ( text = str ( terisi_count ) )
# Render meja cards (5 kolom)
row = 0
col = 0
for meja in sorted ( meja_list , key = lambda x : int ( x . get ( ' nomor_meja ' , 0 ) ) ) :
nomor = meja . get ( ' nomor_meja ' )
status = meja . get ( ' status ' , ' kosong ' )
transaksi_id = meja . get ( ' transaksi_id ' , ' ' )
# Determine color
if status == ' kosong ' :
bg_color = ' #C8E6C9 ' # Light green
status_color = ' #4CAF50 '
status_text = ' 🟢 KOSONG '
else :
bg_color = ' #FFCDD2 ' # Light red
status_color = ' #F44336 '
status_text = ' 🔴 TERISI '
# Create card
card = tk . Frame (
self . meja_cards_frame ,
relief = ' solid ' ,
borderwidth = 2 ,
bg = bg_color ,
padx = 15 ,
pady = 15
)
card . grid ( row = row , column = col , padx = 8 , pady = 8 , sticky = ' nsew ' )
# Nomor meja (besar)
tk . Label (
card ,
text = f " MEJA { nomor } " ,
font = ( " Arial " , 16 , " bold " ) ,
bg = bg_color
) . pack ( pady = ( 0 , 5 ) )
# Status
tk . Label (
card ,
text = status_text ,
font = ( " Arial " , 10 , " bold " ) ,
fg = status_color ,
bg = bg_color
) . pack ( pady = 5 )
# Info transaksi (jika terisi)
if status == ' terisi ' and transaksi_id :
tk . Label (
card ,
text = f " Transaksi: # { transaksi_id } " ,
font = ( " Arial " , 8 ) ,
bg = bg_color
) . pack ( pady = 2 )
# Get detail transaksi
transaksi_data = transaksi_get ( int ( transaksi_id ) )
if transaksi_data :
tid , uid , meja_num , total , status_trx , promo , subtotal , item_disc , promo_disc , tanggal = transaksi_data
tk . Label (
card ,
text = f " Total: Rp { total : ,.0f } " ,
font = ( " Arial " , 8 , " bold " ) ,
bg = bg_color ,
fg = ' #1976D2 '
) . pack ( pady = 2 )
tk . Label (
card ,
text = f " Status: { status_trx . upper ( ) } " ,
font = ( " Arial " , 7 ) ,
bg = bg_color
) . pack ( pady = 2 )
# Tombol aksi
btn_frame = tk . Frame ( card , bg = bg_color )
btn_frame . pack ( pady = ( 10 , 0 ) )
if status == ' terisi ' :
# Tombol Tutup Meja (hanya jika sudah dibayar)
if transaksi_id :
transaksi_data = transaksi_get ( int ( transaksi_id ) )
if transaksi_data and transaksi_data [ 4 ] == ' dibayar ' :
tk . Button (
btn_frame ,
text = " ✅ Tutup Meja " ,
font = ( " Arial " , 9 , " bold " ) ,
bg = ' #4CAF50 ' ,
fg = ' white ' ,
width = 12 ,
borderwidth = 0 ,
cursor = ' hand2 ' ,
command = lambda n = nomor : self . tutup_meja ( n )
) . pack ( )
else :
tk . Label (
btn_frame ,
text = " ⏳ Menunggu Pembayaran " ,
font = ( " Arial " , 8 ) ,
bg = bg_color ,
fg = ' orange '
) . pack ( )
else :
# Meja kosong - tampilkan info saja
tk . Label (
btn_frame ,
text = " Siap digunakan " ,
font = ( " Arial " , 8 ) ,
bg = bg_color ,
fg = ' gray '
) . pack ( )
# Next column
col + = 1
if col > = 5 : # 5 meja per row
col = 0
row + = 1
# Configure grid weights
for i in range ( 5 ) :
self . meja_cards_frame . columnconfigure ( i , weight = 1 )
def tutup_meja ( self , nomor_meja ) :
""" Tutup meja dan reset status """
# Konfirmasi
if not messagebox . askyesno ( " Konfirmasi " , f " Tutup meja { nomor_meja } ? \n \n Pastikan pelanggan sudah selesai dan transaksi sudah dibayar. " ) :
return
# Tutup meja
success = meja_tutup ( nomor_meja )
if success :
messagebox . showinfo ( " ✅ Berhasil " , f " Meja { nomor_meja } berhasil ditutup dan siap digunakan lagi " )
self . reload_meja_status ( )
else :
messagebox . showerror ( " ❌ Gagal " , f " Gagal menutup meja { nomor_meja } " )
# ========================================
# FUNGSI TAMBAHAN UNTUK BACKEND
# (sudah ada tapi ditambahkan untuk kelengkapan)
# ========================================
def meja_list_all ( ) :
""" Ambil semua data meja """
rows = read_all ( MEJA_CSV )
out = [ ]
for r in rows :
try :
nomor = int ( r . get ( " nomor_meja " ) or 0 )
except :
nomor = r . get ( " nomor_meja " )
transaksi_id = r . get ( " transaksi_id " ) or " "
out . append ( ( nomor , r . get ( " status " ) , transaksi_id ) )
out . sort ( key = lambda x : int ( x [ 0 ] ) if isinstance ( x [ 0 ] , int ) else 0 )
return out
# Done