Files
vberp/src/routes/backoffice/issue/+page.svelte
aji@catalis.app 2ad0f5093d perbaikan data
2025-06-22 12:26:43 +07:00

1182 lines
48 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
import { supabase } from "$lib/supabaseClient";
import { onMount } from "svelte";
import { writable } from "svelte/store";
const priority = [
{ label: "Low", value: "Low" },
{ label: "Medium", value: "Medium" },
{ label: "High", value: "High" },
{ label: "Critical", value: "Critical" },
];
const issueSource = [
{ label: "Email", value: "Email" },
{ label: "Phone Call", value: "Phone Call" },
{ label: "In-Person", value: "In-Person" },
{ label: "Online Form", value: "Online Form" },
{ label: "Other", value: "Other" },
];
const issueTypes = [
{ label: "Facilities - Light", value: "Facilities - Light" },
{ label: "Facilities - Linen", value: "Facilities - Linen" },
{ label: "Facilities - Other", value: "Facilities - Other" },
{ label: "Facilities - Towel", value: "Facilities - Towel" },
{ label: "Facilities - Fan", value: "Facilities - Fan" },
{ label: "Cleanliness - Other", value: "Cleanliness - Other" },
{ label: "Cleanliness - Floor", value: "Cleanliness - Floor" },
{ label: "Cleanliness - Kitchen", value: "Cleanliness - Kitchen" },
{ label: "Cleanliness - Bathroom", value: "Cleanliness - Bathroom" },
{
label: "Maintenance - Electrical",
value: "Maintenance - Electrical",
},
{ label: "Maintenance - Plumbing", value: "Maintenance - Plumbing" },
{ label: "Maintenance - HVAC", value: "Maintenance - HVAC" },
{
label: "Maintenance - Structural",
value: "Maintenance - Structural",
},
{ label: "Safety Issue", value: "Safety Issue" },
{ label: "Security Concern", value: "Security Concern" },
{ label: "Other", value: "Other" },
{ label: "General Inquiry", value: "General Inquiry" },
{ label: "Feedback", value: "Feedback" },
{ label: "Complaint", value: "Complaint" },
{ label: "Request", value: "Request" },
{ label: "Suggestion", value: "Suggestion" },
{ label: "Booking Issue", value: "Booking Issue" },
{ label: "Payment Issue", value: "Payment Issue" },
{ label: "Cancellation Request", value: "Cancellation Request" },
{ label: "Refund Request", value: "Refund Request" },
{ label: "Reservation Change", value: "Reservation Change" },
{ label: "Check-in Issue", value: "Check-in Issue" },
{ label: "Check-out Issue", value: "Check-out Issue" },
];
const areaOfVilla = [
{ label: "All Bathrooms", value: "All Bathrooms" },
{ label: "All Guest Houses", value: "All Guest Houses" },
{ label: "All Rooms", value: "All Rooms" },
{ label: "All Villa Areas", value: "All Villa Areas" },
{ label: "Balcony", value: "Balcony" },
{ label: "Bathroom (Guest)", value: "Bathroom (Guest)" },
{ label: "Bathroom (Master)", value: "Bathroom (Master)" },
{ label: "Bathroom 1", value: "Bathroom 1" },
{ label: "Bathroom 2", value: "Bathroom 2" },
{ label: "Bathroom 3", value: "Bathroom 3" },
{ label: "Bedroom (Guest)", value: "Bedroom (Guest)" },
{ label: "Bedroom (Master)", value: "Bedroom (Master)" },
{ label: "Bedroom 1", value: "Bedroom 1" },
{ label: "Bedroom 2", value: "Bedroom 2" },
{ label: "Bedroom 3", value: "Bedroom 3" },
{ label: "Ceiling", value: "Ceiling" },
{ label: "Dining Area", value: "Dining Area" },
{ label: "Door", value: "Door" },
{ label: "Entrance", value: "Entrance" },
{ label: "Garden", value: "Garden" },
{ label: "General", value: "General" },
{ label: "Glass", value: "Glass" },
{ label: "Hallway", value: "Hallway" },
{ label: "Kitchen", value: "Kitchen" },
{ label: "Laundry Area", value: "Laundry Area" },
{ label: "Living Room", value: "Living Room" },
{ label: "Outdoor Area", value: "Outdoor Area" },
{ label: "Parking Area", value: "Parking Area" },
{ label: "Pool Area", value: "Pool Area" },
{ label: "Roof", value: "Roof" },
{ label: "Stairs", value: "Stairs" },
{ label: "Storage", value: "Storage" },
{ label: "Terrace", value: "Terrace" },
{ label: "Toilet", value: "Toilet" },
{ label: "Wall", value: "Wall" },
{ label: "Window", value: "Window" },
{ label: "Others", value: "Others" },
];
const inputBy = [
{ label: "Admin", value: "Admin" },
{ label: "Staff", value: "Staff" },
{ label: "Manager", value: "Manager" },
{ label: "Guest", value: "Guest" },
];
const reportedBy = [
{ label: "Admin", value: "Admin" },
{ label: "Staff", value: "Staff" },
{ label: "Manager", value: "Manager" },
{ label: "Guest", value: "Guest" },
];
type Villa = {
id: string;
villa_name: string;
};
type User = {
id: string;
full_name: string;
};
let dataVilla: Villa[] = [];
let dataUser: User[] = [];
onMount(async () => {
const { data, error } = await supabase
.from("vb_villas")
.select("id, villa_name");
if (error) {
console.error("Error fetching villas:", error);
} else if (data) {
dataVilla = data;
}
const { data: userData, error: userError } = await supabase
.from("vb_users")
.select("id, full_name");
if (userError) {
console.error("Error fetching users:", userError);
} else if (userData) {
dataUser = userData;
}
});
type Issue = {
id: string;
name: string;
villa_id: string;
villa_name: string;
area_of_villa: string;
priority: string;
issue_type: string;
issue_number: string;
move_issue: string;
description_of_the_issue: string;
reported_date: string;
issue_related_image: string;
issue_source: string;
reported_by: string;
input_by: string;
guest_communication: string;
resolution: string;
guest_has_aggreed_issue_has_been_resolved: boolean;
follow_up: boolean;
need_approval: boolean;
created_at: string;
updated_by?: string;
updated_at?: string;
};
type issueInsert = {
name: string;
villa_id: string;
area_of_villa: string;
priority: string;
issue_type: string;
description_of_the_issue: string;
reported_date: string;
issue_related_image: string;
issue_source: string;
reported_by: string;
input_by: string;
guest_communication: string;
resolution: string;
guest_has_aggreed_issue_has_been_resolved: boolean;
follow_up: boolean;
need_approval: boolean;
};
let allRows: Issue[] = [];
let offset = 0;
let limit = 10;
let totalItems = 0;
export let formErrors = writable<{ [key: string]: string }>({});
type columns = {
key: string;
title: string;
};
const columns: columns[] = [
{ key: "name", title: "Name" },
{ key: "villa_name", title: "Villa Name" },
{ key: "villa_id", title: "Villa ID" },
{ key: "input_by", title: "Input By" },
{ key: "reported_by", title: "Reported By" },
{ key: "reported_date", title: "Reported Date" },
{ key: "area_of_villa", title: "Area Of Villa" },
{ key: "priority", title: "Priority" },
{ key: "issue_type", title: "Issue Type" },
{ key: "issue_number", title: "Issue Number" },
{ key: "move_issue", title: "Move Issue" },
{ key: "description_of_the_issue", title: "Description of The Issue" },
{ key: "issue_related_image", title: "Issue Related Image" },
{ key: "issue_source", title: "Issue Source" },
{ key: "reported_name", title: "Reported By" },
{ key: "inputed_name", title: "Input By" },
{ key: "guest_communication", title: "Guest Communication" },
{ key: "resolution", title: "Resolution" },
{
key: "guest_has_aggreed_issue_has_been_resolved",
title: "Guest Has Aggred Issue Has Been Resolved",
},
{ key: "follow_up", title: "Follow Up" },
{ key: "need_approval", title: "Need Approval" },
{ key: "created_at", title: "Created At" },
{ key: "updated_name", title: "Updated By" },
{ key: "updated_at", title: "Updated At" },
{ key: "actions", title: "Actions" },
];
async function fetchIssues(
filter: string | null = null,
search: string | null = null,
sort: string | null = null,
order: "asc" | "desc" = "desc",
offset: number = 0,
limit: number = 10,
) {
let query = supabase
.from("vb_issues_data")
.select("*", { count: "exact" })
.order(sort || "created_at", { ascending: order === "asc" })
.range(offset, offset + limit - 1);
if (filter) {
query = query.eq("move_issue", filter);
}
if (search) {
query = query.ilike("name", `%${search}%`);
}
const { data: issues, error, count } = await query;
if (error) {
console.error("Error fetching issues:", error);
return;
}
if (count !== undefined) {
totalItems = count ?? 0;
}
if (!issues || issues.length === 0) {
allRows = [];
return;
}
// Gabungkan data villa ke dalam setiap issue
allRows = issues;
}
let currentPage = offset + 1;
let rowsPerPage = limit;
$: totalPages = Math.ceil(totalItems / rowsPerPage);
function goToPage(page: number) {
if (page >= 1 && page <= totalPages) currentPage = page;
}
onMount(() => {
fetchIssues(null, null, "created_at", "desc", offset, limit);
});
let showModal = false;
let isEditing = false;
let currentEditingId: string | null = null;
let newIssue: Record<string, any> = {};
const excludedKeys = [
"id",
"created_at",
"move_issue",
"issue_number",
"actions",
"villa_name",
"reported_name",
"inputed_name",
"updated_name",
"updated_at",
"updated_by",
];
const excludedKeysDisplay = [
"villa_id",
"reported_by",
"input_by",
"updated_by",
];
const formColumns = columns.filter(
(col) => !excludedKeys.includes(col.key),
);
const formColumnsDisplay = columns.filter(
(col) => !excludedKeysDisplay.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;
}
//upload image if selected
if (selectedFile) {
const { data, error } = await supabase.storage
.from("villabugis")
.upload(
`issues/${Date.now()}_${selectedFile.name}`,
selectedFile,
);
console.log("Image upload data:", data);
if (error) {
alert("Error uploading image: " + error.message);
console.error("Error uploading image:", error);
return;
}
newIssue.issue_related_image = data.path;
}
if (isEditing && currentEditingId) {
newIssue = formColumns.reduce(
(acc, col) => {
if (col.key in newIssue) {
acc[col.key] = newIssue[col.key];
}
return acc;
},
{} as Record<string, any>,
);
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 issueInsert: issueInsert = {
name: formData.get("name") as string,
villa_id: formData.get("villa_id") as string,
area_of_villa: formData.get("area_of_villa") as string,
priority: formData.get("priority") as string,
issue_type: formData.get("issue_type") as string,
description_of_the_issue: formData.get(
"description_of_the_issue",
) as string,
reported_date: formData.get("reported_date") as string,
issue_related_image: imagePreviewUrl || "",
issue_source: formData.get("issue_source") as string,
reported_by: formData.get("reported_by") as string,
input_by: formData.get("input_by") as string,
guest_communication: formData.get(
"guest_communication",
) as string,
resolution: formData.get("resolution") as string,
guest_has_aggreed_issue_has_been_resolved:
formData.get(
"guest_has_aggreed_issue_has_been_resolved",
) === "true"
? true
: false,
follow_up: formData.get("follow_up") === "true" ? true : false,
need_approval:
formData.get("need_approval") === "true" ? true : false,
};
const { error } = await supabase
.from("vb_issues")
.insert([issueInsert]);
if (error) {
console.error("Error adding issue:", error);
return;
}
}
await fetchIssues();
showModal = false;
}
// function get public URL for image supabase
async function getPublicUrl(path: string): Promise<string> {
const { data } = supabase.storage.from("villabugis").getPublicUrl(path);
return data.publicUrl;
}
async function deleteIssue(id: string) {
if (confirm("Are you sure you want to delete this issue?")) {
const { error } = await supabase
.from("vb_issues")
.delete()
.eq("id", id);
if (error) {
console.error("Error deleting issue:", error);
return;
}
await fetchIssues();
}
}
let selectedFile: File | null = null;
let imagePreviewUrl: string | null = null;
function handleFileChange(event: Event) {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
selectedFile = input.files[0];
imagePreviewUrl = URL.createObjectURL(selectedFile);
}
}
function validateForm(formData: FormData): boolean {
const errors: { [key: string]: string } = {};
const requiredFields = [
"name",
"description_of_the_issue",
"issue_source",
"villa_id",
"reported_date",
"reported_by",
"priority",
"issue_type",
"input_by",
"area_of_villa",
];
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";
}
// insert id issue to project
async function moveIssueToProject(issueId: string) {
// get user id from session
const session = await supabase.auth.getSession();
if (!session) {
console.error("User not authenticated");
return;
}
const userId = session.data.session?.user.id;
// update move_issue field in the issue
const { error: updateError } = await supabase
.from("vb_issues")
.update({
move_issue: "PROJECT",
updated_by: userId,
updated_at: new Date().toISOString(),
})
.eq("id", issueId);
if (updateError) {
console.error("Error updating issue move_issue:", updateError);
return;
}
const { error } = await supabase
.from("vb_projects")
.insert({ issue_id: issueId });
if (error) {
console.error("Error moving issue to project:", error);
return;
}
alert(`Issue ${issueId} moved to project successfully.`);
await fetchIssues();
}
// insert id issue to purchase order
async function moveIssueToPurchaseOrder(issueId: string) {
// get user id from session
const session = await supabase.auth.getSession();
if (!session) {
console.error("User not authenticated");
return;
}
const userId = session.data.session?.user.id;
// update move_issue field in the issue
const { error: updateError } = await supabase
.from("vb_issues")
.update({
move_issue: "PURCHASE_ORDER",
updated_by: userId,
updated_at: new Date().toISOString(),
})
.eq("id", issueId);
if (updateError) {
console.error("Error updating issue move_issue:", updateError);
return;
}
const { error } = await supabase
.from("vb_purchase_orders")
.insert({ issue_id: issueId });
if (error) {
console.error("Error moving issue to purchase order:", error);
return;
}
alert(`Issue ${issueId} moved to purchase order successfully.`);
await fetchIssues();
}
</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-xl font-semibold text-gray-800">📝 Issue List</h2>
<p class="text-sm text-gray-500">
Manage and view all issues reported in the system.
</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();
fetchIssues(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;
fetchIssues(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={() =>
fetchIssues(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 Issue
</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 formColumnsDisplay 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 allRows as row}
<tr class="hover:bg-gray-50 transition">
{#each formColumnsDisplay 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={() => deleteIssue(row.id)}
>
🗑️ Delete
</button>
</td>
{:else if col.key === "move_issue"}
{#if row[col.key as keyof Issue] === "PROJECT"}
<td class="px-4 py-2">
<button
class="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 disabled:opacity-90"
disabled
>
➡️ PROJECT
</button>
</td>
{:else if row[col.key as keyof Issue] === "PURCHASE_ORDER"}
<td class="px-4 py-2">
<button
class="inline-flex items-center gap-1 rounded bg-yellow-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-yellow-700 disabled:opacity-90"
disabled
>
➡️ PURCHASE ORDER
</button>
</td>
{:else}
<td class="px-4 py-2">
<button
class="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={() =>
moveIssueToProject(row.id)}
>
➡️ PROJECT
</button>
<button
class="inline-flex items-center gap-1 rounded bg-yellow-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-yellow-700"
on:click={() =>
moveIssueToPurchaseOrder(
row.id,
)}
>
➡️ PURCHASE ORDER
</button>
</td>
{/if}
{:else if col.key === "guest_has_aggreed_issue_has_been_resolved"}
<td class="px-4 py-2">
{#if row[col.key as keyof Issue]}
{:else}
{/if}
</td>
{:else if col.key === "issue_related_image"}
<td class="px-4 py-2">
{#if typeof row[col.key as keyof Issue] === "string" && row[col.key as keyof Issue]}
{#await getPublicUrl(row[col.key as keyof Issue] as string) then publicUrl}
<a
href={publicUrl}
target="_blank"
class="text-blue-600 hover:underline"
>View Picture</a
>
{:catch}
<span class="text-red-500"
>Error loading image</span
>
{/await}
{:else}
No Picture
{/if}
</td>
{:else if col.key === "reported_date"}
<td class="px-4 py-2">
{typeof row[col.key as keyof Issue] ===
"string" ||
typeof row[col.key as keyof Issue] ===
"number"
? new Date(
row[col.key as keyof Issue] as
| string
| number,
).toLocaleString("en-US")
: ""}
</td>
{:else if col.key === "need_approval"}
<td class="px-4 py-2">
{#if row[col.key as keyof Issue]}
{:else}
{/if}
</td>
{:else if col.key === "follow_up"}
<td class="px-4 py-2">
{#if row[col.key as keyof Issue]}
{:else}
{/if}
</td>
{:else if col.key === "created_at"}
<!-- beri jam jg -->
<td class="px-4 py-2">
{new Date(
row[col.key as keyof Issue] as string,
).toLocaleString("en-US")}
</td>
{:else if col.key === "updated_at"}
<td class="px-4 py-2">
{row[col.key as keyof Issue]
? new Date(
row[
col.key as keyof Issue
] as string,
).toLocaleString("en-US")
: ""}
</td>
{:else}
<td class="px-4 py-2 text-gray-700"
>{row[col.key as keyof Issue]}</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"
>Customer / Guest Name</label
>
<input
class="w-full border px-3 py-2 rounded {errorClass(
'name',
)}"
name="name"
type="text"
bind:value={newIssue[col.key as keyof Issue]}
placeholder={col.title}
required
/>
{#if $formErrors.name}
<p class="text-red-500 text-xs">
{$formErrors.name}
</p>
{/if}
</div>
{:else if col.key === "guest_has_aggreed_issue_has_been_resolved"}
<div class="space-y-1">
<label class="block text-sm font-medium text-gray-700"
>Guest Has Aggred Issue Has Been Resolved</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 Issue]}
>
<option value="true">Yes</option>
<option value="false">No</option>
</select>
</div>
{:else if col.key === "follow_up"}
<div class="space-y-1">
<label class="block text-sm font-medium text-gray-700"
>Follow Up</label
>
<select
name="follow_up"
class="w-full border px-3 py-2 rounded"
bind:value={newIssue[col.key as keyof Issue]}
>
<option value="true">Yes</option>
<option value="false">No</option>
</select>
</div>
{:else if col.key === "issue_related_image"}
<div class="space-y-1">
<label class="block text-sm font-medium text-gray-700">
Issue Related Image
</label>
<input
name="issue_related_image"
class="w-full border px-3 py-2 rounded"
type="file"
accept="image/*"
on:change={handleFileChange}
/>
<p class="text-xs text-gray-500">
Upload an image related to the issue.
</p>
{#if imagePreviewUrl}
<img
src={imagePreviewUrl}
alt="Preview"
class="mt-2 max-h-48 rounded border"
/>
{:else if newIssue.issue_related_image}
{#await getPublicUrl(newIssue.issue_related_image) then url}
<img
src={url}
alt="Preview"
class="mt-2 max-h-48 rounded border"
/>
{/await}
{/if}
</div>
{:else if col.key === "reported_date"}
<div class="space-y-1">
<label class="block text-sm font-medium text-gray-700"
>Reported Date</label
>
<input
name="reported_date"
class="w-full border px-3 py-2 rounded {errorClass(
'reported_date',
)}"
type="date"
bind:value={newIssue[col.key as keyof Issue]}
/>
{#if $formErrors.reported_date}
<p class="text-red-500 text-xs">
{$formErrors.reported_date}
</p>
{/if}
</div>
{:else if col.key === "need_approval"}
<div class="space-y-1">
<label class="block text-sm font-medium text-gray-700"
>Need Approval</label
>
<select
name="need_approval"
class="w-full border px-3 py-2 rounded"
bind:value={newIssue[col.key as keyof Issue]}
>
<option value="true">Yes</option>
<option value="false">No</option>
</select>
</div>
{:else if col.key === "issue_source"}
<div class="space-y-1">
<label class="block text-sm font-medium text-gray-700"
>Issue Source</label
>
<select
name="issue_source"
class="w-full border px-3 py-2 rounded {errorClass(
'issue_source',
)}"
bind:value={newIssue[col.key as keyof Issue]}
>
<option value="" disabled selected
>Select Source</option
>
{#each issueSource as source}
<option value={source.value}
>{source.label}</option
>
{/each}
</select>
{#if $formErrors.issue_source}
<p class="text-red-500 text-xs">
{$formErrors.issue_source}
</p>
{/if}
</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 Issue]}
>
<option value="" disabled selected
>Select Reporter</option
>
{#each dataUser as reporter}
<option value={reporter.id}
>{reporter.full_name}</option
>
{/each}
</select>
{#if $formErrors.reported_by}
<p class="text-red-500 text-xs">
{$formErrors.reported_by}
</p>
{/if}
</div>
{:else if col.key === "input_by"}
<div class="space-y-1">
<label class="block text-sm font-medium text-gray-700"
>Input By</label
>
<select
name="input_by"
class="w-full border px-3 py-2 rounded {errorClass(
'input_by',
)}"
bind:value={newIssue[col.key as keyof Issue]}
>
<option value="" disabled selected
>Select Input By</option
>
{#each dataUser as input}
<option value={input.id}
>{input.full_name}</option
>
{/each}
</select>
{#if $formErrors.input_by}
<p class="text-red-500 text-xs">
{$formErrors.input_by}
</p>
{/if}
</div>
{:else if col.key === "area_of_villa"}
<div class="space-y-1">
<label class="block text-sm font-medium text-gray-700"
>Area of Villa</label
>
<select
name="area_of_villa"
class="w-full border px-3 py-2 rounded {errorClass(
'area_of_villa',
)}"
bind:value={newIssue[col.key as keyof Issue]}
>
<option value="" disabled selected
>Select Area</option
>
{#each areaOfVilla as area}
<option value={area.value}>{area.label}</option>
{/each}
</select>
{#if $formErrors.area_of_villa}
<p class="text-red-500 text-xs">
{$formErrors.area_of_villa}
</p>
{/if}
</div>
{:else if col.key === "issue_type"}
<div class="space-y-1">
<label class="block text-sm font-medium text-gray-700"
>Issue Type</label
>
<select
name="issue_type"
class="w-full border px-3 py-2 rounded {errorClass(
'issue_type',
)}"
bind:value={newIssue[col.key as keyof Issue]}
>
<option value="" disabled selected
>Select Issue Type</option
>
{#each issueTypes as type}
<option value={type.value}>{type.label}</option>
{/each}
</select>
{#if $formErrors.issue_type}
<p class="text-red-500 text-xs">
{$formErrors.issue_type}
</p>
{/if}
</div>
{:else if col.key === "priority"}
<div class="space-y-1">
<label class="block text-sm font-medium text-gray-700"
>Priority</label
>
<select
name="priority"
class="w-full border px-3 py-2 rounded {errorClass(
'priority',
)}"
bind:value={newIssue[col.key as keyof Issue]}
>
<option value="" disabled selected
>Select Priority</option
>
{#each priority as p}
<option value={p.value}>{p.label}</option>
{/each}
</select>
{#if $formErrors.priority}
<p class="text-red-500 text-xs">
{$formErrors.priority}
</p>
{/if}
</div>
{:else if col.key === "villa_id"}
<div class="space-y-1">
<label class="block text-sm font-medium text-gray-700"
>Villa Name</label
>
<select
name="villa_id"
class="w-full border px-3 py-2 rounded {errorClass(
'villa_id',
)}"
bind:value={newIssue[col.key as keyof Issue]}
>
<option value="" disabled selected
>Select Villa</option
>
{#each dataVilla as villa}
<option value={villa.id}
>{villa.villa_name}</option
>
{/each}
</select>
{#if $formErrors.villa_id}
<p class="text-red-500 text-xs">
{$formErrors.villa_id}
</p>
{/if}
</div>
{:else if col.key === "description_of_the_issue"}
<div class="space-y-1">
<label class="block text-sm font-medium text-gray-700"
>Description of The Issue</label
>
<textarea
name="description_of_the_issue"
class="w-full border px-3 py-2 rounded {errorClass(
'description_of_the_issue',
)}"
bind:value={newIssue[col.key as keyof Issue]}
placeholder={col.title}
rows="4"
></textarea>
{#if $formErrors.description_of_the_issue}
<p class="text-red-500 text-xs">
{$formErrors.description_of_the_issue}
</p>
{/if}
</div>
{: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 Issue]}
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}