Files
vberp/note.md
2025-07-08 17:36:04 +14:00

568 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

<script lang="ts">
import { supabase } from "$lib/supabaseClient";
import { onMount } from "svelte";
import { writable } from "svelte/store";
type Transport = {
id: string;
guest_name: string;
requested_date: Date;
area: string;
pickup_date: Date;
request_things: string;
additional_notes: string;
requested_by: string;
vendor_email_address: string;
created_at?: Date;
};
type TransportDisplay = {
guest_name: string;
requested_date: Date;
area: string;
pickup_date: Date;
request_things: string;
additional_notes: string;
requested_by: string;
vendor_email_address: string;
created_at?: Date;
};
const area = [
{ label: "Laksamana", value: "Laksamana" },
{ label: "Drupadi", value: "Drupadi" },
{ label: "Abimanyu", value: "Abimanyu" },
{ label: "Seminyak", value: "Seminyak" },
];
const requestThings = [
{ label: "Airport pickup", value: "Airport pickup" },
{ label: "Airport transfer", value: "Airport transfer" },
{ label: "Day car charter", value: "Day car charter" },
{ label: "One way drop off", value: "One way drop off" },
{ label: "One way pickup", value: "One way pickup" },
{ label: "Other", value: "Other" },
];
let allRows: Transport[] = [];
type columns = {
key: string;
title: string;
};
const columns: columns[] = [
{ key: "guest_name", title: "Guest Name" },
{ key: "requested_date", title: "Requested Date" },
{ key: "area", title: "Area" },
{ key: "pickup_date", title: "Pickup Date" },
{ key: "request_things", title: "Request Things" },
{ key: "additional_notes", title: "Additional Notes" },
{ key: "requested_by", title: "Requested By" },
{ key: "vendor_email_address", title: "Vendor Email Address" },
{ key: "created_at", title: "Created At" },
{ key: "actions", title: "Actions" },
];
async function fetchTransport(
filter: string | null = null,
searchTerm: string | null = null,
sortColumn: string | null = "created_at",
sortOrder: "asc" | "desc" = "desc",
offset: number = 0,
limit: number = 10,
) {
let query = supabase
.from("vb_transport")
.select("*", { count: "exact" })
.order(sortColumn || "created_at", {
ascending: sortOrder === "asc",
});
if (filter) {
query = query.eq("area", filter);
}
if (searchTerm) {
query = query.ilike("guest_name", `%${searchTerm}%`);
}
if (offset) {
query = query.range(offset, offset + limit - 1);
}
if (limit) {
query = query.limit(limit);
}
const { data: transportResponse, error } = await query;
if (error) {
console.error("Error fetching timesheets:", error);
return;
}
allRows = transportResponse.map((row) => ({
...row,
requested_date: new Date(row.requested_date),
pickup_date: new Date(row.pickup_date),
created_at: row.created_at ? new Date(row.created_at) : undefined,
})) as Transport[];
}
let currentPage = 1;
const rowsPerPage = 10;
const totalRows = allRows.length;
const totalPages = Math.ceil(totalRows / rowsPerPage);
$: paginatedRows = allRows.slice(
(currentPage - 1) * rowsPerPage,
currentPage * rowsPerPage,
);
console.log("Total Rows:", totalRows);
function goToPage(page: number) {
if (page >= 1 && page <= totalPages) currentPage = page;
fetchTransport(
null,
null,
"created_at",
"desc",
(currentPage - 1) * rowsPerPage,
rowsPerPage,
);
}
onMount(() => {
fetchTransport();
});
// Initialize the first page
$: currentPage = 1;
let showModal = false;
let isEditing = false;
let currentEditingId: string | null = null;
let newTransport: Transport = {
id: "",
guest_name: "",
requested_date: new Date(),
area: "",
pickup_date: new Date(),
request_things: "",
additional_notes: "",
requested_by: "",
vendor_email_address: "",
};
const excludedKeys = ["id", "actions", "created_at"];
const formColumns = columns.filter(
(col) => !excludedKeys.includes(col.key),
);
function openModal(transport?: Transport) {
if (transport) {
isEditing = true;
currentEditingId = transport.id;
newTransport = { ...transport };
} else {
isEditing = false;
currentEditingId = null;
newTransport = {
id: "",
guest_name: "",
requested_date: new Date(),
area: "",
pickup_date: new Date(),
request_things: "",
additional_notes: "",
requested_by: "",
vendor_email_address: "",
};
}
showModal = true;
}
async function saveIssue(event: Event) {
event.preventDefault();
const formData = new FormData(event.target as HTMLFormElement);
// Validate form data
if (!validateForm(formData)) {
console.error("Form validation failed");
return;
}
if (isEditing && currentEditingId) {
const { error } = await supabase
.from("vb_tansport")
.update(newTransport)
.eq("id", currentEditingId);
if (error) {
alert("Error updating transport: " + error.message);
console.error("Error updating transport:", error);
return;
}
}
await fetchTransport();
showModal = false;
}
async function deleteTransport(id: string) {
if (confirm("Are you sure you want to delete this transport?")) {
const { error } = await supabase
.from("vb_transport")
.delete()
.eq("id", id);
if (error) {
console.error("Error deleting issue:", error);
return;
}
await fetchTransport();
}
}
export let formErrors = writable<{ [key: string]: string }>({});
function validateForm(formData: FormData): boolean {
const errors: { [key: string]: string } = {};
const requiredFields = [
"guest_name",
"requested_date",
"area",
"pickup_date",
"request_things",
"additional_notes",
"requested_by",
"vendor_email_address",
];
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>
<div
class="p-6 bg-white shadow-md rounded-2xl mb-4 flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4"
>
<div>
<h2 class="text-lg font-semibold text-gray-800">
🚗 Transport Request List
</h2>
<p class="text-sm text-gray-600">
Manage and track all transport requests efficiently.
</p>
</div>
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
<input
type="text"
placeholder="🔍 Search by guest name..."
class="border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:outline-none px-4 py-2 rounded-xl text-sm w-64 transition"
on:input={(e) => {
const searchTerm = (
e.target as HTMLInputElement
).value.toLowerCase();
fetchTransport(null, searchTerm, "created_at", "desc");
}}
/>
<select
class="border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:outline-none px-4 py-2 rounded-xl text-sm w-48 transition"
on:change={(e) => {
const filter = (e.target as HTMLSelectElement).value;
fetchTransport(filter, null, null, "desc");
}}
>
<option value="">All Area</option>
{#each area as a}
<option value={a.value}>{a.label}</option>
{/each}
</select>
<!-- request things -->
<select
class="border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:outline-none px-4 py-2 rounded-xl text-sm w-48 transition"
on:change={(e) => {
const filter = (e.target as HTMLSelectElement).value;
fetchTransport(null, filter, "created_at", "desc");
}}
>
<option value="">All Requests</option>
{#each requestThings as r}
<option value={r.value}>{r.label}</option>
{/each}
</select>
<button
class="bg-gray-200 text-gray-700 px-4 py-2 rounded-xl hover:bg-gray-300 text-sm transition"
on:click={() =>
fetchTransport(null, null, "created_at", "desc", 0, 10)}
>
🔄 Reset
</button>
</div>
</div>
<div class="overflow-x-auto rounded-lg shadow mb-4">
<table class="min-w-[1000px] divide-y divide-gray-200 text-sm w-max">
<thead class="bg-gray-100">
<tr>
{#each columns as col}
{#if col.key === "guest_name"}
<th
class="sticky left-0 px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
style="background-color: #f0f8ff; z-index: 10;"
>
{col.title}
</th>
{:else}
<th
class="px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
>
{col.title}
</th>
{/if}
{/each}
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
{#each paginatedRows as row}
<tr class="hover:bg-gray-50 transition">
{#each columns as col}
{#if col.key === "guest_name"}
<td class="px-4 py-2 font-medium text-gray-800">
{row[col.key as keyof Transport]}
</td>
{:else if col.key === "requested_date"}
<td class="px-4 py-2">
{row[col.key as keyof Transport]
? new Date(
row[
col.key as keyof Transport
] as string | number | Date,
).toLocaleDateString()
: "N/A"}
</td>
{:else if col.key === "pickup_date"}
<td class="px-4 py-2">
{new Date(
new Date(
row[col.key as keyof Transport] as
| string
| number
| Date,
).toLocaleDateString() || "N/A",
).toLocaleDateString()}
</td>
{:else if col.key === "created_at"}
<td class="px-4 py-2">
{row[col.key as keyof Transport]
? new Date(
row[
col.key as keyof Transport
] as string | number | Date,
).toLocaleDateString()
: "N/A"}
</td>
{:else if col.key === "area"}
<td class="px-4 py-2">
{row[col.key as keyof Transport]}
</td>
{:else if col.key === "request_things"}
<td class="px-4 py-2">
{row[col.key as keyof Transport]}
</td>
{:else if col.key === "additional_notes"}
<td class="px-4 py-2">
{row[col.key as keyof Transport] ||
"No additional notes"}
</td>
{:else if col.key === "requested_by"}
<td class="px-4 py-2">
{row[col.key as keyof Transport]}
</td>
{:else if col.key === "vendor_email_address"}
<td class="px-4 py-2">
{row[col.key as keyof TransportDisplay] ||
"No email provided"}
</td>
{:else if col.key === "actions"}
<td class="px-4 py-2">
<button
class="inline-flex items-center gap-1 rounded bg-blue-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-blue-700"
on:click={() => openModal(row)}
>
✏️ Edit
</button>
<button
class="inline-flex items-center gap-1 rounded bg-red-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-red-700"
on:click={() => deleteTransport(row.id)}
>
🗑️ Delete
</button>
</td>
{:else}
<td class="px-4 py-2">
{row[col.key as keyof TransportDisplay]}
</td>
{/if}
{/each}
</tr>
{/each}
</tbody>
</table>
</div>
<!-- Pagination controls -->
<div class="flex justify-between items-center text-sm">
<div>
Showing {(currentPage - 1) * rowsPerPage + 1}
{Math.min(currentPage * rowsPerPage, allRows.length)} of {allRows.length}
</div>
<div class="space-x-2">
<button
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
on:click={() => goToPage(currentPage - 1)}
disabled={currentPage === 1}
>
Previous
</button>
{#each Array(totalPages)
.fill(0)
.map((_, i) => i + 1) as page}
<button
class="px-3 py-1 rounded border text-sm
{currentPage === page
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white border-gray-300 hover:bg-gray-100'}"
on:click={() => goToPage(page)}
>
{page}
</button>
{/each}
<button
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
on:click={() => goToPage(currentPage + 1)}
disabled={currentPage === totalPages}
>
Next
</button>
</div>
</div>
</div>
{#if showModal}
<div
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
>
<div class="bg-white rounded-lg shadow-lg p-6 w-full max-w-md">
<h2 class="text-xl font-semibold mb-4">
{isEditing ? "Edit Transport Request" : "New Transport Request"}
</h2>
<form on:submit={saveIssue}>
{#each formColumns as col}
<div class="mb-4">
<label
class="block text-sm font-medium mb-1"
for={col.key}
>
{col.title}
</label>
{#if col.key === "requested_date" || col.key === "pickup_date"}
<input
type="date"
name={col.key}
bind:value={newTransport[col.key]}
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 {errorClass(
col.key,
)}"
/>
{:else if col.key === "area"}
<select
name={col.key}
bind:value={newTransport[col.key]}
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 {errorClass(
col.key,
)}"
>
<option value="" disabled selected
>Select Area</option
>
{#each area as a}
<option value={a.value}>{a.label}</option>
{/each}
</select>
{:else if col.key === "request_things"}
<select
name={col.key}
bind:value={newTransport[col.key]}
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 {errorClass(
col.key,
)}"
>
<option value="" disabled selected
>Select Request</option
>
{#each requestThings as r}
<option value={r.value}>{r.label}</option>
{/each}
</select>
{:else}
<input
type="text"
name={col.key}
bind:value={
newTransport[col.key as keyof Transport]
}
placeholder={col.title}
class="w-full border rounded-xl px-4 py-
2 focus:outline-none focus:ring-2 focus:ring-purple-400 {errorClass(col.key)}"
/>
{/if}
{#if $formErrors[col.key]}
<p class="text-red-500 text-xs mt-1">
{$formErrors[col.key]}
</p>
{/if}
</div>
{/each}
<div class="mb-4">
<label class="block text-sm font-medium mb-1"
>Additional Notes</label
>
<textarea
name="additional_notes"
bind:value={newTransport.additional_notes}
class="w-full border rounded-xl px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-400 {errorClass(
'additional_notes',
)}"
rows="3"
placeholder="Any additional notes..."
></textarea>
{#if $formErrors.additional_notes}
<p class="text-red-500 text-xs mt-1">
{$formErrors.additional_notes}
</p>
{/if}
</div>
<div class="flex justify-end space-x-2">
<button
type="button"
class="px-4 py-2 bg-gray-200 text-gray-700 rounded-xl hover:bg-gray-300"
on:click={() => (showModal = false)}
>
Cancel
</button>
<button
type="submit"
class="px-4 py-2 bg-blue-600 text-white rounded-xl hover:bg-blue-700"
>
{isEditing ? "Update" : "Create"}
</button>
</div>
</form>
</div>
</div>
{/if}