Files
vberp/src/routes/backoffice/purchaseorder/received/+page.svelte
aji@catalis.app 1402513990 fix date
2025-06-22 14:26:09 +07:00

990 lines
39 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

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

<script lang="ts">
import { onMount } from "svelte";
import Select from "svelte-select";
import { supabase } from "$lib/supabaseClient";
type PurchaseOrderInsert = {
issue_id: string;
prepared_date: string;
po_type: string;
po_quantity: number;
po_status: string;
approved_vendor: string;
acknowledge_by: string;
approved_by: string;
approved_price: number;
completed_status: string;
updated_at: Date;
updated_by: string;
};
let purchaseOrderInsert: PurchaseOrderInsert = {
issue_id: "",
prepared_date: "",
po_type: "",
po_quantity: 0,
po_status: "REQUESTED",
approved_vendor: "",
acknowledge_by: "",
approved_by: "",
approved_price: 0,
completed_status: "",
updated_at: new Date(),
updated_by: "",
};
type PurchaseOrders = {
id: string;
purchase_order_number: string;
prepared_date: string;
po_type: string;
po_quantity: number;
po_status: string;
approved_vendor: string;
acknowledged: boolean;
approved_vendor_id: string;
acknowledge_by: string;
approved_price: number;
approved_quantity: number;
total_approved_order_amount: number;
approval: string;
completed_status: string;
received: boolean;
received_by: string;
input_by: string;
issue_id: string;
approved_by: string;
created_at: Date;
updated_at: Date;
updated_by: string;
};
type PurchaseOrderDisplay = {
id: string;
name: string;
purchase_order_number: string;
villa_name: string;
priority: string;
prepared_date: string;
po_type: string;
po_quantity: number;
po_status: string;
approved_vendor: string;
acknowledged: boolean;
acknowledge_by: string;
approved_by: string;
approved_price: number;
approved_quantity: number;
total_approved_order_amount: number;
approval: string;
completed_status: string;
received: boolean;
received_by: string;
updated_at: Date;
updated_by: string;
};
let allRows: PurchaseOrderDisplay[] = [];
type columns = {
key: string;
title: string;
};
const columns: columns[] = [
{ key: "name", title: "Name" },
{ key: "purchase_order_number", title: "Purchase Order Number" },
{ key: "po_quantity", title: "PO Quantity" },
{ key: "po_status", title: "PO Status" },
{ key: "approved_vendor", title: "Approved Vendor" },
{ key: "approved_price", title: "Approved Price" },
{ key: "approved_quantity", title: "Approved Quantity" },
{
key: "total_approved_order_amount",
title: "Total Approved Order Amount",
},
{ key: "received", title: "Received" },
{ key: "received_name", title: "Received By" },
{ key: "updated_at", title: "Updated At" },
{ key: "updated_name", title: "Updated By" },
{ key: "created_at", title: "Created At" },
// { key: "actions", title: "Actions" }, // For edit/delete buttons
];
let currentPage = 1;
let rowsPerPage = 10;
$: 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;
}
async function fetchPurchaseOrder(
filter: string | null = null,
search: string | null = null,
sort: string | null = null,
order: "asc" | "desc" = "desc",
offset: number = 0,
limit: number = 1000,
) {
let query = supabase
.from("vb_purchaseorder_data")
.select("*")
// RECEIVED COMPLETED or RECEIVE - INCOMPLETE
.in("po_status", ["RECEIVED COMPLETE", "RECEIVED INCOMPLETE"])
.order(sort || "created_at", { ascending: order === "asc" })
.range(offset, offset + limit - 1);
if (filter) {
query = query.eq("po_type", filter);
}
if (search) {
query = query.ilike("purchase_order_number", `%${search}%`);
}
const { data, error } = await query;
if (error) {
console.error("Error fetching purchase orders:", error);
return;
}
// fetch issue and villa names
const issueIds = data.map((row) => row.issue_id);
const { data: issues, error: issueError } = await supabase
.from("vb_issues")
.select("*")
.in("id", issueIds);
if (issueError) {
console.error("Error fetching issues:", issueError);
return;
}
const villaIds = issues.map((row) => row.villa_name).filter(Boolean);
const { data: villas, error: villaError } = await supabase
.from("villas")
.select("id, name")
.in("id", villaIds);
if (villaError) {
console.error("Error fetching villas:", villaError);
return;
}
// masukkan villa name dan issue name ke dalam data
allRows = data.map((row) => {
const issue = issues.find((issue) => issue.id === row.issue_id);
const villa = villas.find((villa) => villa.id === issue.villa_id);
const vendor = vendors.find(
(vendor) => vendor.id === row.approved_vendor,
);
return {
...row,
name: issue ? issue.name : "Unknown Issue",
villa_name: row.villa_data,
priority: issue ? issue.priority : "Unknown Priority",
approved_vendor: vendor
? vendor.name
: "Unknown Approved Vendor",
approval: row.approval || "",
completed_status: row.completed_status || "",
} as PurchaseOrderDisplay;
});
}
//fetch all issues
async function fetchIssues() {
const { data, error } = await supabase
.from("vb_issues")
.select("id, name");
if (error) {
console.error("Error fetching issues:", error);
return [];
}
issues = data.map((issue) => ({
id: issue.id,
name: issue.name,
}));
}
async function fetchVendors() {
const { data, error } = await supabase
.from("vb_vendor")
.select("id, name");
if (error) {
console.error("Error fetching vendors:", error);
return [];
}
vendors = data.map((vendor) => ({
id: vendor.id,
name: vendor.name,
}));
}
onMount(() => {
fetchPurchaseOrder();
fetchVendors();
});
$: currentPage = 1; // Reset to first page when allRows changes
let showModal = false;
let isEditing = false;
let currentEditingId: string | null = null;
let newPurchaseOrders: Record<string, any> = {};
let vendors: { id: string; name: string }[] = [];
let issues: { id: string; name: string }[] = [];
const excludedKeys = [
"id",
"priority",
"villa_name",
"purchase_order_number",
"issue_id",
"number_project",
"input_by",
"created_at",
"actions",
"acknowledged",
"acknowledge_by",
"approval",
"completed_status",
"received",
"received_by",
"approved_quantity",
"total_approved_order_amount",
"approved_by",
"name",
"po_status",
];
const formColumns = columns.filter(
(col) => !excludedKeys.includes(col.key),
);
async function openModal(purchase: PurchaseOrderDisplay | null = null) {
await fetchIssues();
if (purchase) {
isEditing = true;
currentEditingId = purchase.id;
newPurchaseOrders = { ...purchase };
} else {
isEditing = false;
currentEditingId = null;
newPurchaseOrders = {};
}
showModal = true;
}
async function saveProject() {
purchaseOrderInsert = {
issue_id: newPurchaseOrders.issue_id || "",
prepared_date: newPurchaseOrders.prepared_date || "",
po_type: newPurchaseOrders.po_type || "",
po_quantity: newPurchaseOrders.po_quantity || 0,
po_status: newPurchaseOrders.po_status || "REQUESTED",
approved_vendor: newPurchaseOrders.approved_vendor || "",
acknowledge_by: newPurchaseOrders.acknowledge_by || "",
approved_price: newPurchaseOrders.approved_price || "",
approved_by: newPurchaseOrders.approved_by || "",
completed_status: newPurchaseOrders.completed_status || "",
updated_at: new Date(),
updated_by: (await supabase.auth.getUser()).data.user?.id || "",
};
if (isEditing && currentEditingId) {
const { data, error } = await supabase
.from("vb_purchase_orders")
.update(purchaseOrderInsert)
.eq("id", currentEditingId);
if (error) {
console.error("Error updating purchase order:", error);
return;
}
} else {
const { data, error } = await supabase
.from("vb_purchase_orders")
.insert(purchaseOrderInsert);
if (error) {
console.error("Error inserting purchase order:", error);
return;
}
}
await fetchPurchaseOrder();
showModal = false;
}
async function deleteProject(id: string) {
const { error } = await supabase
.from("vb_purchase_orders")
.delete()
.eq("id", id);
if (error) {
console.error("Error deleting project:", error);
return;
}
await fetchPurchaseOrder();
}
const statusOptions = [
{ label: "Requested", value: "REQUESTED", color: "#e5e7eb" },
{ label: "Prepared", value: "PREPARED", color: "#93c5fd" },
{ label: "Approved", value: "APPROVED", color: "#34d399" },
{ label: "Acknowledged", value: "ACKNOWLEDGE", color: "#60a5fa" },
{
label: "Received - Incomplete",
value: "RECEIVE - INCOMPLETE",
color: "#fb923c",
},
{
label: "Received - Completed",
value: "RECEIVE COMPLETED",
color: "#10b981",
},
{ label: "Canceled", value: "CANCELED", color: "#f87171" },
];
function getStatusOption(value: string) {
return statusOptions.find((option) => option.value === value) ?? null;
}
//validate input fields purchase order
function validateInput() {
const requiredFields = [
"prepared_date",
"po_type",
"po_quantity",
"approved_vendor",
];
for (const field of requiredFields) {
if (!newPurchaseOrders[field]) {
alert(`Please fill in the ${field} field.`);
return false;
}
}
return true;
}
function validateInputApproval() {
const requiredFields = ["approved_price"];
for (const field of requiredFields) {
if (!newPurchaseOrders[field]) {
alert(`Please fill in the ${field} field.`);
return false;
}
}
return true;
}
async function updatePurchaseOrderStatus(
e: Event,
id: string,
row: PurchaseOrderDisplay,
) {
const selectedOption = (e.target as HTMLSelectElement)?.value ?? "";
const option = getStatusOption(selectedOption);
newPurchaseOrders = {
...row,
po_status: option?.value || row.po_status,
};
if (option?.value === "APPROVED") {
if (!validateInput()) {
e.preventDefault();
return;
}
}
const { data, error } = await supabase
.from("vb_purchase_orders")
.update({ po_status: newPurchaseOrders.po_status })
.eq("id", id);
if (error) {
console.error("Error updating purchase order status:", error);
return;
}
await fetchPurchaseOrder();
}
async function acknowledgedOk(id: string, status: boolean) {
const { data, error } = await supabase
.from("vb_purchase_orders")
.update({ acknowledged: status })
.eq("id", id);
if (error) {
console.error("Error acknowledging purchase order:", error);
return;
}
await fetchPurchaseOrder();
}
async function receivedOk(id: string, status: boolean) {
const sessionId = await supabase.auth.getSession();
if (!sessionId.data.session) {
console.error("User not authenticated");
return;
}
const userId = sessionId.data.session?.user.id || "";
const { data, error } = await supabase
.from("vb_purchase_orders")
.update({
received: status,
received_by: userId,
updated_by: userId,
updated_at: new Date(),
})
.eq("id", id);
if (error) {
console.error("Error acknowledging purchase order:", error);
return;
}
await fetchPurchaseOrder();
}
async function updatePurchaseOrderApprovalStatus(
e: Event,
id: string,
row: PurchaseOrderDisplay,
) {
const selectedOption = (e.target as HTMLSelectElement)?.value ?? "";
const option = getStatusOption(selectedOption);
newPurchaseOrders = {
...row,
approval: option?.value || row.approval,
};
if (option?.value === "APPROVED") {
if (!validateInputApproval()) {
e.preventDefault();
return;
}
}
const { data, error } = await supabase
.from("vb_purchase_orders")
.update({ approval: newPurchaseOrders.approval })
.eq("id", id);
if (error) {
console.error("Error updating purchase order status:", error);
return;
}
await fetchPurchaseOrder();
}
async function completedStatusOk(id: string, status: string) {
const { data, error } = await supabase
.from("vb_purchase_orders")
.update({ completed_status: status, po_status: status })
.eq("id", id);
if (error) {
console.error("Error acknowledging purchase order:", error);
return;
}
await fetchPurchaseOrder();
}
</script>
<div>
<div
class="p-6 bg-white shadow-md rounded-2xl mb-4 flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4"
>
<div>
<h2
class="text-lg font-semibold text-gray-800 flex items-center gap-2"
>
<span>📦</span>
Purchase Order Received List
</h2>
<p class="text-sm text-gray-600">
Manage your purchase orders efficiently. You can add, edit, or
delete purchase orders as needed.
</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();
fetchPurchaseOrder(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;
fetchPurchaseOrder(filter, null, null, "desc");
}}
>
<option value="">All Issues</option>
<option value="PROJECT">Project Issues</option>
<option value="PURCHASE_ORDER">Purchase Order Issues</option>
</select>
</div>
</div>
<div class="overflow-x-auto rounded-lg shadow mb-4">
<table class="min-w-[1000px] divide-y divide-gray-200 text-sm w-max">
<thead class="bg-gray-100">
<tr>
{#each columns as col}
{#if col.key === "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 as keyof PurchaseOrderDisplay]}
</td>
{:else if col.key === "approval"}
<td class="px-4 py-2">
<select
bind:value={
row[
col.key as keyof PurchaseOrderDisplay
]
}
class="w-full p-3 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500 text-gray-900"
on:change={(e: Event) => {
updatePurchaseOrderApprovalStatus(
e,
row.id,
row,
);
}}
>
<option value="" disabled selected
>SELECT APPROVAL</option
>
<option value="APPROVED"
>APPROVED</option
>
<option value="REJECTED"
>REJECTED</option
>
</select>
</td>
{:else if col.key === "acknowledged"}
<td class="px-4 py-2 text-center">
<input
type="checkbox"
checked={row.acknowledged}
on:change={async (e) => {
const isChecked = (
e.target as HTMLInputElement
).checked;
row.acknowledged = isChecked;
if (isChecked) {
// map to project
await acknowledgedOk(
row.id,
isChecked,
);
}
}}
/>
</td>
{:else if col.key === "received"}
<td class="px-4 py-2 text-center">
<input
type="checkbox"
checked={row.received}
on:change={async (e) => {
const isChecked = (
e.target as HTMLInputElement
).checked;
row.received = isChecked;
if (isChecked) {
// map to project
await receivedOk(
row.id,
isChecked,
);
}
}}
/>
</td>
{:else if col.key === "completed_status"}
<td class="px-4 py-2">
<select
bind:value={
row[
col.key as keyof PurchaseOrderDisplay
]
}
class="w-full p-3 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500 text-gray-900"
on:change={async (e) => {
const isValue = (
e.target as HTMLInputElement
).value;
if (isValue) {
// map to project
await completedStatusOk(
row.id,
isValue,
);
}
}}
>
<option value="" disabled selected
>SELECT COMPLETE</option
>
<option value="APPROVED"
>RECEIVED COMPLETE</option
>
<option value="REJECTED"
>COMPLETE INCOMPLETE</option
>
</select>
</td>
{:else if col.key === "updated_at"}
<td class="px-4 py-2">
{new Date(
row[
col.key as keyof PurchaseOrderDisplay
] as string,
).toLocaleString("en-US")}
</td>
{:else if col.key === "created_at"}
<td class="px-4 py-2">
{new Date(
row[
col.key as keyof PurchaseOrderDisplay
] as string,
).toLocaleString("en-US")}
</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={() => deleteProject(row.id)}
>
🗑️ Delete
</button>
</td>
{:else if col.key === "move_issue"}
<td class="px-4 py-2">
<button
class="inline-flex items-center gap-1 rounded bg-green-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-green-700"
on:click={() =>
alert(
`Move issue ${row.id} to project`,
)}
>
➡️ PROJECT
</button>
<button
class="inline-flex items-center gap-1 rounded bg-yellow-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-yellow-700"
on:click={() =>
alert(
`Move issue ${row.id} to another area`,
)}
>
➡️ PURCHASE ORDER
</button>
</td>
{:else}
<td class="px-4 py-2 text-gray-700"
>{row[
col.key as keyof PurchaseOrderDisplay
]}</td
>
{/if}
{/each}
</tr>
{/each}
</tbody>
</table>
</div>
<!-- Pagination controls -->
<div class="flex justify-between items-center text-sm">
<div>
Showing {(currentPage - 1) * rowsPerPage + 1}
{Math.min(currentPage * rowsPerPage, allRows.length)} of {allRows.length}
</div>
<div class="space-x-2">
<button
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
on:click={() => goToPage(currentPage - 1)}
disabled={currentPage === 1}
>
Previous
</button>
{#each Array(totalPages)
.fill(0)
.map((_, i) => i + 1) as page}
<button
class="px-3 py-1 rounded border text-sm
{currentPage === page
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white border-gray-300 hover:bg-gray-100'}"
on:click={() => goToPage(page)}
>
{page}
</button>
{/each}
<button
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
on:click={() => goToPage(currentPage + 1)}
disabled={currentPage === totalPages}
>
Next
</button>
</div>
</div>
</div>
{#if showModal}
<div
class="fixed inset-0 flex items-center justify-center bg-gray-800 bg-opacity-50 z-50"
>
<div
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 mb-4">
{isEditing ? "Edit Project" : "Add Project"}
</h3>
<form on:submit|preventDefault={saveProject}>
<!-- choose issuess -->
<div class="mb-4">
<label
for="issue_id"
class="block text-sm font-medium text-gray-700 mb-1"
>
Choose Issue
</label>
<select
id="issue_id"
bind:value={newPurchaseOrders.issue_id}
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
on:change={(e: Event) => {
const selectedIssue =
(e.target as HTMLSelectElement)?.value ?? "";
newPurchaseOrders.issue_id = selectedIssue;
}}
>
<option value="">Select Issue</option>
{#each issues as issue}
<option value={issue.id}>{issue.name}</option>
{/each}
</select>
</div>
{#each formColumns as col}
{#if col.key === "po_status"}
<div class="mb-4">
<label
for={col.key}
class="block text-sm font-medium text-gray-700 mb-1"
>
{col.title}
</label>
<select
id={col.key}
bind:value={newPurchaseOrders[col.key]}
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
on:change={(e: Event) => {
const selectedOption =
(e.target as HTMLSelectElement)
?.value ?? "";
const option =
getStatusOption(selectedOption);
if (
option?.value === "APPROVED" &&
!validateInput()
) {
e.preventDefault();
return;
}
}}
>
{#each statusOptions as option}
<option
value={option.value}
style="background-color: {option.color};"
>
{option.label}
</option>
{/each}
</select>
</div>
{:else if col.key === "po_type"}
<div class="mb-4">
<label
for={col.key}
class="block text-sm font-medium text-gray-700 mb-1"
>
{col.title}
</label>
<select
id={col.key}
bind:value={newPurchaseOrders[col.key]}
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
>
<option value="">Select PO Type</option>
<option value="Regular">Regular</option>
<option value="Urgent">Urgent</option>
</select>
</div>
{:else if col.key === "prepared_date"}
<div class="mb-4">
<label
for={col.key}
class="block text-sm font-medium text-gray-700 mb-1"
>
{col.title}
</label>
<input
type="date"
id={col.key}
bind:value={newPurchaseOrders[col.key]}
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
/>
</div>
{:else if col.key === "po_quantity"}
<div class="mb-4">
<label
for={col.key}
class="block text-sm font-medium text-gray-700 mb-1"
>
{col.title}
</label>
<input
type="number"
id={col.key}
bind:value={newPurchaseOrders[col.key]}
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
/>
</div>
{:else if col.key === "approved_price"}
<div class="mb-4">
<label
for={col.key}
class="block text-sm font-medium text-gray-700 mb-1"
>
{col.title}
</label>
<input
type="number"
id={col.key}
bind:value={newPurchaseOrders[col.key]}
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
/>
</div>
{:else if col.key === "approved_vendor"}
<div class="mb-4">
<label
for={col.key}
class="block text-sm font-medium text-gray-700 mb-1"
>
{col.title}
</label>
<select
id={col.key}
bind:value={newPurchaseOrders[col.key]}
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
on:change={(e: Event) => {
const selectedVendor =
(e.target as HTMLSelectElement)
?.value ?? "";
newPurchaseOrders[col.key] = selectedVendor;
}}
>
<option value="">Select Vendor</option>
{#each vendors as vendor}
<option value={vendor.id}>
{vendor.name}
</option>
{/each}
</select>
</div>
{:else}
<div class="mb-4">
<label
for={col.key}
class="block text-sm font-medium text-gray-700 mb-1"
>
{col.title}
</label>
<input
type="text"
id={col.key}
bind:value={newPurchaseOrders[col.key]}
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
/>
</div>
{/if}
{/each}
<div class="flex justify-end space-x-2">
<button
type="button"
class="px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300"
on:click={() => (showModal = false)}
>
Cancel
</button>
<button
type="submit"
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Save
</button>
</div>
</form>
</div>
</div>
{/if}