perbaikan timesheet
This commit is contained in:
@@ -4,57 +4,69 @@
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
type Timesheets = {
|
||||
id: string;
|
||||
name: string;
|
||||
staff_id: string;
|
||||
report_by: string;
|
||||
date_in: Date;
|
||||
date_out: Date;
|
||||
type_of_work: string;
|
||||
category_of_work: string;
|
||||
approval: string;
|
||||
id: number;
|
||||
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;
|
||||
villa_name: string;
|
||||
approved_by: string;
|
||||
approved_date: Date;
|
||||
total_hours_work: number;
|
||||
datetime_in: string;
|
||||
datetime_out: string;
|
||||
total_work_hour: number;
|
||||
remarks: string;
|
||||
vacant: boolean;
|
||||
approval: boolean;
|
||||
created_at?: Date;
|
||||
};
|
||||
|
||||
type TimesheetDisplay = {
|
||||
id: string;
|
||||
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 = {
|
||||
id: number;
|
||||
name: string;
|
||||
staff_id: string;
|
||||
date_in: Date;
|
||||
date_out: Date;
|
||||
type_of_work: string;
|
||||
category_of_work: string;
|
||||
type_of_work: "Running" | "Periodic" | "Irregular";
|
||||
category_of_work:
|
||||
| "Cleaning"
|
||||
| "Gardening/Pool"
|
||||
| "Maintenance"
|
||||
| "Supervision"
|
||||
| "Guest Service"
|
||||
| "Administration"
|
||||
| "Non Billable";
|
||||
villa_name?: string;
|
||||
approval: string;
|
||||
villa_id: string;
|
||||
approved_by: string;
|
||||
approved_date: Date;
|
||||
approved_by?: string;
|
||||
approved_date?: Date;
|
||||
total_hours_work: number;
|
||||
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 = [
|
||||
@@ -97,7 +109,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
let allRows: Timesheets[] = [];
|
||||
let allRows: TimesheetDisplay[] = [];
|
||||
|
||||
type columns = {
|
||||
key: string;
|
||||
@@ -106,7 +118,7 @@
|
||||
|
||||
const columns: columns[] = [
|
||||
{ key: "name", title: "Name" },
|
||||
{ key: "report_by", title: "Staff Report" },
|
||||
{ key: "staff_id", title: "Staff Report" },
|
||||
{ key: "date_in", title: "Date In" },
|
||||
{ key: "date_out", title: "Date Out" },
|
||||
{ key: "type_of_work", title: "Type of Work" },
|
||||
@@ -131,7 +143,7 @@
|
||||
limit: number = 10,
|
||||
) {
|
||||
let query = supabase
|
||||
.from("vb_timesheets")
|
||||
.from("vb_timesheet")
|
||||
.select("*", { count: "exact" })
|
||||
.order(sortColumn || "created_at", {
|
||||
ascending: sortOrder === "asc",
|
||||
@@ -140,7 +152,7 @@
|
||||
query = query.eq("category_of_work", filter);
|
||||
}
|
||||
if (searchTerm) {
|
||||
query = query.ilike("name", `%${searchTerm}%`);
|
||||
query = query.ilike("work_description", `%${searchTerm}%`);
|
||||
}
|
||||
if (offset) {
|
||||
query = query.range(offset, offset + limit - 1);
|
||||
@@ -153,6 +165,7 @@
|
||||
console.error("Error fetching timesheets:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ambil semua villa_id unik dari issues
|
||||
const villaIds = [
|
||||
...new Set(timesheet.map((i: Timesheets) => i.villa_id)),
|
||||
@@ -169,13 +182,34 @@
|
||||
}
|
||||
|
||||
// Gabungkan data villa ke dalam setiap issue
|
||||
allRows = timesheet.map((timesheet: Timesheets) => ({
|
||||
...timesheet,
|
||||
villa_name:
|
||||
villas.find((v) => v.id === timesheet.villa_id).name || null,
|
||||
approval: timesheet.approval || "",
|
||||
report_by: timesheet.staff_id || "Unknown",
|
||||
}));
|
||||
allRows = timesheet.map((issue: Timesheets) => {
|
||||
const villa = villas.find((v) => v.id === issue.villa_id);
|
||||
|
||||
return {
|
||||
id: issue.id,
|
||||
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 rowsPerPage = 5;
|
||||
@@ -200,16 +234,7 @@
|
||||
let isEditing = false;
|
||||
let currentEditingId: string | null = null;
|
||||
let newIssue: Record<string, any> = {};
|
||||
const excludedKeys = [
|
||||
"id",
|
||||
"created_at",
|
||||
"approval",
|
||||
"approved_by",
|
||||
"approved_date",
|
||||
"villa_name",
|
||||
"report_by",
|
||||
"actions",
|
||||
];
|
||||
const excludedKeys = ["id"];
|
||||
const formColumns = columns.filter(
|
||||
(col) => !excludedKeys.includes(col.key),
|
||||
);
|
||||
@@ -251,33 +276,33 @@
|
||||
}
|
||||
} else {
|
||||
const TimesheetsInsert: TimesheetsInsert = {
|
||||
name: formData.get("name") as string,
|
||||
staff_id: formData.get("staff_id") as string,
|
||||
date_in: new Date(formData.get("date_in") as string),
|
||||
date_out: new Date(formData.get("date_out") as string),
|
||||
type_of_work: formData.get("type_of_work") as string,
|
||||
category_of_work: formData.get("category_of_work") as string,
|
||||
approval: formData.get("approval") as string,
|
||||
entered_by: formData.get("entered_by") as string,
|
||||
work_description: formData.get("work_description") as string,
|
||||
type_of_work: formData.get("type_of_work") as
|
||||
| "Running"
|
||||
| "Periodic"
|
||||
| "Irregular",
|
||||
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,
|
||||
approved_by: formData.get("approved_by") as string,
|
||||
approved_date: new Date(
|
||||
formData.get("approved_date") as string,
|
||||
datetime_in: formData.get("date_in") as string,
|
||||
datetime_out: formData.get("date_out") 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,
|
||||
vacant: formData.get("vacant") === "true",
|
||||
approval: false, // Default to false for new entries
|
||||
};
|
||||
|
||||
const { error } = await supabase
|
||||
.from("vb_timesheets")
|
||||
.from("vb_timesheet")
|
||||
.insert([TimesheetsInsert]);
|
||||
if (error) {
|
||||
console.error("Error adding issue:", error);
|
||||
@@ -291,7 +316,7 @@
|
||||
async function deleteTimesheet(id: string) {
|
||||
if (confirm("Are you sure you want to delete this issue?")) {
|
||||
const { error } = await supabase
|
||||
.from("vb_timesheets")
|
||||
.from("vb_timesheet")
|
||||
.delete()
|
||||
.eq("id", id);
|
||||
if (error) {
|
||||
@@ -307,13 +332,13 @@
|
||||
function validateForm(formData: FormData): boolean {
|
||||
const errors: { [key: string]: string } = {};
|
||||
const requiredFields = [
|
||||
"name",
|
||||
"work_description",
|
||||
"type_of_work",
|
||||
"villa_id",
|
||||
"date_out",
|
||||
"reported_by",
|
||||
"category_of_work",
|
||||
"villa_id",
|
||||
"date_in",
|
||||
"date_out",
|
||||
"remarks",
|
||||
];
|
||||
|
||||
requiredFields.forEach((field) => {
|
||||
@@ -335,7 +360,7 @@
|
||||
status: string,
|
||||
): Promise<void> {
|
||||
const { error } = await supabase
|
||||
.from("vb_timesheets")
|
||||
.from("vb_timesheet")
|
||||
.update({ approval: status })
|
||||
.eq("id", id);
|
||||
|
||||
@@ -430,6 +455,65 @@
|
||||
>
|
||||
{row[col.key]}
|
||||
</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"}
|
||||
<td class="px-4 py-2">
|
||||
<button
|
||||
@@ -440,91 +524,15 @@
|
||||
</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"
|
||||
on:click={() => deleteTimesheet(row.id)}
|
||||
on:click={() =>
|
||||
deleteTimesheet(String(row.id))}
|
||||
>
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</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}
|
||||
<td class="px-4 py-2">
|
||||
{row[col.key as keyof Timesheets]}
|
||||
{row[col.key as keyof TimesheetDisplay]}
|
||||
</td>
|
||||
{/if}
|
||||
{/each}
|
||||
@@ -586,78 +594,80 @@
|
||||
</h3>
|
||||
{#each formColumns as col}
|
||||
{#if col.key === "name"}
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700"
|
||||
>Work Description</label
|
||||
>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
class="w-full border px-3 py-2 rounded {errorClass(
|
||||
'name',
|
||||
)}"
|
||||
name="name"
|
||||
type="text"
|
||||
bind:value={newIssue[col.key as keyof Timesheets]}
|
||||
placeholder={col.title}
|
||||
name={col.key}
|
||||
bind:value={newIssue[col.key]}
|
||||
class="w-full border p-2 rounded {errorClass(
|
||||
col.key,
|
||||
)}"
|
||||
required
|
||||
/>
|
||||
{#if $formErrors.name}
|
||||
<p class="text-red-500 text-xs">
|
||||
{$formErrors.name}
|
||||
{#if $formErrors[col.key]}
|
||||
<p class="text-red-500 text-xs mt-1">
|
||||
{$formErrors[col.key]}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if col.key === "vacant"}
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700"
|
||||
>Vacant</label
|
||||
>
|
||||
<select
|
||||
name="guest_has_aggreed_issue_has_been_resolved"
|
||||
class="w-full border px-3 py-2 rounded"
|
||||
bind:value={newIssue[col.key as keyof Timesheets]}
|
||||
>
|
||||
<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',
|
||||
{:else if col.key === "remarks"}
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">
|
||||
{col.title}
|
||||
</label>
|
||||
<textarea
|
||||
name={col.key}
|
||||
bind:value={newIssue[col.key]}
|
||||
class="w-full border p-2 rounded {errorClass(
|
||||
col.key,
|
||||
)}"
|
||||
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
|
||||
>Select Reporter</option
|
||||
>
|
||||
{#each reportedBy as reporter}
|
||||
<option value={reporter.value}
|
||||
>{reporter.label}</option
|
||||
>
|
||||
{#each col.key === "type_of_work" ? typeOfWork : categoryOfWork as option}
|
||||
<option value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.reported_by}
|
||||
<p class="text-red-500 text-xs">
|
||||
{$formErrors.reported_by}
|
||||
{#if $formErrors[col.key]}
|
||||
<p class="text-red-500 text-xs mt-1">
|
||||
{$formErrors[col.key]}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if col.key === "villa_name"}
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700"
|
||||
>Villa Name</label
|
||||
>
|
||||
{:else if col.key === "villa_id"}
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">
|
||||
{col.title}
|
||||
</label>
|
||||
<select
|
||||
name="villa_name"
|
||||
class="w-full border px-3 py-2 rounded {errorClass(
|
||||
'villa_name',
|
||||
)}"
|
||||
bind:value={newIssue[col.key as keyof Timesheets]}
|
||||
name={col.key}
|
||||
bind:value={newIssue[col.key]}
|
||||
class="w-full border p-
|
||||
2 rounded {errorClass(col.key)}"
|
||||
required
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select Villa</option
|
||||
@@ -666,139 +676,80 @@
|
||||
<option value={villa.id}>{villa.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.villa_name}
|
||||
<p class="text-red-500 text-xs">
|
||||
{$formErrors.villa_name}
|
||||
{#if $formErrors[col.key]}
|
||||
<p class="text-red-500 text-xs mt-1">
|
||||
{$formErrors[col.key]}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if col.key === "remarks"}
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700"
|
||||
>Remarks</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
|
||||
>
|
||||
{:else if col.key === "date_in" || col.key === "date_out"}
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
name="date_in"
|
||||
class="w-full border px-3 py-2 rounded {errorClass(
|
||||
'date_in',
|
||||
)}"
|
||||
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}
|
||||
class="w-full border px-3 py-2 rounded"
|
||||
type="text"
|
||||
bind:value={newIssue[col.key as keyof Timesheets]}
|
||||
placeholder={col.title}
|
||||
bind:value={newIssue[col.key]}
|
||||
class="w-full border p-2 rounded {errorClass(
|
||||
col.key,
|
||||
)}"
|
||||
required
|
||||
/>
|
||||
{#if $formErrors[col.key]}
|
||||
<p class="text-red-500 text-xs mt-1">
|
||||
{$formErrors[col.key]}
|
||||
</p>
|
||||
{/if}
|
||||
</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}
|
||||
{/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">
|
||||
<button
|
||||
class="px-4 py-2 text-sm rounded bg-gray-200 hover:bg-gray-300"
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { supabase } from '$lib/supabaseClient';
|
||||
import { onMount } from "svelte";
|
||||
import { supabase } from "$lib/supabaseClient";
|
||||
|
||||
type TimesheetForm = {
|
||||
entered_by: string;
|
||||
work_description: string;
|
||||
type_of_work: 'Running' | 'Periodic' | 'Irregular';
|
||||
type_of_work: "Running" | "Periodic" | "Irregular";
|
||||
category_of_work:
|
||||
| 'Cleaning'
|
||||
| 'Gardening/Pool'
|
||||
| 'Maintenance'
|
||||
| 'Supervision'
|
||||
| 'Guest Service'
|
||||
| 'Administration'
|
||||
| 'Non Billable';
|
||||
| "Cleaning"
|
||||
| "Gardening/Pool"
|
||||
| "Maintenance"
|
||||
| "Supervision"
|
||||
| "Guest Service"
|
||||
| "Administration"
|
||||
| "Non Billable";
|
||||
villa_id: string;
|
||||
datetime_in: string;
|
||||
datetime_out: string;
|
||||
@@ -36,50 +36,54 @@
|
||||
let villas: Villa[] = [];
|
||||
|
||||
let form: TimesheetForm = {
|
||||
entered_by: '',
|
||||
work_description: '',
|
||||
type_of_work: 'Running',
|
||||
category_of_work: 'Cleaning',
|
||||
villa_id: '',
|
||||
datetime_in: '',
|
||||
datetime_out: '',
|
||||
entered_by: "",
|
||||
work_description: "",
|
||||
type_of_work: "Running",
|
||||
category_of_work: "Cleaning",
|
||||
villa_id: "",
|
||||
datetime_in: "",
|
||||
datetime_out: "",
|
||||
total_work_hour: 0,
|
||||
remarks: '',
|
||||
remarks: "",
|
||||
approval: false,
|
||||
};
|
||||
|
||||
const typeOfWorkOptions: TimesheetForm['type_of_work'][] = ['Running', 'Periodic', 'Irregular'];
|
||||
const categoryOptions: TimesheetForm['category_of_work'][] = [
|
||||
'Cleaning',
|
||||
'Gardening/Pool',
|
||||
'Maintenance',
|
||||
'Supervision',
|
||||
'Guest Service',
|
||||
'Administration',
|
||||
'Non Billable',
|
||||
const typeOfWorkOptions: TimesheetForm["type_of_work"][] = [
|
||||
"Running",
|
||||
"Periodic",
|
||||
"Irregular",
|
||||
];
|
||||
const categoryOptions: TimesheetForm["category_of_work"][] = [
|
||||
"Cleaning",
|
||||
"Gardening/Pool",
|
||||
"Maintenance",
|
||||
"Supervision",
|
||||
"Guest Service",
|
||||
"Administration",
|
||||
"Non Billable",
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
// Fetch villas
|
||||
const { data: villaData, error: villaError } = await supabase
|
||||
.from('vb_villas')
|
||||
.select('id, villa_name, villa_status')
|
||||
.eq('villa_status', 'Active');
|
||||
.from("vb_villas")
|
||||
.select("id, villa_name, villa_status")
|
||||
.eq("villa_status", "Active");
|
||||
|
||||
if (villaError) {
|
||||
console.error('Failed to fetch villas:', villaError.message);
|
||||
console.error("Failed to fetch villas:", villaError.message);
|
||||
} else if (villaData) {
|
||||
villas = villaData.map((v) => ({ id: v.id, name: v.villa_name }));
|
||||
}
|
||||
|
||||
// Fetch employees
|
||||
const { data: empData, error: empError } = await supabase
|
||||
.from('vb_employee')
|
||||
.select('id, employee_name')
|
||||
.eq('employee_status', 'Active');
|
||||
.from("vb_employee")
|
||||
.select("id, employee_name")
|
||||
.eq("employee_status", "Active");
|
||||
|
||||
if (empError) {
|
||||
console.error('Failed to fetch employees:', empError.message);
|
||||
console.error("Failed to fetch employees:", empError.message);
|
||||
} else if (empData) {
|
||||
employees = empData.map((e) => ({ id: e.id, name: e.employee_name }));
|
||||
}
|
||||
@@ -101,30 +105,30 @@
|
||||
calculateTotalHours();
|
||||
|
||||
if (!form.entered_by) {
|
||||
alert('Please select an employee.');
|
||||
alert("Please select an employee.");
|
||||
return;
|
||||
}
|
||||
if (!form.villa_id) {
|
||||
alert('Please select a villa.');
|
||||
alert("Please select a villa.");
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await supabase.from('vb_timesheet').insert([form]);
|
||||
const { error } = await supabase.from("vb_timesheet").insert([form]);
|
||||
|
||||
if (error) {
|
||||
alert('Failed to submit timesheet: ' + error.message);
|
||||
alert("Failed to submit timesheet: " + error.message);
|
||||
} else {
|
||||
alert('Timesheet submitted successfully!');
|
||||
alert("Timesheet submitted successfully!");
|
||||
form = {
|
||||
entered_by: '',
|
||||
work_description: '',
|
||||
type_of_work: 'Running',
|
||||
category_of_work: 'Cleaning',
|
||||
villa_id: '',
|
||||
datetime_in: '',
|
||||
datetime_out: '',
|
||||
entered_by: "",
|
||||
work_description: "",
|
||||
type_of_work: "Running",
|
||||
category_of_work: "Cleaning",
|
||||
villa_id: "",
|
||||
datetime_in: "",
|
||||
datetime_out: "",
|
||||
total_work_hour: 0,
|
||||
remarks: '',
|
||||
remarks: "",
|
||||
approval: false,
|
||||
};
|
||||
}
|
||||
@@ -139,7 +143,8 @@
|
||||
<h2 class="text-2xl font-bold text-center mb-6">Timesheet Entry</h2>
|
||||
|
||||
<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
|
||||
id="t_eb"
|
||||
class="w-full border p-2 rounded"
|
||||
@@ -154,7 +159,9 @@
|
||||
</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
|
||||
id="t_wd"
|
||||
class="w-full border border-gray-300 p-2 rounded"
|
||||
@@ -165,8 +172,14 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="t_ow" class="block text-sm font-medium mb-1">Type of Work</label>
|
||||
<select id="t_ow" class="w-full border p-2 rounded" bind:value={form.type_of_work}>
|
||||
<label for="t_ow" class="block text-sm font-medium mb-1"
|
||||
>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}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
@@ -174,8 +187,14 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="t_cow" class="block text-sm font-medium mb-1">Category of Work</label>
|
||||
<select id="t_cow" class="w-full border p-2 rounded" bind:value={form.category_of_work}>
|
||||
<label for="t_cow" class="block text-sm font-medium mb-1"
|
||||
>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}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
@@ -184,7 +203,12 @@
|
||||
|
||||
<div>
|
||||
<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>
|
||||
{#each villas as villa}
|
||||
<option value={villa.id}>{villa.name}</option>
|
||||
@@ -193,7 +217,9 @@
|
||||
</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
|
||||
id="tdto"
|
||||
type="datetime-local"
|
||||
@@ -205,7 +231,9 @@
|
||||
</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
|
||||
id="dto"
|
||||
type="datetime-local"
|
||||
|
||||
Reference in New Issue
Block a user