update timesheet format

This commit is contained in:
2025-06-06 16:08:04 +08:00
parent c89d1b9c9e
commit 2818d2ada5
2 changed files with 224 additions and 381 deletions

View File

@@ -1,7 +1,10 @@
<script> <script>
import { supabase } from '$lib/supabaseClient'; import { supabase } from '$lib/supabaseClient';
import StarRating from '$lib/StarRating.svelte'; import StarRating from '$lib/StarRating.svelte';
import villaBugisImage from '$lib/images/villa-bugis.png'; import villaBugisImage from '$lib/images/villa-bugis.png';
const WEBHOOK_URL = 'https://flow.catalis.app/webhook/vb_feedback_new';
let villa_name = ''; let villa_name = '';
let customer_name = ''; let customer_name = '';
@@ -40,25 +43,50 @@
const { data, error } = await supabase const { data, error } = await supabase
.from('vb_feedback') .from('vb_feedback')
.insert([{ .insert([{
villa_name, villa_name,
customer_name, customer_name,
checkin_date, checkin_date,
checkout_date, checkout_date,
feedback, feedback,
book_process, book_process,
airport_greet, airport_greet,
arrival_greet, arrival_greet,
bf_service, bf_service,
overal_star, overal_star,
extend_disc, extend_disc,
nextstay_disc, nextstay_disc,
become_sponsor become_sponsor
}]); }]);
if (error) { if (error) {
console.error('Error submitting feedback:', error.message); console.error('Error submitting feedback:', error.message);
errorMessage = 'Failed to submit feedback. Please try again.'; errorMessage = 'Failed to submit feedback. Please try again.';
} else { } else {
// ✅ Send webhook
try {
await fetch(WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
villa_name,
customer_name,
checkin_date,
checkout_date,
feedback,
book_process,
airport_greet,
arrival_greet,
bf_service,
overal_star,
extend_disc,
nextstay_disc,
become_sponsor
})
});
} catch (webhookError) {
console.error('Webhook failed:', webhookError);
}
// Reset form // Reset form
villa_name = ''; villa_name = '';
customer_name = ''; customer_name = '';
@@ -73,8 +101,9 @@
extend_disc = false; extend_disc = false;
nextstay_disc = false; nextstay_disc = false;
become_sponsor = false; become_sponsor = false;
alert("Feedback submitted successfully!"); alert("Feedback submitted successfully!");
} }
} }
</script> </script>

View File

