From 039c7914a059f1611fb973620ff0626744c03a86 Mon Sep 17 00:00:00 2001 From: arteons Date: Wed, 18 Jun 2025 13:28:37 +0800 Subject: [PATCH] enhance form issue --- src/routes/backoffice/vendor/+page.svelte | 1 - src/routes/issue/+page.svelte | 759 +++++++++------------- 2 files changed, 307 insertions(+), 453 deletions(-) diff --git a/src/routes/backoffice/vendor/+page.svelte b/src/routes/backoffice/vendor/+page.svelte index c185e82..b162c41 100644 --- a/src/routes/backoffice/vendor/+page.svelte +++ b/src/routes/backoffice/vendor/+page.svelte @@ -59,7 +59,6 @@ { key: "contact_pos_tertiary", title: "Tertiary Contact Position" }, { key: "contact_email_tertiary", title: "Tertiary Email" }, { key: "website", title: "Website" }, - { key: "created_at", title: "Created At" }, ]; const excludedKeys = ["id", "created_by", "created_at", "updated_at"]; $: formColumns = columns.filter((col) => !excludedKeys.includes(col.key)); diff --git a/src/routes/issue/+page.svelte b/src/routes/issue/+page.svelte index e7ac5a7..9aece07 100644 --- a/src/routes/issue/+page.svelte +++ b/src/routes/issue/+page.svelte @@ -3,7 +3,6 @@ // For example, you could handle form submission here import { onMount } from "svelte"; import { supabase } from "$lib/supabaseClient"; - import { writable } from "svelte/store"; const priority = [ { label: "Low", value: "Low" }, @@ -12,11 +11,16 @@ { 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" }, + { label: "Guest Assistant", value: "Guest Assistant" }, + { label: "Villa Attendant", value: "Villa Attendant" }, + { label: "Villa Supervisor", value: "Villa Supervisor" }, + { label: "Villa Manager", value: "Villa Manager" }, + { 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 = [ @@ -29,16 +33,10 @@ { 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 - Electrical", value: "Maintenance - Electrical",}, { label: "Maintenance - Plumbing", value: "Maintenance - Plumbing" }, { 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: "Security Concern", value: "Security Concern" }, { label: "Other", value: "Other" }, @@ -72,231 +70,169 @@ { 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: "Kids Room", value: "Kids Room" }, { 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: "Pump Room", value: "Pump Room" }, { 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: "Pool", value: "Pool" }, { 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: "Staff Room", value: "Staff Room" }, + { label: "Transport", value: "Transport" }, { 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" }, - ]; +//InputBy & reportedBy should dropdown data from active employee, from table vb_employee (field employee_name and employee_status = 'Active') const followUp = [ - { label: "Yes", value: "true" }, - { label: "No", value: "false" }, - ]; - - const reportedBy = [ - { label: "Admin", value: "Admin" }, - { label: "Staff", value: "Staff" }, - { label: "Manager", value: "Manager" }, - { label: "Guest", value: "Guest" }, + { label: "Communication Needed by Reservation", value: "true" }, + { label: "Communication Completed", value: "false" }, ]; + let dataUser: { id: string; employee_name: string }[] = []; + let dataVilla: { id: string; villa_name: string }[] = []; let issueImageFile: File | null = null; let issueImageUrl: string = ""; + let isSubmitting = false; function handleFileChange(event: Event): void { const target = event.target as HTMLInputElement; if (target.files && target.files.length > 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; 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 () => { - const { data, error } = await supabase + // Fetch active villas + const { data: villaData, error: villaError } = await supabase .from("vb_villas") - .select("id, villa_name"); + .select("id, villa_name") + .eq("villa_status", "Active"); - if (error) { - console.error("Error fetching villas:", error); - } else if (data) { - dataVilla = data; + if (villaError) { + console.error("Error fetching villas:", villaError); + } else { + dataVilla = villaData || []; } + // Fetch active employees const { data: userData, error: userError } = await supabase .from("vb_employee") - .select("id, employee_name"); + .select("id, employee_name") + .eq("employee_status", "Active"); + if (userError) { - console.error("Error fetching users:", userError); - } else if (userData) { - dataUser = userData; + console.error("Error fetching employees:", userError); + } else { + 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 { 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 (!validateForm(formData)) { - console.error("Form validation failed"); - return; - } + if (issueImageFile) { + const filePath = `issues/${Date.now()}_${issueImageFile.name}`; - // Upload issue image if provided - if (issueImageFile) { - const { data, error } = await supabase.storage - .from("villabugis") - .upload(`issues/${issueImageFile.name}`, issueImageFile); + const { data, error } = await supabase.storage + .from("villabugis") + .upload(filePath, 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) { - console.error("Error uploading image:", error); + alert("Error submitting issue"); + console.error(error); 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!"); + (event.target as HTMLFormElement).reset(); + issueImageUrl = ""; + issueImageFile = null; + } catch (err) { + alert("Unexpected error"); + console.error(err); + } finally { + isSubmitting = false; } } + + async function generateIssueNumber(): Promise { + 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 errors: { [key: string]: string } = {}; - const requiredFields = [ - "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"; + const last = data[0].issue_number?.split("-")[1] || "500"; + const nextNumber = parseInt(last, 10) + 1; + return `ISS-${nextNumber.toString().padStart(6, "0")}`; } @@ -317,304 +253,224 @@

