From 393425a28e48451fe44fe84b762c4b4093e784fe Mon Sep 17 00:00:00 2001 From: arteons Date: Thu, 19 Jun 2025 08:39:33 +0800 Subject: [PATCH] timesheet backend --- src/routes/backoffice/timesheets/+page.svelte | 555 +++++++++++------- 1 file changed, 332 insertions(+), 223 deletions(-) diff --git a/src/routes/backoffice/timesheets/+page.svelte b/src/routes/backoffice/timesheets/+page.svelte index fdf25f2..dc107e2 100644 --- a/src/routes/backoffice/timesheets/+page.svelte +++ b/src/routes/backoffice/timesheets/+page.svelte @@ -73,8 +73,10 @@ { label: "Cleaning", value: "Cleaning" }, { label: "Gardening/Pool", value: "Gardening/Pool" }, { label: "Maintenance", value: "Maintenance" }, - { label: "Security", value: "Security" }, - { label: "Other", value: "Other" }, + { label: "Supervision", value: "Supervision" }, + { label: "Guest Service", value: "Guest Service" }, + { label: "Administration", value: "Administration" }, + { label: "Non Billable", value: "Non Billable" }, ]; const typeOfWork = [ @@ -83,13 +85,6 @@ { label: "Irregular", value: "Irregular" }, ]; - const reportedBy = [ - { label: "Admin", value: "Admin" }, - { label: "Staff", value: "Staff" }, - { label: "Manager", value: "Manager" }, - { label: "Guest", value: "Guest" }, - ]; - let currentUserId: string | null = null; onMount(async () => { @@ -110,7 +105,8 @@ onMount(async () => { const { data, error } = await supabase .from("vb_villas") - .select("id, villa_name"); + .select("id, villa_name") + .eq("villa_status", "Active"); if (error) { console.error("Error fetching villas:", error); @@ -127,8 +123,8 @@ }; const columns: columns[] = [ - { key: "name", title: "Name" }, - { key: "staff_id", title: "Staff Report" }, + { key: "name", title: "Work Description" }, + { key: "staff_id", title: "Staff Name" }, { key: "date_in", title: "Date In" }, { key: "date_out", title: "Date Out" }, { key: "type_of_work", title: "Type of Work" }, @@ -136,13 +132,23 @@ { key: "approval", title: "Approval" }, { key: "villa_name", title: "Villa Name" }, { key: "approved_by", title: "Approved By" }, - { key: "approved_date", title: "Approved Date" }, + { key: "approved_date", title: "Approved/Rejected Date" }, { key: "total_hours_work", title: "Total Hours Work" }, { key: "remarks", title: "Remarks" }, - { key: "vacant", title: "Vacant" }, { key: "created_at", title: "Created At" }, { key: "actions", title: "Actions" }, ]; + function calculateTotalHours() { + if (form.datetime_in && form.datetime_out) { + const start = new Date(form.datetime_in); + const end = new Date(form.datetime_out); + const diffInMs = end.getTime() - start.getTime(); + const hours = diffInMs / (1000 * 60 * 60); + form.total_work_hour = Math.max(Number(hours.toFixed(2)), 0); + } else { + form.total_work_hour = 0; + } + } async function fetchTimeSheets( filter: string | null = null, @@ -202,7 +208,8 @@ let reportedBy: { label: string; value: string }[] = []; const { data: staffData, error: staffError } = await supabase .from("vb_employee") - .select("id, employee_name"); + .select("id, employee_name") + .eq("employee_status", "Active"); if (staffError) { console.error("Error fetching staff:", staffError); } else if (staffData) { @@ -262,6 +269,43 @@ if (page >= 1 && page <= totalPages) currentPage = page; } + onMount(async () => { + // get current user + const { + data: { user }, + } = await supabase.auth.getUser(); + currentUserId = user?.id ?? null; + + // fetch employees + const { data: empData, error: empErr } = await supabase + .from("vb_employee") + .select("id, employee_name") + .eq("employee_status", "Active"); + + if (!empErr && empData) { + employees = empData.map((e) => ({ + id: e.id, + name: e.employee_name, + })); + } else { + console.error("Failed to load employees", empErr); + } + + // fetch villas + const { data: villaData, error: villaErr } = await supabase + .from("vb_villas") + .select("id, villa_name") + .eq("villa_status", "Active"); + + if (!villaErr && villaData) { + villas = villaData; + } else { + console.error("Failed to load villas", villaErr); + } + + fetchTimeSheets(); // this still calls your table loader + }); + onMount(() => { fetchTimeSheets(); }); @@ -280,29 +324,79 @@ function openModal(issue?: Record) { if (issue) { + // Edit mode isEditing = true; currentEditingId = issue.id; - newIssue = { - work_description: issue.name, - entered_by: issue.entered_by || "", // you may need to store the UUID if not already - type_of_work: issue.type_of_work, - category_of_work: issue.category_of_work, - villa_id: dataVilla.find(v => v.villa_name === issue.villa_name)?.id || "", - date_in: issue.date_in?.toISOString().slice(0, 16), // for datetime-local - date_out: issue.date_out?.toISOString().slice(0, 16), - remarks: issue.remarks, - total_work_hour: issue.total_hours_work, + form = { + entered_by: employees.find(e => e.name === issue.staff_id)?.id || "", + work_description: issue.name, + type_of_work: issue.type_of_work, + category_of_work: issue.category_of_work, + villa_id: villas.find(v => v.villa_name === issue.villa_name)?.id || "", + datetime_in: issue.date_in?.toISOString().slice(0, 16), + datetime_out: issue.date_out?.toISOString().slice(0, 16), + total_work_hour: 0, + remarks: issue.remarks, + approval: null // leave null or bring in if editing allowed }; + calculateTotalHours(); } else { + // Add mode isEditing = false; currentEditingId = null; - newIssue = {}; + form = { + entered_by: "", + work_description: "", + type_of_work: "Running", + category_of_work: "Cleaning", + villa_id: "", + datetime_in: "", + datetime_out: "", + total_work_hour: 0, + remarks: "", + approval: null, + }; } + showModal = true; } + type Employee = { + id: string; + name: string; + }; + + let employees: Employee[] = []; + let villas: Villa[] = []; + const typeOfWorkOptions: TimesheetForm["type_of_work"][] = [ + "Running", + "Periodic", + "Irregular", + ]; + const categoryOptions: TimesheetForm["category_of_work"][] = [ + "Cleaning", + "Gardening/Pool", + "Maintenance", + "Supervision", + "Guest Service", + "Administration", + "Non Billable", + ]; + let form: TimesheetForm = { + entered_by: "", + work_description: "", + type_of_work: "Running", + category_of_work: "Cleaning", + villa_id: "", + datetime_in: "", + datetime_out: "", + total_work_hour: 0, + remarks: "", + approval: null, // Default null + }; + async function saveIssue(event: Event) { event.preventDefault(); @@ -344,14 +438,12 @@ villa_id: formData.get("villa_id") as string, datetime_in: formData.get("date_in") as string, datetime_out: formData.get("date_out") as string, - total_work_hour: Math.abs( - new Date(formData.get("date_out") as string).getTime() - - new Date(formData.get("date_in") as string).getTime(), - ), + total_work_hour: newIssue.total_work_hour ?? 0, remarks: formData.get("remarks") as string, - approval: null, // Default null + approval: null }; + const { error } = await supabase .from("vb_timesheet") .insert([TimesheetsInsert]); @@ -428,6 +520,63 @@ await fetchTimeSheets(); } } + async function submitForm() { + calculateTotalHours(); + + if (!form.entered_by || !form.villa_id) { + alert("Please select an employee and villa."); + return; + } + + let error = null; + + if (isEditing && currentEditingId) { + const { error: updateError } = await supabase + .from("vb_timesheet") + .update({ + entered_by: form.entered_by, + work_description: form.work_description, + type_of_work: form.type_of_work, + category_of_work: form.category_of_work, + villa_id: form.villa_id, + datetime_in: form.datetime_in, + datetime_out: form.datetime_out, + total_work_hour: form.total_work_hour, + remarks: form.remarks, + approval: form.approval, + }) + .eq("id", currentEditingId); + + error = updateError; + } else { + const { error: insertError } = await supabase + .from("vb_timesheet") + .insert([form]); + + error = insertError; + } + + if (error) { + alert("Failed to save timesheet: " + error.message); + } else { + alert("Timesheet saved successfully!"); + form = { + entered_by: "", + work_description: "", + type_of_work: "Running", + category_of_work: "Cleaning", + villa_id: "", + datetime_in: "", + datetime_out: "", + total_work_hour: 0, + remarks: "", + approval: null, + }; + await fetchTimeSheets(); + showModal = false; + } + } +
@@ -550,14 +699,12 @@ {row[col.key] || "Not Approved"} - {:else if col.key === "approved_date"} - - {row[col.key] !== undefined - ? new Date( - row[col.key]!, - ).toLocaleDateString() - : "N/A"} - + {:else if col.key === "approved_date"} + + {(row[col.key] && !isNaN(Date.parse(row[col.key]))) + ? new Date(row[col.key]).toLocaleDateString() + : "N/A"} + {:else if col.key === "total_hours_work"} {row[col.key].toFixed(2)} hours @@ -598,6 +745,20 @@ 🗑️ Delete + {:else if col.key === "date_in" || col.key === "date_out"} + + {row[col.key] + ? new Date(row[col.key]).toLocaleString("en-GB", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + hour12: false, + }) + : "N/A"} + + {:else} {row[col.key as keyof TimesheetDisplay]} @@ -650,190 +811,138 @@ {#if showModal} -
+ + +
+

Timesheet Entry

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
{form.total_work_hour}
+
+ +
+ + +
+ + - -
- + {isEditing ? 'Update Timesheet' : 'New Entry'} + +
{/if}