perbaikan timesheet

This commit is contained in:
aji@catalis.app
2025-06-09 16:17:12 +07:00
parent b98814c4d5
commit 1b01db42f8
2 changed files with 381 additions and 402 deletions

View File

@@ -4,57 +4,69 @@
import { writable } from "svelte/store"; import { writable } from "svelte/store";
type Timesheets = { type Timesheets = {
id: string; id: number;
name: string; entered_by: string;
staff_id: string; work_description: string;
report_by: string; type_of_work: "Running" | "Periodic" | "Irregular";
date_in: Date; category_of_work:
date_out: Date; | "Cleaning"
type_of_work: string; | "Gardening/Pool"
category_of_work: string; | "Maintenance"
approval: string; | "Supervision"
| "Guest Service"
| "Administration"
| "Non Billable";
villa_id: string; villa_id: string;
villa_name: string; datetime_in: string;
approved_by: string; datetime_out: string;
approved_date: Date; total_work_hour: number;
total_hours_work: number;
remarks: string; remarks: string;
vacant: boolean; approval: boolean;
created_at?: Date; created_at?: Date;
}; };
type TimesheetDisplay = { type TimesheetDisplay = {
id: string; id: number;
name: string;
report_by: string;
date_in: Date;
date_out: Date;
type_of_work: string;
category_of_work: string;
approval: string;
villa_name: string;
approved_by: string;
approved_date: Date;
total_hours_work: number;
remarks: string;
vacant: boolean;
created_at?: Date;
};
type TimesheetsInsert = {
name: string; name: string;
staff_id: string; staff_id: string;
date_in: Date; date_in: Date;
date_out: Date; date_out: Date;
type_of_work: string; type_of_work: "Running" | "Periodic" | "Irregular";
category_of_work: string; category_of_work:
| "Cleaning"
| "Gardening/Pool"
| "Maintenance"
| "Supervision"
| "Guest Service"
| "Administration"
| "Non Billable";
villa_name?: string;
approval: string; approval: string;
villa_id: string; approved_by?: string;
approved_by: string; approved_date?: Date;
approved_date: Date;
total_hours_work: number; total_hours_work: number;
remarks: string; remarks: string;
vacant: boolean; created_at?: Date;
};
type TimesheetsInsert = {
entered_by: string;
work_description: string;
type_of_work: "Running" | "Periodic" | "Irregular";
category_of_work:
| "Cleaning"
| "Gardening/Pool"
| "Maintenance"
| "Supervision"
| "Guest Service"
| "Administration"
| "Non Billable";
villa_id: string;
datetime_in: string;
datetime_out: string;
total_work_hour: number;
remarks: string;
approval: boolean;
}; };
const categoryOfWork = [ const categoryOfWork = [
@@ -97,7 +109,7 @@
} }
}); });
let allRows: Timesheets[] = []; let allRows: TimesheetDisplay[] = [];
type columns = { type columns = {
key: string; key: string;
@@ -106,7 +118,7 @@
const columns: columns[] = [ const columns: columns[] = [
{ key: "name", title: "Name" }, { key: "name", title: "Name" },
{ key: "report_by", title: "Staff Report" }, { key: "staff_id", title: "Staff Report" },
{ key: "date_in", title: "Date In" }, { key: "date_in", title: "Date In" },
{ key: "date_out", title: "Date Out" }, { key: "date_out", title: "Date Out" },
{ key: "type_of_work", title: "Type of Work" }, { key: "type_of_work", title: "Type of Work" },
@@ -131,7 +143,7 @@
limit: number = 10, limit: number = 10,
) { ) {
let query = supabase let query = supabase
.from("vb_timesheets") .from("vb_timesheet")
.select("*", { count: "exact" }) .select("*", { count: "exact" })
.order(sortColumn || "created_at", { .order(sortColumn || "created_at", {
ascending: sortOrder === "asc", ascending: sortOrder === "asc",
@@ -140,7 +152,7 @@
query = query.eq("category_of_work", filter); query = query.eq("category_of_work", filter);
} }
if (searchTerm) { if (searchTerm) {
query = query.ilike("name", `%${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);
@@ -153,6 +165,7 @@
console.error("Error fetching timesheets:", error); console.error("Error fetching timesheets:", error);
return; return;
} }
// Ambil semua villa_id unik dari issues // Ambil semua villa_id unik dari issues
const villaIds = [ const villaIds = [
...new Set(timesheet.map((i: Timesheets) => i.villa_id)), ...new Set(timesheet.map((i: Timesheets) => i.villa_id)),
@@ -169,13 +182,34 @@
} }
// Gabungkan data villa ke dalam setiap issue // Gabungkan data villa ke dalam setiap issue
allRows = timesheet.map((timesheet: Timesheets) => ({ allRows = timesheet.map((issue: Timesheets) => {
...timesheet, const villa = villas.find((v) => v.id === issue.villa_id);
villa_name:
villas.find((v) => v.id === timesheet.villa_id).name || null, return {
approval: timesheet.approval || "", id: issue.id,
report_by: timesheet.staff_id || "Unknown", name: issue.work_description, // Map work_description to name
})); staff_id: issue.entered_by, // Map entered_by to staff_id
date_in: new Date(issue.datetime_in),
date_out: new Date(issue.datetime_out),
type_of_work: issue.type_of_work,
category_of_work: issue.category_of_work,
villa_name: villa ? villa.name : "Unknown Villa",
approval: issue.approval ? "APPROVED" : "PENDING", // or map as needed
approved_by: undefined, // Set as needed
approved_date: undefined, // Set as needed
total_hours_work:
Math.abs(
new Date(issue.datetime_out).getTime() -
new Date(issue.datetime_in).getTime(),
) /
(1000 * 60 * 60), // Convert milliseconds to hours
remarks: issue.remarks,
created_at: issue.created_at
? new Date(issue.created_at)
: undefined,
} as TimesheetDisplay;
});
// Sort the rows based on the sortColumn and sortOrder
} }
let currentPage = 1; let currentPage = 1;
let rowsPerPage = 5; let rowsPerPage = 5;
@@ -200,16 +234,7 @@
let isEditing = false; let isEditing = false;
let currentEditingId: string | null = null; let currentEditingId: string | null = null;
let newIssue: Record<string, any> = {}; let newIssue: Record<string, any> = {};
const excludedKeys = [ const excludedKeys = ["id"];
"id",
"created_at",
"approval",
"approved_by",
"approved_date",
"villa_name",
"report_by",
"actions",
];
const formColumns = columns.filter( const formColumns = columns.filter(
(col) => !excludedKeys.includes(col.key), (col) => !excludedKeys.includes(col.key),
); );
@@ -251,33 +276,33 @@
} }
} else { } else {
const TimesheetsInsert: TimesheetsInsert = { const TimesheetsInsert: TimesheetsInsert = {
name: formData.get("name") as string, entered_by: formData.get("entered_by") as string,
staff_id: formData.get("staff_id") as string, work_description: formData.get("work_description") as string,
date_in: new Date(formData.get("date_in") as string), type_of_work: formData.get("type_of_work") as
date_out: new Date(formData.get("date_out") as string), | "Running"
type_of_work: formData.get("type_of_work") as string, | "Periodic"
category_of_work: formData.get("category_of_work") as string, | "Irregular",
approval: formData.get("approval") as string, category_of_work: formData.get("category_of_work") as
| "Cleaning"
| "Gardening/Pool"
| "Maintenance"
| "Supervision"
| "Guest Service"
| "Administration"
| "Non Billable",
villa_id: formData.get("villa_id") as string, villa_id: formData.get("villa_id") as string,
approved_by: formData.get("approved_by") as string, datetime_in: formData.get("date_in") as string,
approved_date: new Date( datetime_out: formData.get("date_out") as string,
formData.get("approved_date") as string, total_work_hour: Math.abs(
new Date(formData.get("date_out") as string).getTime() -
new Date(formData.get("date_in") as string).getTime(),
), ),
//calculate total_hours_work
total_hours_work:
Math.abs(
new Date(formData.get("date_out") as string).getTime() -
new Date(
formData.get("date_in") as string,
).getTime(),
) /
(1000 * 60 * 60), // Convert milliseconds to hours
remarks: formData.get("remarks") as string, remarks: formData.get("remarks") as string,
vacant: formData.get("vacant") === "true", approval: false, // Default to false for new entries
}; };
const { error } = await supabase const { error } = await supabase
.from("vb_timesheets") .from("vb_timesheet")
.insert([TimesheetsInsert]); .insert([TimesheetsInsert]);
if (error) { if (error) {
console.error("Error adding issue:", error); console.error("Error adding issue:", error);
@@ -291,7 +316,7 @@
async function deleteTimesheet(id: string) { async function deleteTimesheet(id: string) {
if (confirm("Are you sure you want to delete this issue?")) { if (confirm("Are you sure you want to delete this issue?")) {
const { error } = await supabase const { error } = await supabase
.from("vb_timesheets") .from("vb_timesheet")
.delete() .delete()
.eq("id", id); .eq("id", id);
if (error) { if (error) {
@@ -307,13 +332,13 @@
function validateForm(formData: FormData): boolean { function validateForm(formData: FormData): boolean {
const errors: { [key: string]: string } = {}; const errors: { [key: string]: string } = {};
const requiredFields = [ const requiredFields = [
"name", "work_description",
"type_of_work", "type_of_work",
"villa_id",
"date_out",
"reported_by",
"category_of_work", "category_of_work",
"villa_id",
"date_in", "date_in",
"date_out",
"remarks",
]; ];
requiredFields.forEach((field) => { requiredFields.forEach((field) => {
@@ -335,7 +360,7 @@
status: string, status: string,
): Promise<void> { ): Promise<void> {
const { error } = await supabase const { error } = await supabase
.from("vb_timesheets") .from("vb_timesheet")
.update({ approval: status }) .update({ approval: status })
.eq("id", id); .eq("id", id);
@@ -430,6 +455,65 @@
> >
{row[col.key]} {row[col.key]}
</td> </td>
{:else if col.key === "approval"}
<td class="px-4 py-2">
<span
class="inline-flex items-center gap-1 rounded px-2 py-1 text-xs font-medium
{row[col.key] === 'APPROVED'
? 'bg-green-100 text-green-800'
: 'bg-yellow-100 text-yellow-800'}"
>
{row[col.key]}
</span>
{#if row.approval === "PENDING"}
<button
class="ml-2 inline-flex items-center gap-1 rounded bg-green-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-green-700"
on:click={() =>
updateApprovalStatus(
String(row.id),
"APPROVED",
)}
>
✅ Approve
</button>
{/if}
</td>
{:else if col.key === "approved_by"}
<td class="px-4 py-2">
{row[col.key] || "Not Approved"}
</td>
{:else if col.key === "approved_date"}
<td class="px-4 py-2">
{row[col.key] !== undefined
? new Date(
row[col.key]!,
).toLocaleDateString()
: "N/A"}
</td>
{:else if col.key === "total_hours_work"}
<td class="px-4 py-2">
{row[col.key].toFixed(2)} hours
</td>
{:else if col.key === "created_at"}
<td class="px-4 py-2">
{row[col.key] !== undefined
? new Date(
row[col.key]!,
).toLocaleDateString()
: "N/A"}
</td>
{:else if col.key === "villa_name"}
<td class="px-4 py-2">
{row[col.key] || "Unknown Villa"}
</td>
{:else if col.key === "staff_id"}
<td class="px-4 py-2">
{row[col.key] || "Unknown Staff"}
</td>
{:else if col.key === "remarks"}
<td class="px-4 py-2">
{row[col.key] || "No remarks"}
</td>
{:else if col.key === "actions"} {:else if col.key === "actions"}
<td class="px-4 py-2"> <td class="px-4 py-2">
<button <button
@@ -440,91 +524,15 @@
</button> </button>
<button <button
class="inline-flex items-center gap-1 rounded bg-red-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-red-700" class="inline-flex items-center gap-1 rounded bg-red-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-red-700"
on:click={() => deleteTimesheet(row.id)} on:click={() =>
deleteTimesheet(String(row.id))}
> >
🗑️ Delete 🗑️ Delete
</button> </button>
</td> </td>
{:else if col.key === "approval"}
{#if row[col.key as keyof Timesheets] === "APPROVED"}
<td class="px-4 py-2">
<span
class="text-green-600 font-semibold"
>✅ Approved</span
>
</td>
{:else if row[col.key as keyof Timesheets] === "PENDING"}
<td class="px-4 py-2">
<span
class="text-yellow-600 font-semibold"
>⏳ Pending</span
>
</td>
{:else if row[col.key as keyof Timesheets] === "REJECTED"}
<td class="px-4 py-2">
<span class="text-red-600 font-semibold"
>❌ Rejected</span
>
</td>
{:else}
<!-- dropdown -->
<td class="px-4 py-2">
<select
class="border px-3 py-1 rounded"
bind:value={
row[col.key as keyof Timesheets]
}
on:change={(e) =>
updateApprovalStatus(
row.id,
e.target
? (
e.target as HTMLSelectElement
).value
: "",
)}
>
<option
value=""
disabled
selected
class="bg-gray-100"
>Select Approval Status</option
>
<option
value="PENDING"
class="bg-yellow-100"
>PENDING</option
>
<option
value="APPROVED"
class="bg-green-100"
>APPROVAL</option
>
<option
value="REJECTED"
class="bg-red-100"
>REJECTED</option
>
</select>
</td>
{/if}
{:else if col.key === "vacant"}
<td class="px-4 py-2">
{#if row[col.key as keyof Timesheets]}
<span
class="text-green
font-semibold">✅ Vacant</span
>
{:else}
<span class="text-red-600 font-semibold"
>❌ Not Vacant</span
>
{/if}
</td>
{:else} {:else}
<td class="px-4 py-2"> <td class="px-4 py-2">
{row[col.key as keyof Timesheets]} {row[col.key as keyof TimesheetDisplay]}
</td> </td>
{/if} {/if}
{/each} {/each}
@@ -586,78 +594,80 @@
</h3> </h3>
{#each formColumns as col} {#each formColumns as col}
{#if col.key === "name"} {#if col.key === "name"}
<div class="space-y-1"> <div>
<label class="block text-sm font-medium text-gray-700" <label class="block text-sm font-medium mb-1">
>Work Description</label {col.title}
> </label>
<input <input
class="w-full border px-3 py-2 rounded {errorClass(
'name',
)}"
name="name"
type="text" type="text"
bind:value={newIssue[col.key as keyof Timesheets]} name={col.key}
placeholder={col.title} bind:value={newIssue[col.key]}
class="w-full border p-2 rounded {errorClass(
col.key,
)}"
required required
/> />
{#if $formErrors.name} {#if $formErrors[col.key]}
<p class="text-red-500 text-xs"> <p class="text-red-500 text-xs mt-1">
{$formErrors.name} {$formErrors[col.key]}
</p> </p>
{/if} {/if}
</div> </div>
{:else if col.key === "vacant"} {:else if col.key === "remarks"}
<div class="space-y-1"> <div>
<label class="block text-sm font-medium text-gray-700" <label class="block text-sm font-medium mb-1">
>Vacant</label {col.title}
> </label>
<select <textarea
name="guest_has_aggreed_issue_has_been_resolved" name={col.key}
class="w-full border px-3 py-2 rounded" bind:value={newIssue[col.key]}
bind:value={newIssue[col.key as keyof Timesheets]} class="w-full border p-2 rounded {errorClass(
> col.key,
<option value="true">Yes</option>
<option value="false">No</option>
</select>
</div>
{:else if col.key === "reported_by"}
<div class="space-y-1">
<label class="block text-sm font-medium text-gray-700"
>Reported By</label
>
<select
name="reported_by"
class="w-full border px-3 py-2 rounded {errorClass(
'reported_by',
)}" )}"
bind:value={newIssue[col.key as keyof Timesheets]} required
></textarea>
{#if $formErrors[col.key]}
<p class="text-red-500 text-xs mt-1">
{$formErrors[col.key]}
</p>
{/if}
</div>
{:else if col.key === "type_of_work" || col.key === "category_of_work"}
<div>
<label class="block text-sm font-medium mb-1">
{col.title}
</label>
<select
name={col.key}
bind:value={newIssue[col.key]}
class="w-full border p-2 rounded {errorClass(
col.key,
)}"
required
> >
<option value="" disabled selected {#each col.key === "type_of_work" ? typeOfWork : categoryOfWork as option}
>Select Reporter</option <option value={option.value}>
> {option.label}
{#each reportedBy as reporter} </option>
<option value={reporter.value}
>{reporter.label}</option
>
{/each} {/each}
</select> </select>
{#if $formErrors.reported_by} {#if $formErrors[col.key]}
<p class="text-red-500 text-xs"> <p class="text-red-500 text-xs mt-1">
{$formErrors.reported_by} {$formErrors[col.key]}
</p> </p>
{/if} {/if}
</div> </div>
{:else if col.key === "villa_name"} {:else if col.key === "villa_id"}
<div class="space-y-1"> <div>
<label class="block text-sm font-medium text-gray-700" <label class="block text-sm font-medium mb-1">
>Villa Name</label {col.title}
> </label>
<select <select
name="villa_name" name={col.key}
class="w-full border px-3 py-2 rounded {errorClass( bind:value={newIssue[col.key]}
'villa_name', class="w-full border p-
)}" 2 rounded {errorClass(col.key)}"
bind:value={newIssue[col.key as keyof Timesheets]} required
> >
<option value="" disabled selected <option value="" disabled selected
>Select Villa</option >Select Villa</option
@@ -666,139 +676,80 @@
<option value={villa.id}>{villa.name}</option> <option value={villa.id}>{villa.name}</option>
{/each} {/each}
</select> </select>
{#if $formErrors.villa_name} {#if $formErrors[col.key]}
<p class="text-red-500 text-xs"> <p class="text-red-500 text-xs mt-1">
{$formErrors.villa_name} {$formErrors[col.key]}
</p> </p>
{/if} {/if}
</div> </div>
{:else if col.key === "remarks"} {:else if col.key === "date_in" || col.key === "date_out"}
<div class="space-y-1"> <div>
<label class="block text-sm font-medium text-gray-700" <label class="block text-sm font-medium mb-1">
>Remarks</label {col.title}
> </label>
<textarea
name="remarks"
class="w-full border px-3 py-2 rounded {errorClass(
'remarks',
)}"
bind:value={newIssue[col.key as keyof Timesheets]}
placeholder={col.title}
rows="4"
></textarea>
{#if $formErrors.remarks}
<p class="text-red-500 text-xs">
{$formErrors.remarks}
</p>
{/if}
</div>
{:else if col.key === "date_in"}
<div class="space-y-1">
<label class="block text-sm font-medium text-gray-700"
>Date In</label
>
<input <input
name="date_in"
class="w-full border px-3 py-2 rounded {errorClass(
'date_in',
)}"
type="datetime-local" type="datetime-local"
bind:value={newIssue[col.key as keyof Timesheets]}
required
/>
{#if $formErrors.date_in}
<p class="text-red-500 text-xs">
{$formErrors.date_in}
</p>
{/if}
</div>
{:else if col.key === "date_out"}
<div class="space-y-1">
<label class="block text-sm font-medium text-gray-700"
>Date Out</label
>
<input
name="date_out"
class="w-full border px-3 py-2 rounded {errorClass(
'date_out',
)}"
type="datetime-local"
bind:value={newIssue[col.key as keyof Timesheets]}
required
/>
{#if $formErrors.date_out}
<p class="text-red-500 text-xs">
{$formErrors.date_out}
</p>
{/if}
</div>
{:else if col.key === "type_of_work"}
<div class="space-y-1">
<label class="block text-sm font-medium text-gray-700"
>Type of Work</label
>
<select
name="type_of_work"
class="w-full border px-3 py-2 rounded {errorClass(
'type_of_work',
)}"
bind:value={newIssue[col.key as keyof Timesheets]}
>
<option value="" disabled selected
>Select Type</option
>
{#each typeOfWork as type}
<option value={type.value}>{type.label}</option>
{/each}
</select>
{#if $formErrors.type_of_work}
<p class="text-red-500 text-xs">
{$formErrors.type_of_work}
</p>
{/if}
</div>
{:else if col.key === "category_of_work"}
<div class="space-y-1">
<label class="block text-sm font-medium text-gray-700"
>Category of Work</label
>
<select
name="category_of_work"
class="w-full border px-3 py-2 rounded {errorClass(
'category_of_work',
)}"
bind:value={newIssue[col.key as keyof Timesheets]}
>
<option value="" disabled selected
>Select Category</option
>
{#each categoryOfWork as category}
<option value={category.value}
>{category.label}</option
>
{/each}
</select>
</div>
{#if $formErrors.category_of_work}
<p class="text-red-500 text-xs">
{$formErrors.category_of_work}
</p>
{/if}
{:else}
<div class="space-y-1">
<label class="block text-sm font-medium text-gray-700"
>{col.title}</label
>
<input
name={col.key} name={col.key}
class="w-full border px-3 py-2 rounded" bind:value={newIssue[col.key]}
type="text" class="w-full border p-2 rounded {errorClass(
bind:value={newIssue[col.key as keyof Timesheets]} col.key,
placeholder={col.title} )}"
required
/> />
{#if $formErrors[col.key]}
<p class="text-red-500 text-xs mt-1">
{$formErrors[col.key]}
</p>
{/if}
</div> </div>
{:else if col.key === "entered_by"}
<div>
<label class="block text-sm font-medium mb-1">
{col.title}
</label>
<select
name={col.key}
bind:value={newIssue[col.key]}
class="w-full border p-2 rounded {errorClass(
col.key,
)}"
required
>
{#each reportedBy as option}
<option value={option.value}>
{option.label}
</option>
{/each}
</select>
{#if $formErrors[col.key]}
<p class="text-red-500 text-xs mt-1">
{$formErrors[col.key]}
</p>
{/if}
</div>
{:else}
<!-- Handle other fields if necessary -->
{/if} {/if}
{/each} {/each}
<div>
<label class="block text-sm font-medium mb-1">
Total Work Hour
</label>
<input
type="number"
name="total_work_hour"
bind:value={newIssue.total_work_hour}
class="w-full border p-2 rounded {errorClass(
'total_work_hour',
)}"
readonly
/>
{#if $formErrors.total_work_hour}
<p class="text-red-500 text-xs mt-1">
{$formErrors.total_work_hour}
</p>
{/if}
</div>
<div class="flex justify-end gap-2 mt-4"> <div class="flex justify-end gap-2 mt-4">
<button <button
class="px-4 py-2 text-sm rounded bg-gray-200 hover:bg-gray-300" class="px-4 py-2 text-sm rounded bg-gray-200 hover:bg-gray-300"

View File

@@ -1,19 +1,19 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from "svelte";
import { supabase } from '$lib/supabaseClient'; import { supabase } from "$lib/supabaseClient";
type TimesheetForm = { type TimesheetForm = {
entered_by: string; entered_by: 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:
| 'Cleaning' | "Cleaning"
| 'Gardening/Pool' | "Gardening/Pool"
| 'Maintenance' | "Maintenance"
| 'Supervision' | "Supervision"
| 'Guest Service' | "Guest Service"
| 'Administration' | "Administration"
| 'Non Billable'; | "Non Billable";
villa_id: string; villa_id: string;
datetime_in: string; datetime_in: string;
datetime_out: string; datetime_out: string;
@@ -36,50 +36,54 @@
let villas: Villa[] = []; let villas: Villa[] = [];
let form: TimesheetForm = { let form: TimesheetForm = {
entered_by: '', entered_by: "",
work_description: '', work_description: "",
type_of_work: 'Running', type_of_work: "Running",
category_of_work: 'Cleaning', category_of_work: "Cleaning",
villa_id: '', villa_id: "",
datetime_in: '', datetime_in: "",
datetime_out: '', datetime_out: "",
total_work_hour: 0, total_work_hour: 0,
remarks: '', remarks: "",
approval: false, approval: false,
}; };
const typeOfWorkOptions: TimesheetForm['type_of_work'][] = ['Running', 'Periodic', 'Irregular']; const typeOfWorkOptions: TimesheetForm["type_of_work"][] = [
const categoryOptions: TimesheetForm['category_of_work'][] = [ "Running",
'Cleaning', "Periodic",
'Gardening/Pool', "Irregular",
'Maintenance', ];
'Supervision', const categoryOptions: TimesheetForm["category_of_work"][] = [
'Guest Service', "Cleaning",
'Administration', "Gardening/Pool",
'Non Billable', "Maintenance",
"Supervision",
"Guest Service",
"Administration",
"Non Billable",
]; ];
onMount(async () => { onMount(async () => {
// Fetch villas // Fetch villas
const { data: villaData, error: villaError } = await supabase const { data: villaData, error: villaError } = await supabase
.from('vb_villas') .from("vb_villas")
.select('id, villa_name, villa_status') .select("id, villa_name, villa_status")
.eq('villa_status', 'Active'); .eq("villa_status", "Active");
if (villaError) { if (villaError) {
console.error('Failed to fetch villas:', villaError.message); console.error("Failed to fetch villas:", villaError.message);
} else if (villaData) { } else if (villaData) {
villas = villaData.map((v) => ({ id: v.id, name: v.villa_name })); villas = villaData.map((v) => ({ id: v.id, name: v.villa_name }));
} }
// Fetch employees // Fetch employees
const { data: empData, error: empError } = await supabase const { data: empData, error: empError } = await supabase
.from('vb_employee') .from("vb_employee")
.select('id, employee_name') .select("id, employee_name")
.eq('employee_status', 'Active'); .eq("employee_status", "Active");
if (empError) { if (empError) {
console.error('Failed to fetch employees:', empError.message); console.error("Failed to fetch employees:", empError.message);
} else if (empData) { } else if (empData) {
employees = empData.map((e) => ({ id: e.id, name: e.employee_name })); employees = empData.map((e) => ({ id: e.id, name: e.employee_name }));
} }
@@ -101,30 +105,30 @@
calculateTotalHours(); calculateTotalHours();
if (!form.entered_by) { if (!form.entered_by) {
alert('Please select an employee.'); alert("Please select an employee.");
return; return;
} }
if (!form.villa_id) { if (!form.villa_id) {
alert('Please select a villa.'); alert("Please select a villa.");
return; return;
} }
const { error } = await supabase.from('vb_timesheet').insert([form]); const { error } = await supabase.from("vb_timesheet").insert([form]);
if (error) { if (error) {
alert('Failed to submit timesheet: ' + error.message); alert("Failed to submit timesheet: " + error.message);
} else { } else {
alert('Timesheet submitted successfully!'); alert("Timesheet submitted successfully!");
form = { form = {
entered_by: '', entered_by: "",
work_description: '', work_description: "",
type_of_work: 'Running', type_of_work: "Running",
category_of_work: 'Cleaning', category_of_work: "Cleaning",
villa_id: '', villa_id: "",
datetime_in: '', datetime_in: "",
datetime_out: '', datetime_out: "",
total_work_hour: 0, total_work_hour: 0,
remarks: '', remarks: "",
approval: false, approval: false,
}; };
} }
@@ -139,7 +143,8 @@
<h2 class="text-2xl font-bold text-center mb-6">Timesheet Entry</h2> <h2 class="text-2xl font-bold text-center mb-6">Timesheet Entry</h2>
<div> <div>
<label for="t_eb" class="block text-sm font-medium mb-1">Entered By</label> <label for="t_eb" class="block text-sm font-medium mb-1">Entered By</label
>
<select <select
id="t_eb" id="t_eb"
class="w-full border p-2 rounded" class="w-full border p-2 rounded"
@@ -154,7 +159,9 @@
</div> </div>
<div> <div>
<label for="t_wd" class="block text-sm font-medium mb-1">Work Description</label> <label for="t_wd" class="block text-sm font-medium mb-1"
>Work Description</label
>
<textarea <textarea
id="t_wd" id="t_wd"
class="w-full border border-gray-300 p-2 rounded" class="w-full border border-gray-300 p-2 rounded"
@@ -165,8 +172,14 @@
</div> </div>
<div> <div>
<label for="t_ow" class="block text-sm font-medium mb-1">Type of Work</label> <label for="t_ow" class="block text-sm font-medium mb-1"
<select id="t_ow" class="w-full border p-2 rounded" bind:value={form.type_of_work}> >Type of Work</label
>
<select
id="t_ow"
class="w-full border p-2 rounded"
bind:value={form.type_of_work}
>
{#each typeOfWorkOptions as option} {#each typeOfWorkOptions as option}
<option value={option}>{option}</option> <option value={option}>{option}</option>
{/each} {/each}
@@ -174,8 +187,14 @@
</div> </div>
<div> <div>
<label for="t_cow" class="block text-sm font-medium mb-1">Category of Work</label> <label for="t_cow" class="block text-sm font-medium mb-1"
<select id="t_cow" class="w-full border p-2 rounded" bind:value={form.category_of_work}> >Category of Work</label
>
<select
id="t_cow"
class="w-full border p-2 rounded"
bind:value={form.category_of_work}
>
{#each categoryOptions as option} {#each categoryOptions as option}
<option value={option}>{option}</option> <option value={option}>{option}</option>
{/each} {/each}
@@ -184,7 +203,12 @@
<div> <div>
<label for="t_vn" class="block text-sm font-medium mb-1">Villa</label> <label for="t_vn" class="block text-sm font-medium mb-1">Villa</label>
<select id="t_vn" class="w-full border p-2 rounded" bind:value={form.villa_id} required> <select
id="t_vn"
class="w-full border p-2 rounded"
bind:value={form.villa_id}
required
>
<option value="" disabled selected>Select Villa</option> <option value="" disabled selected>Select Villa</option>
{#each villas as villa} {#each villas as villa}
<option value={villa.id}>{villa.name}</option> <option value={villa.id}>{villa.name}</option>
@@ -193,7 +217,9 @@
</div> </div>
<div> <div>
<label for="tdto" class="block text-sm font-medium mb-1">Date/Time In</label> <label for="tdto" class="block text-sm font-medium mb-1"
>Date/Time In</label
>
<input <input
id="tdto" id="tdto"
type="datetime-local" type="datetime-local"
@@ -205,7 +231,9 @@
</div> </div>
<div> <div>
<label for="dto" class="block text-sm font-medium mb-1">Date/Time Out</label> <label for="dto" class="block text-sm font-medium mb-1"
>Date/Time Out</label
>
<input <input
id="dto" id="dto"
type="datetime-local" type="datetime-local"