add Sort Function

This commit is contained in:
2025-07-16 07:59:12 +08:00
parent 0192f7eeec
commit 5b8c44f494
6 changed files with 182 additions and 60 deletions

1
.gitignore vendored
View File

@@ -23,3 +23,4 @@ vite.config.js.timestamp-*
vite.config.ts.timestamp-*
package-lock.json
yarn.lock

View File

@@ -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}
/>

View File

@@ -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}

View File

@@ -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">

View File

@@ -332,9 +332,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
@@ -422,7 +423,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;
@@ -673,7 +680,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>
@@ -682,8 +689,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
@@ -693,7 +705,7 @@
{col.title}
{#if sortColumn === col.key}
{sortOrder === 'asc' ? ' 🔼' : ' 🔽'}
{/if}
{/if}
</th>
{/if}
{/each}

View File

@@ -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"