@@ -1,374 +1,188 @@
<script lang="ts"> <script lang="ts">
// This is a placeholder for any script you might want to add import { onMount } from 'svelte';
// For example, you could handle form submission here import { supabase } from '$lib/supabaseClient';
import { onMount } from "svelte";
import { supabase } from "$lib/supabaseClient"; type TimesheetForm = {
import { writable } from "svelte/store"; entered_by: string;
work_description: string;
type TimesheetsInsert = { type_of_work: 'Running' | 'Periodic' | 'Irregular';
name: string; category_of_work:
staff_id: string; | 'Cleaning'
date_in: Date; | 'Gardening/Pool'
date_out: Date; | 'Maintenance'
type_of_work: string; | 'Supervision'
category_of_work: string; | 'Guest Service'
approval: string; | 'Administration'
villa_id: string; | 'Non Billable';
approved_by: string; villa_id: string;
approved_date: Date; datetime_in: string;
total_hours_work: number; datetime_out: string;
remarks: string; total_work_hour: number;
vacant: boolean; remarks: string;
approval: 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 = { type Villa = {
id: string; id: string;
name: string; name: string;
}; };
let dataVilla: Villa[] = []; let villas: Villa[] = [];
let form: TimesheetForm = {
entered_by: '',
work_description: '',
type_of_work: 'Running',
category_of_work: 'Cleaning',
villa_id: '',
datetime_in: '',
datetime_out: '',
total_work_hour: 0,
remarks: '',
approval: false
};
const typeOfWorkOptions: TimesheetForm['type_of_work'][] = ['Running', 'Periodic', 'Irregular'];
const categoryOptions: TimesheetForm['category_of_work'][] = [
'Cleaning',
'Gardening/Pool',
'Maintenance',
'Supervision',
'Guest Service',
'Administration',
'Non Billable'
];
onMount(async () => { onMount(async () => {
const { data, error } = await supabase const { data, error } = await supabase
.from("villas") .from('vb_villas')
.select("id, name"); .select('id, villa_name, villa_status')
.eq('villa_status', 'Active');
if (error) {
console.error("Error fetching villas:", error); if (error) {
} else if (data) { console.error('Failed to fetch villas:', error.message);
dataVilla = data; } else {
} villas = data.map(v => ({
id: v.id,
name: v.villa_name
}));
}
}); });
function calculateTotalHours() {
if (form.datetime_in && form.datetime_out) {
const start = new Date(form.datetime_in);
const end = new Date(form.datetime_out);
const diffInMs = end.getTime() - start.getTime();
async function handleSubmit(event: Event): Promise<void> { // Convert milliseconds to hours (with decimal), round to 2 decimal places
event.preventDefault(); const hours = diffInMs / (1000 * 60 * 60);
form.total_work_hour = Math.max(Number(hours.toFixed(2)), 0);
} else {
form.total_work_hour = 0;
}
}
const formData = new FormData(event.target as HTMLFormElement);
async function submitForm() {
// Validate form data calculateTotalHours();
if (!validateForm(formData)) {
console.error("Form validation failed"); const { error } = await supabase.from('timesheet').insert([form]);
return;
} if (error) {
alert('Failed to submit timesheet: ' + error.message);
const timesheets: TimesheetsInsert = { } else {
name: formData.get("name") as string, alert('Timesheet submitted successfully!');
staff_id: formData.get("staff_id") as string, form = {
date_in: new Date(formData.get("date_in") as string), entered_by: '',
date_out: new Date(formData.get("date_out") as string), work_description: '',
type_of_work: formData.get("type_of_work") as string, type_of_work: 'Running',
category_of_work: formData.get("category_of_work") as string, category_of_work: 'Cleaning',
approval: formData.get("approval") as string, villa_id: '',
villa_id: formData.get("villa_id") as string, datetime_in: '',
approved_by: formData.get("approved_by") as string, datetime_out: '',
approved_date: new Date(formData.get("approved_date") as string), total_work_hour: 0,
// total_hours_work can be calculated based on date_in and date_out remarks: '',
total_hours_work: approval: false
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 }>({}); </script>
function validateForm(formData: FormData): boolean { <form on:submit|preventDefault={submitForm} class="space-y-4 max-w-md mx-auto p-4">
const errors: { [key: string]: string } = {}; <h2 class="text-xl font-bold mb-4">Timesheet Entry</h2>
const requiredFields = [
"name", <input
"type_of_work", class="w-full border p-2 rounded"
"villa_id", bind:value={form.entered_by}
"date_out", placeholder="Entered by"
"reported_by", required
"category_of_work", />
"date_in",
]; <textarea
class="w-full border p-2 rounded"
requiredFields.forEach((field) => { bind:value={form.work_description}
if (!formData.get(field) || formData.get(field) === "") { placeholder="Work Description"
errors[field] = `${field.replace(/_/g, " ")} is required.`; required
} ></textarea>
});
<select class="w-full border p-2 rounded" bind:value={form.type_of_work}>
formErrors.set(errors); {#each typeOfWorkOptions as option}
return Object.keys(errors).length === 0; <option value={option}>{option}</option>
} {/each}
</select>
function errorClass(field: string): string {
return $formErrors[field] ? "border-red-500" : "border"; <select class="w-full border p-2 rounded" bind:value={form.category_of_work}>
} {#each categoryOptions as option}
</script> <option value={option}>{option}</option>
{/each}
<div> </select>
<form
class="max-w-6xl mx-auto bg-white p-8 rounded-2xl shadow-xl space-y-8 text-gray-800" <select class="w-full border p-2 rounded" bind:value={form.villa_id} required>
on:submit|preventDefault={handleSubmit} <option disabled value="">Select Villa</option>
{#each villas as villa}
<option value={villa.id}>{villa.name}</option>
{/each}
</select>
<label class="block">
<span class="text-sm">Date/Time In:</span>
<input
type="datetime-local"
class="w-full border p-2 rounded"
bind:value={form.datetime_in}
on:change={calculateTotalHours}
required
/>
</label>
<label class="block">
<span class="text-sm">Date/Time Out:</span>
<input
type="datetime-local"
class="w-full border p-2 rounded"
bind:value={form.datetime_out}
on:change={calculateTotalHours}
required
/>
</label>
<div class="text-sm">
Total Work Hours: <strong>{form.total_work_hour}</strong>
</div>
<textarea
class="w-full border p-2 rounded"
bind:value={form.remarks}
placeholder="Remarks"
></textarea>
<button
type="submit"
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
<!-- logo --> >
<div class="text-center mb-6"> Submit Timesheet
<img </button>
src="/src/lib/images/logo.webp" </form>
alt="Logo"
class="mx-auto"
loading="lazy"
width="250"
/>
</div>
<!-- Title -->
<h2 class="text-2xl font-semibold text-center">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>