enhance form issue
This commit is contained in:
1
src/routes/backoffice/vendor/+page.svelte
vendored
1
src/routes/backoffice/vendor/+page.svelte
vendored
@@ -59,7 +59,6 @@
|
|||||||
{ key: "contact_pos_tertiary", title: "Tertiary Contact Position" },
|
{ key: "contact_pos_tertiary", title: "Tertiary Contact Position" },
|
||||||
{ key: "contact_email_tertiary", title: "Tertiary Email" },
|
{ key: "contact_email_tertiary", title: "Tertiary Email" },
|
||||||
{ key: "website", title: "Website" },
|
{ key: "website", title: "Website" },
|
||||||
{ key: "created_at", title: "Created At" },
|
|
||||||
];
|
];
|
||||||
const excludedKeys = ["id", "created_by", "created_at", "updated_at"];
|
const excludedKeys = ["id", "created_by", "created_at", "updated_at"];
|
||||||
$: formColumns = columns.filter((col) => !excludedKeys.includes(col.key));
|
$: formColumns = columns.filter((col) => !excludedKeys.includes(col.key));
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
// For example, you could handle form submission here
|
// For example, you could handle form submission here
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { supabase } from "$lib/supabaseClient";
|
import { supabase } from "$lib/supabaseClient";
|
||||||
import { writable } from "svelte/store";
|
|
||||||
|
|
||||||
const priority = [
|
const priority = [
|
||||||
{ label: "Low", value: "Low" },
|
{ label: "Low", value: "Low" },
|
||||||
@@ -12,11 +11,16 @@
|
|||||||
{ label: "Critical", value: "Critical" },
|
{ label: "Critical", value: "Critical" },
|
||||||
];
|
];
|
||||||
const issueSource = [
|
const issueSource = [
|
||||||
{ label: "Email", value: "Email" },
|
{ label: "Guest Assistant", value: "Guest Assistant" },
|
||||||
{ label: "Phone Call", value: "Phone Call" },
|
{ label: "Villa Attendant", value: "Villa Attendant" },
|
||||||
{ label: "In-Person", value: "In-Person" },
|
{ label: "Villa Supervisor", value: "Villa Supervisor" },
|
||||||
{ label: "Online Form", value: "Online Form" },
|
{ label: "Villa Manager", value: "Villa Manager" },
|
||||||
{ label: "Other", value: "Other" },
|
{ label: "Operation Manager", value: "Operation Manager" },
|
||||||
|
{ label: "Quality Control Manager", value: "Quality Control Manager" },
|
||||||
|
{ label: "Owner", value: "Owner" },
|
||||||
|
{ label: "Sales/Reservations", value: "Sales/Reservations" },
|
||||||
|
{ label: "Guest", value: "Guest" },
|
||||||
|
{ label: "Agent", value: "Agent" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const issueTypes = [
|
const issueTypes = [
|
||||||
@@ -29,16 +33,10 @@
|
|||||||
{ label: "Cleanliness - Floor", value: "Cleanliness - Floor" },
|
{ label: "Cleanliness - Floor", value: "Cleanliness - Floor" },
|
||||||
{ label: "Cleanliness - Kitchen", value: "Cleanliness - Kitchen" },
|
{ label: "Cleanliness - Kitchen", value: "Cleanliness - Kitchen" },
|
||||||
{ label: "Cleanliness - Bathroom", value: "Cleanliness - Bathroom" },
|
{ label: "Cleanliness - Bathroom", value: "Cleanliness - Bathroom" },
|
||||||
{
|
{ label: "Maintenance - Electrical", value: "Maintenance - Electrical",},
|
||||||
label: "Maintenance - Electrical",
|
|
||||||
value: "Maintenance - Electrical",
|
|
||||||
},
|
|
||||||
{ label: "Maintenance - Plumbing", value: "Maintenance - Plumbing" },
|
{ label: "Maintenance - Plumbing", value: "Maintenance - Plumbing" },
|
||||||
{ label: "Maintenance - HVAC", value: "Maintenance - HVAC" },
|
{ label: "Maintenance - HVAC", value: "Maintenance - HVAC" },
|
||||||
{
|
{ label: "Maintenance - Structural", value: "Maintenance - Structural" },
|
||||||
label: "Maintenance - Structural",
|
|
||||||
value: "Maintenance - Structural",
|
|
||||||
},
|
|
||||||
{ label: "Safety Issue", value: "Safety Issue" },
|
{ label: "Safety Issue", value: "Safety Issue" },
|
||||||
{ label: "Security Concern", value: "Security Concern" },
|
{ label: "Security Concern", value: "Security Concern" },
|
||||||
{ label: "Other", value: "Other" },
|
{ label: "Other", value: "Other" },
|
||||||
@@ -72,231 +70,169 @@
|
|||||||
{ label: "Bedroom 1", value: "Bedroom 1" },
|
{ label: "Bedroom 1", value: "Bedroom 1" },
|
||||||
{ label: "Bedroom 2", value: "Bedroom 2" },
|
{ label: "Bedroom 2", value: "Bedroom 2" },
|
||||||
{ label: "Bedroom 3", value: "Bedroom 3" },
|
{ label: "Bedroom 3", value: "Bedroom 3" },
|
||||||
{ label: "Ceiling", value: "Ceiling" },
|
{ label: "Kids Room", value: "Kids Room" },
|
||||||
{ label: "Dining Area", value: "Dining Area" },
|
|
||||||
{ label: "Door", value: "Door" },
|
|
||||||
{ label: "Entrance", value: "Entrance" },
|
|
||||||
{ label: "Garden", value: "Garden" },
|
{ label: "Garden", value: "Garden" },
|
||||||
{ label: "General", value: "General" },
|
|
||||||
{ label: "Glass", value: "Glass" },
|
|
||||||
{ label: "Hallway", value: "Hallway" },
|
|
||||||
{ label: "Kitchen", value: "Kitchen" },
|
{ label: "Kitchen", value: "Kitchen" },
|
||||||
{ label: "Laundry Area", value: "Laundry Area" },
|
{ label: "Pump Room", value: "Pump Room" },
|
||||||
{ label: "Living Room", value: "Living Room" },
|
{ label: "Living Room", value: "Living Room" },
|
||||||
{ label: "Outdoor Area", value: "Outdoor Area" },
|
{ label: "Outdoor Area", value: "Outdoor Area" },
|
||||||
{ label: "Parking Area", value: "Parking Area" },
|
{ label: "Parking Area", value: "Parking Area" },
|
||||||
{ label: "Pool Area", value: "Pool Area" },
|
{ label: "Pool", value: "Pool" },
|
||||||
{ label: "Roof", value: "Roof" },
|
{ label: "Roof", value: "Roof" },
|
||||||
{ label: "Stairs", value: "Stairs" },
|
{ label: "Stairs", value: "Stairs" },
|
||||||
{ label: "Storage", value: "Storage" },
|
{ label: "Storage", value: "Storage" },
|
||||||
{ label: "Terrace", value: "Terrace" },
|
{ label: "Staff Room", value: "Staff Room" },
|
||||||
{ label: "Toilet", value: "Toilet" },
|
{ label: "Transport", value: "Transport" },
|
||||||
{ label: "Wall", value: "Wall" },
|
|
||||||
{ label: "Window", value: "Window" },
|
{ label: "Window", value: "Window" },
|
||||||
{ label: "Others", value: "Others" },
|
{ label: "Others", value: "Others" },
|
||||||
];
|
];
|
||||||
|
//InputBy & reportedBy should dropdown data from active employee, from table vb_employee (field employee_name and employee_status = 'Active')
|
||||||
const inputBy = [
|
|
||||||
{ label: "Admin", value: "Admin" },
|
|
||||||
{ label: "Staff", value: "Staff" },
|
|
||||||
{ label: "Manager", value: "Manager" },
|
|
||||||
{ label: "Guest", value: "Guest" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const followUp = [
|
const followUp = [
|
||||||
{ label: "Yes", value: "true" },
|
{ label: "Communication Needed by Reservation", value: "true" },
|
||||||
{ label: "No", value: "false" },
|
{ label: "Communication Completed", value: "false" },
|
||||||
];
|
|
||||||
|
|
||||||
const reportedBy = [
|
|
||||||
{ label: "Admin", value: "Admin" },
|
|
||||||
{ label: "Staff", value: "Staff" },
|
|
||||||
{ label: "Manager", value: "Manager" },
|
|
||||||
{ label: "Guest", value: "Guest" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let dataUser: { id: string; employee_name: string }[] = [];
|
||||||
|
let dataVilla: { id: string; villa_name: string }[] = [];
|
||||||
let issueImageFile: File | null = null;
|
let issueImageFile: File | null = null;
|
||||||
let issueImageUrl: string = "";
|
let issueImageUrl: string = "";
|
||||||
|
let isSubmitting = false;
|
||||||
|
|
||||||
function handleFileChange(event: Event): void {
|
function handleFileChange(event: Event): void {
|
||||||
const target = event.target as HTMLInputElement;
|
const target = event.target as HTMLInputElement;
|
||||||
if (target.files && target.files.length > 0) {
|
if (target.files && target.files.length > 0) {
|
||||||
const file = target.files[0];
|
const file = target.files[0];
|
||||||
|
if (!["image/jpeg", "image/png", "image/webp"].includes(file.type)) {
|
||||||
|
alert("Only JPG, PNG, or WEBP images are allowed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (file.size > 2 * 1024 * 1024) {
|
||||||
|
alert("Image must be less than 2MB.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
issueImageFile = file;
|
issueImageFile = file;
|
||||||
issueImageUrl = URL.createObjectURL(file);
|
issueImageUrl = URL.createObjectURL(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Villa = {
|
|
||||||
id: string;
|
|
||||||
villa_name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type User = {
|
|
||||||
id: string;
|
|
||||||
employee_name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
let dataVilla: Villa[] = [];
|
|
||||||
let dataUser: User[] = [];
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const { data, error } = await supabase
|
// Fetch active villas
|
||||||
|
const { data: villaData, error: villaError } = await supabase
|
||||||
.from("vb_villas")
|
.from("vb_villas")
|
||||||
.select("id, villa_name");
|
.select("id, villa_name")
|
||||||
|
.eq("villa_status", "Active");
|
||||||
|
|
||||||
if (error) {
|
if (villaError) {
|
||||||
console.error("Error fetching villas:", error);
|
console.error("Error fetching villas:", villaError);
|
||||||
} else if (data) {
|
} else {
|
||||||
dataVilla = data;
|
dataVilla = villaData || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch active employees
|
||||||
const { data: userData, error: userError } = await supabase
|
const { data: userData, error: userError } = await supabase
|
||||||
.from("vb_employee")
|
.from("vb_employee")
|
||||||
.select("id, employee_name");
|
.select("id, employee_name")
|
||||||
|
.eq("employee_status", "Active");
|
||||||
|
|
||||||
if (userError) {
|
if (userError) {
|
||||||
console.error("Error fetching users:", userError);
|
console.error("Error fetching employees:", userError);
|
||||||
} else if (userData) {
|
} else {
|
||||||
dataUser = userData;
|
dataUser = userData || [];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
type Issue = {
|
|
||||||
name: string;
|
|
||||||
villa_name: string;
|
|
||||||
area_of_villa: string;
|
|
||||||
priority: string;
|
|
||||||
issue_type: string;
|
|
||||||
issue_number: string;
|
|
||||||
move_issue: boolean;
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
type issueInsert = {
|
|
||||||
name: string;
|
|
||||||
villa_id: string;
|
|
||||||
area_of_villa: string;
|
|
||||||
priority: string;
|
|
||||||
issue_type: string;
|
|
||||||
issue_number: string;
|
|
||||||
move_issue: boolean;
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function handleSubmit(event: Event): Promise<void> {
|
async function handleSubmit(event: Event): Promise<void> {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
isSubmitting = true;
|
||||||
|
|
||||||
const formData = new FormData(event.target as HTMLFormElement);
|
try {
|
||||||
|
const formData = new FormData(event.target as HTMLFormElement);
|
||||||
|
|
||||||
// Validate form data
|
if (issueImageFile) {
|
||||||
if (!validateForm(formData)) {
|
const filePath = `issues/${Date.now()}_${issueImageFile.name}`;
|
||||||
console.error("Form validation failed");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload issue image if provided
|
const { data, error } = await supabase.storage
|
||||||
if (issueImageFile) {
|
.from("villabugis")
|
||||||
const { data, error } = await supabase.storage
|
.upload(filePath, issueImageFile);
|
||||||
.from("villabugis")
|
|
||||||
.upload(`issues/${issueImageFile.name}`, issueImageFile);
|
if (error) {
|
||||||
|
alert("Image upload failed");
|
||||||
|
console.error(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: publicData } = supabase.storage
|
||||||
|
.from("villabugis")
|
||||||
|
.getPublicUrl(filePath);
|
||||||
|
|
||||||
|
issueImageUrl = publicData.publicUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const issue_number = await generateIssueNumber();
|
||||||
|
|
||||||
|
const issue = {
|
||||||
|
issue_number,
|
||||||
|
name: formData.get("description_of_the_issue") as string,
|
||||||
|
villa_id: formData.get("villa_name") 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,
|
||||||
|
issue_source: formData.get("issue_source") as string,
|
||||||
|
reported_date: formData.get("reported_date") as string,
|
||||||
|
reported_by: formData.get("reported_by") as string,
|
||||||
|
input_by: formData.get("input_by") as string,
|
||||||
|
resolution: formData.get("resloution") as string,
|
||||||
|
guest_has_aggreed_issue_has_been_resolved: formData.get("guest_has_aggreed_issue_has_been_resolved") as string,
|
||||||
|
follow_up: formData.get("follow_up") as string,
|
||||||
|
guest_communication: formData.get("guest_communication") as string,
|
||||||
|
issue_related_image: issueImageUrl,
|
||||||
|
url_drive: formData.get("url_drive") as string,
|
||||||
|
created_at: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Insert into Supabase
|
||||||
|
const { error } = await supabase.from("vb_issues").insert([issue]);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error("Error uploading image:", error);
|
alert("Error submitting issue");
|
||||||
|
console.error(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
issueImageUrl = data.path; // Assuming data.Key contains the URL or path to the uploaded image
|
// POST to webhook
|
||||||
}
|
await fetch("https://flow.catalis.app/webhook/vb-issuecreate", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(issue)
|
||||||
|
});
|
||||||
|
|
||||||
const issue: issueInsert = {
|
|
||||||
name: formData.get("name") as string,
|
|
||||||
villa_id: formData.get("villa_name") 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,
|
|
||||||
issue_number: formData.get("issue_number") as string,
|
|
||||||
move_issue: formData.get("move_issue") === "false",
|
|
||||||
description_of_the_issue: formData.get(
|
|
||||||
"description_of_the_issue",
|
|
||||||
) as string,
|
|
||||||
reported_date: formData.get("reported_date") as string,
|
|
||||||
issue_related_image: issueImageUrl,
|
|
||||||
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") ===
|
|
||||||
"false",
|
|
||||||
follow_up: formData.get("follow_up") === "true" ? true : false,
|
|
||||||
need_approval: false, // Set this based on your logic
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from("vb_issues")
|
|
||||||
.insert([issue]);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error("Error submitting issue:", error);
|
|
||||||
} else {
|
|
||||||
console.log("Issue submitted successfully:", data);
|
|
||||||
alert("Issue submitted successfully!");
|
alert("Issue submitted successfully!");
|
||||||
|
(event.target as HTMLFormElement).reset();
|
||||||
|
issueImageUrl = "";
|
||||||
|
issueImageFile = null;
|
||||||
|
} catch (err) {
|
||||||
|
alert("Unexpected error");
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
isSubmitting = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function generateIssueNumber(): Promise<string> {
|
||||||
|
const { data } = await supabase
|
||||||
|
.from("vb_issues")
|
||||||
|
.select("issue_number")
|
||||||
|
.order("created_at", { ascending: false })
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
export let formErrors = writable<{ [key: string]: string }>({});
|
if (!data || data.length === 0) {
|
||||||
|
return "ISS-000500";
|
||||||
|
}
|
||||||
|
|
||||||
function validateForm(formData: FormData): boolean {
|
const last = data[0].issue_number?.split("-")[1] || "500";
|
||||||
const errors: { [key: string]: string } = {};
|
const nextNumber = parseInt(last, 10) + 1;
|
||||||
const requiredFields = [
|
return `ISS-${nextNumber.toString().padStart(6, "0")}`;
|
||||||
"description_of_the_issue",
|
|
||||||
"issue_source",
|
|
||||||
"villa_name",
|
|
||||||
"reported_date",
|
|
||||||
"reported_by",
|
|
||||||
"priority",
|
|
||||||
"issue_type",
|
|
||||||
"due_issue_date",
|
|
||||||
"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";
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -317,304 +253,224 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<h2 class="text-2xl font-semibold text-center">Submit New Issue</h2>
|
<h2 class="text-2xl font-semibold text-center">Submit New Issue</h2>
|
||||||
|
|
||||||
<!-- 2 Column Grid -->
|
<!-- 2 Column Grid -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
<!-- Left Column -->
|
<!-- Left Column -->
|
||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1"
|
<label class="block text-sm font-medium mb-1">
|
||||||
>Description of Issues<span class="text-red-500">*</span
|
Description of Issues<span class="text-red-500">*</span>
|
||||||
></label
|
<input
|
||||||
>
|
|
||||||
<input
|
|
||||||
name="description_of_the_issue"
|
name="description_of_the_issue"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Tell detail of the issue"
|
placeholder="Tell detail of the issue"
|
||||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 {errorClass(
|
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400"
|
||||||
'description_of_the_issue',
|
required
|
||||||
)}"
|
|
||||||
/>
|
/>
|
||||||
{#if $formErrors.description_of_the_issue}
|
</label>
|
||||||
<p class="text-sm text-red-500 mt-1">
|
|
||||||
{$formErrors.description_of_the_issue}
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1"
|
<label class="block text-sm font-medium mb-1">Issue Source
|
||||||
>Issue Source<span class="text-red-500">*</span></label
|
<span class="text-red-500">*</span>
|
||||||
>
|
<select name="issue_source" class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600" required>
|
||||||
<select
|
<option value="" disabled selected>Select option...</option>
|
||||||
name="issue_source"
|
{#each issueSource as source}
|
||||||
class={`w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600 ${errorClass("issue_source")}`}
|
<option value={source.value}>{source.label}</option>
|
||||||
>
|
{/each}
|
||||||
<option value="" disabled selected
|
</select>
|
||||||
>Select option...</option
|
</label>
|
||||||
>
|
|
||||||
{#each issueSource as source}
|
|
||||||
<option value={source.value}>{source.label}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
{#if $formErrors.issue_source}
|
|
||||||
<p class="text-sm text-red-500 mt-1">
|
|
||||||
{$formErrors.issue_source}
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1"
|
<label class="block text-sm font-medium mb-1">
|
||||||
>Villa Name<span class="text-red-500">*</span></label
|
Villa Name
|
||||||
>
|
<span class="text-red-500">*</span>
|
||||||
<select
|
<select
|
||||||
name="villa_name"
|
name="villa_name"
|
||||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600 {errorClass(
|
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600" required>
|
||||||
'villa_name',
|
<option value="" disabled selected>Select option...</option>
|
||||||
)}"
|
{#each dataVilla as villa}
|
||||||
>
|
<option value={villa.id}>{villa.villa_name}</option>
|
||||||
<option value="" disabled selected
|
{/each}
|
||||||
>Select option...</option
|
|
||||||
>
|
|
||||||
{#each dataVilla as villa}
|
|
||||||
<option value={villa.id}>{villa.villa_name}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
</select>
|
||||||
{#if $formErrors.villa_name}
|
</label>
|
||||||
<p class="text-sm text-red-500 mt-1">
|
|
||||||
{$formErrors.villa_name}
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1"
|
<label class="block text-sm font-medium mb-1">
|
||||||
>Issue related image</label
|
Issue related image
|
||||||
>
|
<div class="w-full border-2 border-dashed rounded-xl px-4 py-10 text-center text-gray-400 cursor-pointer hover:bg-gray-50 transition relative">
|
||||||
<div
|
<input
|
||||||
class="w-full border-2 border-dashed rounded-xl px-4 py-10 text-center text-gray-400 cursor-pointer hover:bg-gray-50 transition relative"
|
name="issue_related_image"
|
||||||
>
|
type="file"
|
||||||
<input
|
accept="image/*"
|
||||||
name="issue_related_image"
|
class="hidden"
|
||||||
type="file"
|
id="issue_image"
|
||||||
accept="image/*"
|
on:change={handleFileChange}
|
||||||
class="hidden"
|
|
||||||
id="issue_image"
|
|
||||||
on:change={handleFileChange}
|
|
||||||
/>
|
|
||||||
<label for="issue_image" class="cursor-pointer">
|
|
||||||
<span class="block mb-2">Click to upload</span>
|
|
||||||
<span class="text-xs">or drag and drop</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<p class="mt-2 text-xs">
|
|
||||||
Supported formats: JPG, PNG, GIF
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if issueImageUrl}
|
|
||||||
<div class="mt-4">
|
|
||||||
<p class="text-sm text-gray-600 mb-2">Preview:</p>
|
|
||||||
<img
|
|
||||||
src={issueImageUrl}
|
|
||||||
alt="Issue preview"
|
|
||||||
class="w-full h-48 object-cover rounded-xl shadow-sm"
|
|
||||||
/>
|
/>
|
||||||
|
<label for="issue_image" class="cursor-pointer">
|
||||||
|
<span class="block mb-2">Click to upload</span>
|
||||||
|
<span class="text-xs">or drag and drop</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<p class="mt-2 text-xs">
|
||||||
|
Supported formats: JPG, PNG, GIF
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{#if issueImageUrl}
|
||||||
|
<div class="mt-4">
|
||||||
|
<p class="text-sm text-gray-600 mb-2">Preview:</p>
|
||||||
|
<img
|
||||||
|
src={issueImageUrl}
|
||||||
|
alt="Issue preview"
|
||||||
|
class="w-full h-48 object-cover rounded-xl shadow-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1"
|
<label class="block text-sm font-medium mb-1">
|
||||||
>Date Reported<span class="text-red-500">*</span></label
|
Date Reported
|
||||||
>
|
<span class="text-red-500">*</span>
|
||||||
<input
|
<input
|
||||||
name="reported_date"
|
name="reported_date"
|
||||||
type="date"
|
type="date"
|
||||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 {errorClass(
|
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400"
|
||||||
'reported_date',
|
required
|
||||||
)}"
|
|
||||||
/>
|
/>
|
||||||
{#if $formErrors.reported_date}
|
</label>
|
||||||
<p class="text-sm text-red-500 mt-1">
|
|
||||||
{$formErrors.reported_date}
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1"
|
<label class="block text-sm font-medium mb-1">
|
||||||
>Reported By<span class="text-red-500">*</span></label
|
Reported By
|
||||||
>
|
<span class="text-red-500">*</span>
|
||||||
<select
|
<select
|
||||||
name="reported_by"
|
name="reported_by"
|
||||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 text-gray-600 {errorClass(
|
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 text-gray-600"
|
||||||
'reported_by',
|
required
|
||||||
)}"
|
|
||||||
>
|
|
||||||
<option value="" disabled selected
|
|
||||||
>Select option...</option
|
|
||||||
>
|
>
|
||||||
{#each dataUser as reporter}
|
<option value="" disabled selected>Select option...</option>
|
||||||
<option value={reporter.id}>
|
{#each dataUser as reporter}
|
||||||
{reporter.employee_name}
|
<option value={reporter.id}>
|
||||||
</option>
|
{reporter.employee_name}
|
||||||
{/each}
|
</option>
|
||||||
</select>
|
{/each}
|
||||||
{#if $formErrors.reported_by}
|
</select>
|
||||||
<p class="text-sm text-red-500 mt-1">
|
</label>
|
||||||
{$formErrors.reported_by}
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1"
|
<label class="block text-sm font-medium mb-1">URLDrive
|
||||||
>URLDrive</label
|
<input
|
||||||
>
|
|
||||||
<input
|
|
||||||
name="url_drive"
|
name="url_drive"
|
||||||
type="url"
|
type="url"
|
||||||
placeholder="Enter URL"
|
placeholder="Enter URL"
|
||||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400"
|
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400"
|
||||||
/>
|
/>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right Column -->
|
<!-- Right Column -->
|
||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1"
|
<label class="block text-sm font-medium mb-1">
|
||||||
>Priority<span class="text-red-500">*</span></label
|
Priority
|
||||||
>
|
<span class="text-red-500">*</span>
|
||||||
<select
|
<select
|
||||||
name="priority"
|
name="priority"
|
||||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600 {errorClass(
|
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600"
|
||||||
'priority',
|
required
|
||||||
)}"
|
|
||||||
>
|
|
||||||
<option value="" disabled selected
|
|
||||||
>Select option...</option
|
|
||||||
>
|
>
|
||||||
{#each priority as p}
|
<option value="" disabled selected>Select option...</option>
|
||||||
<option value={p.value}>{p.label}</option>
|
{#each priority as p}
|
||||||
{/each}
|
<option value={p.value}>{p.label}</option>
|
||||||
</select>
|
{/each}
|
||||||
{#if $formErrors.priority}
|
</select>
|
||||||
<p class="text-sm text-red-500 mt-1">
|
</label>
|
||||||
{$formErrors.priority}
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1"
|
<label class="block text-sm font-medium mb-1">
|
||||||
>Issue Type<span class="text-red-500">*</span></label
|
Issue Type<span class="text-red-500">*</span>
|
||||||
>
|
<select
|
||||||
<select
|
name="issue_type"
|
||||||
name="issue_type"
|
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600"
|
||||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600 {errorClass(
|
required
|
||||||
'issue_type',
|
|
||||||
)}"
|
|
||||||
>
|
|
||||||
<option value="" disabled selected
|
|
||||||
>Select option...</option
|
|
||||||
>
|
>
|
||||||
{#each issueTypes as type}
|
<option value="" disabled selected>Select option...</option>
|
||||||
<option value={type.value}>{type.label}</option>
|
{#each issueTypes as type}
|
||||||
{/each}
|
<option value={type.value}>{type.label}</option>
|
||||||
</select>
|
{/each}
|
||||||
{#if $formErrors.issue_type}
|
</select>
|
||||||
<p class="text-sm text-red-500 mt-1">
|
</label>
|
||||||
{$formErrors.issue_type}
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1"
|
<label class="block text-sm font-medium mb-1">
|
||||||
>Area of Villa<span class="text-red-500">*</span></label
|
Area of Villa<span class="text-red-500">*</span>
|
||||||
>
|
<select
|
||||||
<select
|
name="area_of_villa"
|
||||||
name="area_of_villa"
|
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600"
|
||||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600 {errorClass(
|
required
|
||||||
'area_of_villa',
|
|
||||||
)}"
|
|
||||||
>
|
|
||||||
<option value="" disabled selected
|
|
||||||
>Select option...</option
|
|
||||||
>
|
>
|
||||||
{#each areaOfVilla as area}
|
<option value="" disabled selected>Select option...</option>
|
||||||
<option value={area.value}>{area.label}</option>
|
{#each areaOfVilla as area}
|
||||||
{/each}
|
<option value={area.value}>{area.label}</option>
|
||||||
</select>
|
{/each}
|
||||||
{#if $formErrors.area_of_villa}
|
</select>
|
||||||
<p class="text-sm text-red-500 mt-1">
|
</label>
|
||||||
{$formErrors.area_of_villa}
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1"
|
<label class="block text-sm font-medium mb-1">
|
||||||
>Customer / Guest Name</label
|
Customer / Guest Name
|
||||||
>
|
<input
|
||||||
<input
|
name="name"
|
||||||
name="name"
|
type="text"
|
||||||
type="text"
|
placeholder="Enter text"
|
||||||
placeholder="Enter text"
|
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400"
|
||||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400"
|
/>
|
||||||
/>
|
</label>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1"
|
<label class="block text-sm font-medium mb-1">
|
||||||
>Due Issue Date<span class="text-red-500">*</span
|
Due Issue Date<span class="text-red-500">*</span>
|
||||||
></label
|
<input
|
||||||
>
|
name="due_issue_date"
|
||||||
<input
|
type="date"
|
||||||
name="due_issue_date"
|
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400"
|
||||||
type="date"
|
/>
|
||||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 {errorClass(
|
</label>
|
||||||
'due_issue_date',
|
|
||||||
)}"
|
|
||||||
/>
|
|
||||||
{#if $formErrors.due_issue_date}
|
|
||||||
<p class="text-sm text-red-500 mt-1">
|
|
||||||
{$formErrors.due_issue_date}
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1"
|
<label class="block text-sm font-medium mb-1">
|
||||||
>Input By<span class="text-red-500">*</span></label
|
Input By<span class="text-red-500">*</span>
|
||||||
>
|
<select
|
||||||
<select
|
name="input_by"
|
||||||
name="input_by"
|
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600"
|
||||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600 {errorClass(
|
required
|
||||||
'input_by',
|
|
||||||
)}"
|
|
||||||
>
|
|
||||||
<option value="" disabled selected
|
|
||||||
>Select option...</option
|
|
||||||
>
|
>
|
||||||
{#each dataUser as input}
|
<option value="" disabled selected
|
||||||
<option value={input.id}>{input.employee_name}</option>
|
>Select option...</option
|
||||||
{/each}
|
>
|
||||||
</select>
|
{#each dataUser as input}
|
||||||
{#if $formErrors.input_by}
|
<option value={input.id}>{input.employee_name}</option>
|
||||||
<p class="text-sm text-red-500 mt-1">
|
{/each}
|
||||||
{$formErrors.input_by}
|
</select>
|
||||||
</p>
|
</label>
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1"
|
<label class="block text-sm font-medium mb-1">
|
||||||
>Follow Up</label
|
Follow Up
|
||||||
>
|
<select
|
||||||
<select
|
name="follow_up"
|
||||||
name="follow_up"
|
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600"
|
||||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600"
|
|
||||||
>
|
|
||||||
<option value="" disabled selected
|
|
||||||
>Select option...</option
|
|
||||||
>
|
>
|
||||||
{#each followUp as follow}
|
<option value="" disabled selected
|
||||||
<option value={follow.value}>{follow.label}</option>
|
>Select option...</option
|
||||||
{/each}
|
>
|
||||||
</select>
|
{#each followUp as follow}
|
||||||
|
<option value={follow.value}>{follow.label}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -622,38 +478,36 @@
|
|||||||
<!-- Full Width Fields -->
|
<!-- Full Width Fields -->
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1"
|
<label class="block text-sm font-medium mb-1">
|
||||||
>Resolved How?</label
|
Resolved How?
|
||||||
>
|
<textarea
|
||||||
<textarea
|
name="resolution"
|
||||||
name="resolution"
|
rows="3"
|
||||||
rows="3"
|
placeholder="How you resolve? e.g. 'copy to project'"
|
||||||
placeholder="How you resolve? e.g. 'copy to project'"
|
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400"
|
||||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400"
|
></textarea>
|
||||||
></textarea>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1"
|
<label class="block text-sm font-medium mb-1">
|
||||||
>Guest Communication</label
|
Guest Communication
|
||||||
>
|
<textarea
|
||||||
<textarea
|
name="guest_communication"
|
||||||
name="guest_communication"
|
rows="3"
|
||||||
rows="3"
|
placeholder="Communication with guest while still in the villa"
|
||||||
placeholder="Communication with guest while still in the villa"
|
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400"
|
||||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400"
|
></textarea>
|
||||||
></textarea>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<input
|
<label class="text-sm">Guest has agreed issue has been resolved
|
||||||
|
<input
|
||||||
name="guest_has_aggreed_issue_has_been_resolved"
|
name="guest_has_aggreed_issue_has_been_resolved"
|
||||||
value="false"
|
value="false"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="guest_agreed"
|
|
||||||
class="h-4 w-4 rounded border-gray-300"
|
class="h-4 w-4 rounded border-gray-300"
|
||||||
/>
|
/>
|
||||||
<label for="guest_agreed" class="text-sm"
|
</label>
|
||||||
>Guest has agreed issue has been resolved</label
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -662,8 +516,9 @@
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="bg-purple-600 text-white px-8 py-3 rounded-xl hover:bg-purple-700 transition-all font-medium shadow-md"
|
class="bg-purple-600 text-white px-8 py-3 rounded-xl hover:bg-purple-700 transition-all font-medium shadow-md"
|
||||||
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
Submit
|
{isSubmitting ? "Submitting..." : "Submit"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user