rev timesheet & project
This commit is contained in:
@@ -2,4 +2,9 @@
|
|||||||
table td {
|
table td {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
/* word-break: break-all; */
|
||||||
|
white-space: pre-line;
|
||||||
|
max-width: 20rem; /* adjust as needed */
|
||||||
}
|
}
|
||||||
65
src/lib/Pagination.svelte
Normal file
65
src/lib/Pagination.svelte
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let totalPages: number = 1;
|
||||||
|
export let currentPage: number = 1;
|
||||||
|
export let goToPage: (page: number) => void;
|
||||||
|
|
||||||
|
// 🗝️ Smart page window logic with ellipsis
|
||||||
|
$: pageWindow = [];
|
||||||
|
|
||||||
|
if (totalPages <= 5) {
|
||||||
|
// For small sets, just show all pages
|
||||||
|
pageWindow = Array(totalPages).fill(0).map((_, i) => i + 1);
|
||||||
|
} else {
|
||||||
|
pageWindow = [1];
|
||||||
|
|
||||||
|
if (currentPage > 3) pageWindow.push("...");
|
||||||
|
|
||||||
|
const start = Math.max(2, currentPage - 1);
|
||||||
|
const end = Math.min(totalPages - 1, currentPage + 1);
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
pageWindow.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPage < totalPages - 2) pageWindow.push("...");
|
||||||
|
|
||||||
|
pageWindow.push(totalPages);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex space-x-1 items-center text-sm">
|
||||||
|
<!-- Previous Button -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Page Numbers + Ellipsis -->
|
||||||
|
{#each pageWindow as page, i}
|
||||||
|
{#if page === "..."}
|
||||||
|
<span key={i}>...</span>
|
||||||
|
{:else}
|
||||||
|
<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>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<!-- Next Button -->
|
||||||
|
<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>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { supabase } from "$lib/supabaseClient";
|
import { supabase } from "$lib/supabaseClient";
|
||||||
|
import Pagination from "$lib/Pagination.svelte";
|
||||||
|
|
||||||
type Project = {
|
type Project = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -55,13 +56,15 @@
|
|||||||
{ key: "priority", title: "Priority" },
|
{ key: "priority", title: "Priority" },
|
||||||
{ key: "add_to_po", title: "Add to PO" },
|
{ key: "add_to_po", title: "Add to PO" },
|
||||||
{ key: "picture_link", title: "Picture Link" },
|
{ key: "picture_link", title: "Picture Link" },
|
||||||
{ key: "need_approval", title: "Need Approval" },
|
|
||||||
{ key: "area_of_villa", title: "Area of Villa" },
|
{ key: "area_of_villa", title: "Area of Villa" },
|
||||||
|
{ key: "project_status", title: "Status" },
|
||||||
|
{ key: "project_comment", title: "Comment" },
|
||||||
{ key: "input_name", title: "Input By" },
|
{ key: "input_name", title: "Input By" },
|
||||||
{ key: "issue_number", title: "Issue Number" },
|
{ key: "issue_number", title: "Issue Number" },
|
||||||
{ key: "villa_name", title: "Villa Name" },
|
{ key: "villa_name", title: "Villa Name" },
|
||||||
{ key: "report_date", title: "Report Date" },
|
{ key: "report_date", title: "Report Date" },
|
||||||
{ key: "project_due_date", title: "Project Due Date" },
|
{ key: "project_due_date", title: "Project Due Date" },
|
||||||
|
{ key: "need_approval", title: "Need Approval" },
|
||||||
{ key: "updated_at", title: "Updated At" },
|
{ key: "updated_at", title: "Updated At" },
|
||||||
{ key: "updated_name", title: "Updated By" },
|
{ key: "updated_name", title: "Updated By" },
|
||||||
{ key: "actions", title: "Actions" },
|
{ key: "actions", title: "Actions" },
|
||||||
@@ -89,23 +92,50 @@
|
|||||||
received: null,
|
received: null,
|
||||||
po_remark: ""
|
po_remark: ""
|
||||||
};
|
};
|
||||||
function openEditModal(row) {
|
let showProjectEditModal = false;
|
||||||
selectedPO = row;
|
let projectEditForm = {
|
||||||
|
project_number: "",
|
||||||
editForm = {
|
issue_id: "",
|
||||||
po_number: row.purchase_order_number || "",
|
project_name: "",
|
||||||
issue_name: row.issue_name || "",
|
project_status: "On Progress",
|
||||||
villa_id: row.villa_id || "",
|
project_comment: "",
|
||||||
po_status: row.po_status || "",
|
picture_link: "",
|
||||||
prepared: row.prepared,
|
assigned_to: "",
|
||||||
approved: row.approved,
|
updated_at: "",
|
||||||
acknowledged: row.acknowledged,
|
updated_by: ""
|
||||||
completed: row.completed,
|
|
||||||
received: row.received,
|
|
||||||
po_remark: row.po_remark || ""
|
|
||||||
};
|
};
|
||||||
|
|
||||||
showEditModal = true;
|
function openProjectEditModal(row) {
|
||||||
|
projectEditForm = {
|
||||||
|
project_number: row.project_number || "",
|
||||||
|
issue_id: row.issue_id || "",
|
||||||
|
project_name: row.project_name || "",
|
||||||
|
project_status: row.project_status || "On Progress",
|
||||||
|
project_comment: row.project_comment || "",
|
||||||
|
picture_link: row.picture_link || "",
|
||||||
|
assigned_to: row.assigned_to || "",
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
updated_by: row.updated_by || ""
|
||||||
|
};
|
||||||
|
showProjectEditModal = true;
|
||||||
|
}
|
||||||
|
function openEditModal(row) {
|
||||||
|
selectedPO = row;
|
||||||
|
|
||||||
|
editForm = {
|
||||||
|
po_number: row.purchase_order_number || "",
|
||||||
|
issue_name: row.issue_name || "",
|
||||||
|
villa_id: row.villa_id || "",
|
||||||
|
po_status: row.po_status || "",
|
||||||
|
prepared: row.prepared,
|
||||||
|
approved: row.approved,
|
||||||
|
acknowledged: row.acknowledged,
|
||||||
|
completed: row.completed,
|
||||||
|
received: row.received,
|
||||||
|
po_remark: row.po_remark || ""
|
||||||
|
};
|
||||||
|
|
||||||
|
showEditModal = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -563,6 +593,45 @@
|
|||||||
imagePreviewUrl = null;
|
imagePreviewUrl = null;
|
||||||
selectedFile = null;
|
selectedFile = null;
|
||||||
}
|
}
|
||||||
|
async function saveProjectEdit() {
|
||||||
|
const session = await supabase.auth.getSession();
|
||||||
|
const user = session?.data?.session?.user;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
console.error("User not authenticated");
|
||||||
|
alert("You must be logged in to edit this project.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format date
|
||||||
|
const today = new Date();
|
||||||
|
const formattedDate = `${today.getMonth() + 1}/${today.getDate()}/${today.getFullYear()}`;
|
||||||
|
|
||||||
|
const { error } = await supabase
|
||||||
|
.from("vb_projects")
|
||||||
|
.update({
|
||||||
|
project_name: projectEditForm.project_name,
|
||||||
|
project_status: projectEditForm.project_status,
|
||||||
|
project_comment: projectEditForm.project_comment,
|
||||||
|
picture_link: projectEditForm.picture_link,
|
||||||
|
assigned_to: projectEditForm.assigned_to,
|
||||||
|
updated_at: formattedDate,
|
||||||
|
updated_by: user.id // ✅ real user info
|
||||||
|
})
|
||||||
|
.eq("project_number", projectEditForm.project_number);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Error saving project:", error);
|
||||||
|
alert("Failed to save changes.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showProjectEditModal = false;
|
||||||
|
await fetchProjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function deleteProject(id: string) {
|
async function deleteProject(id: string) {
|
||||||
const confirmed = confirm("Are you sure you want to delete this project? This action cannot be undone.");
|
const confirmed = confirm("Are you sure you want to delete this project? This action cannot be undone.");
|
||||||
@@ -714,24 +783,23 @@
|
|||||||
</td>
|
</td>
|
||||||
{:else if col.key === "actions"}
|
{:else if col.key === "actions"}
|
||||||
<td class="px-4 py-2">
|
<td class="px-4 py-2">
|
||||||
<td class="px-4 py-2">
|
<button
|
||||||
<button
|
class="inline-flex items-center gap-1 rounded px-3 py-1.5 text-xs font-medium
|
||||||
class="inline-flex items-center gap-1 rounded px-3 py-1.5 text-xs font-medium
|
{row.purchase_order_exists ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-blue-600 text-white hover:bg-blue-700'}"
|
||||||
{row.purchase_order_exists ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-blue-600 text-white hover:bg-blue-700'}"
|
on:click={() => openProjectEditModal(row)}
|
||||||
on:click={() => openModal(row)}
|
disabled={row.purchase_order_exists}
|
||||||
disabled={row.purchase_order_exists}
|
>
|
||||||
>
|
✏️ Edit
|
||||||
✏️ Edit
|
</button>
|
||||||
</button>
|
<button
|
||||||
<button
|
class="inline-flex items-center gap-1 rounded px-3 py-1.5 text-xs font-medium
|
||||||
class="inline-flex items-center gap-1 rounded px-3 py-1.5 text-xs font-medium
|
{row.purchase_order_exists ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-red-600 text-white hover:bg-red-700'}"
|
||||||
{row.purchase_order_exists ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-red-600 text-white hover:bg-red-700'}"
|
on:click={() => deleteProject(row.id)}
|
||||||
on:click={() => deleteProject(row.id)}
|
disabled={row.purchase_order_exists}
|
||||||
disabled={row.purchase_order_exists}
|
>
|
||||||
>
|
🗑️ Delete
|
||||||
🗑️ Delete
|
</button>
|
||||||
</button>
|
</td>
|
||||||
</td>
|
|
||||||
{:else if col.key === "add_to_po"}
|
{:else if col.key === "add_to_po"}
|
||||||
<td class="px-4 py-2 text-center">
|
<td class="px-4 py-2 text-center">
|
||||||
{#if row.purchase_order_exists}
|
{#if row.purchase_order_exists}
|
||||||
@@ -812,7 +880,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
{:else}
|
{:else}
|
||||||
<td class="px-4 py-2 text-gray-700"
|
<td class="px-4 py-2 text-gray-700 max-w-xs whitespace-normal align-top break-words left-0 "
|
||||||
>{row[col.key as keyof Projects]}</td
|
>{row[col.key as keyof Projects]}</td
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -826,38 +894,10 @@
|
|||||||
<!-- 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}–
|
Showing {(currentPage - 1) * rowsPerPage + 1}–{Math.min(currentPage * rowsPerPage, allRows.length)}
|
||||||
{Math.min(currentPage * rowsPerPage, allRows.length)} of {allRows.length}
|
of {allRows.length}
|
||||||
</div>
|
</div>
|
||||||
<div class="space-x-2">
|
<Pagination {totalPages} {currentPage} {goToPage} />
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1087,4 +1127,67 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if showProjectEditModal}
|
||||||
|
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 overflow-y-auto">
|
||||||
|
<div class="min-h-screen flex items-center justify-center p-4">
|
||||||
|
<div class="bg-white rounded-lg shadow-lg w-full max-w-xl max-h-[90vh] overflow-y-auto p-6 space-y-6">
|
||||||
|
|
||||||
|
<h2 class="text-2xl font-semibold">Edit Project</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<!-- Left -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Project Number</label>
|
||||||
|
<input type="text" value={projectEditForm.project_number} disabled class="w-full border p-2 bg-gray-100 rounded" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Issue ID</label>
|
||||||
|
<input type="text" value={projectEditForm.issue_id} disabled class="w-full border p-2 bg-gray-100 rounded" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Project Name</label>
|
||||||
|
<input type="text" bind:value={projectEditForm.project_name} class="w-full border p-2 rounded" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Project Status</label>
|
||||||
|
<select bind:value={projectEditForm.project_status} class="w-full border p-2 rounded">
|
||||||
|
<option value="On Progress">On Progress</option>
|
||||||
|
<option value="Done">Done</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Picture Link</label>
|
||||||
|
<input type="text" bind:value={projectEditForm.picture_link} class="w-full border p-2 rounded" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Assigned To</label>
|
||||||
|
<input type="text" bind:value={projectEditForm.assigned_to} class="w-full border p-2 rounded" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Project Comment</label>
|
||||||
|
<textarea bind:value={projectEditForm.project_comment} rows="4" class="w-full border p-2 rounded"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end space-x-2 pt-4">
|
||||||
|
<button on:click={saveProjectEdit} class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">Save</button>
|
||||||
|
<button on:click={() => showProjectEditModal = false} class="px-4 py-2 rounded border">Cancel</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { supabase } from "$lib/supabaseClient";
|
import { supabase } from "$lib/supabaseClient";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
|
import Pagination from "$lib/Pagination.svelte";
|
||||||
|
|
||||||
type Timesheets = {
|
type Timesheets = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -27,6 +27,9 @@
|
|||||||
approved_date?: Date;
|
approved_date?: Date;
|
||||||
created_at?: Date;
|
created_at?: Date;
|
||||||
};
|
};
|
||||||
|
type TimesheetsJoined = Timesheets & {
|
||||||
|
vb_employee: { id: string; employee_name: string } | null;
|
||||||
|
};
|
||||||
type TimesheetDisplay = {
|
type TimesheetDisplay = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -82,7 +85,6 @@
|
|||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const categoryOfWork = [
|
const categoryOfWork = [
|
||||||
{ label: "Cleaning", value: "Cleaning" },
|
{ label: "Cleaning", value: "Cleaning" },
|
||||||
{ label: "Gardening/Pool", value: "Gardening/Pool" },
|
{ label: "Gardening/Pool", value: "Gardening/Pool" },
|
||||||
@@ -129,12 +131,13 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
// reactive variables
|
// reactive variables
|
||||||
|
let sortColumn: string | null = "created_at";
|
||||||
|
let sortOrder: "asc" | "desc" = "desc";
|
||||||
let currentUserId: string | null = null;
|
let currentUserId: string | null = null;
|
||||||
let currentVillaFilter: string | null = null;
|
let currentVillaFilter: string | null = null;
|
||||||
let currentSearchTerm: string | null = null;
|
let currentSearchTerm: string | null = null;
|
||||||
let dataVilla: Villa[] = [];
|
let dataVilla: Villa[] = [];
|
||||||
let allRows: TimesheetDisplay[] = [];
|
let allRows: TimesheetDisplay[] = [];
|
||||||
let currentPage = 1;
|
|
||||||
let rowsPerPage = 20;
|
let rowsPerPage = 20;
|
||||||
$: totalPages = Math.ceil(allRows.length / rowsPerPage);
|
$: totalPages = Math.ceil(allRows.length / rowsPerPage);
|
||||||
$: paginatedRows = allRows.slice(
|
$: paginatedRows = allRows.slice(
|
||||||
@@ -199,6 +202,16 @@
|
|||||||
|
|
||||||
fetchTimeSheets();
|
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 to calculate total work hours
|
||||||
function calculateTotalHours() {
|
function calculateTotalHours() {
|
||||||
@@ -259,57 +272,48 @@
|
|||||||
|
|
||||||
showModal = true;
|
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
|
// Function to fetch timesheets with optional filters and sorting
|
||||||
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 = 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
|
let query = supabase
|
||||||
.from("vb_timesheet")
|
.from("vb_timesheet")
|
||||||
.select("*", { count: "exact" })
|
.select(`*`)
|
||||||
.order(sortColumn || "created_at", {
|
.order(sortColumn || "created_at", {
|
||||||
ascending: sortOrder === "asc",
|
ascending: sortOrder === "asc",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (villaIdFilter) {
|
if (villaIdFilter) {
|
||||||
const { data: villaMatch } = await supabase
|
const { data: villaMatch } = await supabase
|
||||||
.from("vb_villas")
|
.from("vb_villas")
|
||||||
.select("id")
|
.select("id")
|
||||||
.eq("villa_name", villaIdFilter);
|
.eq("villa_name", villaIdFilter);
|
||||||
|
|
||||||
const matchedId = villaMatch?.[0]?.id;
|
const matchedId = villaMatch?.[0]?.id;
|
||||||
if (matchedId) {
|
if (matchedId) {
|
||||||
query = query.eq("villa_id", matchedId);
|
query = query.eq("villa_id", matchedId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (searchTerm) {
|
|
||||||
query = query.ilike("work_description", `%${searchTerm}%`);
|
|
||||||
}
|
|
||||||
if (offset) {
|
if (offset) {
|
||||||
query = query.range(offset, offset + limit - 1);
|
query = query.range(offset, offset + limit - 1);
|
||||||
}
|
}
|
||||||
@@ -322,80 +326,77 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const villaIds = [
|
const loweredSearch = searchTerm?.toLowerCase();
|
||||||
...new Set(timesheet.map((i: Timesheets) => i.villa_id)),
|
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")
|
.from("vb_villas")
|
||||||
.select("*")
|
.select("*")
|
||||||
.in("id", villaIds);
|
.in("id", villaIds);
|
||||||
|
|
||||||
if (villaError) {
|
if (villaError) {
|
||||||
console.error("Error fetching villas:", villaError);
|
console.error("Error fetching villas:", villaError);
|
||||||
return;
|
} else {
|
||||||
|
villas = villasData;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: approvers, error: approverError } = await supabase
|
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");
|
.select("id, full_name");
|
||||||
|
|
||||||
if (approverError) {
|
if (approverError) {
|
||||||
console.error("Error fetching approvers:", approverError);
|
console.error("Error fetching approvers:", approverError);
|
||||||
}
|
}
|
||||||
|
allRows = filteredTimesheet.map((tsdata: TimesheetsJoined) => {
|
||||||
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) => {
|
|
||||||
const villa = villas.find((v) => v.id === tsdata.villa_id);
|
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);
|
const approver = approvers?.find((u) => u.id === tsdata.approved_by);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: tsdata.id,
|
id: tsdata.id,
|
||||||
name: tsdata.work_description, // Map work_description to name
|
name: tsdata.work_description,
|
||||||
staff_id: staff?.label, // Map entered_by to staff_id
|
staff_id:
|
||||||
date_in: new Date(tsdata.datetime_in),
|
reportedBy.find((s) => s.value === tsdata.entered_by)?.label || "Unknown",
|
||||||
date_out: new Date(tsdata.datetime_out),
|
date_in: new Date(tsdata.datetime_in),
|
||||||
type_of_work: tsdata.type_of_work,
|
date_out: new Date(tsdata.datetime_out),
|
||||||
category_of_work: tsdata.category_of_work,
|
type_of_work: tsdata.type_of_work,
|
||||||
villa_name: villa ? villa.villa_name : "Unknown Villa",
|
category_of_work: tsdata.category_of_work,
|
||||||
approval:
|
villa_name: villa ? villa.villa_name : "Unknown Villa",
|
||||||
tsdata.approval == null
|
approval:
|
||||||
? "PENDING"
|
tsdata.approval == null
|
||||||
: tsdata.approval
|
? "PENDING"
|
||||||
? "APPROVED"
|
: tsdata.approval
|
||||||
: "REJECTED", // or map as needed
|
? "APPROVED"
|
||||||
total_hours_work:
|
: "REJECTED",
|
||||||
Math.abs(
|
total_hours_work:
|
||||||
new Date(tsdata.datetime_out).getTime() -
|
Math.abs(
|
||||||
new Date(tsdata.datetime_in).getTime(),
|
new Date(tsdata.datetime_out).getTime() - new Date(tsdata.datetime_in).getTime()
|
||||||
) /
|
) / (1000 * 60 * 60),
|
||||||
(1000 * 60 * 60), // Convert milliseconds to hours
|
approved_by: approver?.full_name ?? "Not Approved",
|
||||||
approved_by: approver?.full_name ?? "Not Approved",
|
approved_date: tsdata.approved_date,
|
||||||
approved_date: tsdata.approved_date,
|
remarks: tsdata.remarks,
|
||||||
remarks: tsdata.remarks,
|
created_at: tsdata.created_at ? new Date(tsdata.created_at) : undefined,
|
||||||
created_at: tsdata.created_at
|
|
||||||
? new Date(tsdata.created_at)
|
|
||||||
: undefined,
|
|
||||||
} as TimesheetDisplay;
|
} as TimesheetDisplay;
|
||||||
});
|
});
|
||||||
// Sort the rows based on the sortColumn and sortOrder
|
currentPage = 1;
|
||||||
|
|
||||||
|
console.log("Fetched rows:", allRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Function to delete a timesheet
|
// Function to delete a timesheet
|
||||||
async function deleteTimesheet(id: string) {
|
async function deleteTimesheet(id: string) {
|
||||||
if (confirm("Are you sure you want to delete this Timesheet?")) {
|
if (confirm("Are you sure you want to delete this Timesheet?")) {
|
||||||
@@ -491,7 +492,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export let formErrors = writable<{ [key: string]: string }>({});
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -571,9 +571,13 @@
|
|||||||
</th>
|
</th>
|
||||||
{:else}
|
{:else}
|
||||||
<th
|
<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}
|
{col.title}
|
||||||
|
{#if sortColumn === col.key}
|
||||||
|
{sortOrder === 'asc' ? ' 🔼' : ' 🔽'}
|
||||||
|
{/if}
|
||||||
</th>
|
</th>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
@@ -634,7 +638,15 @@
|
|||||||
row[
|
row[
|
||||||
col.key as keyof TimesheetDisplay
|
col.key as keyof TimesheetDisplay
|
||||||
] as string | number | Date,
|
] 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"}
|
: "N/A"}
|
||||||
</td>
|
</td>
|
||||||
{:else if col.key === "total_hours_work"}
|
{:else if col.key === "total_hours_work"}
|
||||||
@@ -646,7 +658,18 @@
|
|||||||
{row[col.key] !== undefined
|
{row[col.key] !== undefined
|
||||||
? new Date(
|
? new Date(
|
||||||
row[col.key]!,
|
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"}
|
: "N/A"}
|
||||||
</td>
|
</td>
|
||||||
{:else if col.key === "villa_name"}
|
{:else if col.key === "villa_name"}
|
||||||
@@ -684,12 +707,13 @@
|
|||||||
? new Date(row[col.key]).toLocaleString(
|
? new Date(row[col.key]).toLocaleString(
|
||||||
"en-GB",
|
"en-GB",
|
||||||
{
|
{
|
||||||
day: "2-digit",
|
timeZone: "Asia/Singapore",
|
||||||
month: "2-digit",
|
day: "2-digit",
|
||||||
year: "numeric",
|
month: "2-digit",
|
||||||
hour: "2-digit",
|
year: "numeric",
|
||||||
minute: "2-digit",
|
hour: "2-digit",
|
||||||
hour12: false,
|
minute: "2-digit",
|
||||||
|
hour12: false,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: "N/A"}
|
: "N/A"}
|
||||||
@@ -709,39 +733,11 @@
|
|||||||
<!-- 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}–
|
Showing {(currentPage - 1) * rowsPerPage + 1}–{Math.min(currentPage * rowsPerPage, allRows.length)}
|
||||||
{Math.min(currentPage * rowsPerPage, allRows.length)} of {allRows.length}
|
of {allRows.length}
|
||||||
</div>
|
</div>
|
||||||
<div class="space-x-2">
|
<Pagination {totalPages} {currentPage} {goToPage} />
|
||||||
<button
|
</div>
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal -->
|
<!-- Modal -->
|
||||||
|
|||||||
Reference in New Issue
Block a user