penambahan fitur relasi to table user
This commit is contained in:
@@ -1,621 +0,0 @@
|
||||
<script lang="ts">
|
||||
// This is a placeholder for any script you might want to add
|
||||
// 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" },
|
||||
{ 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 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" },
|
||||
];
|
||||
|
||||
let issueImageFile: File | null = null;
|
||||
let issueImageUrl: string = "";
|
||||
|
||||
function handleFileChange(event: Event): void {
|
||||
const target = event.target as HTMLInputElement;
|
||||
if (target.files && target.files.length > 0) {
|
||||
const file = target.files[0];
|
||||
issueImageFile = file;
|
||||
issueImageUrl = URL.createObjectURL(file);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
async function handleSubmit(event: Event): Promise<void> {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.target as HTMLFormElement);
|
||||
|
||||
// Validate form data
|
||||
if (!validateForm(formData)) {
|
||||
console.error("Form validation failed");
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload issue image if provided
|
||||
if (issueImageFile) {
|
||||
const { data, error } = await supabase.storage
|
||||
.from("villabugis")
|
||||
.upload(`issue/${issueImageFile.name}`, issueImageFile);
|
||||
|
||||
if (error) {
|
||||
console.error("Error uploading image:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
issueImageUrl = data.path; // Assuming data.Key contains the URL or path to the uploaded image
|
||||
}
|
||||
|
||||
const issue: Issue = {
|
||||
name: formData.get("name") as string,
|
||||
villa_name: 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("issues").insert([issue]);
|
||||
|
||||
if (error) {
|
||||
console.error("Error submitting issue:", error);
|
||||
} else {
|
||||
console.log("Issue submitted successfully:", data);
|
||||
alert("Issue submitted successfully!");
|
||||
}
|
||||
}
|
||||
|
||||
export let formErrors = writable<{ [key: string]: string }>({});
|
||||
|
||||
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";
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<form
|
||||
class="max-w-6xl mx-auto bg-white p-8 rounded-2xl shadow-xl space-y-8 text-gray-800"
|
||||
on:submit|preventDefault={handleSubmit}
|
||||
>
|
||||
<!-- Title -->
|
||||
<h2 class="text-2xl font-semibold">Submit New Issue</h2>
|
||||
|
||||
<!-- 2 Column Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<!-- Left Column -->
|
||||
<div class="space-y-5">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Description of Issues<span class="text-red-500">*</span
|
||||
></label
|
||||
>
|
||||
<input
|
||||
name="description_of_the_issue"
|
||||
type="text"
|
||||
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(
|
||||
'description_of_the_issue',
|
||||
)}"
|
||||
/>
|
||||
{#if $formErrors.description_of_the_issue}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.description_of_the_issue}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Issue Source<span class="text-red-500">*</span></label
|
||||
>
|
||||
<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 ${errorClass("issue_source")}`}
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select option...</option
|
||||
>
|
||||
{#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>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Villa Name<span class="text-red-500">*</span></label
|
||||
>
|
||||
<select
|
||||
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(
|
||||
'villa_name',
|
||||
)}"
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select option...</option
|
||||
>
|
||||
{#each dataVilla as villa}
|
||||
<option value={villa.id}>{villa.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.villa_name}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.villa_name}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Issue related image</label
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<input
|
||||
name="issue_related_image"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Date Reported<span class="text-red-500">*</span></label
|
||||
>
|
||||
<input
|
||||
name="reported_date"
|
||||
type="date"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 {errorClass(
|
||||
'reported_date',
|
||||
)}"
|
||||
/>
|
||||
{#if $formErrors.reported_date}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.reported_date}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Reported By<span class="text-red-500">*</span></label
|
||||
>
|
||||
<select
|
||||
name="reported_by"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 text-gray-600 {errorClass(
|
||||
'reported_by',
|
||||
)}"
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select option...</option
|
||||
>
|
||||
{#each reportedBy as reporter}
|
||||
<option value={reporter.value}>
|
||||
{reporter.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.reported_by}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.reported_by}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>URLDrive</label
|
||||
>
|
||||
<input
|
||||
name="url_drive"
|
||||
type="url"
|
||||
placeholder="Enter URL"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column -->
|
||||
<div class="space-y-5">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Priority<span class="text-red-500">*</span></label
|
||||
>
|
||||
<select
|
||||
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(
|
||||
'priority',
|
||||
)}"
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select option...</option
|
||||
>
|
||||
{#each priority as p}
|
||||
<option value={p.value}>{p.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.priority}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.priority}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Issue Type<span class="text-red-500">*</span></label
|
||||
>
|
||||
<select
|
||||
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 {errorClass(
|
||||
'issue_type',
|
||||
)}"
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select option...</option
|
||||
>
|
||||
{#each issueTypes as type}
|
||||
<option value={type.value}>{type.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.issue_type}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.issue_type}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Area of Villa<span class="text-red-500">*</span></label
|
||||
>
|
||||
<select
|
||||
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 {errorClass(
|
||||
'area_of_villa',
|
||||
)}"
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select option...</option
|
||||
>
|
||||
{#each areaOfVilla as area}
|
||||
<option value={area.value}>{area.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.area_of_villa}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.area_of_villa}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Customer / Guest Name</label
|
||||
>
|
||||
<input
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Enter text"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Due Issue Date<span class="text-red-500">*</span
|
||||
></label
|
||||
>
|
||||
<input
|
||||
name="due_issue_date"
|
||||
type="date"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 {errorClass(
|
||||
'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>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Input By<span class="text-red-500">*</span></label
|
||||
>
|
||||
<select
|
||||
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 {errorClass(
|
||||
'input_by',
|
||||
)}"
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select option...</option
|
||||
>
|
||||
{#each inputBy as input}
|
||||
<option value={input.value}>{input.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.input_by}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.input_by}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Follow Up</label
|
||||
>
|
||||
<select
|
||||
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"
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select option...</option
|
||||
>
|
||||
{#each followUp as follow}
|
||||
<option value={follow.value}>{follow.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Full Width Fields -->
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Resolved How?</label
|
||||
>
|
||||
<textarea
|
||||
name="resolution"
|
||||
rows="3"
|
||||
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"
|
||||
></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Guest Communication</label
|
||||
>
|
||||
<textarea
|
||||
name="guest_communication"
|
||||
rows="3"
|
||||
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"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
name="guest_has_aggreed_issue_has_been_resolved"
|
||||
value="false"
|
||||
type="checkbox"
|
||||
id="guest_agreed"
|
||||
class="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
<label for="guest_agreed" class="text-sm"
|
||||
>Guest has agreed issue has been resolved</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="text-center pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-purple-600 text-white px-8 py-3 rounded-xl hover:bg-purple-700 transition-all font-medium shadow-md"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -1,364 +0,0 @@
|
||||
<script lang="ts">
|
||||
// This is a placeholder for any script you might want to add
|
||||
// For example, you could handle form submission here
|
||||
import { onMount } from "svelte";
|
||||
import { supabase } from "$lib/supabaseClient";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
type Timesheets = {
|
||||
id: string;
|
||||
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;
|
||||
created_at?: Date;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit(event: Event): Promise<void> {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.target as HTMLFormElement);
|
||||
|
||||
// Validate form data
|
||||
if (!validateForm(formData)) {
|
||||
console.error("Form validation failed");
|
||||
return;
|
||||
}
|
||||
|
||||
const timesheets: 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),
|
||||
// total_hours_work can be calculated based on date_in and date_out
|
||||
total_hours_work:
|
||||
Math.abs(
|
||||
new Date(formData.get("date_in") as string).getTime() -
|
||||
new Date(formData.get("date_out") as string).getTime(),
|
||||
) /
|
||||
(1000 * 60 * 60), // Convert milliseconds to hours
|
||||
remarks: formData.get("remarks") as string,
|
||||
vacant: formData.get("vacant") === "false" ? false : true,
|
||||
};
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("timesheets")
|
||||
.insert([timesheets]);
|
||||
|
||||
if (error) {
|
||||
console.error("Error submitting timesheets:", error);
|
||||
} else {
|
||||
console.log("Timesheets submitted successfully:", data);
|
||||
alert("Timesheets submitted successfully!");
|
||||
}
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<form
|
||||
class="max-w-6xl mx-auto bg-white p-8 rounded-2xl shadow-xl space-y-8 text-gray-800"
|
||||
on:submit|preventDefault={handleSubmit}
|
||||
>
|
||||
<!-- Title -->
|
||||
<h2 class="text-2xl font-semibold">Timesheet Form</h2>
|
||||
|
||||
<!-- 2 Column Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<!-- Left Column -->
|
||||
<div class="space-y-5">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Work Description<span class="text-red-500">*</span><br
|
||||
/>
|
||||
<span class="text-xs text-gray-500"
|
||||
>Enter detail of work</span
|
||||
>
|
||||
</label>
|
||||
<input
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Tell detail of work"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 {errorClass(
|
||||
'name',
|
||||
)}"
|
||||
/>
|
||||
{#if $formErrors.name}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.name}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Type of Work<span class="text-red-500">*</span></label
|
||||
>
|
||||
<select
|
||||
name="type_of_work"
|
||||
class={`w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600 ${errorClass("type_of_work")}`}
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select option...</option
|
||||
>
|
||||
{#each typeOfWork as source}
|
||||
<option value={source.value}>{source.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.type_of_work}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.type_of_work}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Villa Name<span class="text-red-500">*</span></label
|
||||
>
|
||||
<select
|
||||
name="villa_id"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600 {errorClass(
|
||||
'villa_name',
|
||||
)}"
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select option...</option
|
||||
>
|
||||
{#each dataVilla as villa}
|
||||
<option value={villa.id}>{villa.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.villa_id}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.villa_id}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Date / Time Out<span class="text-red-500">*</span
|
||||
></label
|
||||
>
|
||||
<!-- date and time -->
|
||||
<input
|
||||
name="date_out"
|
||||
type="datetime-local"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 {errorClass(
|
||||
'Date / Time Out',
|
||||
)}"
|
||||
/>
|
||||
{#if $formErrors.date_out}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.date_out}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column -->
|
||||
<div class="space-y-5">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Reported By<span class="text-red-500">*</span>
|
||||
<br />
|
||||
<span class="text-xs text-gray-500"
|
||||
>Who reported this issue?</span
|
||||
>
|
||||
</label>
|
||||
<select
|
||||
name="reported_by"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 text-gray-600 {errorClass(
|
||||
'reported_by',
|
||||
)}"
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select option...</option
|
||||
>
|
||||
{#each reportedBy as reporter}
|
||||
<option value={reporter.value}>
|
||||
{reporter.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.reported_by}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.reported_by}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Category of Work<span class="text-red-500">*</span
|
||||
></label
|
||||
>
|
||||
<select
|
||||
name="category_of_work"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 text-gray-600 {errorClass(
|
||||
'Category of Work',
|
||||
)}"
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>Select option...</option
|
||||
>
|
||||
{#each categoryOfWork as p}
|
||||
<option value={p.value}>{p.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.category_of_work}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.category_of_work}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Date / Time In<span class="text-red-500">*</span
|
||||
></label
|
||||
>
|
||||
<input
|
||||
name="date_in"
|
||||
type="datetime-local"
|
||||
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 {errorClass(
|
||||
'Date / Time In',
|
||||
)}"
|
||||
/>
|
||||
{#if $formErrors.date_in}
|
||||
<p class="text-sm text-red-500 mt-1">
|
||||
{$formErrors.date_in}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Full Width Fields -->
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Remarks</label>
|
||||
<textarea
|
||||
name="remarks"
|
||||
rows="3"
|
||||
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"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
name="vacant"
|
||||
value="false"
|
||||
type="checkbox"
|
||||
id="guest_agreed"
|
||||
class="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
<label for="guest_agreed" class="text-sm">Vacant</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="text-center pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-purple-600 text-white px-8 py-3 rounded-xl hover:bg-purple-700 transition-all font-medium shadow-md"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
Reference in New Issue
Block a user