04/22 Upgraded code commit
This commit is contained in:
+145
-1
@@ -51,7 +51,10 @@ class AdminShiftsView(ttk.Frame):
|
||||
command=self._open_edit).pack(side="right", padx=(4, 0))
|
||||
ttk.Button(toolbar, text="✕ Delete",
|
||||
style="Danger.TButton",
|
||||
command=self._delete_selected).pack(side="right")
|
||||
command=self._delete_selected).pack(side="right", padx=(4, 0))
|
||||
ttk.Button(toolbar, text="⬇ Export PDF",
|
||||
style="Ghost.TButton",
|
||||
command=self._export_pdf).pack(side="right", padx=(0, 12))
|
||||
|
||||
cols = ("ID", "Shift Name", "Days", "Start", "End",
|
||||
"Users", "Websites", "Active", "Note")
|
||||
@@ -103,6 +106,147 @@ class AdminShiftsView(ttk.Frame):
|
||||
sel = self.tree.selection()
|
||||
return int(sel[0]) if sel else None
|
||||
|
||||
# ─── Export ───────────────────────────────────────────────────────────────
|
||||
|
||||
def _export_pdf(self):
|
||||
"""Export the full shift schedule to a PDF file."""
|
||||
from tkinter import filedialog
|
||||
from models import get_all_shifts, get_shift_assigned_users, get_shift_assigned_websites
|
||||
import datetime
|
||||
|
||||
save_path = filedialog.asksaveasfilename(
|
||||
title="Save Shift Schedule PDF",
|
||||
defaultextension=".pdf",
|
||||
filetypes=[("PDF files", "*.pdf"), ("All files", "*.*")],
|
||||
initialfile=f"shift_schedule_{datetime.date.today().isoformat()}.pdf",
|
||||
)
|
||||
if not save_path:
|
||||
return
|
||||
|
||||
try:
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.lib.units import cm
|
||||
from reportlab.lib import colors
|
||||
from reportlab.platypus import (
|
||||
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable
|
||||
)
|
||||
|
||||
shifts = get_all_shifts()
|
||||
doc = SimpleDocTemplate(save_path, pagesize=A4,
|
||||
leftMargin=2*cm, rightMargin=2*cm,
|
||||
topMargin=2*cm, bottomMargin=2*cm)
|
||||
styles = getSampleStyleSheet()
|
||||
|
||||
ACCENT = colors.HexColor("#5b4de8")
|
||||
LIGHT = colors.HexColor("#e0dff8")
|
||||
DARK = colors.HexColor("#1a1a2e")
|
||||
DIMGREY = colors.HexColor("#6b6b80")
|
||||
|
||||
title_style = ParagraphStyle("Title", parent=styles["Title"],
|
||||
textColor=ACCENT, fontSize=20, spaceAfter=4)
|
||||
sub_style = ParagraphStyle("Sub", parent=styles["Normal"],
|
||||
textColor=DIMGREY, fontSize=10, spaceAfter=16)
|
||||
h2_style = ParagraphStyle("H2", parent=styles["Heading2"],
|
||||
textColor=ACCENT, fontSize=13, spaceBefore=14)
|
||||
body_style = ParagraphStyle("Body", parent=styles["Normal"],
|
||||
textColor=DARK, fontSize=10, leading=14)
|
||||
dim_style = ParagraphStyle("Dim", parent=styles["Normal"],
|
||||
textColor=DIMGREY, fontSize=9, leading=12)
|
||||
|
||||
story = [
|
||||
Paragraph("Shift Schedule", title_style),
|
||||
Paragraph(f"Generated {datetime.datetime.now().strftime('%d %B %Y, %H:%M')}",
|
||||
sub_style),
|
||||
HRFlowable(width="100%", thickness=1, color=ACCENT),
|
||||
Spacer(1, 0.4*cm),
|
||||
]
|
||||
|
||||
for s in shifts:
|
||||
if not s["is_active"]:
|
||||
continue
|
||||
|
||||
days_str = _days_label(s["days_of_week"])
|
||||
start = str(s.get("start_time", ""))[:5]
|
||||
end = str(s.get("end_time", ""))[:5]
|
||||
|
||||
story.append(Paragraph(s["name"], h2_style))
|
||||
story.append(Paragraph(
|
||||
f"<b>Days:</b> {days_str} "
|
||||
f"<b>Time:</b> {start} – {end} "
|
||||
f"<b>Users:</b> {s['user_count']} "
|
||||
f"<b>Websites:</b> {s['website_count']}",
|
||||
body_style))
|
||||
|
||||
if s.get("note"):
|
||||
story.append(Paragraph(f"<i>{s['note']}</i>", dim_style))
|
||||
|
||||
story.append(Spacer(1, 0.25*cm))
|
||||
|
||||
# Users table
|
||||
users = get_shift_assigned_users(s["id"])
|
||||
if users:
|
||||
user_data = [["Assigned Users", ""]]
|
||||
for u in users:
|
||||
user_data.append([u["username"], u["full_name"] or ""])
|
||||
t = Table(user_data, colWidths=[5*cm, 8*cm])
|
||||
t.setStyle(TableStyle([
|
||||
("BACKGROUND", (0,0), (-1,0), LIGHT),
|
||||
("TEXTCOLOR", (0,0), (-1,0), ACCENT),
|
||||
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
|
||||
("FONTSIZE", (0,0), (-1,-1), 9),
|
||||
("ROWBACKGROUNDS", (0,1), (-1,-1), [colors.white, colors.HexColor("#f5f5fa")]),
|
||||
("GRID", (0,0), (-1,-1), 0.5, colors.HexColor("#c5c5d8")),
|
||||
("LEFTPADDING", (0,0), (-1,-1), 8),
|
||||
("TOPPADDING", (0,0), (-1,-1), 4),
|
||||
("BOTTOMPADDING", (0,0), (-1,-1), 4),
|
||||
]))
|
||||
story.append(t)
|
||||
story.append(Spacer(1, 0.2*cm))
|
||||
|
||||
# Websites table
|
||||
sites = get_shift_assigned_websites(s["id"])
|
||||
if sites:
|
||||
site_data = [["#", "Website", "URL"]]
|
||||
for i, w in enumerate(sites, 1):
|
||||
site_data.append([str(i), w["name"], w["url"]])
|
||||
t = Table(site_data, colWidths=[1*cm, 5*cm, 10*cm])
|
||||
t.setStyle(TableStyle([
|
||||
("BACKGROUND", (0,0), (-1,0), LIGHT),
|
||||
("TEXTCOLOR", (0,0), (-1,0), ACCENT),
|
||||
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
|
||||
("FONTSIZE", (0,0), (-1,-1), 9),
|
||||
("ROWBACKGROUNDS", (0,1), (-1,-1), [colors.white, colors.HexColor("#f5f5fa")]),
|
||||
("GRID", (0,0), (-1,-1), 0.5, colors.HexColor("#c5c5d8")),
|
||||
("LEFTPADDING", (0,0), (-1,-1), 6),
|
||||
("TOPPADDING", (0,0), (-1,-1), 4),
|
||||
("BOTTOMPADDING", (0,0), (-1,-1), 4),
|
||||
]))
|
||||
story.append(t)
|
||||
|
||||
story.append(Spacer(1, 0.5*cm))
|
||||
story.append(HRFlowable(width="100%", thickness=0.5,
|
||||
color=colors.HexColor("#c5c5d8")))
|
||||
story.append(Spacer(1, 0.3*cm))
|
||||
|
||||
doc.build(story)
|
||||
|
||||
from models import log_action
|
||||
log_action(self.current_user["id"], "EXPORT_SHIFT_PDF", "shifts",
|
||||
None, f"Shift schedule exported to PDF: {save_path}")
|
||||
logger.info(f"Shift schedule PDF exported: {save_path}")
|
||||
|
||||
from utils.ui_helpers import show_info
|
||||
show_info(f"PDF exported successfully.\n\n{save_path}")
|
||||
|
||||
except ImportError:
|
||||
from utils.ui_helpers import show_error
|
||||
show_error("reportlab is required for PDF export.\n"
|
||||
"Install it with: pip install reportlab")
|
||||
except Exception as e:
|
||||
from utils.ui_helpers import show_error
|
||||
show_error(f"PDF export failed:\n{e}")
|
||||
|
||||
# ─── Actions ──────────────────────────────────────────────────────────────
|
||||
|
||||
def _open_add(self):
|
||||
|
||||
@@ -97,10 +97,11 @@ class AdminWebsitesView(ttk.Frame):
|
||||
if not wid:
|
||||
show_error("Please select a website to edit.")
|
||||
return
|
||||
from models import get_website_by_id, get_website_credentials
|
||||
from models import get_website_by_id, get_website_credentials, get_website_assigned_users
|
||||
data = get_website_by_id(wid)
|
||||
creds = get_website_credentials(wid)
|
||||
data["credentials"] = creds
|
||||
data["credentials"] = creds
|
||||
data["assigned_users"] = get_website_assigned_users(wid)
|
||||
WebsiteDialog(self, self.current_user, website_data=data,
|
||||
on_save=self._load_websites)
|
||||
|
||||
@@ -227,6 +228,66 @@ class WebsiteDialog(tk.Toplevel):
|
||||
note_frame, self.note_txt = scrolled_text(form, height=4)
|
||||
note_frame.grid(row=3, column=1, sticky="ew", pady=6)
|
||||
|
||||
# ── Visibility ────────────────────────────────────────────────────────
|
||||
ttk.Label(form, text="Visibility").grid(
|
||||
row=4, column=0, sticky="w", padx=(0, 10), pady=6)
|
||||
|
||||
vis_frame = tk.Frame(form, bg=COLOURS["bg"])
|
||||
vis_frame.grid(row=4, column=1, sticky="w", pady=6)
|
||||
|
||||
self.visibility_var = tk.StringVar(value="all")
|
||||
for val, lbl in [("all", "👥 All Users"), ("assigned", "🔒 Assigned Only")]:
|
||||
rb = tk.Radiobutton(
|
||||
vis_frame, text=lbl,
|
||||
variable=self.visibility_var, value=val,
|
||||
bg=COLOURS["bg"], fg=COLOURS["text"],
|
||||
activebackground=COLOURS["bg"],
|
||||
activeforeground=COLOURS["accent"],
|
||||
selectcolor=COLOURS["surface2"],
|
||||
font=FONT, cursor="hand2",
|
||||
command=self._on_visibility_change,
|
||||
)
|
||||
rb.pack(side="left", padx=(0, 16))
|
||||
|
||||
# ── Assigned users (shown only when visibility='assigned') ─────────────
|
||||
self._user_assign_frame = ttk.Frame(self.inner)
|
||||
self._user_assign_frame.pack(fill="x", padx=24, pady=(0, 4))
|
||||
|
||||
ttk.Label(self._user_assign_frame, text="Assigned Users",
|
||||
style="Heading.TLabel").pack(anchor="w", pady=(4, 6))
|
||||
|
||||
# Load all active regular users for the picker
|
||||
from models import get_all_users
|
||||
try:
|
||||
all_users = [u for u in get_all_users()
|
||||
if u["is_active"] and u["role"] == "user"]
|
||||
except Exception:
|
||||
all_users = []
|
||||
|
||||
self._user_vars = {} # user_id -> BooleanVar
|
||||
user_grid = tk.Frame(self._user_assign_frame, bg=COLOURS["surface"],
|
||||
padx=12, pady=8)
|
||||
user_grid.pack(fill="x")
|
||||
|
||||
for i, u in enumerate(all_users):
|
||||
var = tk.BooleanVar(value=False)
|
||||
self._user_vars[u["id"]] = var
|
||||
col, row = i % 3, i // 3
|
||||
tk.Checkbutton(
|
||||
user_grid,
|
||||
text=f"{u['username']} ({u['full_name'] or ''})",
|
||||
variable=var,
|
||||
bg=COLOURS["surface"], fg=COLOURS["text"],
|
||||
activebackground=COLOURS["surface"],
|
||||
activeforeground=COLOURS["accent"],
|
||||
selectcolor=COLOURS["surface2"],
|
||||
font=FONT_SMALL, cursor="hand2",
|
||||
anchor="w",
|
||||
).grid(row=row, column=col, sticky="w", padx=8, pady=2)
|
||||
|
||||
# Hide initially; shown when visibility='assigned'
|
||||
self._user_assign_frame.pack_forget()
|
||||
|
||||
# ── Credentials section ───────────────────────────────────────────────
|
||||
ttk.Separator(self.inner, orient="horizontal").pack(
|
||||
fill="x", padx=24, pady=12)
|
||||
@@ -257,11 +318,26 @@ class WebsiteDialog(tk.Toplevel):
|
||||
self.url_var.set(d.get("url") or "")
|
||||
self.check_type_var.set(d.get("check_type") or "daily")
|
||||
self.note_txt.insert("1.0", d.get("note") or "")
|
||||
vis = d.get("visibility") or "all"
|
||||
self.visibility_var.set(vis)
|
||||
# Pre-tick assigned users
|
||||
assigned_ids = {u["id"] for u in d.get("assigned_users", [])}
|
||||
for uid, var in self._user_vars.items():
|
||||
var.set(uid in assigned_ids)
|
||||
self._on_visibility_change()
|
||||
for cred in d.get("credentials", []):
|
||||
self._add_cred_row(cred)
|
||||
else:
|
||||
self._add_cred_row()
|
||||
|
||||
def _on_visibility_change(self):
|
||||
"""Show or hide the user assignment panel based on visibility selection."""
|
||||
if self.visibility_var.get() == "assigned":
|
||||
self._user_assign_frame.pack(fill="x", padx=24, pady=(0, 4),
|
||||
before=self.creds_container)
|
||||
else:
|
||||
self._user_assign_frame.pack_forget()
|
||||
|
||||
def _add_cred_row(self, cred=None):
|
||||
frame = ttk.Frame(self.creds_container, style="Surface.TFrame")
|
||||
frame.pack(fill="x", pady=4, ipady=4)
|
||||
@@ -300,10 +376,15 @@ class WebsiteDialog(tk.Toplevel):
|
||||
url = self.url_var.get().strip()
|
||||
check_type = self.check_type_var.get()
|
||||
note = self.note_txt.get("1.0", "end-1c").strip()
|
||||
visibility = self.visibility_var.get()
|
||||
assigned_user_ids = [uid for uid, var in self._user_vars.items() if var.get()]
|
||||
|
||||
if not name or not url:
|
||||
show_error("Name and URL are required.")
|
||||
return
|
||||
if visibility == "assigned" and not assigned_user_ids:
|
||||
show_error("Please assign at least one user, or set visibility to All Users.")
|
||||
return
|
||||
|
||||
credentials = []
|
||||
for label_var, user_var, pass_var, _ in self.cred_rows:
|
||||
@@ -320,16 +401,18 @@ class WebsiteDialog(tk.Toplevel):
|
||||
from models import update_website
|
||||
update_website(self.current_user["id"],
|
||||
self.website_data["id"],
|
||||
name, url, check_type, note, credentials)
|
||||
name, url, check_type, note, credentials,
|
||||
visibility, assigned_user_ids)
|
||||
logger.info(f"Website id={self.website_data['id']} updated "
|
||||
f"check_type='{check_type}'.")
|
||||
f"check_type='{check_type}' visibility='{visibility}'.")
|
||||
show_info("Website updated successfully.")
|
||||
else:
|
||||
from models import create_website
|
||||
create_website(self.current_user["id"],
|
||||
name, url, check_type, note, credentials)
|
||||
name, url, check_type, note, credentials,
|
||||
visibility, assigned_user_ids)
|
||||
logger.info(f"New website '{name}' created "
|
||||
f"check_type='{check_type}'.")
|
||||
f"check_type='{check_type}' visibility='{visibility}'.")
|
||||
show_info("Website created successfully.")
|
||||
self.on_save()
|
||||
self.destroy()
|
||||
|
||||
Reference in New Issue
Block a user