472 lines
19 KiB
Python
472 lines
19 KiB
Python
import os
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox, simpledialog, filedialog
|
|
|
|
# Try Pillow for image support; fallback to Tkinter PhotoImage
|
|
try:
|
|
from PIL import Image, ImageTk
|
|
PIL_AVAILABLE = True
|
|
except Exception:
|
|
PIL_AVAILABLE = False
|
|
|
|
# -------------------------
|
|
# In-memory "database"
|
|
# -------------------------
|
|
users = [
|
|
{"username": "admin", "password": "123", "role": "admin"},
|
|
{"username": "kasir", "password": "123", "role": "kasir"},
|
|
{"username": "waiter", "password": "123", "role": "waiter"},
|
|
{"username": "owner", "password": "123", "role": "owner"},
|
|
{"username": "pembeli", "password": "123", "role": "pembeli"},
|
|
]
|
|
|
|
menu = [
|
|
{"id": 1, "nama": "Es Teh", "harga": 5000, "kategori": "Minuman", "stok": 10, "foto": None, "promo": None},
|
|
{"id": 2, "nama": "Kopi Susu", "harga": 12000, "kategori": "Minuman", "stok": 8, "foto": None, "promo": "Diskon 10%"},
|
|
{"id": 3, "nama": "Nasi Goreng", "harga": 15000, "kategori": "Makanan", "stok": 5, "foto": None, "promo": None},
|
|
]
|
|
|
|
bahan = {
|
|
"teh": 50,
|
|
"kopi": 20,
|
|
"susu": 15,
|
|
"beras": 10,
|
|
"bumbu": 10
|
|
}
|
|
|
|
current_user = None
|
|
cart = []
|
|
_image_refs = {}
|
|
|
|
# -------------------------
|
|
# Helpers
|
|
# -------------------------
|
|
def find_menu_by_id(mid):
|
|
for it in menu:
|
|
if it["id"] == mid:
|
|
return it
|
|
return None
|
|
|
|
def ensure_image(path, maxsize=(240, 160)):
|
|
if not path or not os.path.exists(path):
|
|
return None
|
|
key = (path, maxsize)
|
|
if key in _image_refs:
|
|
return _image_refs[key]
|
|
try:
|
|
if PIL_AVAILABLE:
|
|
img = Image.open(path)
|
|
img.thumbnail(maxsize)
|
|
tkimg = ImageTk.PhotoImage(img)
|
|
else:
|
|
tkimg = tk.PhotoImage(file=path)
|
|
_image_refs[key] = tkimg
|
|
return tkimg
|
|
except Exception:
|
|
return None
|
|
|
|
def reset_image_refs():
|
|
_image_refs.clear()
|
|
|
|
# -------------------------
|
|
# Keranjang Functions
|
|
# -------------------------
|
|
def _add_to_cart(self):
|
|
sel = self.tree.selection()
|
|
if not sel:
|
|
messagebox.showwarning("Pilih Menu", "Pilih menu dulu.")
|
|
return
|
|
mid = int(sel[0])
|
|
it = find_menu_by_id(mid)
|
|
if not it:
|
|
return
|
|
try:
|
|
qty = int(simpledialog.askstring("Jumlah", f"Berapa {it['nama']} yang ingin ditambahkan?"))
|
|
except:
|
|
return
|
|
if qty <= 0:
|
|
messagebox.showerror("Error", "Jumlah harus > 0")
|
|
return
|
|
if qty > it.get("stok",0):
|
|
messagebox.showerror("Error", f"Stok tidak cukup ({it.get('stok',0)})")
|
|
return
|
|
for c in cart:
|
|
if c["menu_id"] == mid:
|
|
c["jumlah"] += qty
|
|
break
|
|
else:
|
|
cart.append({"menu_id": mid, "jumlah": qty})
|
|
messagebox.showinfo("Keranjang", f"{it['nama']} x{qty} berhasil ditambahkan ke keranjang.")
|
|
|
|
def _view_cart(self):
|
|
if not cart:
|
|
messagebox.showinfo("Keranjang", "Keranjang kosong.")
|
|
return
|
|
s = ""
|
|
total = 0
|
|
for c in cart:
|
|
it = find_menu_by_id(c["menu_id"])
|
|
harga = it["harga"] * c["jumlah"]
|
|
s += f"{it['nama']} x {c['jumlah']} = Rp{harga}\n"
|
|
total += harga
|
|
s += f"\nTotal: Rp{total}"
|
|
if messagebox.askyesno("Checkout", s + "\n\nCheckout sekarang?"):
|
|
for c in cart:
|
|
it = find_menu_by_id(c["menu_id"])
|
|
it["stok"] -= c["jumlah"]
|
|
cart.clear()
|
|
self._refresh_menu()
|
|
messagebox.showinfo("Sukses", "Pesanan berhasil dibuat!")
|
|
|
|
# -------------------------
|
|
# GUI App
|
|
# -------------------------
|
|
class Anggota1App:
|
|
def __init__(self, root):
|
|
self.root = root
|
|
self.root.title("Cafe Ceria")
|
|
self.root.geometry("1000x650")
|
|
self.root.minsize(920, 600)
|
|
self._build_login()
|
|
|
|
# ---------- login ----------
|
|
def _clear_root(self):
|
|
for w in self.root.winfo_children():
|
|
w.destroy()
|
|
|
|
def _build_login(self):
|
|
self._clear_root()
|
|
frame = ttk.Frame(self.root, padding=20)
|
|
frame.pack(expand=True)
|
|
ttk.Label(frame, text="LOGIN", font=("Segoe UI", 18)).grid(row=0, column=0, columnspan=2, pady=(0,10))
|
|
ttk.Label(frame, text="Username:").grid(row=1, column=0, sticky="e", padx=5, pady=5)
|
|
self.e_user = ttk.Entry(frame)
|
|
self.e_user.grid(row=1, column=1, padx=5, pady=5)
|
|
ttk.Label(frame, text="Password:").grid(row=2, column=0, sticky="e", padx=5, pady=5)
|
|
self.e_pass = ttk.Entry(frame, show="*")
|
|
self.e_pass.grid(row=2, column=1, padx=5, pady=5)
|
|
btn = ttk.Button(frame, text="Login", command=self._handle_login)
|
|
btn.grid(row=3, column=0, columnspan=2, pady=12)
|
|
ttk.Label(frame, text="(users: admin/kasir/waiter/owner/pembeli ; pass: 123)").grid(row=4, column=0, columnspan=2, pady=(8,0))
|
|
|
|
def _handle_login(self):
|
|
global current_user
|
|
u = self.e_user.get().strip()
|
|
p = self.e_pass.get().strip()
|
|
for usr in users:
|
|
if usr["username"] == u and usr["password"] == p:
|
|
current_user = usr
|
|
messagebox.showinfo("Login", f"Berhasil login sebagai {u} ({usr['role']})")
|
|
self._build_main_ui()
|
|
return
|
|
messagebox.showerror("Login Gagal", "Username atau password salah.")
|
|
|
|
# ---------- main UI ----------
|
|
def _build_main_ui(self):
|
|
self._clear_root()
|
|
|
|
topbar = ttk.Frame(self.root)
|
|
topbar.pack(fill="x", padx=6, pady=6)
|
|
ttk.Label(topbar, text=f"User: {current_user['username']} ({current_user['role']})", font=("Segoe UI", 10)).pack(side="left")
|
|
ttk.Button(topbar, text="Logout", command=self._logout).pack(side="right")
|
|
|
|
main = ttk.Frame(self.root)
|
|
main.pack(fill="both", expand=True, padx=8, pady=4)
|
|
|
|
ctrl = ttk.Frame(main, width=320, padding=8)
|
|
ctrl.pack(side="left", fill="y")
|
|
ttk.Label(ctrl, text="Search / Cari:").pack(anchor="w")
|
|
self.search_var = tk.StringVar()
|
|
sframe = ttk.Frame(ctrl)
|
|
sframe.pack(fill="x", pady=4)
|
|
self.search_entry = ttk.Entry(sframe, textvariable=self.search_var)
|
|
self.search_entry.pack(side="left", fill="x", expand=True)
|
|
ttk.Button(sframe, text="Search", command=self._search_menu).pack(side="left", padx=4)
|
|
ttk.Button(sframe, text="Refresh", command=self._refresh_menu).pack(side="left")
|
|
ttk.Label(ctrl, text="Filter Kategori:").pack(anchor="w", pady=(10,0))
|
|
self.filter_var = tk.StringVar()
|
|
values = ["All"] + sorted({it.get("kategori","Undefined") for it in menu})
|
|
self.filter_cb = ttk.Combobox(ctrl, values=values, state="readonly", textvariable=self.filter_var)
|
|
self.filter_cb.set("All")
|
|
self.filter_cb.pack(fill="x", pady=4)
|
|
ttk.Button(ctrl, text="Apply Filter", command=self._apply_filter).pack(fill="x", pady=(0,6))
|
|
ttk.Label(ctrl, text="Sort:").pack(anchor="w", pady=(6,0))
|
|
self.sort_var = tk.StringVar()
|
|
self.sort_cb = ttk.Combobox(ctrl, values=["Default","Harga - Rendah→Tinggi","Harga - Tinggi→Rendah","Stok - Rendah→Tinggi","Stok - Tinggi→Rendah"], state="readonly", textvariable=self.sort_var)
|
|
self.sort_cb.set("Default")
|
|
self.sort_cb.pack(fill="x", pady=4)
|
|
ttk.Button(ctrl, text="Apply Sort", command=self._apply_sort).pack(fill="x", pady=(0,6))
|
|
|
|
ttk.Separator(ctrl).pack(fill="x", pady=8)
|
|
self.btn_add = ttk.Button(ctrl, text="Tambah Menu (+Foto)", command=self._gui_add_menu)
|
|
self.btn_edit = ttk.Button(ctrl, text="Edit Menu (+Ganti Foto)", command=self._gui_edit_menu)
|
|
self.btn_delete = ttk.Button(ctrl, text="Hapus Menu", command=self._gui_delete_menu)
|
|
self.btn_refresh = ttk.Button(ctrl, text="Refresh List", command=self._refresh_menu)
|
|
self.btn_add.pack(fill="x", pady=4)
|
|
self.btn_edit.pack(fill="x", pady=4)
|
|
self.btn_delete.pack(fill="x", pady=4)
|
|
self.btn_refresh.pack(fill="x", pady=6)
|
|
if current_user["role"] != "admin":
|
|
self.btn_add.state(["disabled"])
|
|
self.btn_edit.state(["disabled"])
|
|
self.btn_delete.state(["disabled"])
|
|
|
|
ttk.Label(ctrl, text="Stok Bahan:", font=("Segoe UI", 10, "bold")).pack(anchor="w", pady=(10,0))
|
|
self.bahan_listbox = tk.Listbox(ctrl, height=8)
|
|
self.bahan_listbox.pack(fill="both", expand=False, pady=(4,6))
|
|
self._refresh_bahan_listbox()
|
|
self.btn_edit_bahan = ttk.Button(ctrl, text="Tambah/Edit Stok Bahan", command=self._gui_edit_bahan)
|
|
self.btn_edit_bahan.pack(fill="x")
|
|
if current_user["role"] != "admin":
|
|
self.btn_edit_bahan.state(["disabled"])
|
|
|
|
right = ttk.Frame(main, padding=8)
|
|
right.pack(side="right", fill="both", expand=True)
|
|
ttk.Label(right, text="Daftar Menu", font=("Segoe UI", 12)).pack(anchor="w")
|
|
self.tree = ttk.Treeview(right, columns=("harga","kategori","stok"), show="headings", selectmode="browse")
|
|
self.tree.heading("harga", text="Harga")
|
|
self.tree.heading("kategori", text="Kategori")
|
|
self.tree.heading("stok", text="Stok")
|
|
self.tree.column("harga", width=120, anchor="center")
|
|
self.tree.column("kategori", width=120, anchor="center")
|
|
self.tree.column("stok", width=80, anchor="center")
|
|
self.tree.pack(fill="both", expand=True, pady=(6,8))
|
|
self.tree.bind("<<TreeviewSelect>>", self._on_tree_select)
|
|
|
|
detail_frame = ttk.Frame(right)
|
|
detail_frame.pack(fill="x", pady=4)
|
|
self.img_label = ttk.Label(detail_frame)
|
|
self.img_label.pack(side="left", padx=6)
|
|
info_frame = ttk.Frame(detail_frame)
|
|
info_frame.pack(side="left", fill="both", expand=True, padx=6)
|
|
self.detail_text = tk.Text(info_frame, height=8, state="disabled")
|
|
self.detail_text.pack(fill="both", expand=True)
|
|
|
|
# --- Tambahkan tombol keranjang untuk pembeli ---
|
|
if current_user["role"] == "pembeli":
|
|
ttk.Button(right, text="Tambah ke Keranjang", command=lambda: _add_to_cart(self)).pack(fill="x", pady=4)
|
|
ttk.Button(right, text="Lihat Keranjang / Checkout", command=lambda: _view_cart(self)).pack(fill="x", pady=4)
|
|
|
|
self._populate_tree(menu)
|
|
|
|
# ---------- Data ops ----------
|
|
def _populate_tree(self, items):
|
|
for r in self.tree.get_children():
|
|
self.tree.delete(r)
|
|
for it in items:
|
|
self.tree.insert("", "end", iid=str(it["id"]), values=(f"Rp{it['harga']}", it.get("kategori","-"), it.get("stok",0)), text=it["nama"])
|
|
self._clear_details()
|
|
|
|
def _refresh_menu(self):
|
|
self.search_var.set("")
|
|
self.filter_cb['values'] = ["All"] + sorted({it.get("kategori","Undefined") for it in menu})
|
|
self.filter_cb.set("All")
|
|
self.sort_cb.set("Default")
|
|
self._populate_tree(menu)
|
|
reset_image_refs()
|
|
self._refresh_bahan_listbox()
|
|
|
|
def _apply_filter(self):
|
|
cat = self.filter_var.get() if self.filter_var.get() else "All"
|
|
if cat == "All":
|
|
filtered = list(menu)
|
|
else:
|
|
filtered = [it for it in menu if it.get("kategori","") == cat]
|
|
self._populate_tree(filtered)
|
|
|
|
def _search_menu(self):
|
|
q = self.search_var.get().strip().lower()
|
|
if not q:
|
|
self._populate_tree(menu)
|
|
return
|
|
res = []
|
|
for it in menu:
|
|
if q in it["nama"].lower() or q in str(it["harga"]) or q in it.get("kategori","").lower():
|
|
res.append(it)
|
|
self._populate_tree(res)
|
|
|
|
def _apply_sort(self):
|
|
key = self.sort_var.get()
|
|
arr = list(menu)
|
|
if key == "Harga - Rendah→Tinggi":
|
|
arr.sort(key=lambda x: x["harga"])
|
|
elif key == "Harga - Tinggi→Rendah":
|
|
arr.sort(key=lambda x: -x["harga"])
|
|
elif key == "Stok - Rendah→Tinggi":
|
|
arr.sort(key=lambda x: x.get("stok",0))
|
|
elif key == "Stok - Tinggi→Rendah":
|
|
arr.sort(key=lambda x: -x.get("stok",0))
|
|
self._populate_tree(arr)
|
|
|
|
def _clear_details(self):
|
|
self.detail_text.config(state="normal")
|
|
self.detail_text.delete("1.0", tk.END)
|
|
self.detail_text.config(state="disabled")
|
|
self.img_label.config(image="", text="(No Image)")
|
|
self.img_label.image = None
|
|
|
|
def _on_tree_select(self, evt):
|
|
sel = self.tree.selection()
|
|
if not sel:
|
|
return
|
|
mid = int(sel[0])
|
|
it = find_menu_by_id(mid)
|
|
if not it: return
|
|
self.detail_text.config(state="normal")
|
|
self.detail_text.delete("1.0", tk.END)
|
|
s = f"ID: {it['id']}\nNama: {it['nama']}\nHarga: Rp{it['harga']}\nKategori: {it.get('kategori','-')}\nStok: {it.get('stok',0)}\nFoto: {os.path.basename(it['foto']) if it.get('foto') else 'Tidak ada'}\n"
|
|
self.detail_text.insert(tk.END, s)
|
|
self.detail_text.config(state="disabled")
|
|
img = ensure_image(it.get('foto'))
|
|
if img:
|
|
self.img_label.config(image=img, text="")
|
|
self.img_label.image = img
|
|
else:
|
|
self.img_label.config(image="", text="(No Image)")
|
|
self.img_label.image = None
|
|
|
|
# ---------- CRUD Menu ----------
|
|
def _gui_add_menu(self):
|
|
if current_user["role"] != "admin":
|
|
messagebox.showerror("Akses", "Hanya admin bisa menambah menu.")
|
|
return
|
|
dialog = AddEditDialog(self.root, title="Tambah Menu")
|
|
self.root.wait_window(dialog.top)
|
|
if dialog.result is None:
|
|
return
|
|
nama, harga, stok, kategori, foto_path = dialog.result
|
|
try:
|
|
harga = int(harga); stok = int(stok)
|
|
except:
|
|
messagebox.showerror("Error", "Harga & Stok harus angka.")
|
|
return
|
|
new_id = menu[-1]["id"] + 1 if menu else 1
|
|
menu.append({"id": new_id, "nama": nama, "harga": harga, "kategori": kategori, "stok": stok, "foto": foto_path})
|
|
messagebox.showinfo("Sukses", "Menu ditambahkan.")
|
|
self._refresh_menu()
|
|
|
|
def _gui_edit_menu(self):
|
|
if current_user["role"] != "admin":
|
|
messagebox.showerror("Akses", "Hanya admin bisa edit menu.")
|
|
return
|
|
sel = self.tree.selection()
|
|
if not sel:
|
|
messagebox.showwarning("Pilih", "Pilih menu untuk diedit.")
|
|
return
|
|
mid = int(sel[0])
|
|
it = find_menu_by_id(mid)
|
|
if not it:
|
|
messagebox.showerror("Error", "Menu tidak ditemukan.")
|
|
return
|
|
dialog = AddEditDialog(self.root, title="Edit Menu", existing=it)
|
|
self.root.wait_window(dialog.top)
|
|
if dialog.result is None:
|
|
return
|
|
nama, harga, stok, kategori, foto_path = dialog.result
|
|
try:
|
|
harga = int(harga); stok = int(stok)
|
|
except:
|
|
messagebox.showerror("Error", "Harga & Stok harus angka.")
|
|
return
|
|
it.update({"nama": nama, "harga": harga, "kategori": kategori, "stok": stok, "foto": foto_path})
|
|
messagebox.showinfo("Sukses", "Menu diperbarui.")
|
|
self._refresh_menu()
|
|
|
|
def _gui_delete_menu(self):
|
|
if current_user["role"] != "admin":
|
|
messagebox.showerror("Akses", "Hanya admin bisa hapus menu.")
|
|
return
|
|
sel = self.tree.selection()
|
|
if not sel:
|
|
messagebox.showwarning("Pilih", "Pilih menu untuk dihapus.")
|
|
return
|
|
mid = int(sel[0])
|
|
it = find_menu_by_id(mid)
|
|
if not it:
|
|
messagebox.showerror("Error", "Menu tidak ditemukan.")
|
|
return
|
|
if messagebox.askyesno("Hapus", f"Yakin ingin hapus {it['nama']}?"):
|
|
menu.remove(it)
|
|
self._refresh_menu()
|
|
|
|
# ---------- Bahan ----------
|
|
def _refresh_bahan_listbox(self):
|
|
self.bahan_listbox.delete(0, tk.END)
|
|
for k,v in bahan.items():
|
|
self.bahan_listbox.insert(tk.END, f"{k}: {v}")
|
|
|
|
def _gui_edit_bahan(self):
|
|
if current_user["role"] != "admin":
|
|
messagebox.showerror("Akses", "Hanya admin bisa edit stok bahan.")
|
|
return
|
|
k = simpledialog.askstring("Bahan", "Nama bahan:")
|
|
if not k:
|
|
return
|
|
try:
|
|
v = int(simpledialog.askstring("Stok", "Jumlah:"))
|
|
except:
|
|
return
|
|
bahan[k] = v
|
|
self._refresh_bahan_listbox()
|
|
|
|
def _logout(self):
|
|
global current_user
|
|
current_user = None
|
|
self._build_login()
|
|
|
|
# ---------- Dialog ----------
|
|
class AddEditDialog:
|
|
def __init__(self, parent, title="Tambah/Edit", existing=None):
|
|
self.top = tk.Toplevel(parent)
|
|
self.top.title(title)
|
|
self.result = None
|
|
self.existing = existing
|
|
ttk.Label(self.top, text="Nama:").grid(row=0, column=0, sticky="e", padx=5, pady=4)
|
|
self.e_nama = ttk.Entry(self.top)
|
|
self.e_nama.grid(row=0, column=1, padx=5, pady=4)
|
|
ttk.Label(self.top, text="Harga:").grid(row=1, column=0, sticky="e", padx=5, pady=4)
|
|
self.e_harga = ttk.Entry(self.top)
|
|
self.e_harga.grid(row=1, column=1, padx=5, pady=4)
|
|
ttk.Label(self.top, text="Stok:").grid(row=2, column=0, sticky="e", padx=5, pady=4)
|
|
self.e_stok = ttk.Entry(self.top)
|
|
self.e_stok.grid(row=2, column=1, padx=5, pady=4)
|
|
ttk.Label(self.top, text="Kategori:").grid(row=3, column=0, sticky="e", padx=5, pady=4)
|
|
self.e_kategori = ttk.Entry(self.top)
|
|
self.e_kategori.grid(row=3, column=1, padx=5, pady=4)
|
|
ttk.Label(self.top, text="Foto:").grid(row=4, column=0, sticky="e", padx=5, pady=4)
|
|
self.foto_path = tk.StringVar()
|
|
self.e_foto = ttk.Entry(self.top, textvariable=self.foto_path)
|
|
self.e_foto.grid(row=4, column=1, padx=5, pady=4)
|
|
ttk.Button(self.top, text="Browse", command=self._browse_file).grid(row=4, column=2, padx=4, pady=4)
|
|
ttk.Button(self.top, text="OK", command=self._ok).grid(row=5, column=0, pady=8)
|
|
ttk.Button(self.top, text="Cancel", command=self.top.destroy).grid(row=5, column=1)
|
|
if existing:
|
|
self.e_nama.insert(0, existing["nama"])
|
|
self.e_harga.insert(0, str(existing["harga"]))
|
|
self.e_stok.insert(0, str(existing.get("stok",0)))
|
|
self.e_kategori.insert(0, existing.get("kategori",""))
|
|
self.foto_path.set(existing.get("foto",""))
|
|
|
|
def _browse_file(self):
|
|
path = filedialog.askopenfilename(filetypes=[("Image files","*.png *.jpg *.jpeg *.gif")])
|
|
if path:
|
|
self.foto_path.set(path)
|
|
|
|
def _ok(self):
|
|
self.result = (
|
|
self.e_nama.get(),
|
|
self.e_harga.get(),
|
|
self.e_stok.get(),
|
|
self.e_kategori.get(),
|
|
self.foto_path.get()
|
|
)
|
|
self.top.destroy()
|
|
|
|
# -------------------------
|
|
# Main
|
|
# -------------------------
|
|
if __name__ == "__main__":
|
|
root = tk.Tk()
|
|
app = Anggota1App(root)
|
|
root.mainloop()
|