update bug issue and order timesheet
This commit is contained in:
@@ -85,48 +85,6 @@
|
|||||||
icon: "📦",
|
icon: "📦",
|
||||||
url: "/backoffice/purchaseorder",
|
url: "/backoffice/purchaseorder",
|
||||||
roles: ["it", "guest", "accounting", "ga", "office", "hm", "vm"],
|
roles: ["it", "guest", "accounting", "ga", "office", "hm", "vm"],
|
||||||
sub: [
|
|
||||||
{
|
|
||||||
name: "Approval",
|
|
||||||
icon: "✅",
|
|
||||||
url: "/backoffice/purchaseorder/approval",
|
|
||||||
roles: ["it", "user", "ga", "office", "hm", "vm"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Acknowledged",
|
|
||||||
icon: "📋",
|
|
||||||
url: "/backoffice/purchaseorder/acknowledged",
|
|
||||||
roles: ["it", "user", "ga", "office", "hm", "vm"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Complete",
|
|
||||||
icon: "✔️",
|
|
||||||
url: "/backoffice/purchaseorder/complete",
|
|
||||||
roles: [
|
|
||||||
"it",
|
|
||||||
"user",
|
|
||||||
"accounting",
|
|
||||||
"ga",
|
|
||||||
"office",
|
|
||||||
"hm",
|
|
||||||
"vm",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Received",
|
|
||||||
icon: "📥",
|
|
||||||
url: "/backoffice/purchaseorder/received",
|
|
||||||
roles: [
|
|
||||||
"it",
|
|
||||||
"user",
|
|
||||||
"accounting",
|
|
||||||
"ga",
|
|
||||||
"office",
|
|
||||||
"hm",
|
|
||||||
"vm",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Timesheets",
|
name: "Timesheets",
|
||||||
|
|||||||
@@ -84,6 +84,17 @@
|
|||||||
{ label: "Other - Transport", value: "Other - Transport" },
|
{ label: "Other - Transport", value: "Other - Transport" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
type PurchaseOrder = {
|
||||||
|
villa_id: string;
|
||||||
|
issue_id: string;
|
||||||
|
po_status: string;
|
||||||
|
requested_by: string;
|
||||||
|
requested_date: string;
|
||||||
|
po_due: string;
|
||||||
|
po_item: string;
|
||||||
|
po_quantity: string;
|
||||||
|
po_type: string;
|
||||||
|
};
|
||||||
const areaOfVilla = [
|
const areaOfVilla = [
|
||||||
{ label: "All Bathrooms", value: "All Bathrooms" },
|
{ label: "All Bathrooms", value: "All Bathrooms" },
|
||||||
{ label: "All Guest Houses", value: "All Guest Houses" },
|
{ label: "All Guest Houses", value: "All Guest Houses" },
|
||||||
@@ -152,6 +163,18 @@
|
|||||||
let currentSearchTerm: string | null = null;
|
let currentSearchTerm: string | null = null;
|
||||||
let showProjectModal = false;
|
let showProjectModal = false;
|
||||||
let selectedIssueId: string | null = null;
|
let selectedIssueId: string | null = null;
|
||||||
|
let showPurchaseOrderModal = false;
|
||||||
|
let newPO: PurchaseOrder = {
|
||||||
|
villa_id: "",
|
||||||
|
issue_id: "",
|
||||||
|
po_status: "requested",
|
||||||
|
requested_by: "",
|
||||||
|
requested_date: "",
|
||||||
|
po_due: "",
|
||||||
|
po_item: "",
|
||||||
|
po_quantity: "",
|
||||||
|
po_type: ""
|
||||||
|
};
|
||||||
let newProject = {
|
let newProject = {
|
||||||
project_name: "",
|
project_name: "",
|
||||||
issue_id: "",
|
issue_id: "",
|
||||||
@@ -165,6 +188,7 @@
|
|||||||
let dataUser: User[] = [];
|
let dataUser: User[] = [];
|
||||||
|
|
||||||
let projectIssueMap: Set<string> = new Set();
|
let projectIssueMap: Set<string> = new Set();
|
||||||
|
let purchaseOrderMap: Set<string> = new Set();
|
||||||
|
|
||||||
async function fetchExistingProjectLinks() {
|
async function fetchExistingProjectLinks() {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
@@ -177,6 +201,10 @@
|
|||||||
console.error("Error loading existing projects:", error);
|
console.error("Error loading existing projects:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function fetchExistingPurchaseOrders() {
|
||||||
|
const { data } = await supabase.from("vb_purchase_orders").select("issue_id");
|
||||||
|
if (data) purchaseOrderMap = new Set(data.map(p => p.issue_id));
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await fetchExistingProjectLinks();
|
await fetchExistingProjectLinks();
|
||||||
@@ -241,6 +269,16 @@
|
|||||||
need_approval: boolean;
|
need_approval: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type POItem = {
|
||||||
|
item_name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let poItems: POItem[] = [];
|
||||||
|
|
||||||
|
async function fetchPoItems() {
|
||||||
|
const { data } = await supabase.from("vb_po_item").select("item_name");
|
||||||
|
if (data) poItems = data;
|
||||||
|
}
|
||||||
let allRows: Issue[] = [];
|
let allRows: Issue[] = [];
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
let limit = 10;
|
let limit = 10;
|
||||||
@@ -330,7 +368,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
fetchIssues(null, null, "created_at", "desc", offset, limit);
|
fetchIssues(null, null, "created_at", "desc", offset, limit);
|
||||||
|
fetchPoItems();
|
||||||
|
fetchExistingPurchaseOrders();
|
||||||
});
|
});
|
||||||
|
|
||||||
let showModal = false;
|
let showModal = false;
|
||||||
@@ -367,6 +407,10 @@
|
|||||||
|
|
||||||
function openModal(issue?: Record<string, any>) {
|
function openModal(issue?: Record<string, any>) {
|
||||||
if (issue) {
|
if (issue) {
|
||||||
|
if (projectIssueMap.has(issue.id)) {
|
||||||
|
alert("This issue is linked to a project and cannot be edited.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
isEditing = true;
|
isEditing = true;
|
||||||
currentEditingId = issue.id;
|
currentEditingId = issue.id;
|
||||||
newIssue = { ...issue };
|
newIssue = { ...issue };
|
||||||
@@ -503,19 +547,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteIssue(id: string) {
|
async function deleteIssue(id: string) {
|
||||||
if (confirm("Are you sure you want to delete this issue?")) {
|
if (projectIssueMap.has(id)) {
|
||||||
const { error } = await supabase
|
alert("This issue is linked to a project and cannot be deleted.");
|
||||||
.from("vb_issues")
|
return;
|
||||||
.delete()
|
}
|
||||||
.eq("id", id);
|
|
||||||
if (error) {
|
const confirmDelete = confirm("Are you sure you want to delete this issue?");
|
||||||
console.error("Error deleting issue:", error);
|
if (!confirmDelete) return;
|
||||||
return;
|
|
||||||
}
|
const { error } = await supabase.from("vb_issues").delete().eq("id", id);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Delete failed:", error);
|
||||||
|
alert("Failed to delete issue.");
|
||||||
|
} else {
|
||||||
await fetchIssues();
|
await fetchIssues();
|
||||||
|
alert("Issue deleted.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let selectedFile: File | null = null;
|
let selectedFile: File | null = null;
|
||||||
let imagePreviewUrl: string | null = null;
|
let imagePreviewUrl: string | null = null;
|
||||||
|
|
||||||
@@ -615,6 +666,64 @@
|
|||||||
showProjectModal = true;
|
showProjectModal = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openPurchaseOrderModal(issue) {
|
||||||
|
if (purchaseOrderMap.has(issue.id)) {
|
||||||
|
alert("This issue already has a purchase order.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
const dueDate = new Date(today);
|
||||||
|
dueDate.setDate(dueDate.getDate() + 2);
|
||||||
|
|
||||||
|
const formatDate = (d) => d.toISOString().split("T")[0];
|
||||||
|
|
||||||
|
newPO = {
|
||||||
|
issue_id: issue.id,
|
||||||
|
villa_id: issue.villa_id,
|
||||||
|
po_status: "requested",
|
||||||
|
requested_by: "",
|
||||||
|
requested_date: formatDate(today),
|
||||||
|
po_due: formatDate(dueDate),
|
||||||
|
po_item: "",
|
||||||
|
po_quantity: "",
|
||||||
|
po_type: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
selectedIssueSummary = issue.description_of_the_issue ?? "";
|
||||||
|
showPurchaseOrderModal = true;
|
||||||
|
}
|
||||||
|
async function submitPurchaseOrder() {
|
||||||
|
if (!newPO.requested_by || !newPO.po_item || !newPO.po_quantity || !newPO.po_type) {
|
||||||
|
alert("Please complete all required fields.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: existing } = await supabase
|
||||||
|
.from("vb_purchase_orders")
|
||||||
|
.select("id")
|
||||||
|
.eq("issue_id", newPO.issue_id)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
alert("Purchase order already exists for this issue.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error } = await supabase.from("vb_purchase_orders").insert(newPO);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
alert("Failed to create PO.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// reactively update map
|
||||||
|
purchaseOrderMap = new Set([...purchaseOrderMap, newPO.issue_id]);
|
||||||
|
|
||||||
|
alert("Purchase Order submitted!");
|
||||||
|
showPurchaseOrderModal = false;
|
||||||
|
}
|
||||||
async function submitProject() {
|
async function submitProject() {
|
||||||
if (!newProject.project_name || !newProject.input_by || !newProject.assigned_to) {
|
if (!newProject.project_name || !newProject.input_by || !newProject.assigned_to) {
|
||||||
alert("Please fill all required fields");
|
alert("Please fill all required fields");
|
||||||
@@ -805,14 +914,35 @@
|
|||||||
</td>
|
</td>
|
||||||
{:else if col.key === "actions"}
|
{:else if col.key === "actions"}
|
||||||
<td class="px-4 py-2">
|
<td class="px-4 py-2">
|
||||||
{#if projectIssueMap.has(row.id)}
|
{#if projectIssueMap.has(row.id) || purchaseOrderMap.has(row.id)}
|
||||||
<button class="text-gray-400 cursor-not-allowed" disabled title="Cannot edit: linked to a project">✏️ Edit</button>
|
<button
|
||||||
<button class="text-gray-400 cursor-not-allowed" disabled title="Cannot delete: linked to a project">🗑️ Delete</button>
|
class="text-gray-400 cursor-not-allowed"
|
||||||
{:else}
|
disabled
|
||||||
<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>
|
title="Cannot edit: linked to a {projectIssueMap.has(row.id) ? 'project' : 'purchase order'}"
|
||||||
<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={() => deleteIssue(row.id)}>🗑️ Delete</button>
|
>
|
||||||
{/if}
|
✏️ Edit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="text-gray-400 cursor-not-allowed"
|
||||||
|
disabled
|
||||||
|
title="Cannot delete: linked to a {projectIssueMap.has(row.id) ? 'project' : 'purchase order'}"
|
||||||
|
>
|
||||||
|
🗑️ Delete
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<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={() => deleteIssue(row.id)}
|
||||||
|
>
|
||||||
|
🗑️ Delete
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
{:else if col.key === "move_issue"}
|
{:else if col.key === "move_issue"}
|
||||||
{#if row[col.key as keyof Issue] === "PROJECT"}
|
{#if row[col.key as keyof Issue] === "PROJECT"}
|
||||||
@@ -834,28 +964,46 @@
|
|||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
{:else}
|
{:else}
|
||||||
<td class="px-4 py-2">
|
<td class="px-4 py-2 flex gap-2">
|
||||||
{#if projectIssueMap.has(row.id)}
|
{#if projectIssueMap.has(row.id)}
|
||||||
|
<!-- Project already exists -->
|
||||||
<button
|
<button
|
||||||
class="inline-flex items-center gap-1 rounded bg-gray-400 px-3 py-1.5 text-white text-xs font-medium cursor-not-allowed"
|
class="bg-green-400 text-white px-3 py-1.5 rounded text-xs font-medium cursor-not-allowed"
|
||||||
disabled
|
disabled
|
||||||
|
title="Already linked to a project"
|
||||||
>
|
>
|
||||||
✔ LINKED PROJECT
|
✔ LINKED PROJECT
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<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"
|
class="bg-green-600 text-white px-3 py-1.5 rounded text-xs font-medium hover:bg-green-700"
|
||||||
on:click={() => openProjectModal(row)}
|
on:click={() => openProjectModal(row)}
|
||||||
|
disabled={purchaseOrderMap.has(row.id)}
|
||||||
|
title={purchaseOrderMap.has(row.id) ? "Disabled: already in purchase order" : ""}
|
||||||
>
|
>
|
||||||
➡️ PROJECT
|
➡️ PROJECT
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
<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"
|
{#if purchaseOrderMap.has(row.id)}
|
||||||
on:click={() => moveIssueToPurchaseOrder(row.id)}
|
<!-- PO already exists -->
|
||||||
>
|
<button
|
||||||
➡️ PURCHASE ORDER
|
class="bg-yellow-400 text-white px-3 py-1.5 rounded text-xs font-medium cursor-not-allowed"
|
||||||
</button>
|
disabled
|
||||||
|
title="Already linked to a purchase order"
|
||||||
|
>
|
||||||
|
✔ LINKED PO
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class="bg-yellow-600 text-white px-3 py-1.5 rounded text-xs font-medium hover:bg-yellow-700"
|
||||||
|
on:click={() => openPurchaseOrderModal(row)}
|
||||||
|
disabled={projectIssueMap.has(row.id)}
|
||||||
|
title={projectIssueMap.has(row.id) ? "Disabled: already in project" : ""}
|
||||||
|
>
|
||||||
|
➡️ PURCHASE ORDER
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -1405,4 +1553,64 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if showPurchaseOrderModal}
|
||||||
|
<div class="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50">
|
||||||
|
<form on:submit|preventDefault={submitPurchaseOrder}
|
||||||
|
class="bg-white p-6 rounded shadow-lg w-[600px] max-h-[90vh] overflow-y-auto space-y-4">
|
||||||
|
<h2 class="text-lg font-semibold">Create Purchase Order</h2>
|
||||||
|
|
||||||
|
<p class="text-sm text-gray-600 italic">Related Issue: “{selectedIssueSummary}”</p>
|
||||||
|
|
||||||
|
<label class="block">
|
||||||
|
Requester *
|
||||||
|
<select bind:value={newPO.requested_by} class="input w-full">
|
||||||
|
<option value="">-- Select Employee --</option>
|
||||||
|
{#each dataUser as emp}
|
||||||
|
<option value={emp.id}>{emp.employee_name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="block">
|
||||||
|
Product *
|
||||||
|
<select bind:value={newPO.po_item} class="input w-full">
|
||||||
|
<option value="">-- Select Product --</option>
|
||||||
|
{#each poItems as item}
|
||||||
|
<option value={item.item_name}>{item.item_name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="block">
|
||||||
|
Quantity *
|
||||||
|
<input type="number" bind:value={newPO.po_quantity} class="input w-full" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="block">
|
||||||
|
Type *
|
||||||
|
<select bind:value={newPO.po_type} class="input w-full">
|
||||||
|
<option value="">-- Select Type --</option>
|
||||||
|
<option value="project">Project</option>
|
||||||
|
<option value="purchase">Purchase</option>
|
||||||
|
<option value="repair">Repair</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="block">
|
||||||
|
Request Date
|
||||||
|
<input type="date" bind:value={newPO.requested_date} class="input w-full" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="block">
|
||||||
|
PO Due Date
|
||||||
|
<input type="date" bind:value={newPO.po_due} class="input w-full" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button type="button" class="btn" on:click={() => showPurchaseOrderModal = false}>Cancel</button>
|
||||||
|
<button type="submit" class="btn-primary">Submit</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -1,972 +0,0 @@
|
|||||||
<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_by?: string;
|
|
||||||
updated_at?: 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_by: "",
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
updated_at: string;
|
|
||||||
updated_by: string;
|
|
||||||
created_at: 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_name: string;
|
|
||||||
updated_at: string;
|
|
||||||
created_at: 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: "acknowledged", title: "Acknowledged" },
|
|
||||||
{ key: "updated_name", title: "Updated By" },
|
|
||||||
{ key: "updated_at", title: "Updated At" },
|
|
||||||
{ key: "created_at", title: "Created At" },
|
|
||||||
];
|
|
||||||
|
|
||||||
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("*")
|
|
||||||
.eq("po_status", "APPROVED")
|
|
||||||
.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",
|
|
||||||
"updated_name",
|
|
||||||
"updated_at",
|
|
||||||
];
|
|
||||||
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() {
|
|
||||||
const sessionId = await supabase.auth.getSession();
|
|
||||||
if (!sessionId) {
|
|
||||||
console.error("User not authenticated");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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_by: sessionId.data.session?.user.id || "",
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
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 sessionId = await supabase.auth.getSession();
|
|
||||||
if (!sessionId) {
|
|
||||||
console.error("User not authenticated");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from("vb_purchase_orders")
|
|
||||||
.update({
|
|
||||||
acknowledged: status,
|
|
||||||
po_status: "ACKNOWLEDGED",
|
|
||||||
acknowledge_by: sessionId.data.session?.user.id,
|
|
||||||
updated_by: sessionId.data.session?.user.id,
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
.eq("id", id);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error("Error acknowledging purchase order:", error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await fetchPurchaseOrder();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function receivedOk(id: string, status: boolean) {
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from("vb_purchase_orders")
|
|
||||||
.update({ receivedOk: status })
|
|
||||||
.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 })
|
|
||||||
.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 Acknowledged 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 === "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 if col.key === "updated_at"}
|
|
||||||
<td class="px-4 py-2">
|
|
||||||
{new Date(row.updated_at).toLocaleString()}
|
|
||||||
</td>
|
|
||||||
{:else if col.key === "created_at"}
|
|
||||||
<td class="px-4 py-2">
|
|
||||||
{new Date(row.created_at).toLocaleString()}
|
|
||||||
</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}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,916 +0,0 @@
|
|||||||
<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?: string;
|
|
||||||
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().toISOString(),
|
|
||||||
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;
|
|
||||||
updated_at: Date;
|
|
||||||
updated_by: string;
|
|
||||||
created_at: Date;
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
created_at: Date;
|
|
||||||
};
|
|
||||||
|
|
||||||
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: "completed_status", title: "Completed Status" },
|
|
||||||
{ key: "acknowledged_name", title: "Acknowledged By" },
|
|
||||||
{ key: "updated_name", title: "Updated By" },
|
|
||||||
{ key: "updated_at", title: "Updated At" },
|
|
||||||
{ 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("*")
|
|
||||||
.eq("po_status", "ACKNOWLEDGED")
|
|
||||||
.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 || "",
|
|
||||||
};
|
|
||||||
|
|
||||||
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 receivedOk(id: string, status: boolean) {
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from("vb_purchase_orders")
|
|
||||||
.update({ receivedOk: status })
|
|
||||||
.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 sesssionId = await supabase.auth.getSession();
|
|
||||||
if (!sesssionId.data.session) {
|
|
||||||
console.error("User not authenticated");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const userId = sesssionId.data.session?.user.id || "";
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from("vb_purchase_orders")
|
|
||||||
.update({
|
|
||||||
completed_status: status,
|
|
||||||
po_status: status,
|
|
||||||
updated_by: userId,
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
.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 Complete 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 === "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="RECEIVED COMPLETE"
|
|
||||||
>RECEIVED COMPLETE</option
|
|
||||||
>
|
|
||||||
<option value="RECEIVED INCOMPLETE"
|
|
||||||
>COMPLETE INCOMPLETE</option
|
|
||||||
>
|
|
||||||
</select>
|
|
||||||
</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 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}
|
|
||||||
<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}
|
|
||||||
@@ -1,989 +0,0 @@
|
|||||||
<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}
|
|
||||||
@@ -69,7 +69,8 @@
|
|||||||
const { data: villaData, error: villaError } = await supabase
|
const { data: villaData, error: villaError } = await supabase
|
||||||
.from("vb_villas")
|
.from("vb_villas")
|
||||||
.select("id, villa_name, villa_status")
|
.select("id, villa_name, villa_status")
|
||||||
.eq("villa_status", "Active");
|
.eq("villa_status", "Active")
|
||||||
|
.order("villa_name", { ascending: true });
|
||||||
|
|
||||||
if (villaError) {
|
if (villaError) {
|
||||||
console.error("Failed to fetch villas:", villaError.message);
|
console.error("Failed to fetch villas:", villaError.message);
|
||||||
@@ -81,7 +82,8 @@
|
|||||||
const { data: empData, error: empError } = await supabase
|
const { data: empData, error: empError } = await supabase
|
||||||
.from("vb_employee")
|
.from("vb_employee")
|
||||||
.select("id, employee_name")
|
.select("id, employee_name")
|
||||||
.eq("employee_status", "Active");
|
.eq("employee_status", "Active")
|
||||||
|
.order("employee_name", { ascending: true });
|
||||||
|
|
||||||
if (empError) {
|
if (empError) {
|
||||||
console.error("Failed to fetch employees:", empError.message);
|
console.error("Failed to fetch employees:", empError.message);
|
||||||
|
|||||||
Reference in New Issue
Block a user