:wq
: Merge branch 'main' of https://gitea.catalify.catalis.app/aji/villa_bugis
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,3 +23,4 @@ vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
@@ -2,7 +2,8 @@
|
||||
export let value: number = 0; // The raw number
|
||||
export let label: string = ""; // Field label
|
||||
export let onInput: (() => void) | null = null; // Optional extra handler
|
||||
|
||||
export let disabled: boolean = false;
|
||||
export let className: string = "";
|
||||
let formatted = "";
|
||||
|
||||
// Format whenever value changes
|
||||
@@ -26,7 +27,8 @@
|
||||
<input
|
||||
type="text"
|
||||
bind:value={formatted}
|
||||
placeholder="Rp 0"
|
||||
placeholder="Rp 0"
|
||||
class="w-full border p-2 rounded ${className}"
|
||||
on:input={handleInput}
|
||||
disabled={disabled}
|
||||
/>
|
||||
@@ -270,6 +270,8 @@
|
||||
let dataUser: User[] = [];
|
||||
let projectIssueMap: Set<string> = new Set();
|
||||
let purchaseOrderMap: Set<string> = new Set();
|
||||
let sortColumn: string | null = "created_at"; // or any default column
|
||||
let sortOrder: "asc" | "desc" = "desc";
|
||||
let poItems: POItem[] = [];
|
||||
let newPO: PurchaseOrder = {
|
||||
villa_id: "",
|
||||
@@ -349,11 +351,20 @@
|
||||
const { data } = await supabase.from("vb_po_item").select("item_name");
|
||||
if (data) poItems = data;
|
||||
}
|
||||
function getDBColumn(key: string) {
|
||||
switch (key) {
|
||||
case "villa_name": return "villa_name";
|
||||
case "reported_by": return "reported_by";
|
||||
// add mappings if needed
|
||||
default: return key;
|
||||
}
|
||||
}
|
||||
// Fetch issues with optional filters
|
||||
async function fetchIssues() {
|
||||
let query = supabase
|
||||
.from("vb_issues_data")
|
||||
.select("*").order("issue_number", { ascending: true });
|
||||
.select("*")
|
||||
.order(sortColumn || "created_at", { ascending: sortOrder === "asc" });
|
||||
|
||||
const { data: issues, error } = await query;
|
||||
|
||||
@@ -599,7 +610,15 @@
|
||||
alert("Purchase Order submitted!");
|
||||
showPurchaseOrderModal = false;
|
||||
}
|
||||
|
||||
function toggleSort(column: string) {
|
||||
if (sortColumn === column) {
|
||||
sortOrder = sortOrder === "asc" ? "desc" : "asc";
|
||||
} else {
|
||||
sortColumn = column;
|
||||
sortOrder = "asc";
|
||||
}
|
||||
fetchIssues(); // re-fetch or re-sort your rows
|
||||
}
|
||||
// Function to open purchase order modal
|
||||
function openPurchaseOrderModal(issue) {
|
||||
if (purchaseOrderMap.has(issue.id)) {
|
||||
@@ -787,7 +806,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto rounded-lg shadow mb-4">
|
||||
<div class="overflow-x-auto rounded-lg shadow mb-4 max-h-[70vh]">
|
||||
<table class="min-w-[1000px] divide-y divide-gray-200 text-sm w-max">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
@@ -796,14 +815,22 @@
|
||||
<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;"
|
||||
on:click={() => toggleSort(col.key)}
|
||||
>
|
||||
{col.title}
|
||||
{#if sortColumn === col.key}
|
||||
{sortOrder === 'asc' ? ' 🔼' : ' 🔽'}
|
||||
{/if}
|
||||
</th>
|
||||
{:else}
|
||||
<th
|
||||
class="px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||||
on:click={() => toggleSort(col.key)}
|
||||
>
|
||||
{col.title}
|
||||
{#if sortColumn === col.key}
|
||||
{sortOrder === 'asc' ? ' 🔼' : ' 🔽'}
|
||||
{/if}
|
||||
</th>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
@@ -161,6 +161,8 @@
|
||||
updated_by: "",
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
let sortColumn: string | null = "created_at"; // or any default
|
||||
let sortOrder: "asc" | "desc" = "desc";
|
||||
let showEditModal = false;
|
||||
let editForm = {
|
||||
po_number: "",
|
||||
@@ -191,7 +193,13 @@
|
||||
approval: "",
|
||||
approved_by: "",
|
||||
approved_date: "",
|
||||
reject_comment: ""
|
||||
reject_comment: "",
|
||||
po_remark: "",
|
||||
approved_quantity: 0,
|
||||
approved_vendor: "",
|
||||
approved_price: 0,
|
||||
po_item: "",
|
||||
total_approved_order_amount: 0
|
||||
};
|
||||
let villaOptions = [];
|
||||
let poItemOptions = [];
|
||||
@@ -283,6 +291,21 @@
|
||||
currency: "IDR",
|
||||
minimumFractionDigits: 0,
|
||||
});
|
||||
// function Sort
|
||||
function toggleSort(column: string) {
|
||||
if (sortColumn === column) {
|
||||
sortOrder = sortOrder === "asc" ? "desc" : "asc";
|
||||
} else {
|
||||
sortColumn = column;
|
||||
sortOrder = "asc";
|
||||
}
|
||||
fetchPurchaseOrder(
|
||||
selectedVillaId,
|
||||
searchTerm,
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
);
|
||||
}
|
||||
|
||||
// Function to format numbers as ordinal (1st, 2nd, 3rd, etc.)
|
||||
function ordinal(num: number) {
|
||||
@@ -323,7 +346,13 @@
|
||||
approval: row.approval ? "approve" : "reject",
|
||||
approved_by: currentUserId,
|
||||
approved_date: new Date().toISOString().split("T")[0],
|
||||
reject_comment: row.reject_comment || ""
|
||||
reject_comment: row.reject_comment || "",
|
||||
po_remark: row.po_remark || "",
|
||||
approved_quantity: row.approved_quantity || 0,
|
||||
approved_vendor: row.approved_vendor || "",
|
||||
approved_price: row.approved_price || 0,
|
||||
po_item: row.po_item || "",
|
||||
total_approved_order_amount: row.total_approved_order_amount || 0
|
||||
};
|
||||
|
||||
showApprovalModal = true;
|
||||
@@ -478,7 +507,8 @@
|
||||
approved_by: approvalForm.approval === "approve" ? currentUserId : null,
|
||||
approved_date: approvalForm.approved_date,
|
||||
reject_comment: approvalForm.reject_comment || null,
|
||||
po_status: "approved"
|
||||
po_status: "approved",
|
||||
po_remark: approvalForm.po_remark || null
|
||||
})
|
||||
.eq("id", selectedPO.id);
|
||||
|
||||
@@ -692,7 +722,7 @@
|
||||
let query = supabase
|
||||
.from("vb_purchaseorder_data")
|
||||
.select("*")
|
||||
.order(sort || "purchase_order_number", { ascending: order === "desc" })
|
||||
.order(sortColumn || "created_at", { ascending: sortOrder === "asc" })
|
||||
.range(offset, offset + limit - 1);
|
||||
|
||||
if (filter) {
|
||||
@@ -1070,7 +1100,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto rounded-lg shadow mb-4">
|
||||
<div class="overflow-x-auto rounded-lg shadow mb-4 max-h-[70vh]">
|
||||
<table class="min-w-[1000px] divide-y divide-gray-200 text-sm w-max">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
@@ -1079,14 +1109,22 @@
|
||||
<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;"
|
||||
on:click={() => toggleSort(col.key)}
|
||||
>
|
||||
{col.title}
|
||||
{#if sortColumn === col.key}
|
||||
{sortOrder === 'asc' ? ' 🔼' : ' 🔽'}
|
||||
{/if}
|
||||
</th>
|
||||
{:else}
|
||||
<th
|
||||
class="px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||||
on:click={() => toggleSort(col.key)}
|
||||
>
|
||||
{col.title}
|
||||
{#if sortColumn === col.key}
|
||||
{sortOrder === 'asc' ? ' 🔼' : ' 🔽'}
|
||||
{/if}
|
||||
</th>
|
||||
{/if}
|
||||
{/each}
|
||||
@@ -1650,11 +1688,10 @@
|
||||
<!-- Payment Amounts -->
|
||||
{#each [1,2,3,4,5,6] as num}
|
||||
<label>{ordinal(num)} Pay Amount</label>
|
||||
<input
|
||||
type="number"
|
||||
<CurrencyInput
|
||||
bind:value={paymentForm[`${ordinal(num)}_pay_amt`]}
|
||||
on:input={updateDueRemaining}
|
||||
class="w-full border p-2"
|
||||
onInput={updateDueRemaining}
|
||||
className="w-full"
|
||||
/>
|
||||
|
||||
<label>{ordinal(num)} Pay Date</label>
|
||||
@@ -1667,7 +1704,7 @@
|
||||
|
||||
<!-- Due Remaining -->
|
||||
<label>Due Remaining</label>
|
||||
<input type="number" value={paymentForm.due_remaining} disabled class="w-full border p-2 bg-gray-100"/>
|
||||
<CurrencyInput value={paymentForm.due_remaining} disabled className="w-full border p-2 bg-gray-100"/>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex justify-end space-x-2 pt-4">
|
||||
@@ -1693,27 +1730,89 @@
|
||||
class="w-full border p-2 bg-gray-100"
|
||||
/>
|
||||
|
||||
<!-- Approval -->
|
||||
<!-- PO Item -->
|
||||
{#if approvalForm.po_item}
|
||||
<label>PO Item</label>
|
||||
<input
|
||||
type="text"
|
||||
value={approvalForm.po_item}
|
||||
disabled
|
||||
class="w-full border p-2 bg-gray-100"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Approved Quantity -->
|
||||
{#if approvalForm.approved_quantity != null}
|
||||
<label>Approved Quantity</label>
|
||||
<input
|
||||
type="number"
|
||||
value={approvalForm.approved_quantity}
|
||||
disabled
|
||||
class="w-full border p-2 bg-gray-100"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Approved Price -->
|
||||
{#if approvalForm.approved_price != null}
|
||||
<label>Approved Price</label>
|
||||
<CurrencyInput
|
||||
value={approvalForm.approved_price}
|
||||
disabled
|
||||
className="w-full border p-2 bg-gray-100"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Total Approved Order Amount -->
|
||||
{#if approvalForm.total_approved_order_amount != null}
|
||||
<label>Total Approved Order Amount</label>
|
||||
<CurrencyInput
|
||||
value={approvalForm.total_approved_order_amount}
|
||||
disabled
|
||||
className="w-full border p-2 bg-gray-100"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Approved Vendor -->
|
||||
{#if approvalForm.approved_vendor}
|
||||
<label>Approved Vendor</label>
|
||||
<input
|
||||
type="text"
|
||||
value={approvalForm.approved_vendor}
|
||||
disabled
|
||||
class="w-full border p-2 bg-gray-100"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Approval Decision -->
|
||||
<label>Approval</label>
|
||||
<select bind:value={approvalForm.approval} class="w-full border p-2">
|
||||
<option value="" disabled>Select Approval</option>
|
||||
<option value="approve">Approve</option>
|
||||
<option value="reject">Reject</option>
|
||||
</select>
|
||||
|
||||
<!-- Reject Comment -->
|
||||
{#if approvalForm.approval === 'reject'}
|
||||
<label>Reject Comment</label>
|
||||
<textarea
|
||||
<label>Reject Comment</label>
|
||||
<textarea
|
||||
bind:value={approvalForm.reject_comment}
|
||||
class="w-full border p-2"
|
||||
rows="3"
|
||||
placeholder="Enter reason for rejection..."
|
||||
></textarea>
|
||||
></textarea>
|
||||
{/if}
|
||||
|
||||
<!-- Hidden: approved_by -->
|
||||
<input type="hidden" value={approvalForm.approved_by} />
|
||||
<!-- PO Remark (editable!) -->
|
||||
<label>PO Remark</label>
|
||||
<textarea
|
||||
bind:value={approvalForm.po_remark}
|
||||
class="w-full border p-2"
|
||||
rows="3"
|
||||
placeholder="Add or edit PO remark..."
|
||||
></textarea>
|
||||
|
||||
<!-- Hidden: approved_date -->
|
||||
<!-- Hidden fields -->
|
||||
<input type="hidden" value={approvalForm.approved_by} />
|
||||
<input type="hidden" value={approvalForm.approved_date} />
|
||||
|
||||
<!-- Actions -->
|
||||
@@ -1726,6 +1825,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
{#if showAcknowledgedModal}
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 overflow-y-auto">
|
||||
<div class="min-h-screen flex items-center justify-center p-4">
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { supabase } from "$lib/supabaseClient";
|
||||
import { onMount } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
import Pagination from "$lib/Pagination.svelte";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
type Timesheets = {
|
||||
@@ -163,7 +161,6 @@
|
||||
total_work_hour: 0,
|
||||
remarks: "",
|
||||
approval: null, // Default null
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
// Fetch initial data on mount
|
||||
onMount(async () => {
|
||||
@@ -333,9 +330,10 @@
|
||||
'*',
|
||||
{ count: "exact" },
|
||||
)
|
||||
.order(sortColumn || "created_at", {
|
||||
ascending: sortOrder === "asc",
|
||||
}).range(fromIndex, toIndex);
|
||||
.order(getDBColumn(sortColumn) || "created_at", {
|
||||
ascending: sortOrder === "asc"
|
||||
})
|
||||
.range(fromIndex, toIndex);
|
||||
|
||||
if (typeof searchTerm === "string" && searchTerm.length > 4) {
|
||||
// Supabase ilike only supports one column at a time, so use or for multiple columns
|
||||
@@ -383,7 +381,11 @@
|
||||
new Date(tsdata.datetime_out).getTime() -
|
||||
new Date(tsdata.datetime_in).getTime(),
|
||||
) / (1000 * 60 * 60),
|
||||
approved_by: tsdata.approved_name || "Not Approved",
|
||||
approved_by: tsdata.approved_name?.trim()
|
||||
? tsdata.approved_name
|
||||
: tsdata.approval === true
|
||||
? "Auto Approve"
|
||||
: "Not Approved",
|
||||
approved_date: tsdata.approved_date
|
||||
? new Date(tsdata.approved_date)
|
||||
: undefined,
|
||||
@@ -423,7 +425,13 @@
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
function getDBColumn(key: string) {
|
||||
switch (key) {
|
||||
case "name": return "work_description";
|
||||
case "staff_id": return "entered_by";
|
||||
default: return key;
|
||||
}
|
||||
}
|
||||
function changePage(page: number) {
|
||||
if (page < 1 || page > totalPages || page === currentPage) return;
|
||||
currentPage = page;
|
||||
@@ -477,132 +485,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// // Function to fetch timesheets with optional filters and sorting
|
||||
// async function fetchTimeSheets(
|
||||
// villaIdFilter: string | null = null,
|
||||
// searchTerm: string | null = null,
|
||||
// offset: number = 0,
|
||||
// limit: number = 1000,
|
||||
// ) {
|
||||
// let reportedBy: { label: string; value: string }[] = [];
|
||||
// const { data: staffData, error: staffError } = await supabase
|
||||
// .from("vb_employee")
|
||||
// .select("id, employee_name")
|
||||
// .eq("employee_status", "Active")
|
||||
// .order("employee_name", { ascending: true });
|
||||
|
||||
// if (staffError) {
|
||||
// console.error("Error fetching staff:", staffError);
|
||||
// } else if (staffData) {
|
||||
// reportedBy = staffData.map((s) => ({
|
||||
// label: s.employee_name,
|
||||
// value: s.id,
|
||||
// }));
|
||||
// }
|
||||
|
||||
// let query = supabase
|
||||
// .from("vb_timesheet")
|
||||
// .select(`*`)
|
||||
// .order(sortColumn || "created_at", {
|
||||
// ascending: sortOrder === "asc",
|
||||
// });
|
||||
|
||||
// if (villaIdFilter) {
|
||||
// const { data: villaMatch } = await supabase
|
||||
// .from("vb_villas")
|
||||
// .select("id")
|
||||
// .eq("villa_name", villaIdFilter);
|
||||
|
||||
// const matchedId = villaMatch?.[0]?.id;
|
||||
// if (matchedId) {
|
||||
// query = query.eq("villa_id", matchedId);
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (offset) {
|
||||
// query = query.range(offset, offset + limit - 1);
|
||||
// }
|
||||
// if (limit) {
|
||||
// query = query.limit(limit);
|
||||
// }
|
||||
// const { data: timesheet, error } = await query;
|
||||
// if (error) {
|
||||
// console.error("Error fetching timesheets:", error);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const loweredSearch = searchTerm?.toLowerCase();
|
||||
// let filteredTimesheet = timesheet;
|
||||
// if (loweredSearch) {
|
||||
// filteredTimesheet = timesheet.filter((ts) => {
|
||||
// const workDesc = ts.work_description?.toLowerCase() || "";
|
||||
// const staffName = reportedBy.find((s) => s.value === ts.entered_by)?.label?.toLowerCase() || "";
|
||||
// return (
|
||||
// workDesc.includes(loweredSearch) ||
|
||||
// staffName.includes(loweredSearch)
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
|
||||
// const villaIds = [...new Set(filteredTimesheet.map((i: Timesheets) => i.villa_id))];
|
||||
// let villas = [];
|
||||
// if (villaIds.length > 0) {
|
||||
// const { data: villasData, error: villaError } = await supabase
|
||||
// .from("vb_villas")
|
||||
// .select("*")
|
||||
// .in("id", villaIds);
|
||||
|
||||
// if (villaError) {
|
||||
// console.error("Error fetching villas:", villaError);
|
||||
// } else {
|
||||
// villas = villasData;
|
||||
// }
|
||||
// }
|
||||
// const { data: approvers, error: approverError } = await supabase
|
||||
// .from("vb_users")
|
||||
// .select("id, full_name");
|
||||
|
||||
// if (approverError) {
|
||||
// console.error("Error fetching approvers:", approverError);
|
||||
// }
|
||||
// allRows = filteredTimesheet.map((tsdata: TimesheetsJoined) => {
|
||||
// const villa = villas.find((v) => v.id === tsdata.villa_id);
|
||||
// const approver = approvers?.find((u) => u.id === tsdata.approved_by);
|
||||
|
||||
// return {
|
||||
// id: tsdata.id,
|
||||
// name: tsdata.work_description,
|
||||
// staff_id:
|
||||
// reportedBy.find((s) => s.value === tsdata.entered_by)?.label || "Unknown",
|
||||
// date_in: new Date(tsdata.datetime_in),
|
||||
// date_out: new Date(tsdata.datetime_out),
|
||||
// type_of_work: tsdata.type_of_work,
|
||||
// category_of_work: tsdata.category_of_work,
|
||||
// villa_name: villa ? villa.villa_name : "Unknown Villa",
|
||||
// approval:
|
||||
// tsdata.approval == null
|
||||
// ? "PENDING"
|
||||
// : tsdata.approval
|
||||
// ? "APPROVED"
|
||||
// : "REJECTED",
|
||||
// total_hours_work:
|
||||
// Math.abs(
|
||||
// new Date(tsdata.datetime_out).getTime() - new Date(tsdata.datetime_in).getTime()
|
||||
// ) / (1000 * 60 * 60),
|
||||
// approved_by: approver?.full_name ?? "Not Approved",
|
||||
// approved_date: tsdata.approved_date,
|
||||
// remarks: tsdata.remarks,
|
||||
// // created_at: tsdata.created_at ? new Date(tsdata.created_at) : undefined,
|
||||
// } as TimesheetDisplay;
|
||||
// });
|
||||
// currentPage = 1;
|
||||
|
||||
// console.log("Fetched rows:", allRows);
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// Function to delete a timesheet
|
||||
async function deleteTimesheet(id: string) {
|
||||
if (confirm("Are you sure you want to delete this Timesheet?")) {
|
||||
@@ -614,7 +496,14 @@
|
||||
console.error("Error deleting Timesheet:", error);
|
||||
return;
|
||||
}
|
||||
await fetchTimeSheets();
|
||||
await fetchTimeSheets(
|
||||
currentVillaFilter,
|
||||
currentSearchTerm,
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
(currentPage - 1) * rowsPerPage,
|
||||
rowsPerPage,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Function to update the approval status of a timesheet
|
||||
@@ -637,7 +526,14 @@
|
||||
if (error) {
|
||||
console.error("Error updating approval status:", error);
|
||||
} else {
|
||||
await fetchTimeSheets();
|
||||
await fetchTimeSheets(
|
||||
currentVillaFilter,
|
||||
currentSearchTerm,
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
(currentPage - 1) * rowsPerPage,
|
||||
rowsPerPage,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Function to submit the form data
|
||||
@@ -684,7 +580,6 @@
|
||||
total_work_hour: form.total_work_hour,
|
||||
remarks: form.remarks,
|
||||
approval: form.approval || null, // Allow null for new entries
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const { error: insertError } = await supabase
|
||||
@@ -709,9 +604,15 @@
|
||||
total_work_hour: 0,
|
||||
remarks: "",
|
||||
approval: null,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
await fetchTimeSheets();
|
||||
await fetchTimeSheets(
|
||||
currentVillaFilter,
|
||||
currentSearchTerm,
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
(currentPage - 1) * rowsPerPage,
|
||||
rowsPerPage,
|
||||
);
|
||||
showModal = false;
|
||||
}
|
||||
}
|
||||
@@ -781,7 +682,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto rounded-lg shadow mb-4">
|
||||
<div class="overflow-x-auto rounded-lg shadow mb-4 max-h-[70vh]">
|
||||
<table class="min-w-[1000px] divide-y divide-gray-200 text-sm w-max">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
@@ -790,8 +691,13 @@
|
||||
<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;"
|
||||
on:click={() => toggleSort(col.key)}
|
||||
|
||||
>
|
||||
{col.title}
|
||||
{#if sortColumn === col.key}
|
||||
{sortOrder === 'asc' ? ' 🔼' : ' 🔽'}
|
||||
{/if}
|
||||
</th>
|
||||
{:else}
|
||||
<th
|
||||
@@ -801,7 +707,7 @@
|
||||
{col.title}
|
||||
{#if sortColumn === col.key}
|
||||
{sortOrder === 'asc' ? ' 🔼' : ' 🔽'}
|
||||
{/if}
|
||||
{/if}
|
||||
</th>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
274
src/routes/purchaseorder/+page.svelte
Normal file
274
src/routes/purchaseorder/+page.svelte
Normal file
@@ -0,0 +1,274 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { supabase } from "$lib/supabaseClient";
|
||||
import logo from "$lib/images/logo.webp";
|
||||
|
||||
type TimesheetForm = {
|
||||
entered_by: string;
|
||||
work_description: string;
|
||||
type_of_work: "Running" | "Periodic" | "Irregular";
|
||||
category_of_work:
|
||||
| "Cleaning"
|
||||
| "Gardening/Pool"
|
||||
| "Maintenance"
|
||||
| "Supervision"
|
||||
| "Guest Service"
|
||||
| "Administration"
|
||||
| "Non Billable";
|
||||
villa_id: string;
|
||||
datetime_in: string;
|
||||
datetime_out: string;
|
||||
total_work_hour: number;
|
||||
remarks: string;
|
||||
approval: boolean | null; // Allow null for new entries
|
||||
};
|
||||
|
||||
type Villa = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type Employee = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
let employees: Employee[] = [];
|
||||
let villas: Villa[] = [];
|
||||
|
||||
let form: TimesheetForm = {
|
||||
entered_by: "",
|
||||
work_description: "",
|
||||
type_of_work: "Running",
|
||||
category_of_work: "Cleaning",
|
||||
villa_id: "",
|
||||
datetime_in: "",
|
||||
datetime_out: "",
|
||||
total_work_hour: 0,
|
||||
remarks: "",
|
||||
approval: null, // Default null
|
||||
};
|
||||
|
||||
const typeOfWorkOptions: TimesheetForm["type_of_work"][] = [
|
||||
"Running",
|
||||
"Periodic",
|
||||
"Irregular",
|
||||
];
|
||||
const categoryOptions: TimesheetForm["category_of_work"][] = [
|
||||
"Cleaning",
|
||||
"Gardening/Pool",
|
||||
"Maintenance",
|
||||
"Supervision",
|
||||
"Guest Service",
|
||||
"Administration",
|
||||
"Non Billable",
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
// Fetch villas
|
||||
const { data: villaData, error: villaError } = await supabase
|
||||
.from("vb_villas")
|
||||
.select("id, villa_name, villa_status")
|
||||
.eq("villa_status", "Active")
|
||||
.order("villa_name", { ascending: true });
|
||||
|
||||
if (villaError) {
|
||||
console.error("Failed to fetch villas:", villaError.message);
|
||||
} else if (villaData) {
|
||||
villas = villaData.map((v) => ({ id: v.id, name: v.villa_name }));
|
||||
}
|
||||
|
||||
// Fetch employees
|
||||
const { data: empData, error: empError } = await supabase
|
||||
.from("vb_employee")
|
||||
.select("id, employee_name")
|
||||
.eq("employee_status", "Active")
|
||||
.order("employee_name", { ascending: true });
|
||||
|
||||
if (empError) {
|
||||
console.error("Failed to fetch employees:", empError.message);
|
||||
} else if (empData) {
|
||||
employees = empData.map((e) => ({ id: e.id, name: e.employee_name }));
|
||||
}
|
||||
});
|
||||
|
||||
function calculateTotalHours() {
|
||||
if (form.datetime_in && form.datetime_out) {
|
||||
const start = new Date(form.datetime_in);
|
||||
const end = new Date(form.datetime_out);
|
||||
const diffInMs = end.getTime() - start.getTime();
|
||||
const hours = diffInMs / (1000 * 60 * 60);
|
||||
form.total_work_hour = Math.max(Number(hours.toFixed(2)), 0);
|
||||
} else {
|
||||
form.total_work_hour = 0;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
calculateTotalHours();
|
||||
|
||||
if (!form.entered_by) {
|
||||
alert("Please select an employee.");
|
||||
return;
|
||||
}
|
||||
if (!form.villa_id) {
|
||||
alert("Please select a villa.");
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await supabase.from("vb_timesheet").insert([form]);
|
||||
|
||||
if (error) {
|
||||
alert("Failed to submit timesheet: " + error.message);
|
||||
} else {
|
||||
alert("Timesheet submitted successfully!");
|
||||
form = {
|
||||
entered_by: "",
|
||||
work_description: "",
|
||||
type_of_work: "Running",
|
||||
category_of_work: "Cleaning",
|
||||
villa_id: "",
|
||||
datetime_in: "",
|
||||
datetime_out: "",
|
||||
total_work_hour: 0,
|
||||
remarks: "",
|
||||
approval: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-gray-100 flex items-center justify-center">
|
||||
<form
|
||||
on:submit|preventDefault={submitForm}
|
||||
class="w-full max-w-lg bg-white p-6 rounded-2xl shadow-xl space-y-4"
|
||||
>
|
||||
<img src={logo} alt="Villa Logo" class="mx-auto mb-6" width="250" />
|
||||
|
||||
<h2 class="text-2xl font-bold text-center mb-6">Timesheet Entry</h2>
|
||||
|
||||
<div>
|
||||
<label for="t_eb" class="block text-sm font-medium mb-1">Entered By</label
|
||||
>
|
||||
<select
|
||||
id="t_eb"
|
||||
class="w-full border p-2 rounded"
|
||||
bind:value={form.entered_by}
|
||||
required
|
||||
>
|
||||
<option value="" disabled selected>Select Employee</option>
|
||||
{#each employees as employee}
|
||||
<option value={employee.id}>{employee.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="t_wd" class="block text-sm font-medium mb-1"
|
||||
>Work Description</label
|
||||
>
|
||||
<textarea
|
||||
id="t_wd"
|
||||
class="w-full border border-gray-300 p-2 rounded"
|
||||
bind:value={form.work_description}
|
||||
placeholder="Describe the work"
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="t_ow" class="block text-sm font-medium mb-1"
|
||||
>Type of Work</label
|
||||
>
|
||||
<select
|
||||
id="t_ow"
|
||||
class="w-full border p-2 rounded"
|
||||
bind:value={form.type_of_work}
|
||||
>
|
||||
{#each typeOfWorkOptions as option}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="t_cow" class="block text-sm font-medium mb-1"
|
||||
>Category of Work</label
|
||||
>
|
||||
<select
|
||||
id="t_cow"
|
||||
class="w-full border p-2 rounded"
|
||||
bind:value={form.category_of_work}
|
||||
>
|
||||
{#each categoryOptions as option}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="t_vn" class="block text-sm font-medium mb-1">Villa</label>
|
||||
<select
|
||||
id="t_vn"
|
||||
class="w-full border p-2 rounded"
|
||||
bind:value={form.villa_id}
|
||||
required
|
||||
>
|
||||
<option value="" disabled selected>Select Villa</option>
|
||||
{#each villas as villa}
|
||||
<option value={villa.id}>{villa.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="tdto" class="block text-sm font-medium mb-1"
|
||||
>Date/Time In</label
|
||||
>
|
||||
<input
|
||||
id="tdto"
|
||||
type="datetime-local"
|
||||
class="w-full border p-2 rounded"
|
||||
bind:value={form.datetime_in}
|
||||
on:change={calculateTotalHours}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="dto" class="block text-sm font-medium mb-1"
|
||||
>Date/Time Out</label
|
||||
>
|
||||
<input
|
||||
id="dto"
|
||||
type="datetime-local"
|
||||
class="w-full border p-2 rounded"
|
||||
bind:value={form.datetime_out}
|
||||
on:change={calculateTotalHours}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-sm">
|
||||
<label for="ttwo" class="block font-medium mb-1">Total Work Hours</label>
|
||||
<div id="ttwo" class="px-3 py-2">{form.total_work_hour}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="trmk" class="block text-sm font-medium mb-1">Remarks</label>
|
||||
<textarea
|
||||
id="trmk"
|
||||
class="w-full border border-gray-300 p-2 rounded"
|
||||
bind:value={form.remarks}
|
||||
placeholder="Optional remarks"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition"
|
||||
>
|
||||
Submit Timesheet
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -20,6 +20,7 @@
|
||||
datetime_out: string;
|
||||
total_work_hour: number;
|
||||
remarks: string;
|
||||
approved_date: string | null;
|
||||
approval: boolean | null; // Allow null for new entries
|
||||
};
|
||||
|
||||
@@ -46,9 +47,9 @@
|
||||
datetime_out: "",
|
||||
total_work_hour: 0,
|
||||
remarks: "",
|
||||
approved_date: null,
|
||||
approval: null, // Default null
|
||||
};
|
||||
|
||||
const typeOfWorkOptions: TimesheetForm["type_of_work"][] = [
|
||||
"Running",
|
||||
"Periodic",
|
||||
@@ -115,7 +116,16 @@
|
||||
alert("Please select a villa.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (form.approval) {
|
||||
const today = new Date();
|
||||
const yyyy = today.getFullYear();
|
||||
const mm = String(today.getMonth() + 1).padStart(2, "0");
|
||||
const dd = String(today.getDate()).padStart(2, "0");
|
||||
form.approved_date = `${yyyy}/${mm}/${dd}`;
|
||||
} else {
|
||||
form.approved_date = null;
|
||||
}
|
||||
form.approval = form.total_work_hour <= 1;
|
||||
const { error } = await supabase.from("vb_timesheet").insert([form]);
|
||||
|
||||
if (error) {
|
||||
@@ -263,7 +273,6 @@
|
||||
placeholder="Optional remarks"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition"
|
||||
|
||||
44
yarn.lock
44
yarn.lock
@@ -10,10 +10,10 @@
|
||||
"@jridgewell/gen-mapping" "^0.3.5"
|
||||
"@jridgewell/trace-mapping" "^0.3.24"
|
||||
|
||||
"@esbuild/linux-x64@0.25.4":
|
||||
"@esbuild/win32-x64@0.25.4":
|
||||
version "0.25.4"
|
||||
resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz"
|
||||
integrity sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==
|
||||
resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz"
|
||||
integrity sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==
|
||||
|
||||
"@floating-ui/core@^1.5.0", "@floating-ui/core@^1.7.0":
|
||||
version "1.7.0"
|
||||
@@ -119,20 +119,10 @@
|
||||
estree-walker "^2.0.2"
|
||||
picomatch "^4.0.2"
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@4.41.1":
|
||||
"@rollup/rollup-win32-x64-msvc@4.41.1":
|
||||
version "4.41.1"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz"
|
||||
integrity sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@4.9.5":
|
||||
version "4.9.5"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.5.tgz"
|
||||
integrity sha512-Dq1bqBdLaZ1Gb/l2e5/+o3B18+8TI9ANlA1SkejZqDgdU/jK/ThYaMPMJpVMMXy2uRHvGKbkz9vheVGdq3cJfA==
|
||||
|
||||
"@rollup/rollup-linux-x64-musl@4.41.1":
|
||||
version "4.41.1"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz"
|
||||
integrity sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz"
|
||||
integrity sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==
|
||||
|
||||
"@supabase/auth-js@2.69.1":
|
||||
version "2.69.1"
|
||||
@@ -268,15 +258,10 @@
|
||||
source-map-js "^1.2.1"
|
||||
tailwindcss "4.1.7"
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu@4.1.7":
|
||||
"@tailwindcss/oxide-win32-x64-msvc@4.1.7":
|
||||
version "4.1.7"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.7.tgz"
|
||||
integrity sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg==
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl@4.1.7":
|
||||
version "4.1.7"
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.7.tgz"
|
||||
integrity sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA==
|
||||
resolved "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.7.tgz"
|
||||
integrity sha512-rYHGmvoHiLJ8hWucSfSOEmdCBIGZIq7SpkPRSqLsH2Ab2YUNgKeAPT1Fi2cx3+hnYOrAb0jp9cRyode3bBW4mQ==
|
||||
|
||||
"@tailwindcss/oxide@4.1.7":
|
||||
version "4.1.7"
|
||||
@@ -525,15 +510,10 @@ kleur@^4.1.5:
|
||||
resolved "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz"
|
||||
integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==
|
||||
|
||||
lightningcss-linux-x64-gnu@1.30.1:
|
||||
lightningcss-win32-x64-msvc@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz"
|
||||
integrity sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==
|
||||
|
||||
lightningcss-linux-x64-musl@1.30.1:
|
||||
version "1.30.1"
|
||||
resolved "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz"
|
||||
integrity sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==
|
||||
resolved "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz"
|
||||
integrity sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==
|
||||
|
||||
lightningcss@^1.21.0, lightningcss@1.30.1:
|
||||
version "1.30.1"
|
||||
|
||||
Reference in New Issue
Block a user