rev timesheet & project
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import { supabase } from "$lib/supabaseClient";
|
||||
import { onMount } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import Pagination from "$lib/Pagination.svelte";
|
||||
|
||||
type Timesheets = {
|
||||
id: number;
|
||||
@@ -27,6 +27,9 @@
|
||||
approved_date?: Date;
|
||||
created_at?: Date;
|
||||
};
|
||||
type TimesheetsJoined = Timesheets & {
|
||||
vb_employee: { id: string; employee_name: string } | null;
|
||||
};
|
||||
type TimesheetDisplay = {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -82,7 +85,6 @@
|
||||
name: string;
|
||||
};
|
||||
|
||||
|
||||
const categoryOfWork = [
|
||||
{ label: "Cleaning", value: "Cleaning" },
|
||||
{ label: "Gardening/Pool", value: "Gardening/Pool" },
|
||||
@@ -129,12 +131,13 @@
|
||||
];
|
||||
|
||||
// reactive variables
|
||||
let sortColumn: string | null = "created_at";
|
||||
let sortOrder: "asc" | "desc" = "desc";
|
||||
let currentUserId: string | null = null;
|
||||
let currentVillaFilter: string | null = null;
|
||||
let currentSearchTerm: string | null = null;
|
||||
let dataVilla: Villa[] = [];
|
||||
let allRows: TimesheetDisplay[] = [];
|
||||
let currentPage = 1;
|
||||
let rowsPerPage = 20;
|
||||
$: totalPages = Math.ceil(allRows.length / rowsPerPage);
|
||||
$: paginatedRows = allRows.slice(
|
||||
@@ -199,6 +202,16 @@
|
||||
|
||||
fetchTimeSheets();
|
||||
});
|
||||
// Reactive variables for sorting
|
||||
function toggleSort(column: string) {
|
||||
if (sortColumn === column) {
|
||||
sortOrder = sortOrder === "asc" ? "desc" : "asc";
|
||||
} else {
|
||||
sortColumn = column;
|
||||
sortOrder = "asc";
|
||||
}
|
||||
fetchTimeSheets(); // re-fetch with new sort
|
||||
}
|
||||
|
||||
// Function to calculate total work hours
|
||||
function calculateTotalHours() {
|
||||
@@ -259,57 +272,48 @@
|
||||
|
||||
showModal = true;
|
||||
}
|
||||
// Function to validate the form data
|
||||
function validateForm(formData: FormData): boolean {
|
||||
const errors: { [key: string]: string } = {};
|
||||
const requiredFields = [
|
||||
"work_description",
|
||||
"type_of_work",
|
||||
"category_of_work",
|
||||
"villa_id",
|
||||
"date_in",
|
||||
"date_out",
|
||||
"remarks",
|
||||
];
|
||||
|
||||
requiredFields.forEach((field) => {
|
||||
if (!formData.get(field) || formData.get(field) === "") {
|
||||
errors[field] = `${field.replace(/_/g, " ")} is required.`;
|
||||
}
|
||||
});
|
||||
|
||||
formErrors.set(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
}
|
||||
// Function to fetch timesheets with optional filters and sorting
|
||||
async function fetchTimeSheets(
|
||||
villaIdFilter: string | null = null,
|
||||
searchTerm: string | null = null,
|
||||
sortColumn: string | null = "created_at",
|
||||
sortOrder: "asc" | "desc" = "desc",
|
||||
offset: number = 0,
|
||||
limit: number = 1000,
|
||||
) {
|
||||
) {
|
||||
let reportedBy: { label: string; value: string }[] = [];
|
||||
const { data: staffData, error: staffError } = await supabase
|
||||
.from("vb_employee")
|
||||
.select("id, employee_name")
|
||||
.eq("employee_status", "Active")
|
||||
.order("employee_name", { ascending: true });
|
||||
|
||||
if (staffError) {
|
||||
console.error("Error fetching staff:", staffError);
|
||||
} else if (staffData) {
|
||||
reportedBy = staffData.map((s) => ({
|
||||
label: s.employee_name,
|
||||
value: s.id,
|
||||
}));
|
||||
}
|
||||
|
||||
let query = supabase
|
||||
.from("vb_timesheet")
|
||||
.select("*", { count: "exact" })
|
||||
.select(`*`)
|
||||
.order(sortColumn || "created_at", {
|
||||
ascending: sortOrder === "asc",
|
||||
});
|
||||
ascending: sortOrder === "asc",
|
||||
});
|
||||
|
||||
if (villaIdFilter) {
|
||||
const { data: villaMatch } = await supabase
|
||||
.from("vb_villas")
|
||||
.select("id")
|
||||
.eq("villa_name", villaIdFilter);
|
||||
.from("vb_villas")
|
||||
.select("id")
|
||||
.eq("villa_name", villaIdFilter);
|
||||
|
||||
const matchedId = villaMatch?.[0]?.id;
|
||||
if (matchedId) {
|
||||
query = query.eq("villa_id", matchedId);
|
||||
query = query.eq("villa_id", matchedId);
|
||||
}
|
||||
}
|
||||
if (searchTerm) {
|
||||
query = query.ilike("work_description", `%${searchTerm}%`);
|
||||
}
|
||||
|
||||
if (offset) {
|
||||
query = query.range(offset, offset + limit - 1);
|
||||
}
|
||||
@@ -322,80 +326,77 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const villaIds = [
|
||||
...new Set(timesheet.map((i: Timesheets) => i.villa_id)),
|
||||
];
|
||||
const loweredSearch = searchTerm?.toLowerCase();
|
||||
let filteredTimesheet = timesheet;
|
||||
if (loweredSearch) {
|
||||
filteredTimesheet = timesheet.filter((ts) => {
|
||||
const workDesc = ts.work_description?.toLowerCase() || "";
|
||||
const staffName = reportedBy.find((s) => s.value === ts.entered_by)?.label?.toLowerCase() || "";
|
||||
return (
|
||||
workDesc.includes(loweredSearch) ||
|
||||
staffName.includes(loweredSearch)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const { data: villas, error: villaError } = await supabase
|
||||
const villaIds = [...new Set(filteredTimesheet.map((i: Timesheets) => i.villa_id))];
|
||||
let villas = [];
|
||||
if (villaIds.length > 0) {
|
||||
const { data: villasData, error: villaError } = await supabase
|
||||
.from("vb_villas")
|
||||
.select("*")
|
||||
.in("id", villaIds);
|
||||
|
||||
if (villaError) {
|
||||
if (villaError) {
|
||||
console.error("Error fetching villas:", villaError);
|
||||
return;
|
||||
} else {
|
||||
villas = villasData;
|
||||
}
|
||||
}
|
||||
|
||||
const { data: approvers, error: approverError } = await supabase
|
||||
.from("vb_users") // or vb_employee if you store them there
|
||||
.from("vb_users")
|
||||
.select("id, full_name");
|
||||
|
||||
if (approverError) {
|
||||
console.error("Error fetching approvers:", approverError);
|
||||
}
|
||||
|
||||
let reportedBy: { label: string; value: string }[] = [];
|
||||
const { data: staffData, error: staffError } = await supabase
|
||||
.from("vb_employee")
|
||||
.select("id, employee_name")
|
||||
.eq("employee_status", "Active")
|
||||
.order("employee_name", { ascending: true });
|
||||
if (staffError) {
|
||||
console.error("Error fetching staff:", staffError);
|
||||
} else if (staffData) {
|
||||
reportedBy = staffData.map((s) => ({
|
||||
label: s.employee_name,
|
||||
value: s.id,
|
||||
}));
|
||||
}
|
||||
|
||||
allRows = timesheet.map((tsdata: Timesheets) => {
|
||||
allRows = filteredTimesheet.map((tsdata: TimesheetsJoined) => {
|
||||
const villa = villas.find((v) => v.id === tsdata.villa_id);
|
||||
// Map entered_by to staff_id
|
||||
const staff = reportedBy.find((s) => s.value === tsdata.entered_by);
|
||||
const approver = approvers?.find((u) => u.id === tsdata.approved_by);
|
||||
|
||||
return {
|
||||
id: tsdata.id,
|
||||
name: tsdata.work_description, // Map work_description to name
|
||||
staff_id: staff?.label, // Map entered_by to staff_id
|
||||
date_in: new Date(tsdata.datetime_in),
|
||||
date_out: new Date(tsdata.datetime_out),
|
||||
type_of_work: tsdata.type_of_work,
|
||||
category_of_work: tsdata.category_of_work,
|
||||
villa_name: villa ? villa.villa_name : "Unknown Villa",
|
||||
approval:
|
||||
tsdata.approval == null
|
||||
? "PENDING"
|
||||
: tsdata.approval
|
||||
? "APPROVED"
|
||||
: "REJECTED", // or map as needed
|
||||
total_hours_work:
|
||||
Math.abs(
|
||||
new Date(tsdata.datetime_out).getTime() -
|
||||
new Date(tsdata.datetime_in).getTime(),
|
||||
) /
|
||||
(1000 * 60 * 60), // Convert milliseconds to hours
|
||||
approved_by: approver?.full_name ?? "Not Approved",
|
||||
approved_date: tsdata.approved_date,
|
||||
remarks: tsdata.remarks,
|
||||
created_at: tsdata.created_at
|
||||
? new Date(tsdata.created_at)
|
||||
: undefined,
|
||||
id: tsdata.id,
|
||||
name: tsdata.work_description,
|
||||
staff_id:
|
||||
reportedBy.find((s) => s.value === tsdata.entered_by)?.label || "Unknown",
|
||||
date_in: new Date(tsdata.datetime_in),
|
||||
date_out: new Date(tsdata.datetime_out),
|
||||
type_of_work: tsdata.type_of_work,
|
||||
category_of_work: tsdata.category_of_work,
|
||||
villa_name: villa ? villa.villa_name : "Unknown Villa",
|
||||
approval:
|
||||
tsdata.approval == null
|
||||
? "PENDING"
|
||||
: tsdata.approval
|
||||
? "APPROVED"
|
||||
: "REJECTED",
|
||||
total_hours_work:
|
||||
Math.abs(
|
||||
new Date(tsdata.datetime_out).getTime() - new Date(tsdata.datetime_in).getTime()
|
||||
) / (1000 * 60 * 60),
|
||||
approved_by: approver?.full_name ?? "Not Approved",
|
||||
approved_date: tsdata.approved_date,
|
||||
remarks: tsdata.remarks,
|
||||
created_at: tsdata.created_at ? new Date(tsdata.created_at) : undefined,
|
||||
} as TimesheetDisplay;
|
||||
});
|
||||
// Sort the rows based on the sortColumn and sortOrder
|
||||
currentPage = 1;
|
||||
|
||||
console.log("Fetched rows:", allRows);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Function to delete a timesheet
|
||||
async function deleteTimesheet(id: string) {
|
||||
if (confirm("Are you sure you want to delete this Timesheet?")) {
|
||||
@@ -491,7 +492,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
export let formErrors = writable<{ [key: string]: string }>({});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -571,9 +571,13 @@
|
||||
</th>
|
||||
{:else}
|
||||
<th
|
||||
class="px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||||
class="cursor-pointer px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap" on:click={() => toggleSort(col.key)}
|
||||
>
|
||||
|
||||
{col.title}
|
||||
{#if sortColumn === col.key}
|
||||
{sortOrder === 'asc' ? ' 🔼' : ' 🔽'}
|
||||
{/if}
|
||||
</th>
|
||||
{/if}
|
||||
{/each}
|
||||
@@ -634,7 +638,15 @@
|
||||
row[
|
||||
col.key as keyof TimesheetDisplay
|
||||
] as string | number | Date,
|
||||
).toLocaleDateString()
|
||||
).toLocaleDateString("en-GB", {
|
||||
timeZone: "Asia/Singapore",
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
})
|
||||
: "N/A"}
|
||||
</td>
|
||||
{:else if col.key === "total_hours_work"}
|
||||
@@ -646,7 +658,18 @@
|
||||
{row[col.key] !== undefined
|
||||
? new Date(
|
||||
row[col.key]!,
|
||||
).toLocaleDateString()
|
||||
).toLocaleDateString(
|
||||
"en-GB",
|
||||
{
|
||||
timeZone: "Asia/Singapore",
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
},
|
||||
)
|
||||
: "N/A"}
|
||||
</td>
|
||||
{:else if col.key === "villa_name"}
|
||||
@@ -684,12 +707,13 @@
|
||||
? new Date(row[col.key]).toLocaleString(
|
||||
"en-GB",
|
||||
{
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
timeZone: "Asia/Singapore",
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
},
|
||||
)
|
||||
: "N/A"}
|
||||
@@ -709,39 +733,11 @@
|
||||
<!-- Pagination controls -->
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div>
|
||||
Showing {(currentPage - 1) * rowsPerPage + 1}–
|
||||
{Math.min(currentPage * rowsPerPage, allRows.length)} of {allRows.length}
|
||||
Showing {(currentPage - 1) * rowsPerPage + 1}–{Math.min(currentPage * rowsPerPage, allRows.length)}
|
||||
of {allRows.length}
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<button
|
||||
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
|
||||
on:click={() => goToPage(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
{#each Array(totalPages)
|
||||
.fill(0)
|
||||
.map((_, i) => i + 1) as page}
|
||||
<button
|
||||
class="px-3 py-1 rounded border text-sm
|
||||
{currentPage === page
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'bg-white border-gray-300 hover:bg-gray-100'}"
|
||||
on:click={() => goToPage(page)}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
{/each}
|
||||
<button
|
||||
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
|
||||
on:click={() => goToPage(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Pagination {totalPages} {currentPage} {goToPage} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
|
||||
Reference in New Issue
Block a user