penambahan fitur upload
This commit is contained in:
763
src/routes/backoffice/timesheets/+page.svelte
Normal file
763
src/routes/backoffice/timesheets/+page.svelte
Normal file
@@ -0,0 +1,763 @@
|
||||
<script lang="ts">
|
||||
import { supabase } from "$lib/supabaseClient";
|
||||
import { onMount } from "svelte";
|
||||
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;
|
||||
villa_id: string;
|
||||
villa_name: string;
|
||||
approved_by: string;
|
||||
approved_date: Date;
|
||||
total_hours_work: number;
|
||||
remarks: string;
|
||||
vacant: 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 = {
|
||||
name: string;
|
||||
staff_id: string;
|
||||
date_in: Date;
|
||||
date_out: Date;
|
||||
type_of_work: string;
|
||||
category_of_work: string;
|
||||
approval: string;
|
||||
villa_id: string;
|
||||
approved_by: string;
|
||||
approved_date: Date;
|
||||
total_hours_work: number;
|
||||
remarks: string;
|
||||
vacant: boolean;
|
||||
};
|
||||
|
||||
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: Timesheets[] = [];
|
||||
|
||||
type columns = {
|
||||
key: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
const columns: columns[] = [
|
||||
{ key: "name", title: "Name" },
|
||||
{ key: "report_by", 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() {
|
||||
const { data: timesheet, error: timesheetError } = await supabase
|
||||
.from("timesheets")
|
||||
.select("*")
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
if (timesheetError) {
|
||||
console.error("Error fetching issues:", timesheetError);
|
||||
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;
|
||||
}
|
||||
|
||||
// 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",
|
||||
}));
|
||||
}
|
||||
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",
|
||||
"created_at",
|
||||
"approval",
|
||||
"approved_by",
|
||||
"approved_date",
|
||||
"villa_name",
|
||||
"report_by",
|
||||
"actions",
|
||||
];
|
||||
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("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 = {
|
||||
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,
|
||||
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,
|
||||
),
|
||||
//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",
|
||||
};
|
||||
|
||||
const { error } = await supabase
|
||||
.from("timesheets")
|
||||
.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("timesheets")
|
||||
.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 = [
|
||||
"name",
|
||||
"type_of_work",
|
||||
"villa_id",
|
||||
"date_out",
|
||||
"reported_by",
|
||||
"category_of_work",
|
||||
"date_in",
|
||||
];
|
||||
|
||||
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("timesheets")
|
||||
.update({ approval: status })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error updating approval status:", error);
|
||||
} else {
|
||||
await fetchTimeSheets();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<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>
|
||||
<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 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 === "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(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]}
|
||||
</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 class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700"
|
||||
>Work Description</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}
|
||||
required
|
||||
/>
|
||||
{#if $formErrors.name}
|
||||
<p class="text-red-500 text-xs">
|
||||
{$formErrors.name}
|
||||
</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',
|
||||
)}"
|
||||
bind:value={newIssue[col.key as keyof Timesheets]}
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select Reporter</option
|
||||
>
|
||||
{#each reportedBy as reporter}
|
||||
<option value={reporter.value}
|
||||
>{reporter.label}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.reported_by}
|
||||
<p class="text-red-500 text-xs">
|
||||
{$formErrors.reported_by}
|
||||
</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
|
||||
>
|
||||
<select
|
||||
name="villa_name"
|
||||
class="w-full border px-3 py-2 rounded {errorClass(
|
||||
'villa_name',
|
||||
)}"
|
||||
bind:value={newIssue[col.key as keyof Timesheets]}
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select Villa</option
|
||||
>
|
||||
{#each dataVilla as villa}
|
||||
<option value={villa.id}>{villa.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.villa_name}
|
||||
<p class="text-red-500 text-xs">
|
||||
{$formErrors.villa_name}
|
||||
</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
|
||||
>
|
||||
<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}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
<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}
|
||||
Reference in New Issue
Block a user