Submit New Issue

- +
- - + Description of Issues* + - {#if $formErrors.description_of_the_issue} -

- {$formErrors.description_of_the_issue} -

- {/if} +
- - - {#if $formErrors.issue_source} -

- {$formErrors.issue_source} -

- {/if} +
- - - - {#each dataVilla as villa} - - {/each} + class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600" required> + + {#each dataVilla as villa} + + {/each} - {#if $formErrors.villa_name} -

- {$formErrors.villa_name} -

- {/if} +
-
- -
- - - -

- Supported formats: JPG, PNG, GIF -

-
- - {#if issueImageUrl} -
-

Preview:

- Issue preview + Issue related image +
+ + + +

+ Supported formats: JPG, PNG, GIF +

- {/if} + {#if issueImageUrl} +
+

Preview:

+ Issue preview +
+ {/if} +
- - + Date Reported + * + - {#if $formErrors.reported_date} -

- {$formErrors.reported_date} -

- {/if} +
- - - {#each dataUser as reporter} - - {/each} - - {#if $formErrors.reported_by} -

- {$formErrors.reported_by} -

- {/if} + + {#each dataUser as reporter} + + {/each} + +
- - URLDrive + + /> +
-
- - - {#each priority as p} - - {/each} - - {#if $formErrors.priority} -

- {$formErrors.priority} -

- {/if} + + {#each priority as p} + + {/each} + +
- - - {#each issueTypes as type} - - {/each} - - {#if $formErrors.issue_type} -

- {$formErrors.issue_type} -

- {/if} + + {#each issueTypes as type} + + {/each} + +
- - - {#each areaOfVilla as area} - - {/each} - - {#if $formErrors.area_of_villa} -

- {$formErrors.area_of_villa} -

- {/if} + + {#each areaOfVilla as area} + + {/each} + +
- - + +
- - - {#if $formErrors.due_issue_date} -

- {$formErrors.due_issue_date} -

- {/if} +
- - - {#each dataUser as input} - - {/each} - - {#if $formErrors.input_by} -

- {$formErrors.input_by} -

- {/if} + + {#each dataUser as input} + + {/each} + +
- - - {#each followUp as follow} - - {/each} - + + {#each followUp as follow} + + {/each} + +
@@ -622,38 +478,36 @@
- - +
- - +
- Guest has agreed issue has been resolved + - +
@@ -662,8 +516,9 @@