perbaikan timesheet
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
type Timesheets = {
|
||||
id: number;
|
||||
entered_by: string;
|
||||
entered_name?: string;
|
||||
work_description: string;
|
||||
type_of_work: "Running" | "Periodic" | "Irregular";
|
||||
category_of_work:
|
||||
@@ -24,11 +25,13 @@
|
||||
remarks: string;
|
||||
approval: boolean;
|
||||
approved_by?: string;
|
||||
approved_name?: string;
|
||||
approved_date?: Date;
|
||||
created_at?: Date;
|
||||
};
|
||||
type TimesheetsJoined = Timesheets & {
|
||||
vb_employee: { id: string; employee_name: string } | null;
|
||||
villa_name?: string;
|
||||
};
|
||||
type TimesheetDisplay = {
|
||||
id: number;
|
||||
@@ -138,13 +141,8 @@
|
||||
let currentSearchTerm: string | null = null;
|
||||
let dataVilla: Villa[] = [];
|
||||
let allRows: TimesheetDisplay[] = [];
|
||||
let totalItems = 0;
|
||||
let rowsPerPage = 20;
|
||||
$: totalPages = Math.ceil(allRows.length / rowsPerPage);
|
||||
$: paginatedRows = allRows.slice(
|
||||
(currentPage - 1) * rowsPerPage,
|
||||
currentPage * rowsPerPage,
|
||||
);
|
||||
$: currentPage = 1;
|
||||
let showModal = false;
|
||||
let isEditing = false;
|
||||
let currentEditingId: string | null = null;
|
||||
@@ -200,7 +198,14 @@
|
||||
console.error("Failed to load villas", villaErr);
|
||||
}
|
||||
|
||||
fetchTimeSheets();
|
||||
fetchTimeSheets(
|
||||
currentVillaFilter,
|
||||
currentSearchTerm,
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
0,
|
||||
rowsPerPage,
|
||||
);
|
||||
});
|
||||
// Reactive variables for sorting
|
||||
function toggleSort(column: string) {
|
||||
@@ -210,7 +215,14 @@
|
||||
sortColumn = column;
|
||||
sortOrder = "asc";
|
||||
}
|
||||
fetchTimeSheets(); // re-fetch with new sort
|
||||
fetchTimeSheets(
|
||||
currentVillaFilter,
|
||||
currentSearchTerm,
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
(currentPage - 1) * rowsPerPage,
|
||||
rowsPerPage,
|
||||
);
|
||||
}
|
||||
|
||||
// Function to calculate total work hours
|
||||
@@ -227,7 +239,22 @@
|
||||
}
|
||||
// Function to go to a specific page
|
||||
function goToPage(page: number) {
|
||||
|
||||
console.log("Going to page:", 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 openModal(tsdata?: Record<string, any>) {
|
||||
@@ -272,108 +299,72 @@
|
||||
|
||||
showModal = true;
|
||||
}
|
||||
// Function to fetch timesheets with optional filters and sorting
|
||||
|
||||
// Function to fetch timesheets
|
||||
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,
|
||||
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.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",
|
||||
console.log("Fetching timesheets with filters:", {
|
||||
villaIdFilter,
|
||||
searchTerm,
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
offset,
|
||||
limit,
|
||||
});
|
||||
|
||||
|
||||
const fromIndex = offset;
|
||||
const toIndex = offset + limit - 1;
|
||||
|
||||
let query = supabase
|
||||
.from("vb_timesheet_data")
|
||||
.select(
|
||||
'*',
|
||||
{ count: "exact" },
|
||||
)
|
||||
.order(sortColumn || "created_at", {
|
||||
ascending: sortOrder === "asc",
|
||||
}).range(fromIndex, toIndex);
|
||||
|
||||
if (searchTerm) {
|
||||
query = query.ilike("work_description", `%${searchTerm}%`)
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
query = query.eq("villa_name", villaIdFilter);
|
||||
}
|
||||
|
||||
if (offset) {
|
||||
query = query.range(offset, offset + limit - 1);
|
||||
}
|
||||
if (limit) {
|
||||
query = query.limit(limit);
|
||||
}
|
||||
const { data: timesheet, error } = await query;
|
||||
// Jalankan query
|
||||
const { data, count, error } = await query;
|
||||
|
||||
console.log("Fetched timesheets:", count);
|
||||
|
||||
console.log("Fetched timesheets data:", data);
|
||||
|
||||
|
||||
|
||||
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);
|
||||
|
||||
allRows = data.map((tsdata: TimesheetsJoined) => {
|
||||
return {
|
||||
id: tsdata.id,
|
||||
name: tsdata.work_description,
|
||||
staff_id:
|
||||
reportedBy.find((s) => s.value === tsdata.entered_by)?.label || "Unknown",
|
||||
staff_id: tsdata.entered_name || "Unknown Staff",
|
||||
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",
|
||||
villa_name: tsdata.villa_name || "Unknown Villa",
|
||||
approval:
|
||||
tsdata.approval == null
|
||||
? "PENDING"
|
||||
@@ -382,19 +373,186 @@
|
||||
: "REJECTED",
|
||||
total_hours_work:
|
||||
Math.abs(
|
||||
new Date(tsdata.datetime_out).getTime() - new Date(tsdata.datetime_in).getTime()
|
||||
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,
|
||||
approved_by: tsdata.approved_name || "Not Approved",
|
||||
approved_date: tsdata.approved_date
|
||||
? new Date(tsdata.approved_date)
|
||||
: undefined,
|
||||
remarks: tsdata.remarks || "No remarks",
|
||||
created_at: tsdata.created_at
|
||||
? new Date(tsdata.created_at)
|
||||
: undefined,
|
||||
} 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
|
||||
@@ -446,6 +604,8 @@
|
||||
let error = null;
|
||||
|
||||
if (isEditing && currentEditingId) {
|
||||
|
||||
|
||||
const { error: updateError } = await supabase
|
||||
.from("vb_timesheet")
|
||||
.update({
|
||||
@@ -486,6 +646,7 @@
|
||||
total_work_hour: 0,
|
||||
remarks: "",
|
||||
approval: null,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
await fetchTimeSheets();
|
||||
showModal = false;
|
||||
@@ -584,7 +745,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<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">
|
||||
{#each columns as col}
|
||||
{#if col.key === "name"}
|
||||
@@ -633,20 +794,8 @@
|
||||
{:else if col.key === "approved_date"}
|
||||
<td class="px-4 py-2">
|
||||
{row[col.key] &&
|
||||
!isNaN(Date.parse(String(row[col.key])))
|
||||
? new Date(
|
||||
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,
|
||||
})
|
||||
!isNaN(new Date(row[col.key] as string | number | Date).getTime())
|
||||
? new Date(row[col.key] as string | number | Date).toLocaleString()
|
||||
: "N/A"}
|
||||
</td>
|
||||
{:else if col.key === "total_hours_work"}
|
||||
@@ -656,20 +805,7 @@
|
||||
{:else if col.key === "created_at"}
|
||||
<td class="px-4 py-2">
|
||||
{row[col.key] !== undefined
|
||||
? new Date(
|
||||
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,
|
||||
},
|
||||
)
|
||||
? new Date(row[col.key] as string | number | Date).toLocaleString()
|
||||
: "N/A"}
|
||||
</td>
|
||||
{:else if col.key === "villa_name"}
|
||||
@@ -704,18 +840,7 @@
|
||||
{:else if col.key === "date_in" || col.key === "date_out"}
|
||||
<td class="px-4 py-2">
|
||||
{row[col.key]
|
||||
? 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,
|
||||
},
|
||||
)
|
||||
? new Date(row[col.key]).toLocaleString()
|
||||
: "N/A"}
|
||||
</td>
|
||||
{:else}
|
||||
@@ -733,10 +858,39 @@
|
||||
<!-- 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, 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>
|
||||
<Pagination {totalPages} {currentPage} {goToPage} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user