penambahan fitur upload
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
<script>
|
<script>
|
||||||
let menuItems = [
|
let menuItems = [
|
||||||
{ name: "Beranda", icon: "🏠", url: "/" },
|
{ name: "Beranda", icon: "🏠", url: "/" },
|
||||||
|
{ name: "Profile", icon: "🧑", url: "/profile" },
|
||||||
{ name: "Issues", icon: "📂", url: "/backoffice/issue" },
|
{ name: "Issues", icon: "📂", url: "/backoffice/issue" },
|
||||||
{ name: "Issue Member", icon: "📂", url: "/backoffice/issuemember" },
|
{ name: "Issue View", icon: "📂", url: "/backoffice/issue/view" },
|
||||||
{ name: "Projects", icon: "📂", url: "/backoffice/project" },
|
{ name: "Projects", icon: "📂", url: "/backoffice/project" },
|
||||||
{
|
{
|
||||||
name: "Purchase Orders",
|
name: "Purchase Orders",
|
||||||
@@ -10,10 +11,17 @@
|
|||||||
url: "/backoffice/purchaseorder",
|
url: "/backoffice/purchaseorder",
|
||||||
},
|
},
|
||||||
{ name: "Timesheets", icon: "📂", url: "/backoffice/timesheets" },
|
{ name: "Timesheets", icon: "📂", url: "/backoffice/timesheets" },
|
||||||
|
{
|
||||||
|
name: "Timesheets View",
|
||||||
|
icon: "📂",
|
||||||
|
url: "/backoffice/timesheets/view",
|
||||||
|
},
|
||||||
{ name: "Villa", icon: "📂", url: "/backoffice/villa" },
|
{ name: "Villa", icon: "📂", url: "/backoffice/villa" },
|
||||||
{ name: "Inventories", icon: "📂", url: "/backoffice/inventories" },
|
{ name: "Inventories", icon: "📂", url: "/backoffice/inventories" },
|
||||||
{ name: "Vendor", icon: "📂", url: "/backoffice/vendor" },
|
{ name: "Vendor", icon: "📂", url: "/backoffice/vendor" },
|
||||||
{ name: "Booking", icon: "📂", url: "/backoffice/booking" },
|
{ name: "Booking", icon: "📂", url: "/backoffice/booking" },
|
||||||
|
{ name: "Users", icon: "👤", url: "/backoffice/users" },
|
||||||
|
{ name: "Logout", icon: "🚪", url: "/logout" },
|
||||||
];
|
];
|
||||||
|
|
||||||
let active = "Purchase Orders";
|
let active = "Purchase Orders";
|
||||||
|
|||||||
@@ -128,7 +128,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
type Issue = {
|
type Issue = {
|
||||||
id: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
villa_name: string;
|
villa_name: string;
|
||||||
area_of_villa: string;
|
area_of_villa: string;
|
||||||
@@ -170,6 +170,10 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
let allRows: Issue[] = [];
|
let allRows: Issue[] = [];
|
||||||
|
let offset = 0;
|
||||||
|
let limit = 10;
|
||||||
|
let totalItems = 0;
|
||||||
|
export let formErrors = writable<{ [key: string]: string }>({});
|
||||||
|
|
||||||
type columns = {
|
type columns = {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -202,14 +206,40 @@
|
|||||||
{ key: "actions", title: "Actions" },
|
{ key: "actions", title: "Actions" },
|
||||||
];
|
];
|
||||||
|
|
||||||
async function fetchIssues() {
|
async function fetchIssues(
|
||||||
const { data: issues, error: issueError } = await supabase
|
filter: string | null = null,
|
||||||
|
search: string | null = null,
|
||||||
|
sort: string | null = null,
|
||||||
|
order: "asc" | "desc" = "desc",
|
||||||
|
offset: number = 0,
|
||||||
|
limit: number = 10,
|
||||||
|
) {
|
||||||
|
let query = supabase
|
||||||
.from("issues")
|
.from("issues")
|
||||||
.select("*")
|
.select("*", { count: "exact" })
|
||||||
.order("created_at", { ascending: false });
|
.order(sort || "created_at", { ascending: order === "asc" })
|
||||||
|
.range(offset, offset + limit - 1);
|
||||||
|
|
||||||
if (issueError) {
|
if (filter) {
|
||||||
console.error("Error fetching issues:", issueError);
|
query = query.eq("move_issue", filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
query = query.ilike("name", `%${search}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: issues, error, count } = await query;
|
||||||
|
if (error) {
|
||||||
|
console.error("Error fetching issues:", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count !== undefined) {
|
||||||
|
totalItems = count ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!issues || issues.length === 0) {
|
||||||
|
allRows = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,13 +263,10 @@
|
|||||||
villas.find((v) => v.id === issue.villa_name).name || null,
|
villas.find((v) => v.id === issue.villa_name).name || null,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
let currentPage = 1;
|
|
||||||
let rowsPerPage = 5;
|
let currentPage = offset + 1;
|
||||||
$: totalPages = Math.ceil(allRows.length / rowsPerPage);
|
let rowsPerPage = limit;
|
||||||
$: paginatedRows = allRows.slice(
|
$: totalPages = Math.ceil(totalItems / rowsPerPage);
|
||||||
(currentPage - 1) * rowsPerPage,
|
|
||||||
currentPage * rowsPerPage,
|
|
||||||
);
|
|
||||||
|
|
||||||
function editIssue(id: number) {
|
function editIssue(id: number) {
|
||||||
alert(`Edit issue with ID ${id}`);
|
alert(`Edit issue with ID ${id}`);
|
||||||
@@ -250,12 +277,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
fetchIssues();
|
fetchIssues(null, null, "created_at", "desc", offset, limit);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize the first page
|
|
||||||
$: currentPage = 1;
|
|
||||||
|
|
||||||
let showModal = false;
|
let showModal = false;
|
||||||
let isEditing = false;
|
let isEditing = false;
|
||||||
let currentEditingId: string | null = null;
|
let currentEditingId: string | null = null;
|
||||||
@@ -295,6 +319,25 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//upload image if selected
|
||||||
|
if (selectedFile) {
|
||||||
|
const { data, error } = await supabase.storage
|
||||||
|
.from("villabugis")
|
||||||
|
.upload(
|
||||||
|
`issues/${Date.now()}_${selectedFile.name}`,
|
||||||
|
selectedFile,
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("Image upload data:", data);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
alert("Error uploading image: " + error.message);
|
||||||
|
console.error("Error uploading image:", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
newIssue.issue_related_image = data.path;
|
||||||
|
}
|
||||||
|
|
||||||
if (isEditing && currentEditingId) {
|
if (isEditing && currentEditingId) {
|
||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
.from("issues")
|
.from("issues")
|
||||||
@@ -344,11 +387,18 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await fetchIssues();
|
await fetchIssues();
|
||||||
showModal = false;
|
showModal = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteIssue(id: number) {
|
// function get public URL for image supabase
|
||||||
|
async function getPublicUrl(path: string): Promise<string> {
|
||||||
|
const { data } = supabase.storage.from("villabugis").getPublicUrl(path);
|
||||||
|
return data.publicUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteIssue(id: string) {
|
||||||
if (confirm("Are you sure you want to delete this issue?")) {
|
if (confirm("Are you sure you want to delete this issue?")) {
|
||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
.from("issues")
|
.from("issues")
|
||||||
@@ -373,8 +423,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export let formErrors = writable<{ [key: string]: string }>({});
|
|
||||||
|
|
||||||
function validateForm(formData: FormData): boolean {
|
function validateForm(formData: FormData): boolean {
|
||||||
const errors: { [key: string]: string } = {};
|
const errors: { [key: string]: string } = {};
|
||||||
const requiredFields = [
|
const requiredFields = [
|
||||||
@@ -405,7 +453,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// insert id issue to project
|
// insert id issue to project
|
||||||
async function moveIssueToProject(issueId: number) {
|
async function moveIssueToProject(issueId: string) {
|
||||||
// update move_issue field in the issue
|
// update move_issue field in the issue
|
||||||
const { error: updateError } = await supabase
|
const { error: updateError } = await supabase
|
||||||
.from("issues")
|
.from("issues")
|
||||||
@@ -431,7 +479,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// insert id issue to purchase order
|
// insert id issue to purchase order
|
||||||
async function moveIssueToPurchaseOrder(issueId: number) {
|
async function moveIssueToPurchaseOrder(issueId: string) {
|
||||||
// update move_issue field in the issue
|
// update move_issue field in the issue
|
||||||
const { error: updateError } = await supabase
|
const { error: updateError } = await supabase
|
||||||
.from("issues")
|
.from("issues")
|
||||||
@@ -457,19 +505,53 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
|
<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>
|
<div>
|
||||||
<h2 class="text-lg font-semibold text-gray-800">Issue List</h2>
|
<h2 class="text-xl font-semibold text-gray-800">📝 Issue List</h2>
|
||||||
<p class="text-sm text-gray-600">
|
<p class="text-sm text-gray-500">
|
||||||
Manage and view all issues reported in the system.
|
Manage and view all issues reported in the system.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
|
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
|
||||||
on:click={() => openModal()}
|
<input
|
||||||
>
|
type="text"
|
||||||
➕ Add Issue
|
placeholder="🔍 Search by name..."
|
||||||
</button>
|
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();
|
||||||
|
fetchIssues(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;
|
||||||
|
fetchIssues(filter, null, null, "desc");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">All Issues</option>
|
||||||
|
<option value="PROJECT">Project Issues</option>
|
||||||
|
<option value="PURCHASE_ORDER">Purchase Order Issues</option>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
class="bg-gray-200 text-gray-700 px-4 py-2 rounded-xl hover:bg-gray-300 text-sm transition"
|
||||||
|
on:click={() =>
|
||||||
|
fetchIssues(null, null, "created_at", "desc", 0, 10)}
|
||||||
|
>
|
||||||
|
🔄 Reset
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
|
||||||
|
on:click={() => openModal()}
|
||||||
|
>
|
||||||
|
➕ Add Issue
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-x-auto rounded-lg shadow mb-4">
|
<div class="overflow-x-auto rounded-lg shadow mb-4">
|
||||||
<table class="min-w-[1000px] divide-y divide-gray-200 text-sm w-max">
|
<table class="min-w-[1000px] divide-y divide-gray-200 text-sm w-max">
|
||||||
@@ -494,7 +576,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200 bg-white">
|
<tbody class="divide-y divide-gray-200 bg-white">
|
||||||
{#each paginatedRows as row}
|
{#each allRows as row}
|
||||||
<tr class="hover:bg-gray-50 transition">
|
<tr class="hover:bg-gray-50 transition">
|
||||||
{#each columns as col}
|
{#each columns as col}
|
||||||
{#if col.key === "name"}
|
{#if col.key === "name"}
|
||||||
@@ -566,6 +648,38 @@
|
|||||||
❌
|
❌
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
|
{:else if col.key === "issue_related_image"}
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
{#if typeof row[col.key as keyof Issue] === "string" && row[col.key as keyof Issue]}
|
||||||
|
{#await getPublicUrl(row[col.key as keyof Issue] as string) then publicUrl}
|
||||||
|
<a
|
||||||
|
href={publicUrl}
|
||||||
|
target="_blank"
|
||||||
|
class="text-blue-600 hover:underline"
|
||||||
|
>View Picture</a
|
||||||
|
>
|
||||||
|
{:catch}
|
||||||
|
<span class="text-red-500"
|
||||||
|
>Error loading image</span
|
||||||
|
>
|
||||||
|
{/await}
|
||||||
|
{:else}
|
||||||
|
No Picture
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
{:else if col.key === "reported_date"}
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
{typeof row[col.key as keyof Issue] ===
|
||||||
|
"string" ||
|
||||||
|
typeof row[col.key as keyof Issue] ===
|
||||||
|
"number"
|
||||||
|
? new Date(
|
||||||
|
row[col.key as keyof Issue] as
|
||||||
|
| string
|
||||||
|
| number,
|
||||||
|
).toLocaleDateString("en-US")
|
||||||
|
: ""}
|
||||||
|
</td>
|
||||||
{:else if col.key === "need_approval"}
|
{:else if col.key === "need_approval"}
|
||||||
<td class="px-4 py-2">
|
<td class="px-4 py-2">
|
||||||
{#if row[col.key as keyof Issue]}
|
{#if row[col.key as keyof Issue]}
|
||||||
@@ -716,6 +830,14 @@
|
|||||||
alt="Preview"
|
alt="Preview"
|
||||||
class="mt-2 max-h-48 rounded border"
|
class="mt-2 max-h-48 rounded border"
|
||||||
/>
|
/>
|
||||||
|
{:else if newIssue.issue_related_image}
|
||||||
|
{#await getPublicUrl(newIssue.issue_related_image) then url}
|
||||||
|
<img
|
||||||
|
src={url}
|
||||||
|
alt="Preview"
|
||||||
|
class="mt-2 max-h-48 rounded border"
|
||||||
|
/>
|
||||||
|
{/await}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if col.key === "reported_date"}
|
{:else if col.key === "reported_date"}
|
||||||
|
|||||||
@@ -179,6 +179,20 @@
|
|||||||
return;
|
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 = {
|
const issue: Issue = {
|
||||||
name: formData.get("name") as string,
|
name: formData.get("name") as string,
|
||||||
villa_name: formData.get("villa_name") as string,
|
villa_name: formData.get("villa_name") as string,
|
||||||
@@ -279,9 +279,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// function get public URL for image supabase
|
||||||
|
async function getPublicUrl(path: string): Promise<string> {
|
||||||
|
const { data } = supabase.storage.from("villabugis").getPublicUrl(path);
|
||||||
|
return data.publicUrl;
|
||||||
|
}
|
||||||
|
|
||||||
async function saveProject(event: Event) {
|
async function saveProject(event: Event) {
|
||||||
const formData = new FormData(event.target as HTMLFormElement);
|
const formData = new FormData(event.target as HTMLFormElement);
|
||||||
|
|
||||||
|
// Upload image if selected
|
||||||
|
if (selectedFile) {
|
||||||
|
const { data, error } = await supabase.storage
|
||||||
|
.from("villabugis")
|
||||||
|
.upload(`project/${selectedFile.name}`, selectedFile);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Error uploading image:", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
imagePreviewUrl = data?.path;
|
||||||
|
}
|
||||||
|
|
||||||
const projectUpdate: insetProject = {
|
const projectUpdate: insetProject = {
|
||||||
issue_id: formData.get("issue_id") as string,
|
issue_id: formData.get("issue_id") as string,
|
||||||
input_by: formData.get("input_by") as string,
|
input_by: formData.get("input_by") as string,
|
||||||
@@ -439,15 +459,21 @@
|
|||||||
❌
|
❌
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
{:else if col.key === "project_picture_link"}
|
{:else if col.key === "picture_link"}
|
||||||
<td class="px-4 py-2">
|
<td class="px-4 py-2">
|
||||||
{#if row.picture_link}
|
{#if row.picture_link}
|
||||||
<a
|
{#await getPublicUrl(row.picture_link) then publicUrl}
|
||||||
href={row.picture_link}
|
<a
|
||||||
target="_blank"
|
href={publicUrl}
|
||||||
class="text-blue-600 hover:underline"
|
target="_blank"
|
||||||
>View Picture</a
|
class="text-blue-600 hover:underline"
|
||||||
>
|
>View Picture</a
|
||||||
|
>
|
||||||
|
{:catch}
|
||||||
|
<span class="text-red-500"
|
||||||
|
>Error loading image</span
|
||||||
|
>
|
||||||
|
{/await}
|
||||||
{:else}
|
{:else}
|
||||||
No Picture
|
No Picture
|
||||||
{/if}
|
{/if}
|
||||||
@@ -556,6 +582,18 @@
|
|||||||
alt="Preview"
|
alt="Preview"
|
||||||
class="mt-2 max-h-48 rounded border"
|
class="mt-2 max-h-48 rounded border"
|
||||||
/>
|
/>
|
||||||
|
{:else if newProjects.picture_link}
|
||||||
|
{#await getPublicUrl(newProjects.picture_link) then url}
|
||||||
|
<img
|
||||||
|
src={url}
|
||||||
|
alt="Preview"
|
||||||
|
class="mt-2 max-h-48 rounded border"
|
||||||
|
/>
|
||||||
|
{/await}
|
||||||
|
{:else}
|
||||||
|
<p class="text-red-500 mt-2">
|
||||||
|
No image selected or uploaded.
|
||||||
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
763
src/routes/backoffice/timesheets/+page.svelte
Normal file
763
src/routes/backoffice/timesheets/+page.svelte
Normal file
@@ -0,0 +1,763 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { supabase } from "$lib/supabaseClient";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
|
type Timesheets = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
staff_id: string;
|
||||||
|
report_by: string;
|
||||||
|
date_in: Date;
|
||||||
|
date_out: Date;
|
||||||
|
type_of_work: string;
|
||||||
|
category_of_work: string;
|
||||||
|
approval: string;
|
||||||
|
villa_id: string;
|
||||||
|
villa_name: string;
|
||||||
|
approved_by: string;
|
||||||
|
approved_date: Date;
|
||||||
|
total_hours_work: number;
|
||||||
|
remarks: string;
|
||||||
|
vacant: boolean;
|
||||||
|
created_at?: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TimesheetDisplay = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
report_by: string;
|
||||||
|
date_in: Date;
|
||||||
|
date_out: Date;
|
||||||
|
type_of_work: string;
|
||||||
|
category_of_work: string;
|
||||||
|
approval: string;
|
||||||
|
villa_name: string;
|
||||||
|
approved_by: string;
|
||||||
|
approved_date: Date;
|
||||||
|
total_hours_work: number;
|
||||||
|
remarks: string;
|
||||||
|
vacant: boolean;
|
||||||
|
created_at?: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let allRows: Timesheets[] = [];
|
||||||
|
|
||||||
|
type columns = {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: columns[] = [
|
||||||
|
{ key: "name", title: "Name" },
|
||||||
|
{ key: "report_by", title: "Staff Report" },
|
||||||
|
{ key: "date_in", title: "Date In" },
|
||||||
|
{ key: "date_out", title: "Date Out" },
|
||||||
|
{ key: "type_of_work", title: "Type of Work" },
|
||||||
|
{ key: "category_of_work", title: "Category of Work" },
|
||||||
|
{ key: "approval", title: "Approval" },
|
||||||
|
{ key: "villa_name", title: "Villa Name" },
|
||||||
|
{ key: "approved_by", title: "Approved By" },
|
||||||
|
{ key: "approved_date", title: "Approved Date" },
|
||||||
|
{ key: "total_hours_work", title: "Total Hours Work" },
|
||||||
|
{ key: "remarks", title: "Remarks" },
|
||||||
|
{ key: "vacant", title: "Vacant" },
|
||||||
|
{ key: "created_at", title: "Created At" },
|
||||||
|
{ key: "actions", title: "Actions" },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function fetchTimeSheets() {
|
||||||
|
const { data: timesheet, error: timesheetError } = await supabase
|
||||||
|
.from("timesheets")
|
||||||
|
.select("*")
|
||||||
|
.order("created_at", { ascending: false });
|
||||||
|
|
||||||
|
if (timesheetError) {
|
||||||
|
console.error("Error fetching issues:", timesheetError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ambil semua villa_id unik dari issues
|
||||||
|
const villaIds = [
|
||||||
|
...new Set(timesheet.map((i: Timesheets) => i.villa_id)),
|
||||||
|
];
|
||||||
|
|
||||||
|
const { data: villas, error: villaError } = await supabase
|
||||||
|
.from("villas")
|
||||||
|
.select("*")
|
||||||
|
.in("id", villaIds);
|
||||||
|
|
||||||
|
if (villaError) {
|
||||||
|
console.error("Error fetching villas:", villaError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gabungkan data villa ke dalam setiap issue
|
||||||
|
allRows = timesheet.map((timesheet: Timesheets) => ({
|
||||||
|
...timesheet,
|
||||||
|
villa_name:
|
||||||
|
villas.find((v) => v.id === timesheet.villa_id).name || null,
|
||||||
|
approval: timesheet.approval || "",
|
||||||
|
report_by: timesheet.staff_id || "Unknown",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
let currentPage = 1;
|
||||||
|
let rowsPerPage = 5;
|
||||||
|
$: totalPages = Math.ceil(allRows.length / rowsPerPage);
|
||||||
|
$: paginatedRows = allRows.slice(
|
||||||
|
(currentPage - 1) * rowsPerPage,
|
||||||
|
currentPage * rowsPerPage,
|
||||||
|
);
|
||||||
|
|
||||||
|
function goToPage(page: number) {
|
||||||
|
if (page >= 1 && page <= totalPages) currentPage = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
fetchTimeSheets();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize the first page
|
||||||
|
$: currentPage = 1;
|
||||||
|
|
||||||
|
let showModal = false;
|
||||||
|
let isEditing = false;
|
||||||
|
let currentEditingId: string | null = null;
|
||||||
|
let newIssue: Record<string, any> = {};
|
||||||
|
const excludedKeys = [
|
||||||
|
"id",
|
||||||
|
"created_at",
|
||||||
|
"approval",
|
||||||
|
"approved_by",
|
||||||
|
"approved_date",
|
||||||
|
"villa_name",
|
||||||
|
"report_by",
|
||||||
|
"actions",
|
||||||
|
];
|
||||||
|
const formColumns = columns.filter(
|
||||||
|
(col) => !excludedKeys.includes(col.key),
|
||||||
|
);
|
||||||
|
|
||||||
|
function openModal(issue?: Record<string, any>) {
|
||||||
|
if (issue) {
|
||||||
|
isEditing = true;
|
||||||
|
currentEditingId = issue.id;
|
||||||
|
newIssue = { ...issue };
|
||||||
|
} else {
|
||||||
|
isEditing = false;
|
||||||
|
currentEditingId = null;
|
||||||
|
newIssue = {};
|
||||||
|
}
|
||||||
|
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("issues")
|
||||||
|
.update(newIssue)
|
||||||
|
.eq("id", currentEditingId);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
alert("Error updating issue: " + error.message);
|
||||||
|
console.error("Error updating issue:", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const TimesheetsInsert: 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,
|
||||||
|
),
|
||||||
|
//calculate total_hours_work
|
||||||
|
total_hours_work:
|
||||||
|
Math.abs(
|
||||||
|
new Date(formData.get("date_out") as string).getTime() -
|
||||||
|
new Date(
|
||||||
|
formData.get("date_in") as string,
|
||||||
|
).getTime(),
|
||||||
|
) /
|
||||||
|
(1000 * 60 * 60), // Convert milliseconds to hours
|
||||||
|
remarks: formData.get("remarks") as string,
|
||||||
|
vacant: formData.get("vacant") === "true",
|
||||||
|
};
|
||||||
|
|
||||||
|
const { error } = await supabase
|
||||||
|
.from("timesheets")
|
||||||
|
.insert([TimesheetsInsert]);
|
||||||
|
if (error) {
|
||||||
|
console.error("Error adding issue:", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await fetchTimeSheets();
|
||||||
|
showModal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteTimesheet(id: string) {
|
||||||
|
if (confirm("Are you sure you want to delete this issue?")) {
|
||||||
|
const { error } = await supabase
|
||||||
|
.from("timesheets")
|
||||||
|
.delete()
|
||||||
|
.eq("id", id);
|
||||||
|
if (error) {
|
||||||
|
console.error("Error deleting issue:", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await fetchTimeSheets();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateApprovalStatus(
|
||||||
|
id: string,
|
||||||
|
status: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const { error } = await supabase
|
||||||
|
.from("timesheets")
|
||||||
|
.update({ approval: status })
|
||||||
|
.eq("id", id);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Error updating approval status:", error);
|
||||||
|
} else {
|
||||||
|
await fetchTimeSheets();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold text-gray-800">Timesheet List</h2>
|
||||||
|
<p class="text-sm text-gray-600">
|
||||||
|
Manage and track timesheets for staff members.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
|
||||||
|
on:click={() => openModal()}
|
||||||
|
>
|
||||||
|
➕ Add Timesheet
|
||||||
|
</button>
|
||||||
|
</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 === "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 === "name"}
|
||||||
|
<td
|
||||||
|
class="sticky left-0 px-4 py-2 font-medium text-blue-600"
|
||||||
|
style="background-color: #f0f8ff; cursor: pointer;"
|
||||||
|
>
|
||||||
|
{row[col.key]}
|
||||||
|
</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={() => deleteTimesheet(row.id)}
|
||||||
|
>
|
||||||
|
🗑️ Delete
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
{:else if col.key === "approval"}
|
||||||
|
{#if row[col.key as keyof Timesheets] === "APPROVED"}
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
<span
|
||||||
|
class="text-green-600 font-semibold"
|
||||||
|
>✅ Approved</span
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
{:else if row[col.key as keyof Timesheets] === "PENDING"}
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
<span
|
||||||
|
class="text-yellow-600 font-semibold"
|
||||||
|
>⏳ Pending</span
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
{:else if row[col.key as keyof Timesheets] === "REJECTED"}
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
<span class="text-red-600 font-semibold"
|
||||||
|
>❌ Rejected</span
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
{:else}
|
||||||
|
<!-- dropdown -->
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
<select
|
||||||
|
class="border px-3 py-1 rounded"
|
||||||
|
bind:value={
|
||||||
|
row[col.key as keyof Timesheets]
|
||||||
|
}
|
||||||
|
on:change={(e) =>
|
||||||
|
updateApprovalStatus(
|
||||||
|
row.id,
|
||||||
|
e.target
|
||||||
|
? (
|
||||||
|
e.target as HTMLSelectElement
|
||||||
|
).value
|
||||||
|
: "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value=""
|
||||||
|
disabled
|
||||||
|
selected
|
||||||
|
class="bg-gray-100"
|
||||||
|
>Select Approval Status</option
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value="PENDING"
|
||||||
|
class="bg-yellow-100"
|
||||||
|
>PENDING</option
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value="APPROVED"
|
||||||
|
class="bg-green-100"
|
||||||
|
>APPROVAL</option
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value="REJECTED"
|
||||||
|
class="bg-red-100"
|
||||||
|
>REJECTED</option
|
||||||
|
>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
{/if}
|
||||||
|
{:else if col.key === "vacant"}
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
{#if row[col.key as keyof Timesheets]}
|
||||||
|
<span
|
||||||
|
class="text-green
|
||||||
|
font-semibold">✅ Vacant</span
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<span class="text-red-600 font-semibold"
|
||||||
|
>❌ Not Vacant</span
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
{:else}
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
{row[col.key as keyof Timesheets]}
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
{#if showModal}
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
on:submit|preventDefault={saveIssue}
|
||||||
|
class="bg-white p-6 rounded shadow-lg w-[600px] max-h-[90vh] overflow-y-auto space-y-4"
|
||||||
|
>
|
||||||
|
<h3 class="text-lg font-semibold">
|
||||||
|
{isEditing ? "Edit Issue" : "Add New Issue"}
|
||||||
|
</h3>
|
||||||
|
{#each formColumns as col}
|
||||||
|
{#if col.key === "name"}
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium text-gray-700"
|
||||||
|
>Work Description</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="w-full border px-3 py-2 rounded {errorClass(
|
||||||
|
'name',
|
||||||
|
)}"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
bind:value={newIssue[col.key as keyof Timesheets]}
|
||||||
|
placeholder={col.title}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{#if $formErrors.name}
|
||||||
|
<p class="text-red-500 text-xs">
|
||||||
|
{$formErrors.name}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else if col.key === "vacant"}
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium text-gray-700"
|
||||||
|
>Vacant</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
name="guest_has_aggreed_issue_has_been_resolved"
|
||||||
|
class="w-full border px-3 py-2 rounded"
|
||||||
|
bind:value={newIssue[col.key as keyof Timesheets]}
|
||||||
|
>
|
||||||
|
<option value="true">Yes</option>
|
||||||
|
<option value="false">No</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{:else if col.key === "reported_by"}
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium text-gray-700"
|
||||||
|
>Reported By</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
name="reported_by"
|
||||||
|
class="w-full border px-3 py-2 rounded {errorClass(
|
||||||
|
'reported_by',
|
||||||
|
)}"
|
||||||
|
bind:value={newIssue[col.key as keyof Timesheets]}
|
||||||
|
>
|
||||||
|
<option value="" disabled selected
|
||||||
|
>Select Reporter</option
|
||||||
|
>
|
||||||
|
{#each reportedBy as reporter}
|
||||||
|
<option value={reporter.value}
|
||||||
|
>{reporter.label}</option
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{#if $formErrors.reported_by}
|
||||||
|
<p class="text-red-500 text-xs">
|
||||||
|
{$formErrors.reported_by}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else if col.key === "villa_name"}
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium text-gray-700"
|
||||||
|
>Villa Name</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
name="villa_name"
|
||||||
|
class="w-full border px-3 py-2 rounded {errorClass(
|
||||||
|
'villa_name',
|
||||||
|
)}"
|
||||||
|
bind:value={newIssue[col.key as keyof Timesheets]}
|
||||||
|
>
|
||||||
|
<option value="" disabled selected
|
||||||
|
>Select Villa</option
|
||||||
|
>
|
||||||
|
{#each dataVilla as villa}
|
||||||
|
<option value={villa.id}>{villa.name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{#if $formErrors.villa_name}
|
||||||
|
<p class="text-red-500 text-xs">
|
||||||
|
{$formErrors.villa_name}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else if col.key === "remarks"}
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium text-gray-700"
|
||||||
|
>Remarks</label
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
name="remarks"
|
||||||
|
class="w-full border px-3 py-2 rounded {errorClass(
|
||||||
|
'remarks',
|
||||||
|
)}"
|
||||||
|
bind:value={newIssue[col.key as keyof Timesheets]}
|
||||||
|
placeholder={col.title}
|
||||||
|
rows="4"
|
||||||
|
></textarea>
|
||||||
|
{#if $formErrors.remarks}
|
||||||
|
<p class="text-red-500 text-xs">
|
||||||
|
{$formErrors.remarks}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else if col.key === "date_in"}
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium text-gray-700"
|
||||||
|
>Date In</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
name="date_in"
|
||||||
|
class="w-full border px-3 py-2 rounded {errorClass(
|
||||||
|
'date_in',
|
||||||
|
)}"
|
||||||
|
type="datetime-local"
|
||||||
|
bind:value={newIssue[col.key as keyof Timesheets]}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{#if $formErrors.date_in}
|
||||||
|
<p class="text-red-500 text-xs">
|
||||||
|
{$formErrors.date_in}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else if col.key === "date_out"}
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium text-gray-700"
|
||||||
|
>Date Out</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
name="date_out"
|
||||||
|
class="w-full border px-3 py-2 rounded {errorClass(
|
||||||
|
'date_out',
|
||||||
|
)}"
|
||||||
|
type="datetime-local"
|
||||||
|
bind:value={newIssue[col.key as keyof Timesheets]}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{#if $formErrors.date_out}
|
||||||
|
<p class="text-red-500 text-xs">
|
||||||
|
{$formErrors.date_out}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else if col.key === "type_of_work"}
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium text-gray-700"
|
||||||
|
>Type of Work</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
name="type_of_work"
|
||||||
|
class="w-full border px-3 py-2 rounded {errorClass(
|
||||||
|
'type_of_work',
|
||||||
|
)}"
|
||||||
|
bind:value={newIssue[col.key as keyof Timesheets]}
|
||||||
|
>
|
||||||
|
<option value="" disabled selected
|
||||||
|
>Select Type</option
|
||||||
|
>
|
||||||
|
{#each typeOfWork as type}
|
||||||
|
<option value={type.value}>{type.label}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{#if $formErrors.type_of_work}
|
||||||
|
<p class="text-red-500 text-xs">
|
||||||
|
{$formErrors.type_of_work}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else if col.key === "category_of_work"}
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium text-gray-700"
|
||||||
|
>Category of Work</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
name="category_of_work"
|
||||||
|
class="w-full border px-3 py-2 rounded {errorClass(
|
||||||
|
'category_of_work',
|
||||||
|
)}"
|
||||||
|
bind:value={newIssue[col.key as keyof Timesheets]}
|
||||||
|
>
|
||||||
|
<option value="" disabled selected
|
||||||
|
>Select Category</option
|
||||||
|
>
|
||||||
|
{#each categoryOfWork as category}
|
||||||
|
<option value={category.value}
|
||||||
|
>{category.label}</option
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{#if $formErrors.category_of_work}
|
||||||
|
<p class="text-red-500 text-xs">
|
||||||
|
{$formErrors.category_of_work}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium text-gray-700"
|
||||||
|
>{col.title}</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
name={col.key}
|
||||||
|
class="w-full border px-3 py-2 rounded"
|
||||||
|
type="text"
|
||||||
|
bind:value={newIssue[col.key as keyof Timesheets]}
|
||||||
|
placeholder={col.title}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
<div class="flex justify-end gap-2 mt-4">
|
||||||
|
<button
|
||||||
|
class="px-4 py-2 text-sm rounded bg-gray-200 hover:bg-gray-300"
|
||||||
|
on:click={() => (showModal = false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="px-4 py-2 text-sm rounded bg-blue-600 text-white hover:bg-blue-700"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
364
src/routes/backoffice/timesheets/view/+page.svelte
Normal file
364
src/routes/backoffice/timesheets/view/+page.svelte
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
<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>
|
||||||
132
src/routes/backoffice/vendor/+page.svelte
vendored
132
src/routes/backoffice/vendor/+page.svelte
vendored
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { supabase } from "$lib/supabaseClient";
|
import { supabase } from "$lib/supabaseClient";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
type Vendor = {
|
type Vendor = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -18,6 +19,10 @@
|
|||||||
|
|
||||||
let allRowsVendor: Vendor[] = [];
|
let allRowsVendor: Vendor[] = [];
|
||||||
let allRowsContactVendor: ContactVendor[] = [];
|
let allRowsContactVendor: ContactVendor[] = [];
|
||||||
|
let offset = 0;
|
||||||
|
let limit = 10;
|
||||||
|
let totalItems = 0;
|
||||||
|
export let formErrors = writable<{ [key: string]: string }>({});
|
||||||
|
|
||||||
type columns = {
|
type columns = {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -35,26 +40,48 @@
|
|||||||
{ key: "created_at", title: "Created At" },
|
{ key: "created_at", title: "Created At" },
|
||||||
];
|
];
|
||||||
|
|
||||||
onMount(async () => {
|
async function fetchVendor(
|
||||||
const { data: vendorData, error: vendorError } = await supabase
|
searchTerm: string = "",
|
||||||
|
orderBy: string = "created_at",
|
||||||
|
orderDirection: "asc" | "desc" = "desc",
|
||||||
|
limit: number = 10,
|
||||||
|
offset: number = 0,
|
||||||
|
) {
|
||||||
|
let query = supabase
|
||||||
.from("vendor")
|
.from("vendor")
|
||||||
.select("*")
|
.select("*", { count: "exact" })
|
||||||
.order("created_at", { ascending: false });
|
.order(orderBy, { ascending: orderDirection === "asc" })
|
||||||
|
.range(offset, offset + limit - 1);
|
||||||
|
|
||||||
if (vendorError) {
|
if (searchTerm) {
|
||||||
console.error("Error fetching vendors:", vendorError);
|
query = query.ilike("name", `%${searchTerm}%`);
|
||||||
} else {
|
|
||||||
allRowsVendor = vendorData as Vendor[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { data, error, count } = await query;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Error fetching vendors:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
allRowsVendor = data as Vendor[];
|
||||||
|
|
||||||
|
const { count: total } = await supabase
|
||||||
|
.from("vendor")
|
||||||
|
.select("*", { count: "exact" })
|
||||||
|
.ilike("name", `%${searchTerm}%`);
|
||||||
|
|
||||||
|
totalItems = total || 0;
|
||||||
|
offset = Math.floor(totalItems / limit) * limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
fetchVendor();
|
||||||
});
|
});
|
||||||
|
|
||||||
let currentPage = 1;
|
let currentPage = offset + 1;
|
||||||
let itemsPerPage = 10;
|
let itemsPerPage = limit;
|
||||||
$: totalPages = Math.ceil(allRowsVendor.length / itemsPerPage);
|
let totalPages = Math.ceil(totalItems / itemsPerPage);
|
||||||
$: paginatedRows = allRowsVendor.slice(
|
|
||||||
(currentPage - 1) * itemsPerPage,
|
|
||||||
currentPage * itemsPerPage,
|
|
||||||
);
|
|
||||||
|
|
||||||
function nextPage() {
|
function nextPage() {
|
||||||
if (currentPage < totalPages) {
|
if (currentPage < totalPages) {
|
||||||
@@ -276,22 +303,65 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Table untuk daftar Vendor -->
|
<!-- Table untuk daftar Vendor -->
|
||||||
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
|
<div>
|
||||||
<div>
|
<div
|
||||||
<h2 class="text-lg font-semibold text-gray-800">Vendor List</h2>
|
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"
|
||||||
<p class="text-sm text-gray-600">Manage your vendor and contact data</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
|
|
||||||
on:click={() => {
|
|
||||||
showModal = true;
|
|
||||||
isEditing = false;
|
|
||||||
newVendor = {};
|
|
||||||
currentEditingId = null;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
➕ Add Vendor
|
<div>
|
||||||
</button>
|
<h2 class="text-xl font-semibold text-gray-800">🏡 Vendor List</h2>
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
Manage your vendors and their contact information here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="🔍 Search by 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();
|
||||||
|
fetchVendor(searchTerm, "created_at", "desc", limit, 0);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<!-- filter -->
|
||||||
|
<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 value = (e.target as HTMLSelectElement).value;
|
||||||
|
fetchVendor("", value, "desc", limit, 0);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="created_at">Sort by Created At</option>
|
||||||
|
<option value="name">Sort by Name</option>
|
||||||
|
<option value="vendor_type">Sort by Vendor Type</option>
|
||||||
|
<option value="vendor_status">Sort by Vendor Status</option>
|
||||||
|
<option value="vendor_subtype">Sort by Vendor Subtype</option>
|
||||||
|
</select>
|
||||||
|
<!-- button reset -->
|
||||||
|
<button
|
||||||
|
class="bg-gray-200 text-gray-700 px-4 py-2 rounded hover:bg-gray-300 text-sm"
|
||||||
|
on:click={() => {
|
||||||
|
fetchVendor();
|
||||||
|
resetPagination();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🔄 Reset
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
|
||||||
|
on:click={() => {
|
||||||
|
showModal = true;
|
||||||
|
isEditing = false;
|
||||||
|
newVendor = {};
|
||||||
|
currentEditingId = null;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
➕ Add Vendor
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Vendor Table -->
|
<!-- Vendor Table -->
|
||||||
@@ -310,7 +380,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200 bg-white">
|
<tbody class="divide-y divide-gray-200 bg-white">
|
||||||
{#each paginatedRows as vendor}
|
{#each allRowsVendor as vendor}
|
||||||
<tr
|
<tr
|
||||||
class="hover:bg-gray-50 transition"
|
class="hover:bg-gray-50 transition"
|
||||||
on:click={() => handleVendorClick(vendor.id)}
|
on:click={() => handleVendorClick(vendor.id)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { supabase } from "$lib/supabaseClient";
|
import { supabase } from "$lib/supabaseClient";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
type Villa = {
|
type Villa = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -30,6 +31,10 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
let allRows: Villa[] = [];
|
let allRows: Villa[] = [];
|
||||||
|
let offset = 0;
|
||||||
|
let limit = 10;
|
||||||
|
let totalItems = 0;
|
||||||
|
export let formErrors = writable<{ [key: string]: string }>({});
|
||||||
|
|
||||||
type columns = {
|
type columns = {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -75,27 +80,68 @@
|
|||||||
{ key: "actions", title: "Actions" },
|
{ key: "actions", title: "Actions" },
|
||||||
];
|
];
|
||||||
|
|
||||||
async function fetchVillas() {
|
async function fetchVillas(
|
||||||
const { data, error } = await supabase
|
filter: string | null = null,
|
||||||
.from("villas")
|
sort: string | null = null,
|
||||||
.select("*")
|
order: "asc" | "desc" = "desc",
|
||||||
.order("created_at", { ascending: false });
|
limit: number | null = null,
|
||||||
if (error) {
|
offset: number | null = null,
|
||||||
console.error("Error fetching data:", error);
|
) {
|
||||||
} else {
|
let query = supabase.from("villas").select("*");
|
||||||
allRows = data;
|
|
||||||
|
if (filter) {
|
||||||
|
query = query.ilike("name", `%${filter}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sort) {
|
||||||
|
query = query.order(sort, { ascending: order === "asc" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (limit !== null && offset !== null) {
|
||||||
|
query = query.range(offset, offset + limit - 1);
|
||||||
|
} else if (limit !== null) {
|
||||||
|
query = query.limit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await query;
|
||||||
|
|
||||||
|
console.log("Fetched villas:", data);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Error fetching villas:", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
allRows = data.map((villa) => ({
|
||||||
|
...villa,
|
||||||
|
villa_condition: villa.villa_condition || "",
|
||||||
|
villa_status: villa.villa_status || "",
|
||||||
|
created_at: new Date(villa.created_at).toISOString(),
|
||||||
|
})) as Villa[];
|
||||||
|
|
||||||
|
// count the total number of rows
|
||||||
|
const { count, error: countError } = await supabase
|
||||||
|
.from("villas")
|
||||||
|
.select("*", { count: "exact" })
|
||||||
|
.ilike("name", filter ? `%${filter}%` : "%");
|
||||||
|
|
||||||
|
if (countError) {
|
||||||
|
console.error("Error counting villas:", countError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("Total villas count:", count);
|
||||||
|
|
||||||
|
totalItems = count || 0;
|
||||||
|
offset = offset || 0; // Ensure offset is set to 0 if not provided
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(fetchVillas);
|
onMount(() => {
|
||||||
|
fetchVillas(null, "created_at", "desc", 10, 0);
|
||||||
|
});
|
||||||
|
|
||||||
let currentPage = 1;
|
let currentPage = offset + 1; // Start at page 1
|
||||||
let rowsPerPage = 5;
|
let rowsPerPage = limit || 10; // Default to 10 rows per page
|
||||||
$: totalPages = Math.ceil(allRows.length / rowsPerPage);
|
$: totalPages = Math.ceil(totalItems / rowsPerPage);
|
||||||
$: paginatedRows = allRows.slice(
|
|
||||||
(currentPage - 1) * rowsPerPage,
|
|
||||||
currentPage * rowsPerPage,
|
|
||||||
);
|
|
||||||
|
|
||||||
function goToPage(page: number) {
|
function goToPage(page: number) {
|
||||||
if (page >= 1 && page <= totalPages) currentPage = page;
|
if (page >= 1 && page <= totalPages) currentPage = page;
|
||||||
@@ -106,7 +152,7 @@
|
|||||||
let isEditing = false;
|
let isEditing = false;
|
||||||
let currentEditingId: string | null = null;
|
let currentEditingId: string | null = null;
|
||||||
let newVilla: Record<string, any> = {};
|
let newVilla: Record<string, any> = {};
|
||||||
const excludedKeys = ["id", "createdAt", "actions"];
|
const excludedKeys = ["id", "created_at", "created_by", "actions"];
|
||||||
$: formColumns = columns.filter((col) => !excludedKeys.includes(col.key));
|
$: formColumns = columns.filter((col) => !excludedKeys.includes(col.key));
|
||||||
|
|
||||||
function openModal(villa?: Villa) {
|
function openModal(villa?: Villa) {
|
||||||
@@ -121,7 +167,58 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveVilla() {
|
//validation function
|
||||||
|
function validateForm(formData: FormData): boolean {
|
||||||
|
// Basic validation: check if required fields are filled
|
||||||
|
const errors: { [key: string]: string } = {};
|
||||||
|
const requiredFields = [
|
||||||
|
"name",
|
||||||
|
"villa_manager",
|
||||||
|
"villa_location",
|
||||||
|
"villa_status",
|
||||||
|
"no_of_bedrooms",
|
||||||
|
"villa_condition",
|
||||||
|
"session_1_rate",
|
||||||
|
"session_2_rate",
|
||||||
|
"session_3_rate",
|
||||||
|
"session_4_rate",
|
||||||
|
"session_5_rate",
|
||||||
|
"session_6_rate",
|
||||||
|
"session_7_rate",
|
||||||
|
"villa_email_address",
|
||||||
|
"owner_portal_username",
|
||||||
|
"owner_portal_password",
|
||||||
|
"closeable_living_room",
|
||||||
|
"monthly_rental_pre_approved_status",
|
||||||
|
"long_term_rental_pre_approval",
|
||||||
|
"pet_allowed_pre_approval_status",
|
||||||
|
"villa_recovery_email_adress",
|
||||||
|
"villa_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";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveVilla(event: Event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(event.target as HTMLFormElement);
|
||||||
|
|
||||||
|
if (!validateForm(formData)) {
|
||||||
|
console.error("Form validation failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isEditing && currentEditingId) {
|
if (isEditing && currentEditingId) {
|
||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
.from("villas")
|
.from("villas")
|
||||||
@@ -156,23 +253,100 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset form errors when modal is closed
|
||||||
|
$: if (!showModal) {
|
||||||
|
formErrors.set({});
|
||||||
|
}
|
||||||
|
|
||||||
|
// update villa status
|
||||||
|
async function updateVillaStatus(villaId: string, status: string) {
|
||||||
|
const { error } = await supabase
|
||||||
|
.from("villas")
|
||||||
|
.update({ villa_status: status })
|
||||||
|
.eq("id", villaId);
|
||||||
|
if (error) {
|
||||||
|
console.error("Error updating villa status:", error);
|
||||||
|
} else {
|
||||||
|
await fetchVillas();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update villa condition
|
||||||
|
async function updateVillaCondition(villaId: string, condition: string) {
|
||||||
|
const { error } = await supabase
|
||||||
|
.from("villas")
|
||||||
|
.update({ villa_condition: condition })
|
||||||
|
.eq("id", villaId);
|
||||||
|
if (error) {
|
||||||
|
console.error("Error updating villa condition:", error);
|
||||||
|
} else {
|
||||||
|
await fetchVillas();
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
|
<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>
|
<div>
|
||||||
<h2 class="text-lg font-semibold text-gray-800">Villa List</h2>
|
<h2 class="text-xl font-semibold text-gray-800">🏡 Villa List</h2>
|
||||||
<p class="text-sm text-gray-600">
|
<p class="text-sm text-gray-500">
|
||||||
Manage and track villas efficiently
|
Manage and track villas efficiently
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
|
||||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
|
<input
|
||||||
on:click={() => openModal()}
|
type="text"
|
||||||
>
|
placeholder="🔍 Search by name..."
|
||||||
➕ Add Villa
|
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"
|
||||||
</button>
|
on:input={(e) => {
|
||||||
|
const searchTerm = (
|
||||||
|
e.target as HTMLInputElement
|
||||||
|
).value.toLowerCase();
|
||||||
|
fetchVillas(searchTerm, "created_at", "desc", limit, 0);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<!-- filter -->
|
||||||
|
<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 sortBy = (e.target as HTMLSelectElement).value;
|
||||||
|
fetchVillas(null, sortBy, "desc", limit, 0);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="created_at">Sort by Created At</option>
|
||||||
|
<option value="name">Sort by Name</option>
|
||||||
|
<option value="villa_manager">Sort by Villa Manager</option>
|
||||||
|
<option value="villa_location">Sort by Villa Location</option>
|
||||||
|
<option value="villa_status">Sort by Villa Status</option>
|
||||||
|
<option value="no_of_bedrooms">Sort by No Of Bedrooms</option>
|
||||||
|
<option value="villa_condition">Sort by Villa Condition</option>
|
||||||
|
<option value="session_1_rate">Sort by Session 1 Rate</option>
|
||||||
|
<option value="session_2_rate">Sort by Session 2 Rate</option>
|
||||||
|
<option value="session_3_rate">Sort by Session 3 Rate</option>
|
||||||
|
<option value="session_4_rate">Sort by Session 4 Rate</option>
|
||||||
|
<option value="session_5_rate">Sort by Session 5 Rate</option>
|
||||||
|
<option value="session_6_rate">Sort by Session 6 Rate</option>
|
||||||
|
<option value="session_7_rate">Sort by Session 7 Rate</option>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
class="bg-gray-200 text-gray-700 px-4 py-2 rounded-xl hover:bg-gray-300 text-sm transition"
|
||||||
|
on:click={() =>
|
||||||
|
fetchVillas(null, "created_at", "desc", limit, 0)}
|
||||||
|
>
|
||||||
|
🔄 Reset
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="bg-blue-600 text-white px-4 py-2 rounded-xl hover:bg-blue-700 text-sm transition"
|
||||||
|
on:click={() => openModal()}
|
||||||
|
>
|
||||||
|
➕ Add Villa
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overflow-x-auto rounded-lg shadow mb-4">
|
<div class="overflow-x-auto rounded-lg shadow mb-4">
|
||||||
<table class="min-w-[1000px] divide-y divide-gray-200 text-sm w-max">
|
<table class="min-w-[1000px] divide-y divide-gray-200 text-sm w-max">
|
||||||
<thead class="bg-gray-100">
|
<thead class="bg-gray-100">
|
||||||
@@ -196,7 +370,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200 bg-white">
|
<tbody class="divide-y divide-gray-200 bg-white">
|
||||||
{#each paginatedRows as row}
|
{#each allRows as row}
|
||||||
<tr class="hover:bg-gray-50 transition">
|
<tr class="hover:bg-gray-50 transition">
|
||||||
{#each columns as col}
|
{#each columns as col}
|
||||||
{#if col.key === "name"}
|
{#if col.key === "name"}
|
||||||
@@ -206,6 +380,138 @@
|
|||||||
>
|
>
|
||||||
{row[col.key]}
|
{row[col.key]}
|
||||||
</td>
|
</td>
|
||||||
|
{:else if col.key === "created_at"}
|
||||||
|
<td class="px-4 py-2 text-gray-500">
|
||||||
|
{new Date(row[col.key]).toLocaleDateString(
|
||||||
|
"en-US",
|
||||||
|
{
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
{:else if col.key === "villa_status"}
|
||||||
|
<!-- dropdown -->
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
<select
|
||||||
|
class="border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:outline-none px-3 py-1 rounded text-sm w-full"
|
||||||
|
bind:value={row[col.key]}
|
||||||
|
on:change={(e) => {
|
||||||
|
(row as any)[col.key] = (
|
||||||
|
e.target as HTMLSelectElement
|
||||||
|
).value;
|
||||||
|
updateVillaStatus(
|
||||||
|
row.id,
|
||||||
|
(row as any)[col.key],
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="" disabled
|
||||||
|
>SELECT STATUS</option
|
||||||
|
>
|
||||||
|
<option value="available">ACTIVE</option
|
||||||
|
>
|
||||||
|
<option value="rented">INACTIVE</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
{:else if col.key === "villa_condition"}
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
<select
|
||||||
|
class="border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:outline-none px-3 py-1 rounded text-sm w-full"
|
||||||
|
bind:value={row[col.key]}
|
||||||
|
on:change={(e) => {
|
||||||
|
(row as any)[col.key] = (
|
||||||
|
e.target as HTMLSelectElement
|
||||||
|
).value;
|
||||||
|
updateVillaCondition(
|
||||||
|
row.id,
|
||||||
|
(row as any)[col.key],
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="" disabled
|
||||||
|
>SELECT CONDITION</option
|
||||||
|
>
|
||||||
|
<option value="new">New</option>
|
||||||
|
<option value="good">Good</option>
|
||||||
|
<option value="needs_maintenance"
|
||||||
|
>Needs Maintenance</option
|
||||||
|
>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
{:else if col.key === "villa_manager"}
|
||||||
|
<td class="px-4 py-2 text-gray-500">
|
||||||
|
{row[col.key] || "N/A"}
|
||||||
|
</td>
|
||||||
|
{:else if col.key === "villa_location"}
|
||||||
|
<td class="px-4 py-2 text-gray-500">
|
||||||
|
{row[col.key] || "N/A"}
|
||||||
|
</td>
|
||||||
|
{:else if col.key === "no_of_bedrooms"}
|
||||||
|
<td class="px-4 py-2 text-gray-500">
|
||||||
|
{row[col.key] || "N/A"}
|
||||||
|
</td>
|
||||||
|
{:else if col.key === "created_by"}
|
||||||
|
<td class="px-4 py-2 text-gray-500">
|
||||||
|
{row[col.key] || "N/A"}
|
||||||
|
</td>
|
||||||
|
{:else if col.key === "villa_email_address"}
|
||||||
|
<td class="px-4 py-2 text-gray-500">
|
||||||
|
{row[col.key] || "N/A"}
|
||||||
|
</td>
|
||||||
|
{:else if col.key === "owner_portal_username"}
|
||||||
|
<td class="px-4 py-2 text-gray-500">
|
||||||
|
{row[col.key] || "N/A"}
|
||||||
|
</td>
|
||||||
|
{:else if col.key === "owner_portal_password"}
|
||||||
|
<td class="px-4 py-2 text-gray-500">
|
||||||
|
{row[col.key] || "N/A"}
|
||||||
|
</td>
|
||||||
|
{:else if col.key === "closeable_living_room"}
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
{row[col.key] ? "✔️ Yes" : "❌ No"}
|
||||||
|
</td>
|
||||||
|
{:else if col.key === "monthly_rental_pre_approved_status"}
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
{row[col.key] ? "✔️ Yes" : "❌ No"}
|
||||||
|
</td>
|
||||||
|
{:else if col.key === "long_term_rental_pre_approval"}
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
{row[col.key] ? "✔️ Yes" : "❌ No"}
|
||||||
|
</td>
|
||||||
|
{:else if col.key === "pet_allowed_pre_approval_status"}
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
{row[col.key] ? "✔️ Yes" : "❌ No"}
|
||||||
|
</td>
|
||||||
|
{:else if col.key === "session_1_rate"}
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
{row[col.key] || "N/A"}
|
||||||
|
</td>
|
||||||
|
{:else if col.key === "session_2_rate"}
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
{row[col.key] || "N/A"}
|
||||||
|
</td>
|
||||||
|
{:else if col.key === "session_3_rate"}
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
{row[col.key] || "N/A"}
|
||||||
|
</td>
|
||||||
|
{:else if col.key === "session_4_rate"}
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
{row[col.key] || "N/A"}
|
||||||
|
</td>
|
||||||
|
{:else if col.key === "session_5_rate"}
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
{row[col.key] || "N/A"}
|
||||||
|
</td>
|
||||||
|
{:else if col.key === "session_6_rate"}
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
{row[col.key] || "N/A"}
|
||||||
|
</td>
|
||||||
|
{:else if col.key === "session_7_rate"}
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
{row[col.key] || "N/A"}
|
||||||
|
</td>
|
||||||
{:else if col.key === "actions"}
|
{:else if col.key === "actions"}
|
||||||
<td class="px-4 py-2">
|
<td class="px-4 py-2">
|
||||||
<button
|
<button
|
||||||
@@ -235,10 +541,7 @@
|
|||||||
<!-- Pagination controls -->
|
<!-- Pagination controls -->
|
||||||
<div class="flex justify-between items-center text-sm">
|
<div class="flex justify-between items-center text-sm">
|
||||||
<div>
|
<div>
|
||||||
Showing {(currentPage - 1) * rowsPerPage + 1}–{Math.min(
|
Showing {currentPage} of {totalPages} pages ({allRows.length} items total)
|
||||||
currentPage * rowsPerPage,
|
|
||||||
allRows.length,
|
|
||||||
)} of {allRows.length}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="space-x-2">
|
<div class="space-x-2">
|
||||||
<button
|
<button
|
||||||
@@ -277,24 +580,202 @@
|
|||||||
<div
|
<div
|
||||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||||
>
|
>
|
||||||
<div
|
<form
|
||||||
class="bg-white p-6 rounded shadow-lg w-[600px] max-h-[90vh] overflow-y-auto space-y-4"
|
class="bg-white p-6 rounded shadow-lg w-[600px] max-h-[90vh] overflow-y-auto space-y-4"
|
||||||
|
on:submit={saveVilla}
|
||||||
>
|
>
|
||||||
<h3 class="text-lg font-semibold">
|
<h3 class="text-lg font-semibold">
|
||||||
{isEditing ? "Edit Villa" : "Add New Villa"}
|
{isEditing ? "Edit Villa" : "Add New Villa"}
|
||||||
</h3>
|
</h3>
|
||||||
{#each formColumns as col}
|
{#each formColumns as col}
|
||||||
<div class="space-y-1">
|
{#if col.key === "villa_status"}
|
||||||
<label class="block text-sm font-medium text-gray-700"
|
<div class="space-y-1">
|
||||||
>{col.title}</label
|
<label class="block text-sm font-medium text-gray-700"
|
||||||
>
|
>{col.title}</label
|
||||||
<input
|
>
|
||||||
class="w-full border px-3 py-2 rounded"
|
<select
|
||||||
type="text"
|
name={col.key}
|
||||||
bind:value={newVilla[col.key]}
|
class="w-full border px-3 py-2 rounded {errorClass(
|
||||||
placeholder={col.title}
|
col.key,
|
||||||
/>
|
)}"
|
||||||
</div>
|
bind:value={newVilla[col.key]}
|
||||||
|
>
|
||||||
|
<option value="" disabled selected
|
||||||
|
>SELECT STATUS</option
|
||||||
|
>
|
||||||
|
<option value="available">ACTIVE</option>
|
||||||
|
<option value="rented">INACTIVE</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{#if $formErrors.villa_status}
|
||||||
|
<p class="text-red-500 text-xs">
|
||||||
|
{$formErrors.villa_status}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
{:else if col.key === "villa_condition"}
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium text-gray-700"
|
||||||
|
>{col.title}</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
name={col.key}
|
||||||
|
class="w-full border px-3 py-2 rounded {errorClass(
|
||||||
|
col.key,
|
||||||
|
)}"
|
||||||
|
bind:value={newVilla[col.key]}
|
||||||
|
>
|
||||||
|
<option value="" disabled selected
|
||||||
|
>SELECT CONDITION</option
|
||||||
|
>
|
||||||
|
<option value="new">New</option>
|
||||||
|
<option value="good">Good</option>
|
||||||
|
<option value="needs_maintenance"
|
||||||
|
>Needs Maintenance</option
|
||||||
|
>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{#if $formErrors.villa_condition}
|
||||||
|
<p class="text-red-500 text-xs">
|
||||||
|
{$formErrors.villa_condition}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
{:else if col.key === "closeable_living_room"}
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium text-gray-700"
|
||||||
|
>{col.title}</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
name={col.key}
|
||||||
|
class="w-full border px-3 py-2 rounded {errorClass(
|
||||||
|
col.key,
|
||||||
|
)}"
|
||||||
|
bind:value={newVilla[col.key]}
|
||||||
|
>
|
||||||
|
<option value="" disabled selected
|
||||||
|
>SELECT OPTION</option
|
||||||
|
>
|
||||||
|
<option value="true">Yes</option>
|
||||||
|
<option value="false">No</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{#if $formErrors.closeable_living_room}
|
||||||
|
<p class="text-red-500 text-xs">
|
||||||
|
{$formErrors.closeable_living_room}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
{:else if col.key === "monthly_rental_pre_approved_status"}
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium text-gray-700"
|
||||||
|
>{col.title}</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
name={col.key}
|
||||||
|
class="w-full border px-3 py-2 rounded {errorClass(
|
||||||
|
col.key,
|
||||||
|
)}"
|
||||||
|
bind:value={newVilla[col.key]}
|
||||||
|
>
|
||||||
|
<option value="" disabled selected
|
||||||
|
>SELECT OPTION</option
|
||||||
|
>
|
||||||
|
<option value="true">Yes</option>
|
||||||
|
<option value="false">No</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{#if $formErrors.monthly_rental_pre_approved_status}
|
||||||
|
<p class="text-red-500 text-xs">
|
||||||
|
{$formErrors.monthly_rental_pre_approved_status}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
{:else if col.key === "long_term_rental_pre_approval"}
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium text-gray-700"
|
||||||
|
>{col.title}</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
name={col.key}
|
||||||
|
class="w-full border px-3 py-2 rounded {errorClass(
|
||||||
|
col.key,
|
||||||
|
)}"
|
||||||
|
bind:value={newVilla[col.key]}
|
||||||
|
>
|
||||||
|
<option value="" disabled selected
|
||||||
|
>SELECT OPTION</option
|
||||||
|
>
|
||||||
|
<option value="true">Yes</option>
|
||||||
|
<option value="false">No</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{#if $formErrors.long_term_rental_pre_approval}
|
||||||
|
<p class="text-red-500 text-xs">
|
||||||
|
{$formErrors.long_term_rental_pre_approval}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
{:else if col.key === "pet_allowed_pre_approval_status"}
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium text-gray-700"
|
||||||
|
>{col.title}</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
name={col.key}
|
||||||
|
class="w-full border px-3 py-2 rounded {errorClass(
|
||||||
|
col.key,
|
||||||
|
)}"
|
||||||
|
bind:value={newVilla[col.key]}
|
||||||
|
>
|
||||||
|
<option value="" disabled selected
|
||||||
|
>SELECT OPTION</option
|
||||||
|
>
|
||||||
|
<option value="true">Yes</option>
|
||||||
|
<option value="false">No</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{#if $formErrors.pet_allowed_pre_approval_status}
|
||||||
|
<p class="text-red-500 text-xs">
|
||||||
|
{$formErrors.pet_allowed_pre_approval_status}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
{:else if col.key === "session_1_rate" || col.key === "session_2_rate" || col.key === "session_3_rate" || col.key === "session_4_rate" || col.key === "session_5_rate" || col.key === "session_6_rate" || col.key === "session_7_rate"}
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium text-gray-700"
|
||||||
|
>{col.title}</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
name={col.key}
|
||||||
|
class="w-full border px-3 py-2 rounded {errorClass(
|
||||||
|
col.key,
|
||||||
|
)}"
|
||||||
|
type="number"
|
||||||
|
bind:value={newVilla[col.key]}
|
||||||
|
placeholder={col.title}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#if $formErrors[col.key]}
|
||||||
|
<p class="text-red-500 text-xs">
|
||||||
|
{$formErrors[col.key]}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium text-gray-700"
|
||||||
|
>{col.title}</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
name={col.key}
|
||||||
|
class="w-full border px-3 py-2 rounded {errorClass(
|
||||||
|
col.key,
|
||||||
|
)}"
|
||||||
|
type="text"
|
||||||
|
bind:value={newVilla[col.key]}
|
||||||
|
placeholder={col.title}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#if $formErrors[col.key]}
|
||||||
|
<p class="text-red-500 text-xs">
|
||||||
|
{$formErrors[col.key]}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
<div class="flex justify-end gap-2 mt-4">
|
<div class="flex justify-end gap-2 mt-4">
|
||||||
<button
|
<button
|
||||||
@@ -305,11 +786,11 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="px-4 py-2 text-sm rounded bg-blue-600 text-white hover:bg-blue-700"
|
class="px-4 py-2 text-sm rounded bg-blue-600 text-white hover:bg-blue-700"
|
||||||
on:click={saveVilla}
|
type="submit"
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user