This commit is contained in:
2025-06-30 04:47:42 +08:00
parent e7f2ae73be
commit 00c6abe44d

View File

@@ -5,6 +5,14 @@
import { timestampToDateTime } from "$lib/utils/conversion";
import { getSessionAuthId } from "$lib/utils/authUtil";
function ordinal(num: number) {
if (num === 1) return "1st";
if (num === 2) return "2nd";
if (num === 3) return "3rd";
return `${num}th`;
}
type PurchaseOrderInsert = {
issue_id: string;
prepared_date: string;
@@ -70,6 +78,7 @@
acknowledged: boolean;
acknowledge_by: string;
approved_by: string;
prepared: boolean | null;
approved_price: number;
approved_quantity: number;
total_approved_order_amount: number;
@@ -94,35 +103,21 @@
};
const columns: columns[] = [
{ key: "name", title: "Name" },
{ key: "purchase_order_number", title: "Purchase Order Number" },
{ key: "villa_name", title: "Villa Name" },
{ key: "priority", title: "Priority" },
{ key: "prepared_date", title: "Prepared Date" },
{ key: "po_type", title: "PO Type" },
{ key: "po_quantity", title: "PO Quantity" },
{ key: "po_price", title: "PO Price" },
{ key: "po_total_price", title: "PO Total Price" },
{ key: "issue_name", title: "Issue Name" },
{ key: "requested_date", title: "Requested Date" },
{ key: "purchase_order_number", title: "PO Number" },
{ key: "po_status", title: "PO Status" },
{ key: "proses_to_approval", title: "PROSES TO APPROVAL" },
{ key: "approved_vendor", title: "Approved Vendor" },
{ key: "acknowledged", title: "Acknowledged" },
{ key: "acknowledged_name", title: "Acknowledged By" },
{ key: "approved_name", title: "Approved By" },
{ key: "approved_price", title: "Approved Price" },
{ key: "approved_quantity", title: "Approved Quantity" },
{
key: "total_approved_order_amount",
title: "Total Approved Order Amount",
},
{ key: "approval", title: "Approval" },
{ key: "completed_status", title: "Completed Status" },
{ key: "received", title: "Received" },
{ key: "received_name", title: "Received By" },
{ key: "inputed_name", title: "Input By" },
{ key: "updated_name", title: "Updated By" },
{ key: "villa_data", title: "Villa Name" },
{ key: "po_item", title: "PO Product" },
{ key: "po_remark", title: "PO Remark" },
{ key: "po_due", title: "PO Due" },
{ key: "prepared", title: "Prepare PO" },
{ key: "approved", title: "Approval" },
{ key: "acknowledged", title: "Acknowledge" },
{ key: "completed", title: "Complete" },
{ key: "received", title: "Receive" },
{ key: "updated_at", title: "Updated At" },
{ key: "created_at", title: "Created At" },
{ key: "payment", title: "Payment" },
{ key: "actions", title: "Actions" }, // For edit/delete buttons
];
@@ -138,6 +133,94 @@
if (page >= 1 && page <= totalPages) currentPage = page;
}
let showPaymentModal = false;
let paymentForm = {
payment_method: "",
total_approved_order_amount: 0,
"1st_pay_amt": 0,
"2nd_pay_amt": 0,
"3rd_pay_amt": 0,
"4th_pay_amt": 0,
"5th_pay_amt": 0,
"6th_pay_amt": 0,
"1st_pay_date": "",
"2nd_pay_date": "",
"3rd_pay_date": "",
"4th_pay_date": "",
"5th_pay_date": "",
"6th_pay_date": "",
due_remaining: 0
};
let showPreparedModal = false;
let selectedPO = null;
let preparedByOptions: any[] = [];
let poItemOptions: any[] = [];
let vendorOptions: any[] = [];
let preparedForm = {
prepared_by: "",
prepared_date: "",
po_item: "",
po_quantity: 1,
q1_vendor: "",
q2_vendor: "",
q3_vendor: "",
q1_vendor_price: 0,
q2_vendor_price: 0,
q3_vendor_price: 0,
po_remark: "",
approved_quantity: 0,
approved_vendor: "",
approved_price: 0,
total_approved_order_amount: 0
};
async function openPreparedModal(row) {
selectedPO = row;
// Prefill existing values
preparedForm = {
prepared_by: row.prepared_by || "",
prepared_date: row.prepared_date || "",
po_item: row.po_item || "",
po_quantity: row.po_quantity || 1,
q1_vendor: row.q1_vendor || "",
q2_vendor: row.q2_vendor || "",
q3_vendor: row.q3_vendor || "",
q1_vendor_price: row.q1_vendor_price || 0,
q2_vendor_price: row.q2_vendor_price || 0,
q3_vendor_price: row.q3_vendor_price || 0,
po_remark: row.po_remark || "",
approved_quantity: row.approved_quantity || 0,
approved_vendor: row.approved_vendor || "",
approved_price: row.approved_price || 0,
total_approved_order_amount: row.approved_quantity * row.approved_price
};
// Load dropdowns
await fetchPreparedDropdowns();
showPreparedModal = true;
}
async function fetchPreparedDropdowns() {
const [{ data: employees }, { data: items }, { data: vendors }] = await Promise.all([
supabase.from("vb_employee").select("id, employee_name").eq("employee_status", "Active"),
supabase.from("vb_po_item").select("id, item_name"),
supabase.from("vb_vendor").select("id, name")
]);
preparedByOptions = employees || [];
poItemOptions = items || [];
vendorOptions = vendors || [];
}
async function fetchPurchaseOrder(
filter: string | null = null,
search: string | null = null,
@@ -292,6 +375,7 @@
(col) => !excludedKeys.includes(col.key),
);
async function openModal(purchase: PurchaseOrderDisplay | null = null) {
await fetchIssues();
if (purchase) {
@@ -306,6 +390,65 @@
showModal = true;
}
function updateTotalAmount() {
preparedForm.total_approved_order_amount =
preparedForm.approved_quantity * preparedForm.approved_price;
}
async function savePrepared() {
const { error } = await supabase
.from("vb_purchase_orders")
.update({
prepared_by: preparedForm.prepared_by,
prepared_date: preparedForm.prepared_date,
po_item: preparedForm.po_item,
po_quantity: preparedForm.po_quantity,
q1_vendor: preparedForm.q1_vendor,
q2_vendor: preparedForm.q2_vendor,
q3_vendor: preparedForm.q3_vendor,
q1_vendor_price: preparedForm.q1_vendor_price,
q2_vendor_price: preparedForm.q2_vendor_price,
q3_vendor_price: preparedForm.q3_vendor_price,
approved_quantity: preparedForm.approved_quantity,
approved_vendor: preparedForm.approved_vendor,
approved_price: preparedForm.approved_price,
total_approved_order_amount: preparedForm.total_approved_order_amount,
po_remark: preparedForm.po_remark,
po_status: "prepared",
prepared: true,
})
.eq("id", selectedPO.id);
if (error) {
console.error("Error saving prepared data:", error);
alert("Failed to save.");
return;
}
try {
await fetch("https://flow.catalis.app/webhook/vb_approval_po_new", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
po_id: selectedPO.id,
po_status: "prepared",
prepared_by: preparedForm.prepared_by,
prepared_date: preparedForm.prepared_date,
po_item: preparedForm.po_item,
po_quantity: preparedForm.po_quantity,
approved_quantity: preparedForm.approved_quantity,
approved_vendor: preparedForm.approved_vendor,
approved_price: preparedForm.approved_price,
total_approved_order_amount: preparedForm.total_approved_order_amount,
}),
});
} catch (webhookError) {
console.error("Failed to fire webhook:", webhookError);
alert("Save worked but webhook failed.");
}
showPreparedModal = false;
await fetchPurchaseOrder();
}
async function saveProject() {
// session check
const session = await supabase.auth.getSession();
@@ -618,6 +761,84 @@
await fetchPurchaseOrder();
}
function openPaymentModal(row) {
selectedPO = row;
paymentForm = {
payment_method: row.payment_method || "",
total_approved_order_amount: row.total_approved_order_amount || 0,
"1st_pay_amt": row["1st_pay_amt"] || 0,
"2nd_pay_amt": row["2nd_pay_amt"] || 0,
"3rd_pay_amt": row["3rd_pay_amt"] || 0,
"4th_pay_amt": row["4th_pay_amt"] || 0,
"5th_pay_amt": row["5th_pay_amt"] || 0,
"6th_pay_amt": row["6th_pay_amt"] || 0,
"1st_pay_date": row["1st_pay_date"] || "",
"2nd_pay_date": row["2nd_pay_date"] || "",
"3rd_pay_date": row["3rd_pay_date"] || "",
"4th_pay_date": row["4th_pay_date"] || "",
"5th_pay_date": row["5th_pay_date"] || "",
"6th_pay_date": row["6th_pay_date"] || "",
due_remaining: calculateDueRemaining(row)
};
showPaymentModal = true;
}
function calculateDueRemaining(source) {
const total = source.total_approved_order_amount || 0;
const sum =
(source["1st_pay_amt"] || 0) +
(source["2nd_pay_amt"] || 0) +
(source["3rd_pay_amt"] || 0) +
(source["4th_pay_amt"] || 0) +
(source["5th_pay_amt"] || 0) +
(source["6th_pay_amt"] || 0);
return total - sum;
}
function updateDueRemaining() {
paymentForm.due_remaining =
paymentForm.total_approved_order_amount -
paymentForm["1st_pay_amt"] -
paymentForm["2nd_pay_amt"] -
paymentForm["3rd_pay_amt"] -
paymentForm["4th_pay_amt"] -
paymentForm["5th_pay_amt"] -
paymentForm["6th_pay_amt"];
}
async function savePayment() {
const { error } = await supabase
.from("vb_purchase_orders")
.update({
payment_method: paymentForm.payment_method,
"1st_pay_amt": paymentForm["1st_pay_amt"],
"2nd_pay_amt": paymentForm["2nd_pay_amt"],
"3rd_pay_amt": paymentForm["3rd_pay_amt"],
"4th_pay_amt": paymentForm["4th_pay_amt"],
"5th_pay_amt": paymentForm["5th_pay_amt"],
"6th_pay_amt": paymentForm["6th_pay_amt"],
"1st_pay_date": paymentForm["1st_pay_date"] || null,
"2nd_pay_date": paymentForm["2nd_pay_date"] || null,
"3rd_pay_date": paymentForm["3rd_pay_date"] || null,
"4th_pay_date": paymentForm["4th_pay_date"] || null,
"5th_pay_date": paymentForm["5th_pay_date"] || null,
"6th_pay_date": paymentForm["6th_pay_date"] || null,
due_remaining: paymentForm.due_remaining
})
.eq("id", selectedPO.id);
if (error) {
console.error("Error saving payment data:", error);
alert("Failed to save.");
return;
}
showPaymentModal = false;
await fetchPurchaseOrder();
}
</script>
<div>
@@ -691,201 +912,63 @@
</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,
);
}}
disabled
>
<option value="" disabled selected
>ON PROSES</option
>
<option value="APPROVED"
>APPROVED</option
>
<option value="REJECTED"
>REJECTED</option
>
</select>
</td>
{:else if col.key === "proses_to_approval"}
<td class="px-4 py-2">
<!-- checkbox -->
<input
type="checkbox"
checked={row.proses_to_approval}
on:change={(e) => {
const isChecked = (
e.target as HTMLInputElement
).checked;
if (isChecked) {
newPurchaseOrders = {
...row,
proses_to_approval: true,
};
// map to project
updateProsesToApproval(
row.id,
isChecked,
);
} else {
newPurchaseOrders = {
...row,
proses_to_approval: false,
};
// uncheck
updateProsesToApproval(
row.id,
false,
);
}
}}
disabled={row.proses_to_approval}
/>
</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,
);
}
}}
disabled
/>
</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,
);
}
}}
disabled
/>
</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,
);
}
}}
disabled
>
<option value="" disabled selected
>ON PROSES</option
>
<option value="RECEIVED COMPLETE"
>RECEIVED COMPLETE</option
>
<option value="RECEIVED INCOMPLETE"
>COMPLETE INCOMPLETE</option
>
</select>
</td>
{:else if col.key === "updated_at"}
<td class="px-4 py-2 text-gray-500"
>{new Date(
row[
col.key as keyof PurchaseOrderDisplay
] as string,
).toLocaleString()}</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 === "created_at"}
<td class="px-4 py-2 text-gray-500"
>{new Date(
row[
col.key as keyof PurchaseOrderDisplay
] as string,
).toLocaleString()}</td
>
{:else}
<td class="px-4 py-2 text-gray-700"
>{row[
col.key as keyof PurchaseOrderDisplay
]}</td
>
{/if}
{/each}
</tr>
<tr class="hover:bg-gray-50 transition">
{#each columns as col}
{#if col.key === "requested_date" || col.key === "po_due" || col.key === "updated_at"}
<td class="px-4 py-2 text-gray-500">
{#if row[col.key]}
{new Date(row[col.key]).toLocaleString()}
{:else}
{/if}
</td>
{:else if col.key === "prepared"}
<td class="px-4 py-2 text-center">
<button
class="bg-blue-600 text-white text-xs px-3 py-1.5 rounded hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
on:click={() => openPreparedModal(row)}
disabled={row.prepared === true }
>
{row.prepared === true ? "Prepared" : "Set Prepared"}
</button>
</td>
{:else if col.key === "approved" || col.key === "acknowledged" || col.key === "completed" || col.key === "received"}
<td class="px-4 py-2 text-center">
<input type="checkbox" checked={row[col.key]} disabled />
</td>
{:else if col.key === "payment"}
<td class="px-4 py-2 text-center">
<button
class="inline-flex items-center gap-1 rounded bg-emerald-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-emerald-700"
on:click={() => openPaymentModal(row)}
>
Payment
</button>
</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}
<td class="px-4 py-2 text-gray-700">
{row[col.key] ?? "—"}
</td>
{/if}
{/each}
</tr>
{/each}
</tbody>
</tbody>
</table>
</div>
@@ -1165,3 +1248,126 @@
</div>
</div>
{/if}
{#if showPreparedModal}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 overflow-y-auto">
<div class="min-h-screen flex items-center justify-center p-4">
<div class="bg-white rounded-lg shadow-lg w-full max-w-2xl max-h-[90vh] overflow-y-auto p-6 space-y-4">
<h2 class="text-lg font-bold">Set Prepared</h2>
<!-- prepared_by -->
<label>Prepared By</label>
<select bind:value={preparedForm.prepared_by} class="w-full border p-2">
<option value="" disabled>Select Employee</option>
{#each preparedByOptions as emp}
<option value={emp.id}>{emp.employee_name}</option>
{/each}
</select>
<!-- prepared_date -->
<label>Prepared Date</label>
<input type="date" bind:value={preparedForm.prepared_date} class="w-full border p-2" />
<!-- po_item -->
<label>PO Item</label>
<select bind:value={preparedForm.po_item} class="w-full border p-2">
<option value="" disabled>Select Item</option>
{#each poItemOptions as item}
<option value={item.item_name}>{item.item_name}</option>
{/each}
</select>
<!-- po_quantity -->
<label>PO Quantity</label>
<input type="number" bind:value={preparedForm.po_quantity} class="w-full border p-2" />
<!-- Vendors and prices -->
<label>Q1 Vendor</label>
<select bind:value={preparedForm.q1_vendor} class="w-full border p-2">
<option value="" disabled>Select Vendor</option>
{#each vendorOptions as v}
<option value={v.name}>{v.name}</option>
{/each}
</select>
<input type="number" bind:value={preparedForm.q1_vendor_price} placeholder="Q1 Vendor Price" class="w-full border p-2" />
<!-- Repeat for Q2, Q3 -->
<!-- Approved -->
<label>Approved Quantity</label>
<input type="number" bind:value={preparedForm.approved_quantity} on:input={() => updateTotalAmount()} class="w-full border p-2" />
<label>Approved Vendor</label>
<select bind:value={preparedForm.approved_vendor} class="w-full border p-2">
<option value="" disabled>Select Vendor</option>
{#each vendorOptions as v}
<option value={v.name}>{v.name}</option>
{/each}
</select>
<label>Approved Price</label>
<input type="number" bind:value={preparedForm.approved_price} on:input={() => updateTotalAmount()} class="w-full border p-2" />
<label>Total Approved Order Amount</label>
<input type="number" value={preparedForm.total_approved_order_amount} disabled class="w-full border p-2 bg-gray-100" />
<label>PO Remark</label>
<textarea bind:value={preparedForm.po_remark} class="w-full border p-2"></textarea>
<div class="flex justify-end space-x-2 pt-4">
<button on:click={savePrepared} class="bg-blue-600 text-white px-4 py-2 rounded">Save</button>
<button on:click={() => showPreparedModal = false} class="px-4 py-2 rounded border">Cancel</button>
</div>
</div>
</div>
</div>
{/if}
{#if showPaymentModal}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 overflow-y-auto">
<div class="min-h-screen flex items-center justify-center p-4">
<div class="bg-white rounded-lg shadow-lg w-full max-w-2xl max-h-[90vh] overflow-y-auto p-6 space-y-4">
<h2 class="text-xl font-bold">Set Payment</h2>
<!-- Payment Method -->
<label>Payment Method</label>
<select bind:value={paymentForm.payment_method} class="w-full border p-2">
<option value="" disabled>Select Method</option>
<option value="Gradualy">Gradualy</option>
<option value="Full Before Received">Full Before Received</option>
<option value="Full After Received">Full After Received</option>
</select>
<!-- Total Approved Order Amount (hidden input, readonly) -->
<label>Total Approved Order Amount</label>
<input type="number" value={paymentForm.total_approved_order_amount} disabled class="w-full border p-2 bg-gray-100"/>
<!-- Payment Amounts -->
{#each [1,2,3,4,5,6] as num}
<label>{ordinal(num)} Pay Amount</label>
<input
type="number"
bind:value={paymentForm[`${ordinal(num)}_pay_amt`]}
on:input={updateDueRemaining}
class="w-full border p-2"
/>
<label>{ordinal(num)} Pay Date</label>
<input
type="date"
bind:value={paymentForm[`${ordinal(num)}_pay_date`]}
class="w-full border p-2"
/>
{/each}
<!-- Due Remaining -->
<label>Due Remaining</label>
<input type="number" value={paymentForm.due_remaining} disabled class="w-full border p-2 bg-gray-100"/>
<!-- Buttons -->
<div class="flex justify-end space-x-2 pt-4">
<button on:click={savePayment} class="bg-blue-600 text-white px-4 py-2 rounded">Save</button>
<button on:click={() => showPaymentModal = false} class="px-4 py-2 rounded border">Cancel</button>
</div>
</div>
</div>
</div>
{/if}