perbaikan timesheet
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user