perbaikan timesheet

This commit is contained in:
Aji Setiaji
2025-07-09 15:03:10 +14:00
parent 32bdc943c8
commit 0c1b5d22a8

View File

@@ -7,6 +7,7 @@
type Timesheets = { type Timesheets = {
id: number; id: number;
entered_by: string; entered_by: string;
entered_name?: string;
work_description: string; work_description: string;
type_of_work: "Running" | "Periodic" | "Irregular"; type_of_work: "Running" | "Periodic" | "Irregular";
category_of_work: category_of_work:
@@ -24,11 +25,13 @@
remarks: string; remarks: string;
approval: boolean; approval: boolean;
approved_by?: string; approved_by?: string;
approved_name?: string;
approved_date?: Date; approved_date?: Date;
created_at?: Date; created_at?: Date;
}; };
type TimesheetsJoined = Timesheets & { type TimesheetsJoined = Timesheets & {
vb_employee: { id: string; employee_name: string } | null; vb_employee: { id: string; employee_name: string } | null;
villa_name?: string;
}; };
type TimesheetDisplay = { type TimesheetDisplay = {
id: number; id: number;
@@ -138,13 +141,8 @@
let currentSearchTerm: string | null = null; let currentSearchTerm: string | null = null;
let dataVilla: Villa[] = []; let dataVilla: Villa[] = [];
let allRows: TimesheetDisplay[] = []; let allRows: TimesheetDisplay[] = [];
let totalItems = 0;
let rowsPerPage = 20; let rowsPerPage = 20;
$: totalPages = Math.ceil(allRows.length / rowsPerPage);
$: paginatedRows = allRows.slice(
(currentPage - 1) * rowsPerPage,
currentPage * rowsPerPage,
);
$: currentPage = 1;
let showModal = false; let showModal = false;
let isEditing = false; let isEditing = false;
let currentEditingId: string | null = null; let currentEditingId: string | null = null;
@@ -200,7 +198,14 @@
console.error("Failed to load villas", villaErr); console.error("Failed to load villas", villaErr);
} }
fetchTimeSheets(); fetchTimeSheets(
currentVillaFilter,
currentSearchTerm,
sortColumn,
sortOrder,
0,
rowsPerPage,
);
}); });
// Reactive variables for sorting // Reactive variables for sorting
function toggleSort(column: string) { function toggleSort(column: string) {
@@ -210,7 +215,14 @@
sortColumn = column; sortColumn = column;
sortOrder = "asc"; sortOrder = "asc";
} }
fetchTimeSheets(); // re-fetch with new sort fetchTimeSheets(
currentVillaFilter,
currentSearchTerm,
sortColumn,
sortOrder,
(currentPage - 1) * rowsPerPage,
rowsPerPage,
);
} }
// Function to calculate total work hours // Function to calculate total work hours
@@ -227,7 +239,22 @@
} }
// Function to go to a specific page // Function to go to a specific page
function goToPage(page: number) { function goToPage(page: number) {
console.log("Going to page:", page);
if (page >= 1 && page <= totalPages) currentPage = page; if (page >= 1 && page <= totalPages) currentPage = page;
// Re-fetch timesheets with the current filters and pagination
console.log("Fetching timesheets for page:", currentPage);
fetchTimeSheets(
currentVillaFilter,
currentSearchTerm,
sortColumn,
sortOrder,
(currentPage - 1) * rowsPerPage,
rowsPerPage,
);
} }
// Function to open the modal for adding or editing a timesheet // Function to open the modal for adding or editing a timesheet
function openModal(tsdata?: Record<string, any>) { function openModal(tsdata?: Record<string, any>) {
@@ -272,129 +299,260 @@
showModal = true; showModal = true;
} }
// Function to fetch timesheets with optional filters and sorting
// Function to fetch timesheets
async function fetchTimeSheets( async function fetchTimeSheets(
villaIdFilter: string | null = null, villaIdFilter: string | null = null,
searchTerm: string | null = null, searchTerm: string | null = null,
sortColumn: string | null = "created_at",
sortOrder: "asc" | "desc" = "desc",
offset: number = 0, offset: number = 0,
limit: number = 1000, limit: number = 20,
) { ) {
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.log("Fetching timesheets with filters:", {
console.error("Error fetching staff:", staffError); villaIdFilter,
} else if (staffData) { searchTerm,
reportedBy = staffData.map((s) => ({ sortColumn,
label: s.employee_name, sortOrder,
value: s.id, offset,
})); limit,
} });
const fromIndex = offset;
const toIndex = offset + limit - 1;
let query = supabase let query = supabase
.from("vb_timesheet") .from("vb_timesheet_data")
.select(`*`) .select(
'*',
{ count: "exact" },
)
.order(sortColumn || "created_at", { .order(sortColumn || "created_at", {
ascending: sortOrder === "asc", ascending: sortOrder === "asc",
}); }).range(fromIndex, toIndex);
if (searchTerm) {
query = query.ilike("work_description", `%${searchTerm}%`)
}
if (villaIdFilter) { if (villaIdFilter) {
const { data: villaMatch } = await supabase query = query.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);
}
} }
if (offset) { // Jalankan query
query = query.range(offset, offset + limit - 1); const { data, count, error } = await query;
}
if (limit) { console.log("Fetched timesheets:", count);
query = query.limit(limit);
} console.log("Fetched timesheets data:", data);
const { data: timesheet, error } = await query;
if (error) { if (error) {
console.error("Error fetching timesheets:", error); console.error("Error fetching timesheets:", error);
return; return;
} }
const loweredSearch = searchTerm?.toLowerCase(); allRows = data.map((tsdata: TimesheetsJoined) => {
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 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) {
console.error("Error fetching villas:", villaError);
} else {
villas = villasData;
}
}
const { data: approvers, error: approverError } = await supabase
.from("vb_users")
.select("id, full_name");
if (approverError) {
console.error("Error fetching approvers:", approverError);
}
allRows = filteredTimesheet.map((tsdata: TimesheetsJoined) => {
const villa = villas.find((v) => v.id === tsdata.villa_id);
const approver = approvers?.find((u) => u.id === tsdata.approved_by);
return { return {
id: tsdata.id, id: tsdata.id,
name: tsdata.work_description, name: tsdata.work_description,
staff_id: staff_id: tsdata.entered_name || "Unknown Staff",
reportedBy.find((s) => s.value === tsdata.entered_by)?.label || "Unknown", date_in: new Date(tsdata.datetime_in),
date_in: new Date(tsdata.datetime_in), date_out: new Date(tsdata.datetime_out),
date_out: new Date(tsdata.datetime_out), type_of_work: tsdata.type_of_work,
type_of_work: tsdata.type_of_work, category_of_work: tsdata.category_of_work,
category_of_work: tsdata.category_of_work, villa_name: tsdata.villa_name || "Unknown Villa",
villa_name: villa ? villa.villa_name : "Unknown Villa", approval:
approval: tsdata.approval == null
tsdata.approval == null ? "PENDING"
? "PENDING" : tsdata.approval
: tsdata.approval ? "APPROVED"
? "APPROVED" : "REJECTED",
: "REJECTED", total_hours_work:
total_hours_work: Math.abs(
Math.abs( new Date(tsdata.datetime_out).getTime() -
new Date(tsdata.datetime_out).getTime() - new Date(tsdata.datetime_in).getTime() new Date(tsdata.datetime_in).getTime(),
) / (1000 * 60 * 60), ) / (1000 * 60 * 60),
approved_by: approver?.full_name ?? "Not Approved", approved_by: tsdata.approved_name || "Not Approved",
approved_date: tsdata.approved_date, approved_date: tsdata.approved_date
remarks: tsdata.remarks, ? new Date(tsdata.approved_date)
// created_at: tsdata.created_at ? new Date(tsdata.created_at) : undefined, : undefined,
remarks: tsdata.remarks || "No remarks",
created_at: tsdata.created_at
? new Date(tsdata.created_at)
: undefined,
} as TimesheetDisplay; } as TimesheetDisplay;
}); });
currentPage = 1;
console.log("Fetched rows:", allRows); totalItems = count || 0;
} }
$: totalPages = Math.ceil(totalItems / rowsPerPage);
$: currentPage = 1;
function pageRange(totalPages: number, currentPage: number,): (number | string)[] {
const range: (number | string)[] = [];
const maxDisplay = 5;
if (totalPages <= maxDisplay + 2) {
for (let i = 1; i <= totalPages; i++) range.push(i);
} else {
const start = Math.max(2, currentPage - 2);
const end = Math.min(totalPages - 1, currentPage + 2);
range.push(1);
if (start > 2) range.push("...");
for (let i = start; i <= end; i++) {
range.push(i);
}
if (end < totalPages - 1) range.push("...");
range.push(totalPages);
}
return range;
}
function changePage(page: number) {
if (page < 1 || page > totalPages || page === currentPage) return;
currentPage = page;
fetchTimeSheets(
currentVillaFilter,
currentSearchTerm,
sortColumn,
sortOrder,
(currentPage - 1) * rowsPerPage,
rowsPerPage,
);
}
// // Function to fetch timesheets with optional filters and sorting
// async function fetchTimeSheets(
// villaIdFilter: string | null = null,
// searchTerm: string | null = null,
// 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(`*`)
// .order(sortColumn || "created_at", {
// ascending: sortOrder === "asc",
// });
// if (villaIdFilter) {
// const { data: villaMatch } = await supabase
// .from("vb_villas")
// .select("id")
// .eq("villa_name", villaIdFilter);
// const matchedId = villaMatch?.[0]?.id;
// if (matchedId) {
// query = query.eq("villa_id", matchedId);
// }
// }
// if (offset) {
// query = query.range(offset, offset + limit - 1);
// }
// if (limit) {
// query = query.limit(limit);
// }
// const { data: timesheet, error } = await query;
// if (error) {
// console.error("Error fetching timesheets:", error);
// return;
// }
// 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 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) {
// console.error("Error fetching villas:", villaError);
// } else {
// villas = villasData;
// }
// }
// const { data: approvers, error: approverError } = await supabase
// .from("vb_users")
// .select("id, full_name");
// if (approverError) {
// console.error("Error fetching approvers:", approverError);
// }
// allRows = filteredTimesheet.map((tsdata: TimesheetsJoined) => {
// const villa = villas.find((v) => v.id === tsdata.villa_id);
// const approver = approvers?.find((u) => u.id === tsdata.approved_by);
// return {
// 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;
// });
// currentPage = 1;
// console.log("Fetched rows:", allRows);
// }
// Function to delete a timesheet // Function to delete a timesheet
@@ -446,6 +604,8 @@
let error = null; let error = null;
if (isEditing && currentEditingId) { if (isEditing && currentEditingId) {
const { error: updateError } = await supabase const { error: updateError } = await supabase
.from("vb_timesheet") .from("vb_timesheet")
.update({ .update({
@@ -486,6 +646,7 @@
total_work_hour: 0, total_work_hour: 0,
remarks: "", remarks: "",
approval: null, approval: null,
created_at: new Date().toISOString(),
}; };
await fetchTimeSheets(); await fetchTimeSheets();
showModal = false; showModal = false;
@@ -584,7 +745,7 @@
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200 bg-white text-align-top"> <tbody class="divide-y divide-gray-200 bg-white text-align-top">
{#each paginatedRows as row} {#each allRows as row}
<tr class="hover:bg-gray-50 transition"> <tr class="hover:bg-gray-50 transition">
{#each columns as col} {#each columns as col}
{#if col.key === "name"} {#if col.key === "name"}
@@ -633,20 +794,8 @@
{:else if col.key === "approved_date"} {:else if col.key === "approved_date"}
<td class="px-4 py-2"> <td class="px-4 py-2">
{row[col.key] && {row[col.key] &&
!isNaN(Date.parse(String(row[col.key]))) !isNaN(new Date(row[col.key] as string | number | Date).getTime())
? new Date( ? new Date(row[col.key] as string | number | Date).toLocaleString()
row[
col.key as keyof TimesheetDisplay
] as string | number | Date,
).toLocaleDateString("en-GB", {
timeZone: "Asia/Singapore",
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
: "N/A"} : "N/A"}
</td> </td>
{:else if col.key === "total_hours_work"} {:else if col.key === "total_hours_work"}
@@ -656,20 +805,7 @@
{:else if col.key === "created_at"} {:else if col.key === "created_at"}
<td class="px-4 py-2"> <td class="px-4 py-2">
{row[col.key] !== undefined {row[col.key] !== undefined
? new Date( ? new Date(row[col.key] as string | number | Date).toLocaleString()
row[col.key]!,
).toLocaleDateString(
"en-GB",
{
timeZone: "Asia/Singapore",
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
},
)
: "N/A"} : "N/A"}
</td> </td>
{:else if col.key === "villa_name"} {:else if col.key === "villa_name"}
@@ -704,18 +840,7 @@
{:else if col.key === "date_in" || col.key === "date_out"} {:else if col.key === "date_in" || col.key === "date_out"}
<td class="px-4 py-2"> <td class="px-4 py-2">
{row[col.key] {row[col.key]
? new Date(row[col.key]).toLocaleString( ? new Date(row[col.key]).toLocaleString()
"en-GB",
{
timeZone: "Asia/Singapore",
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
},
)
: "N/A"} : "N/A"}
</td> </td>
{:else} {:else}
@@ -733,10 +858,39 @@
<!-- Pagination controls --> <!-- Pagination controls -->
<div class="flex justify-between items-center text-sm"> <div class="flex justify-between items-center text-sm">
<div> <div>
Showing {(currentPage - 1) * rowsPerPage + 1}{Math.min(currentPage * rowsPerPage, allRows.length)} Showing {(currentPage - 1) * rowsPerPage + 1}
of {allRows.length} {Math.min(currentPage * rowsPerPage, totalItems)} of {totalItems} items
</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 pageRange(totalPages, currentPage) as page}
{#if page === "..."}
<span class="px-2">...</span>
{:else}
<button
on:click={() => changePage(page as number)}
class="px-2 py-1 border rounded {page === currentPage
? 'bg-blue-600 text-white'
: ''}"
>
{page}
</button>
{/if}
{/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>
</div> </div>