fix
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -2,8 +2,8 @@
|
||||
import { supabase } from "$lib/supabaseClient";
|
||||
import { onMount } from "svelte";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
import * as XLSX from "xlsx";
|
||||
|
||||
type Timesheets = {
|
||||
id: number;
|
||||
entered_by: string;
|
||||
@@ -32,7 +32,7 @@
|
||||
type TimesheetsJoined = Timesheets & {
|
||||
vb_employee: { id: string; employee_name: string } | null;
|
||||
villa_name?: string;
|
||||
};
|
||||
};
|
||||
type TimesheetDisplay = {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -241,9 +241,8 @@
|
||||
}
|
||||
// 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
|
||||
@@ -272,8 +271,8 @@
|
||||
type_of_work: tsdata.type_of_work,
|
||||
category_of_work: tsdata.category_of_work,
|
||||
villa_id:
|
||||
villas.find((v) => v.villa_name === tsdata.villa_name)?.id ||
|
||||
"",
|
||||
villas.find((v) => v.villa_name === tsdata.villa_name)
|
||||
?.id || "",
|
||||
datetime_in: tsdata.date_in?.toISOString().slice(0, 16),
|
||||
datetime_out: tsdata.date_out?.toISOString().slice(0, 16),
|
||||
total_work_hour: 0,
|
||||
@@ -311,35 +310,21 @@
|
||||
offset: number = 0,
|
||||
limit: number = 20,
|
||||
) {
|
||||
|
||||
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" },
|
||||
)
|
||||
.select("*", { count: "exact" })
|
||||
.order(getDBColumn(sortColumn) || "created_at", {
|
||||
ascending: sortOrder === "asc"
|
||||
ascending: sortOrder === "asc",
|
||||
})
|
||||
.range(fromIndex, toIndex);
|
||||
|
||||
.range(fromIndex, toIndex);
|
||||
|
||||
if (typeof searchTerm === "string" && searchTerm.length > 4) {
|
||||
// Supabase ilike only supports one column at a time, so use or for multiple columns
|
||||
query = query.or(
|
||||
`work_description.ilike.%${searchTerm}%,entered_name.ilike.%${searchTerm}%`
|
||||
`work_description.ilike.%${searchTerm}%,entered_name.ilike.%${searchTerm}%`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -353,8 +338,6 @@
|
||||
console.log("Fetched timesheets:", count);
|
||||
|
||||
console.log("Fetched timesheets data:", data);
|
||||
|
||||
|
||||
|
||||
if (error) {
|
||||
console.error("Error fetching timesheets:", error);
|
||||
@@ -375,18 +358,19 @@
|
||||
tsdata.approval == null
|
||||
? "PENDING"
|
||||
: tsdata.approval
|
||||
? "APPROVED"
|
||||
: "REJECTED",
|
||||
? "APPROVED"
|
||||
: "REJECTED",
|
||||
total_hours_work:
|
||||
Math.abs(
|
||||
new Date(tsdata.datetime_out).getTime() -
|
||||
new Date(tsdata.datetime_in).getTime(),
|
||||
) / (1000 * 60 * 60),
|
||||
approved_by: tsdata.approved_name?.trim()
|
||||
) /
|
||||
(1000 * 60 * 60),
|
||||
approved_by: tsdata.approved_name?.trim()
|
||||
? tsdata.approved_name
|
||||
: tsdata.approval === true
|
||||
? "Auto Approve"
|
||||
: "Not Approved",
|
||||
? "Auto Approve"
|
||||
: "Not Approved",
|
||||
approved_date: tsdata.approved_date
|
||||
? new Date(tsdata.approved_date)
|
||||
: undefined,
|
||||
@@ -403,7 +387,10 @@
|
||||
$: totalPages = Math.ceil(totalItems / rowsPerPage);
|
||||
$: currentPage = 1;
|
||||
|
||||
function pageRange(totalPages: number, currentPage: number,): (number | string)[] {
|
||||
function pageRange(
|
||||
totalPages: number,
|
||||
currentPage: number,
|
||||
): (number | string)[] {
|
||||
const range: (number | string)[] = [];
|
||||
const maxDisplay = 5;
|
||||
|
||||
@@ -426,12 +413,16 @@
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
function getDBColumn(key: string) {
|
||||
switch (key) {
|
||||
case "name": return "work_description";
|
||||
case "staff_id": return "entered_by";
|
||||
default: return key;
|
||||
}
|
||||
case "name":
|
||||
return "work_description";
|
||||
case "staff_id":
|
||||
return "entered_by";
|
||||
default:
|
||||
return key;
|
||||
}
|
||||
}
|
||||
function changePage(page: number) {
|
||||
if (page < 1 || page > totalPages || page === currentPage) return;
|
||||
@@ -448,43 +439,46 @@
|
||||
|
||||
async function exportTimesheets() {
|
||||
if (!selectedMonth || !selectedYear) {
|
||||
alert("Please select a month and year to export.");
|
||||
return;
|
||||
}
|
||||
alert("Please select a month and year to export.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("https://flow.catalis.app/webhook-test/villabugis-timesheets", {
|
||||
try {
|
||||
const response = await fetch(
|
||||
"https://flow.catalis.app/webhook-test/villabugis-timesheets",
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
month: selectedMonth,
|
||||
year: selectedYear
|
||||
})
|
||||
});
|
||||
year: selectedYear,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Export failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const randomUuid = uuidv4().toString();
|
||||
|
||||
// Jika response adalah file (application/octet-stream atau Excel)
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `timesheet-${selectedYear}-${String(selectedMonth).padStart(2, '0')}-${randomUuid}.xlsx`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error("Export error:", error);
|
||||
alert("Failed to export timesheet.");
|
||||
if (!response.ok) {
|
||||
throw new Error(`Export failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const randomUuid = uuidv4().toString();
|
||||
|
||||
// Jika response adalah file (application/octet-stream atau Excel)
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `timesheet-${selectedYear}-${String(selectedMonth).padStart(2, "0")}-${randomUuid}.xlsx`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error("Export error:", error);
|
||||
alert("Failed to export timesheet.");
|
||||
}
|
||||
}
|
||||
async function exportToExcelFromFrontend() {
|
||||
if (!selectedMonth || !selectedYear) {
|
||||
alert("Please select both month and year");
|
||||
@@ -497,7 +491,8 @@
|
||||
// 1. Fetch timesheet data
|
||||
const { data: timesheetData, error: timesheetErr } = await supabase
|
||||
.from("vb_timesheet_data")
|
||||
.select(`
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
work_description,
|
||||
total_work_hour,
|
||||
@@ -511,7 +506,8 @@
|
||||
villa_name,
|
||||
entered_name,
|
||||
approved_name
|
||||
`)
|
||||
`,
|
||||
)
|
||||
.gte("datetime_in", startDate.toISOString())
|
||||
.lte("datetime_in", endDate.toISOString());
|
||||
|
||||
@@ -540,7 +536,7 @@
|
||||
"Supervision",
|
||||
"Guest Service",
|
||||
"Administration",
|
||||
"Non Billable"
|
||||
"Non Billable",
|
||||
];
|
||||
|
||||
const sheet2Name = "Data";
|
||||
@@ -586,20 +582,26 @@
|
||||
return row;
|
||||
};
|
||||
|
||||
[headerRowIdx, officeRowIdx, fbRowIdx, villasOnlyRowIdx].forEach((idx) => {
|
||||
sheet1Rows[idx] = sheet1Rows[idx] || [];
|
||||
ensureCols(sheet1Rows[idx], 13); // at least to column M
|
||||
});
|
||||
[headerRowIdx, officeRowIdx, fbRowIdx, villasOnlyRowIdx].forEach(
|
||||
(idx) => {
|
||||
sheet1Rows[idx] = sheet1Rows[idx] || [];
|
||||
ensureCols(sheet1Rows[idx], 13); // at least to column M
|
||||
},
|
||||
);
|
||||
|
||||
// Insert labels and formulas
|
||||
sheet1Rows[headerRowIdx][10] = "Total Work Hours"; // K2
|
||||
sheet1Rows[headerRowIdx][11] = { f: `SUM(${sheet2Name}!$C:$C)` }; // L2
|
||||
|
||||
sheet1Rows[officeRowIdx][10] = "Office"; // K3
|
||||
sheet1Rows[officeRowIdx][11] = { f: `SUMIFS(${sheet2Name}!$C:$C, ${sheet2Name}!$K:$K, K3)` }; // L3
|
||||
sheet1Rows[officeRowIdx][11] = {
|
||||
f: `SUMIFS(${sheet2Name}!$C:$C, ${sheet2Name}!$K:$K, K3)`,
|
||||
}; // L3
|
||||
|
||||
sheet1Rows[fbRowIdx][10] = "FB"; // K4
|
||||
sheet1Rows[fbRowIdx][11] = { f: `SUMIFS(${sheet2Name}!$C:$C, ${sheet2Name}!$K:$K, K4)` }; // L4
|
||||
sheet1Rows[fbRowIdx][11] = {
|
||||
f: `SUMIFS(${sheet2Name}!$C:$C, ${sheet2Name}!$K:$K, K4)`,
|
||||
}; // L4
|
||||
|
||||
sheet1Rows[villasOnlyRowIdx][10] = "Villas Only"; // K5
|
||||
sheet1Rows[villasOnlyRowIdx][11] = { f: `L2-L3` }; // L5
|
||||
@@ -687,8 +689,6 @@
|
||||
let error = null;
|
||||
|
||||
if (isEditing && currentEditingId) {
|
||||
|
||||
|
||||
const { error: updateError } = await supabase
|
||||
.from("vb_timesheet")
|
||||
.update({
|
||||
@@ -755,8 +755,6 @@
|
||||
showModal = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<div>
|
||||
@@ -778,7 +776,9 @@
|
||||
placeholder="🔍 Search by work description or name..."
|
||||
class="border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:outline-none px-4 py-2 rounded-xl text-sm w-64 transition"
|
||||
on:input={(e) => {
|
||||
currentSearchTerm = (e.target as HTMLInputElement).value.toLowerCase();
|
||||
currentSearchTerm = (
|
||||
e.target as HTMLInputElement
|
||||
).value.toLowerCase();
|
||||
fetchTimeSheets(currentVillaFilter, currentSearchTerm);
|
||||
}}
|
||||
/>
|
||||
@@ -786,7 +786,8 @@
|
||||
id="villa-select"
|
||||
class="border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:outline-none px-4 py-2 rounded-xl text-sm w-48 transition"
|
||||
on:change={(e) => {
|
||||
currentVillaFilter = (e.target as HTMLSelectElement).value || null;
|
||||
currentVillaFilter =
|
||||
(e.target as HTMLSelectElement).value || null;
|
||||
fetchTimeSheets(currentVillaFilter, currentSearchTerm);
|
||||
}}
|
||||
>
|
||||
@@ -797,17 +798,21 @@
|
||||
</select>
|
||||
<button
|
||||
class="bg-gray-200 text-gray-700 px-4 py-2 rounded-xl hover:bg-gray-300 text-sm transition"
|
||||
on:click={() =>{
|
||||
on:click={() => {
|
||||
currentVillaFilter = null;
|
||||
currentSearchTerm = null;
|
||||
|
||||
|
||||
// Optional: reset UI elements if you use bind:value
|
||||
const searchInput = document.querySelector('#search-input') as HTMLInputElement;
|
||||
const searchInput = document.querySelector(
|
||||
"#search-input",
|
||||
) as HTMLInputElement;
|
||||
if (searchInput) searchInput.value = "";
|
||||
|
||||
const villaSelect = document.querySelector('#villa-select') as HTMLSelectElement;
|
||||
|
||||
const villaSelect = document.querySelector(
|
||||
"#villa-select",
|
||||
) as HTMLSelectElement;
|
||||
if (villaSelect) villaSelect.value = "";
|
||||
|
||||
|
||||
fetchTimeSheets(null, null);
|
||||
}}
|
||||
>
|
||||
@@ -831,22 +836,21 @@
|
||||
class="sticky left-0 px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||||
style="background-color: #f0f8ff; z-index: 10;"
|
||||
on:click={() => toggleSort(col.key)}
|
||||
|
||||
>
|
||||
{col.title}
|
||||
{#if sortColumn === col.key}
|
||||
{sortOrder === 'asc' ? ' 🔼' : ' 🔽'}
|
||||
{sortOrder === "asc" ? " 🔼" : " 🔽"}
|
||||
{/if}
|
||||
</th>
|
||||
{:else}
|
||||
<th
|
||||
class="cursor-pointer px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap" on:click={() => toggleSort(col.key)}
|
||||
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}
|
||||
{#if sortColumn === col.key}
|
||||
{sortOrder === "asc" ? " 🔼" : " 🔽"}
|
||||
{/if}
|
||||
</th>
|
||||
{/if}
|
||||
{/each}
|
||||
@@ -902,8 +906,20 @@
|
||||
{:else if col.key === "approved_date"}
|
||||
<td class="px-4 py-2">
|
||||
{row[col.key] &&
|
||||
!isNaN(new Date(row[col.key] as string | number | Date).getTime())
|
||||
? new Date(row[col.key] as string | number | Date).toLocaleString()
|
||||
!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"}
|
||||
@@ -913,12 +929,19 @@
|
||||
{:else if col.key === "created_at"}
|
||||
<td class="px-4 py-2">
|
||||
{row[col.key] !== undefined
|
||||
? new Date(row[col.key] as string | number | Date).toLocaleString()
|
||||
? new Date(
|
||||
row[col.key] as
|
||||
| string
|
||||
| number
|
||||
| Date,
|
||||
).toLocaleString()
|
||||
: "N/A"}
|
||||
</td>
|
||||
{:else if col.key === "villa_name"}
|
||||
<td class="sticky left-0 px-4 py-2 font-medium text-blue-600 max-w-xs whitespace-normal align-top break-words"
|
||||
style="background-color: #f0f8ff; cursor: pointer;">
|
||||
<td
|
||||
class="sticky left-0 px-4 py-2 font-medium text-blue-600 max-w-xs whitespace-normal align-top break-words"
|
||||
style="background-color: #f0f8ff; cursor: pointer;"
|
||||
>
|
||||
{row[col.key] || "Unknown Villa"}
|
||||
</td>
|
||||
{:else if col.key === "staff_id"}
|
||||
@@ -948,7 +971,9 @@
|
||||
{: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()
|
||||
? new Date(
|
||||
row[col.key],
|
||||
).toLocaleString()
|
||||
: "N/A"}
|
||||
</td>
|
||||
{:else}
|
||||
@@ -975,35 +1000,35 @@
|
||||
<!-- Month Selector -->
|
||||
<label for="month" class="text-sm">Month:</label>
|
||||
<select
|
||||
id="month"
|
||||
class="border border-gray-300 px-2 py-1 rounded text-sm"
|
||||
bind:value={selectedMonth}
|
||||
id="month"
|
||||
class="border border-gray-300 px-2 py-1 rounded text-sm"
|
||||
bind:value={selectedMonth}
|
||||
>
|
||||
<option value="" disabled selected>Select</option>
|
||||
{#each Array.from({ length: 12 }, (_, i) => i + 1) as m}
|
||||
<option value={m}>{m}</option>
|
||||
{/each}
|
||||
<option value="" disabled selected>Select</option>
|
||||
{#each Array.from({ length: 12 }, (_, i) => i + 1) as m}
|
||||
<option value={m}>{m}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<!-- Year Selector -->
|
||||
<label for="year" class="text-sm ml-2">Year:</label>
|
||||
<select
|
||||
id="year"
|
||||
class="border border-gray-300 px-2 py-1 rounded text-sm"
|
||||
bind:value={selectedYear}
|
||||
id="year"
|
||||
class="border border-gray-300 px-2 py-1 rounded text-sm"
|
||||
bind:value={selectedYear}
|
||||
>
|
||||
<option value="" disabled selected>Select</option>
|
||||
{#each Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i) as y}
|
||||
<option value={y}>{y}</option>
|
||||
{/each}
|
||||
<option value="" disabled selected>Select</option>
|
||||
{#each Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i) as y}
|
||||
<option value={y}>{y}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<!-- Export Button -->
|
||||
<button
|
||||
class="bg-green-600 text-white px-3 py-1 rounded hover:bg-green-700 text-sm"
|
||||
on:click={exportToExcelFromFrontend}
|
||||
class="bg-green-600 text-white px-3 py-1 rounded hover:bg-green-700 text-sm"
|
||||
on:click={exportToExcelFromFrontend}
|
||||
>
|
||||
⬇️ Export
|
||||
⬇️ Export
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
@@ -1041,7 +1066,8 @@
|
||||
{:else}
|
||||
<button
|
||||
on:click={() => changePage(page as number)}
|
||||
class="px-2 py-1 border rounded {page === currentPage
|
||||
class="px-2 py-1 border rounded {page ===
|
||||
currentPage
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'bg-white border-gray-300 hover:bg-gray-100'}"
|
||||
>
|
||||
@@ -1058,7 +1084,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
|
||||
@@ -227,14 +227,14 @@
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="text-center text-sm text-gray-500">
|
||||
<!-- <p class="text-center text-sm text-gray-500">
|
||||
Don’t have an account?
|
||||
<a
|
||||
href="/register"
|
||||
class="text-blue-600 hover:underline font-bold ml-1"
|
||||
>Sign Up Now</a
|
||||
>
|
||||
</p>
|
||||
</p> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user