800 lines
31 KiB
Svelte
800 lines
31 KiB
Svelte
<script lang="ts">
|
||
import { supabase } from "$lib/supabaseClient";
|
||
import { onMount } from "svelte";
|
||
import { writable } from "svelte/store";
|
||
|
||
type Timesheets = {
|
||
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;
|
||
datetime_in: string;
|
||
datetime_out: string;
|
||
total_work_hour: number;
|
||
remarks: string;
|
||
approval: boolean;
|
||
created_at?: Date;
|
||
};
|
||
|
||
type TimesheetDisplay = {
|
||
id: number;
|
||
name: string;
|
||
staff_id: string;
|
||
date_in: Date;
|
||
date_out: Date;
|
||
type_of_work: "Running" | "Periodic" | "Irregular";
|
||
category_of_work:
|
||
| "Cleaning"
|
||
| "Gardening/Pool"
|
||
| "Maintenance"
|
||
| "Supervision"
|
||
| "Guest Service"
|
||
| "Administration"
|
||
| "Non Billable";
|
||
villa_name?: string;
|
||
approval: string;
|
||
approved_by?: string;
|
||
approved_date?: Date;
|
||
total_hours_work: number;
|
||
remarks: string;
|
||
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 | null; // Allow null for new entries
|
||
};
|
||
|
||
const categoryOfWork = [
|
||
{ label: "Cleaning", value: "Cleaning" },
|
||
{ label: "Gardening/Pool", value: "Gardening/Pool" },
|
||
{ label: "Maintenance", value: "Maintenance" },
|
||
{ label: "Security", value: "Security" },
|
||
{ label: "Other", value: "Other" },
|
||
];
|
||
|
||
const typeOfWork = [
|
||
{ label: "Running", value: "Running" },
|
||
{ label: "Periodic", value: "Periodic" },
|
||
{ label: "Irregular", value: "Irregular" },
|
||
];
|
||
|
||
const reportedBy = [
|
||
{ label: "Admin", value: "Admin" },
|
||
{ label: "Staff", value: "Staff" },
|
||
{ label: "Manager", value: "Manager" },
|
||
{ label: "Guest", value: "Guest" },
|
||
];
|
||
|
||
type Villa = {
|
||
id: string;
|
||
name: string;
|
||
};
|
||
|
||
let dataVilla: Villa[] = [];
|
||
|
||
onMount(async () => {
|
||
const { data, error } = await supabase
|
||
.from("villas")
|
||
.select("id, name");
|
||
|
||
if (error) {
|
||
console.error("Error fetching villas:", error);
|
||
} else if (data) {
|
||
dataVilla = data;
|
||
}
|
||
});
|
||
|
||
let allRows: TimesheetDisplay[] = [];
|
||
|
||
type columns = {
|
||
key: string;
|
||
title: string;
|
||
};
|
||
|
||
const columns: columns[] = [
|
||
{ key: "name", title: "Name" },
|
||
{ 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" },
|
||
{ key: "category_of_work", title: "Category of Work" },
|
||
{ key: "approval", title: "Approval" },
|
||
{ key: "villa_name", title: "Villa Name" },
|
||
{ key: "approved_by", title: "Approved By" },
|
||
{ key: "approved_date", title: "Approved Date" },
|
||
{ key: "total_hours_work", title: "Total Hours Work" },
|
||
{ key: "remarks", title: "Remarks" },
|
||
{ key: "vacant", title: "Vacant" },
|
||
{ key: "created_at", title: "Created At" },
|
||
{ key: "actions", title: "Actions" },
|
||
];
|
||
|
||
async function fetchTimeSheets(
|
||
filter: string | null = null,
|
||
searchTerm: string | null = null,
|
||
sortColumn: string | null = "created_at",
|
||
sortOrder: "asc" | "desc" = "desc",
|
||
offset: number = 0,
|
||
limit: number = 10,
|
||
) {
|
||
let query = supabase
|
||
.from("vb_timesheet")
|
||
.select("*", { count: "exact" })
|
||
.order(sortColumn || "created_at", {
|
||
ascending: sortOrder === "asc",
|
||
});
|
||
if (filter) {
|
||
query = query.eq("category_of_work", filter);
|
||
}
|
||
if (searchTerm) {
|
||
query = query.ilike("work_description", `%${searchTerm}%`);
|
||
}
|
||
if (offset) {
|
||
query = query.range(offset, offset + limit - 1);
|
||
}
|
||
if (limit) {
|
||
query = query.limit(limit);
|
||
}
|
||
const { data: timesheet, error } = await query;
|
||
if (error) {
|
||
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)),
|
||
];
|
||
|
||
const { data: villas, error: villaError } = await supabase
|
||
.from("villas")
|
||
.select("*")
|
||
.in("id", villaIds);
|
||
|
||
if (villaError) {
|
||
console.error("Error fetching villas:", villaError);
|
||
return;
|
||
}
|
||
|
||
let reportedBy: { label: string; value: string }[] = [];
|
||
const { data: staffData, error: staffError } = await supabase
|
||
.from("vb_employee")
|
||
.select("id, employee_name");
|
||
if (staffError) {
|
||
console.error("Error fetching staff:", staffError);
|
||
} else if (staffData) {
|
||
reportedBy = staffData.map((s) => ({
|
||
label: s.employee_name,
|
||
value: s.id,
|
||
}));
|
||
}
|
||
|
||
// Gabungkan data villa ke dalam setiap issue
|
||
allRows = timesheet.map((issue: Timesheets) => {
|
||
const villa = villas.find((v) => v.id === issue.villa_id);
|
||
// Map entered_by to staff_id
|
||
const staff = reportedBy.find((s) => s.value === issue.entered_by);
|
||
|
||
return {
|
||
id: issue.id,
|
||
name: issue.work_description, // Map work_description to name
|
||
staff_id: staff?.label, // 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 == null
|
||
? "PENDING"
|
||
: issue.approval
|
||
? "APPROVED"
|
||
: "REJECTED", // 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;
|
||
$: totalPages = Math.ceil(allRows.length / rowsPerPage);
|
||
$: paginatedRows = allRows.slice(
|
||
(currentPage - 1) * rowsPerPage,
|
||
currentPage * rowsPerPage,
|
||
);
|
||
|
||
function goToPage(page: number) {
|
||
if (page >= 1 && page <= totalPages) currentPage = page;
|
||
}
|
||
|
||
onMount(() => {
|
||
fetchTimeSheets();
|
||
});
|
||
|
||
// Initialize the first page
|
||
$: currentPage = 1;
|
||
|
||
let showModal = false;
|
||
let isEditing = false;
|
||
let currentEditingId: string | null = null;
|
||
let newIssue: Record<string, any> = {};
|
||
const excludedKeys = ["id"];
|
||
const formColumns = columns.filter(
|
||
(col) => !excludedKeys.includes(col.key),
|
||
);
|
||
|
||
function openModal(issue?: Record<string, any>) {
|
||
if (issue) {
|
||
isEditing = true;
|
||
currentEditingId = issue.id;
|
||
newIssue = { ...issue };
|
||
} else {
|
||
isEditing = false;
|
||
currentEditingId = null;
|
||
newIssue = {};
|
||
}
|
||
showModal = true;
|
||
}
|
||
|
||
async function saveIssue(event: Event) {
|
||
event.preventDefault();
|
||
|
||
const formData = new FormData(event.target as HTMLFormElement);
|
||
|
||
// Validate form data
|
||
if (!validateForm(formData)) {
|
||
console.error("Form validation failed");
|
||
return;
|
||
}
|
||
|
||
if (isEditing && currentEditingId) {
|
||
const { error } = await supabase
|
||
.from("vb_issues")
|
||
.update(newIssue)
|
||
.eq("id", currentEditingId);
|
||
|
||
if (error) {
|
||
alert("Error updating issue: " + error.message);
|
||
console.error("Error updating issue:", error);
|
||
return;
|
||
}
|
||
} else {
|
||
const TimesheetsInsert: TimesheetsInsert = {
|
||
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,
|
||
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(),
|
||
),
|
||
remarks: formData.get("remarks") as string,
|
||
approval: null, // Default null
|
||
};
|
||
|
||
const { error } = await supabase
|
||
.from("vb_timesheet")
|
||
.insert([TimesheetsInsert]);
|
||
if (error) {
|
||
console.error("Error adding issue:", error);
|
||
return;
|
||
}
|
||
}
|
||
await fetchTimeSheets();
|
||
showModal = false;
|
||
}
|
||
|
||
async function deleteTimesheet(id: string) {
|
||
if (confirm("Are you sure you want to delete this issue?")) {
|
||
const { error } = await supabase
|
||
.from("vb_timesheet")
|
||
.delete()
|
||
.eq("id", id);
|
||
if (error) {
|
||
console.error("Error deleting issue:", error);
|
||
return;
|
||
}
|
||
await fetchTimeSheets();
|
||
}
|
||
}
|
||
|
||
export let formErrors = writable<{ [key: string]: string }>({});
|
||
|
||
function validateForm(formData: FormData): boolean {
|
||
const errors: { [key: string]: string } = {};
|
||
const requiredFields = [
|
||
"work_description",
|
||
"type_of_work",
|
||
"category_of_work",
|
||
"villa_id",
|
||
"date_in",
|
||
"date_out",
|
||
"remarks",
|
||
];
|
||
|
||
requiredFields.forEach((field) => {
|
||
if (!formData.get(field) || formData.get(field) === "") {
|
||
errors[field] = `${field.replace(/_/g, " ")} is required.`;
|
||
}
|
||
});
|
||
|
||
formErrors.set(errors);
|
||
return Object.keys(errors).length === 0;
|
||
}
|
||
|
||
function errorClass(field: string): string {
|
||
return $formErrors[field] ? "border-red-500" : "border";
|
||
}
|
||
|
||
async function updateApprovalStatus(
|
||
id: string,
|
||
status: string,
|
||
): Promise<void> {
|
||
const { error } = await supabase
|
||
.from("vb_timesheet")
|
||
.update({ approval: status })
|
||
.eq("id", id);
|
||
|
||
if (error) {
|
||
console.error("Error updating approval status:", error);
|
||
} else {
|
||
await fetchTimeSheets();
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<div>
|
||
<div
|
||
class="p-6 bg-white shadow-md rounded-2xl mb-4 flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4"
|
||
>
|
||
<div>
|
||
<h2 class="text-lg font-semibold text-gray-800">
|
||
🕒 Timesheet List
|
||
</h2>
|
||
<p class="text-sm text-gray-600">
|
||
Manage and track timesheets for staff members.
|
||
</p>
|
||
</div>
|
||
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
|
||
<input
|
||
type="text"
|
||
placeholder="🔍 Search by name..."
|
||
class="border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:outline-none px-4 py-2 rounded-xl text-sm w-64 transition"
|
||
on:input={(e) => {
|
||
const searchTerm = (
|
||
e.target as HTMLInputElement
|
||
).value.toLowerCase();
|
||
fetchTimeSheets(null, searchTerm, "created_at", "desc");
|
||
}}
|
||
/>
|
||
<select
|
||
class="border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:outline-none px-4 py-2 rounded-xl text-sm w-48 transition"
|
||
on:change={(e) => {
|
||
const filter = (e.target as HTMLSelectElement).value;
|
||
fetchTimeSheets(filter, null, null, "desc");
|
||
}}
|
||
>
|
||
<option value="">All Issues</option>
|
||
<option value="PROJECT">Project Issues</option>
|
||
<option value="PURCHASE_ORDER">Purchase Order Issues</option>
|
||
</select>
|
||
<button
|
||
class="bg-gray-200 text-gray-700 px-4 py-2 rounded-xl hover:bg-gray-300 text-sm transition"
|
||
on:click={() =>
|
||
fetchTimeSheets(null, null, "created_at", "desc", 0, 10)}
|
||
>
|
||
🔄 Reset
|
||
</button>
|
||
<button
|
||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
|
||
on:click={() => openModal()}
|
||
>
|
||
➕ Add Timesheet
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="overflow-x-auto rounded-lg shadow mb-4">
|
||
<table class="min-w-[1000px] divide-y divide-gray-200 text-sm w-max">
|
||
<thead class="bg-gray-100">
|
||
<tr>
|
||
{#each columns as col}
|
||
{#if col.key === "name"}
|
||
<th
|
||
class="sticky left-0 px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||
style="background-color: #f0f8ff; z-index: 10;"
|
||
>
|
||
{col.title}
|
||
</th>
|
||
{:else}
|
||
<th
|
||
class="px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||
>
|
||
{col.title}
|
||
</th>
|
||
{/if}
|
||
{/each}
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-gray-200 bg-white">
|
||
{#each paginatedRows as row}
|
||
<tr class="hover:bg-gray-50 transition">
|
||
{#each columns as col}
|
||
{#if col.key === "name"}
|
||
<td
|
||
class="sticky left-0 px-4 py-2 font-medium text-blue-600"
|
||
style="background-color: #f0f8ff; cursor: pointer;"
|
||
>
|
||
{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),
|
||
"true",
|
||
)}
|
||
>
|
||
✅ Approve
|
||
</button>
|
||
<button
|
||
class="ml-2 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={() =>
|
||
updateApprovalStatus(
|
||
String(row.id),
|
||
"false",
|
||
)}
|
||
>
|
||
❌ Reject
|
||
</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
|
||
class="inline-flex items-center gap-1 rounded bg-blue-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-blue-700"
|
||
on:click={() => openModal(row)}
|
||
>
|
||
✏️ Edit
|
||
</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(String(row.id))}
|
||
>
|
||
🗑️ Delete
|
||
</button>
|
||
</td>
|
||
{:else}
|
||
<td class="px-4 py-2">
|
||
{row[col.key as keyof TimesheetDisplay]}
|
||
</td>
|
||
{/if}
|
||
{/each}
|
||
</tr>
|
||
{/each}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Pagination controls -->
|
||
<div class="flex justify-between items-center text-sm">
|
||
<div>
|
||
Showing {(currentPage - 1) * rowsPerPage + 1}–
|
||
{Math.min(currentPage * rowsPerPage, allRows.length)} of {allRows.length}
|
||
</div>
|
||
<div class="space-x-2">
|
||
<button
|
||
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
|
||
on:click={() => goToPage(currentPage - 1)}
|
||
disabled={currentPage === 1}
|
||
>
|
||
Previous
|
||
</button>
|
||
{#each Array(totalPages)
|
||
.fill(0)
|
||
.map((_, i) => i + 1) as page}
|
||
<button
|
||
class="px-3 py-1 rounded border text-sm
|
||
{currentPage === page
|
||
? 'bg-blue-600 text-white border-blue-600'
|
||
: 'bg-white border-gray-300 hover:bg-gray-100'}"
|
||
on:click={() => goToPage(page)}
|
||
>
|
||
{page}
|
||
</button>
|
||
{/each}
|
||
<button
|
||
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
|
||
on:click={() => goToPage(currentPage + 1)}
|
||
disabled={currentPage === totalPages}
|
||
>
|
||
Next
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal -->
|
||
{#if showModal}
|
||
<div
|
||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||
>
|
||
<form
|
||
on:submit|preventDefault={saveIssue}
|
||
class="bg-white p-6 rounded shadow-lg w-[600px] max-h-[90vh] overflow-y-auto space-y-4"
|
||
>
|
||
<h3 class="text-lg font-semibold">
|
||
{isEditing ? "Edit Issue" : "Add New Issue"}
|
||
</h3>
|
||
{#each formColumns as col}
|
||
{#if col.key === "name"}
|
||
<div>
|
||
<label class="block text-sm font-medium mb-1">
|
||
{col.title}
|
||
</label>
|
||
<input
|
||
type="text"
|
||
name={col.key}
|
||
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 === "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,
|
||
)}"
|
||
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
|
||
>
|
||
{#each col.key === "type_of_work" ? typeOfWork : categoryOfWork 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 if col.key === "villa_id"}
|
||
<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 Villa</option
|
||
>
|
||
{#each dataVilla as villa}
|
||
<option value={villa.id}>{villa.name}</option>
|
||
{/each}
|
||
</select>
|
||
{#if $formErrors[col.key]}
|
||
<p class="text-red-500 text-xs mt-1">
|
||
{$formErrors[col.key]}
|
||
</p>
|
||
{/if}
|
||
</div>
|
||
{:else if col.key === "date_in" || col.key === "date_out"}
|
||
<div>
|
||
<label class="block text-sm font-medium mb-1">
|
||
{col.title}
|
||
</label>
|
||
<input
|
||
type="datetime-local"
|
||
name={col.key}
|
||
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"
|
||
on:click={() => (showModal = false)}
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
class="px-4 py-2 text-sm rounded bg-blue-600 text-white hover:bg-blue-700"
|
||
type="submit"
|
||
>
|
||
Save
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
{/if}
|