1877 lines
72 KiB
Svelte
1877 lines
72 KiB
Svelte
<script lang="ts">
|
||
import { onMount } from "svelte";
|
||
import { supabase } from "$lib/supabaseClient";
|
||
import { getSessionAuthId } from "$lib/utils/authUtil";
|
||
|
||
|
||
type PurchaseOrderInsert = {
|
||
issue_id: string;
|
||
prepared_date: string;
|
||
po_type: string;
|
||
po_quantity: number;
|
||
po_price: number;
|
||
po_total_price: number;
|
||
input_by: string;
|
||
updated_by: string;
|
||
updated_at: string;
|
||
};
|
||
type PurchaseOrders = {
|
||
id: string;
|
||
purchase_order_number: string;
|
||
prepared_date: string;
|
||
po_type: string;
|
||
po_quantity: number;
|
||
po_status: string;
|
||
approved_vendor: string;
|
||
proses_to_approval: boolean;
|
||
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: 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_price: number;
|
||
po_total_price: number;
|
||
po_status: string;
|
||
approved_vendor: string;
|
||
acknowledged: boolean;
|
||
acknowledge_by: string;
|
||
approved_by: string;
|
||
prepared: boolean | null;
|
||
approved_price: number;
|
||
approved_quantity: number;
|
||
total_approved_order_amount: number;
|
||
proses_to_approval: boolean;
|
||
approval: string;
|
||
completed_status: string;
|
||
received: boolean;
|
||
received_by: string;
|
||
created_at: string;
|
||
updated_at?: string;
|
||
updated_by?: string;
|
||
updated_name?: string;
|
||
input_by?: string;
|
||
input_name?: string;
|
||
};
|
||
type columns = {
|
||
key: string;
|
||
title: string;
|
||
};
|
||
|
||
const columns: columns[] = [
|
||
{ 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: "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: "payment", title: "Payment" },
|
||
{ key: "actions", title: "Actions" }, // For edit/delete buttons
|
||
];
|
||
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",
|
||
"proses_to_approval",
|
||
"approved_by",
|
||
"name",
|
||
"po_status",
|
||
"approved_quantity",
|
||
"total_approved_order_amount",
|
||
"approved_vendor",
|
||
"approved_price",
|
||
"updated_at",
|
||
"updated_by",
|
||
"updated_name",
|
||
"inputed_name",
|
||
"approved_name",
|
||
"acknowledged_name",
|
||
"received_name",
|
||
];
|
||
const formColumns = columns.filter(
|
||
(col) => !excludedKeys.includes(col.key),
|
||
);
|
||
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: "RECEIVED INCOMPLETE",
|
||
color: "#fb923c",
|
||
},
|
||
{
|
||
label: "Received - Completed",
|
||
value: "RECEIVED COMPLETED",
|
||
color: "#10b981",
|
||
},
|
||
{ label: "Canceled", value: "CANCELED", color: "#f87171" },
|
||
];
|
||
|
||
//reactive variables
|
||
let purchaseOrderInsert: PurchaseOrderInsert = {
|
||
issue_id: "",
|
||
prepared_date: "",
|
||
po_type: "",
|
||
po_quantity: 0,
|
||
po_price: 0,
|
||
po_total_price: 0,
|
||
input_by: "",
|
||
updated_by: "",
|
||
updated_at: new Date().toISOString(),
|
||
};
|
||
let showEditModal = false;
|
||
let editForm = {
|
||
po_number: "",
|
||
issue_name: "",
|
||
villa_name: "",
|
||
po_status: "",
|
||
prepared: null,
|
||
approved: null,
|
||
acknowledged: null,
|
||
completed: null,
|
||
received: null,
|
||
po_remark: ""
|
||
};
|
||
let showAddPOModal = false;
|
||
let addPOForm = {
|
||
po_type: "",
|
||
villa_id: "",
|
||
po_item: "",
|
||
po_quantity: 1,
|
||
po_status: "requested",
|
||
po_remark: "",
|
||
requested_by: "",
|
||
requested_date: new Date().toISOString().split("T")[0]
|
||
};
|
||
let showApprovalModal = false;
|
||
let approvalForm = {
|
||
po_number: "",
|
||
approval: "",
|
||
approved_by: "",
|
||
approved_date: "",
|
||
reject_comment: ""
|
||
};
|
||
let villaOptions = [];
|
||
let poItemOptions = [];
|
||
let employeeOptions = [];
|
||
let allRows: PurchaseOrderDisplay[] = [];
|
||
let currentPage = 1;
|
||
let rowsPerPage = 10;
|
||
let currentUserId = "";
|
||
let currentUserFullName = "";
|
||
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 showAcknowledgedModal = false;
|
||
let acknowledgedForm = {
|
||
po_number: "",
|
||
acknowledged: "",
|
||
acknowledged_by: "",
|
||
acknowledged_date: ""
|
||
};
|
||
let showPreparedModal = false;
|
||
let selectedPO = null;
|
||
let preparedByOptions: 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
|
||
};
|
||
let showReceivedModal = false;
|
||
let receivedForm = {
|
||
po_number: "",
|
||
received: "",
|
||
received_by: "",
|
||
received_date: ""
|
||
};
|
||
let showCompletedModal = false;
|
||
let completedForm = {
|
||
po_number: "",
|
||
completed: "",
|
||
completed_by: "",
|
||
completed_date: ""
|
||
};
|
||
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 }[] = [];
|
||
// Pagination variables
|
||
$: totalPages = Math.ceil(allRows.length / rowsPerPage);
|
||
$: paginatedRows = allRows.slice(
|
||
(currentPage - 1) * rowsPerPage,
|
||
currentPage * rowsPerPage,
|
||
);
|
||
$: currentPage = 1; // Reset to first page when allRows changes
|
||
|
||
// Function to format numbers as ordinal (1st, 2nd, 3rd, etc.)
|
||
function ordinal(num: number) {
|
||
if (num === 1) return "1st";
|
||
if (num === 2) return "2nd";
|
||
if (num === 3) return "3rd";
|
||
return `${num}th`;
|
||
}
|
||
// Open edit modal for purchase order
|
||
function openEditModal(row) {
|
||
selectedPO = row;
|
||
|
||
editForm = {
|
||
po_number: row.purchase_order_number || "",
|
||
issue_name: row.issue_name || "",
|
||
villa_name: row.villa_data || "",
|
||
po_status: row.po_status || "",
|
||
prepared: row.prepared,
|
||
approved: row.approved,
|
||
acknowledged: row.acknowledged,
|
||
completed: row.completed,
|
||
received: row.received,
|
||
po_remark: row.po_remark || ""
|
||
};
|
||
|
||
showEditModal = true;
|
||
}
|
||
// pagination functions
|
||
function goToPage(page: number) {
|
||
if (page >= 1 && page <= totalPages) currentPage = page;
|
||
}
|
||
// Open approval modal for purchase order
|
||
function openApprovalModal(row) {
|
||
selectedPO = row;
|
||
|
||
approvalForm = {
|
||
po_number: row.purchase_order_number || "",
|
||
approval: row.approval ? "approve" : "reject",
|
||
approved_by: currentUserId,
|
||
approved_date: new Date().toISOString().split("T")[0],
|
||
reject_comment: row.reject_comment || ""
|
||
};
|
||
|
||
showApprovalModal = true;
|
||
}
|
||
// acknowledge purchase order
|
||
function openAcknowledgedModal(row) {
|
||
selectedPO = row;
|
||
|
||
acknowledgedForm = {
|
||
po_number: row.purchase_order_number || "",
|
||
acknowledged: row.acknowledged ? "acknowledged" : "reject",
|
||
acknowledged_by: currentUserId,
|
||
acknowledged_date: new Date().toISOString().split("T")[0]
|
||
};
|
||
|
||
showAcknowledgedModal = true;
|
||
}
|
||
// open receive modal
|
||
function openReceivedModal(row) {
|
||
selectedPO = row;
|
||
|
||
receivedForm = {
|
||
po_number: row.purchase_order_number || "",
|
||
received: row.received ? "received" : "reject",
|
||
received_by: currentUserId,
|
||
received_date: new Date().toISOString().split("T")[0]
|
||
};
|
||
|
||
showReceivedModal = true;
|
||
}
|
||
// open completed modal
|
||
function openCompletedModal(row) {
|
||
selectedPO = row;
|
||
|
||
completedForm = {
|
||
po_number: row.purchase_order_number || "",
|
||
completed: row.completed ? "completed" : "reject",
|
||
completed_by: currentUserId,
|
||
completed_date: new Date().toISOString().split("T")[0]
|
||
};
|
||
|
||
showCompletedModal = true;
|
||
}
|
||
// Update total amount in prepared form
|
||
function updateTotalAmount() {
|
||
preparedForm.total_approved_order_amount =
|
||
preparedForm.approved_quantity * preparedForm.approved_price;
|
||
}
|
||
function getStatusOption(value: string) {
|
||
return statusOptions.find((option) => option.value === value) ?? null;
|
||
}
|
||
// open payment modal for purchase order
|
||
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;
|
||
}
|
||
//validate input fields purchase order
|
||
function validateInput() {
|
||
const requiredFields = [
|
||
"prepared_date",
|
||
"po_type",
|
||
"po_quantity",
|
||
"po_price",
|
||
];
|
||
for (const field of requiredFields) {
|
||
if (!newPurchaseOrders[field]) {
|
||
alert(`Please fill in the ${field} field.`);
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
// Calculate remaining due amount
|
||
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;
|
||
}
|
||
// Update due remaining amount in payment form
|
||
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"];
|
||
}
|
||
// Save payment data for purchase order
|
||
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();
|
||
}
|
||
// Save approval for purchase order
|
||
async function saveApproval() {
|
||
const { error } = await supabase
|
||
.from("vb_purchase_orders")
|
||
.update({
|
||
approved: approvalForm.approval === "approve",
|
||
approved_by: approvalForm.approval === "approve" ? currentUserId : null,
|
||
approved_date: approvalForm.approved_date,
|
||
reject_comment: approvalForm.reject_comment || null,
|
||
po_status: "approved"
|
||
})
|
||
.eq("id", selectedPO.id);
|
||
|
||
if (error) {
|
||
console.error("Error saving approval:", error);
|
||
alert("Failed to save.");
|
||
return;
|
||
}
|
||
|
||
showApprovalModal = false;
|
||
await fetchPurchaseOrder();
|
||
}
|
||
// populate dropdowns for adding purchase orders
|
||
async function fetchAddPODropdowns() {
|
||
const [{ data: villas }, { data: items }, { data: employees }] = await Promise.all([
|
||
supabase.from("vb_villas").select("id, villa_name").eq("villa_status", "Active").order("villa_name", { ascending: true }),
|
||
supabase.from("vb_po_item").select("id, item_name").order("item_name", { ascending: true }),
|
||
supabase.from("vb_employee").select("id, employee_name").eq("employee_status", "Active").order("employee_name", { ascending: true }),
|
||
]);
|
||
villaOptions = villas || [];
|
||
poItemOptions = items || [];
|
||
employeeOptions = employees || [];
|
||
}
|
||
// Fetch purchase orders add
|
||
async function openAddPOModal() {
|
||
await fetchAddPODropdowns();
|
||
addPOForm = {
|
||
po_type: "",
|
||
villa_id: "",
|
||
po_item: "",
|
||
po_quantity: 1,
|
||
po_status: "requested",
|
||
po_remark: "",
|
||
requested_by: "",
|
||
requested_date: new Date().toISOString().split("T")[0]
|
||
};
|
||
showAddPOModal = true;
|
||
}
|
||
// Save new purchase order
|
||
async function saveAddPO() {
|
||
// Basic required field check
|
||
if (
|
||
!addPOForm.po_type ||
|
||
!addPOForm.villa_id ||
|
||
!addPOForm.po_item ||
|
||
!addPOForm.requested_by ||
|
||
!addPOForm.po_quantity ||
|
||
addPOForm.po_quantity <= 0
|
||
) {
|
||
alert("Please fill in all required fields correctly.");
|
||
return;
|
||
}
|
||
|
||
// If you use auth:
|
||
const sessionId = await getSessionAuthId();
|
||
if (!sessionId) {
|
||
alert("You must be logged in.");
|
||
return;
|
||
}
|
||
|
||
const { error } = await supabase.from("vb_purchase_orders").insert({
|
||
po_type: addPOForm.po_type,
|
||
villa_id: addPOForm.villa_id,
|
||
po_item: addPOForm.po_item,
|
||
po_quantity: addPOForm.po_quantity,
|
||
po_status: "requested",
|
||
po_remark: addPOForm.po_remark,
|
||
requested_by: addPOForm.requested_by,
|
||
requested_date: addPOForm.requested_date,
|
||
created_at: new Date().toISOString()
|
||
});
|
||
|
||
if (error) {
|
||
console.error("Error adding PO:", error);
|
||
alert("Failed to add purchase order.");
|
||
return;
|
||
}
|
||
|
||
showAddPOModal = false;
|
||
await fetchPurchaseOrder();
|
||
}
|
||
// Fetch current user details
|
||
async function fetchCurrentUser() {
|
||
const {
|
||
data: { user }
|
||
} = await supabase.auth.getUser();
|
||
|
||
if (user) {
|
||
currentUserId = user.id;
|
||
|
||
const { data, error } = await supabase
|
||
.from("vb_users")
|
||
.select("full_name")
|
||
.eq("id", user.id)
|
||
.single();
|
||
|
||
if (!error && data) {
|
||
currentUserFullName = data.full_name;
|
||
}
|
||
}
|
||
}
|
||
// Save edited purchase order
|
||
async function saveEdit() {
|
||
const { error } = await supabase
|
||
.from("vb_purchase_orders")
|
||
.update({
|
||
prepared: editForm.prepared,
|
||
approved: editForm.approved,
|
||
acknowledged: editForm.acknowledged,
|
||
completed: editForm.completed,
|
||
received: editForm.received,
|
||
po_remark: editForm.po_remark,
|
||
updated_at: new Date().toISOString(), // ✅ safe current timestamp
|
||
updated_by: currentUserId // ✅ user UUID
|
||
})
|
||
.eq("id", selectedPO.id);
|
||
|
||
if (error) {
|
||
console.error("Error saving edit:", error);
|
||
alert("Failed to save changes.");
|
||
return;
|
||
}
|
||
|
||
showEditModal = false;
|
||
await fetchPurchaseOrder();
|
||
}
|
||
// save completed purchase order
|
||
async function saveCompleted() {
|
||
const { error } = await supabase
|
||
.from("vb_purchase_orders")
|
||
.update({
|
||
completed: completedForm.completed === "completed",
|
||
completed_by: completedForm.completed === "completed" ? currentUserId : null,
|
||
completed_date: completedForm.completed_date,
|
||
po_status: "completed"
|
||
})
|
||
.eq("id", selectedPO.id);
|
||
|
||
if (error) {
|
||
console.error("Error saving completed:", error);
|
||
alert("Failed to save.");
|
||
return;
|
||
}
|
||
// ✅ Fire the webhook after saving:
|
||
try {
|
||
await fetch("https://flow.catalis.app/webhook-test/vb_need_complete_new", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
po_id: selectedPO.id,
|
||
completed: completedForm.completed === "completed",
|
||
completed_by: completedForm.completed === "completed" ? currentUserId : null,
|
||
completed_date: completedForm.completed_date
|
||
})
|
||
});
|
||
} catch (webhookError) {
|
||
console.error("Webhook failed:", webhookError);
|
||
alert("Saved, but webhook failed.");
|
||
}
|
||
showCompletedModal = false;
|
||
await fetchPurchaseOrder();
|
||
}
|
||
// open prepared modal for purchase order
|
||
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;
|
||
}
|
||
// Fetch dropdowns for prepared modal
|
||
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 || [];
|
||
}
|
||
// Save fetch purchase order
|
||
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("*")
|
||
.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;
|
||
}
|
||
|
||
if (!data || data.length === 0) {
|
||
console.warn("No purchase orders found.");
|
||
allRows = [];
|
||
return;
|
||
}
|
||
|
||
allRows = data.map((row: any) => {
|
||
return {
|
||
...row,
|
||
issue_name: row.issue_name || "N/A",
|
||
villa_name: row.villa_data || "N/A",
|
||
po_number: row.purchase_order_number || "",
|
||
approval: row.approval || "",
|
||
completed_status: row.completed_status || "",
|
||
proses_to_approval: row.proses_to_approval || false,
|
||
po_price: row.po_price || 0,
|
||
po_total_price: row.po_total_price || 0,
|
||
} as PurchaseOrderDisplay;
|
||
});
|
||
|
||
console.log("✅ Fetched rows:", data);
|
||
console.log("✅ Final mapped rows:", allRows);
|
||
}
|
||
//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,
|
||
}));
|
||
}
|
||
// Fetch vendors for dropdown
|
||
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,
|
||
}));
|
||
}
|
||
// Open modal for adding or editing purchase order
|
||
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;
|
||
}
|
||
// Save prepared purchase order
|
||
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();
|
||
}
|
||
// Save project function
|
||
async function saveProject() {
|
||
// session check
|
||
const session = await supabase.auth.getSession();
|
||
|
||
if (!session.data.session) {
|
||
alert("You must be logged in to perform this action.");
|
||
return;
|
||
}
|
||
|
||
const user = session.data.session.user;
|
||
|
||
const inputBy = user.id;
|
||
|
||
if (!validateInput()) {
|
||
return;
|
||
}
|
||
|
||
purchaseOrderInsert = {
|
||
issue_id: newPurchaseOrders.issue_id || "",
|
||
prepared_date: newPurchaseOrders.prepared_date || "",
|
||
po_type: newPurchaseOrders.po_type || "",
|
||
po_quantity: newPurchaseOrders.po_quantity || 0,
|
||
po_price: newPurchaseOrders.po_price || 0,
|
||
po_total_price:
|
||
newPurchaseOrders.po_quantity * newPurchaseOrders.po_price || 0,
|
||
input_by: inputBy,
|
||
updated_by: inputBy,
|
||
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();
|
||
}
|
||
// Save received purchase order
|
||
async function saveReceived() {
|
||
const { error } = await supabase
|
||
.from("vb_purchase_orders")
|
||
.update({
|
||
received: receivedForm.received === "received",
|
||
received_by: receivedForm.received === "received" ? currentUserId : null,
|
||
received_date: receivedForm.received_date,
|
||
po_status: "received",
|
||
})
|
||
.eq("id", selectedPO.id);
|
||
|
||
if (error) {
|
||
console.error("Error saving received:", error);
|
||
alert("Failed to save.");
|
||
return;
|
||
}
|
||
// ✅ Fire the webhook after the DB update succeeds:
|
||
try {
|
||
await fetch("https://flow.catalis.app/webhook-test/vb_need_received_new", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
po_id: selectedPO.id,
|
||
received: receivedForm.received === "received",
|
||
received_by: receivedForm.received === "received" ? currentUserId : null,
|
||
received_date: receivedForm.received_date
|
||
})
|
||
});
|
||
} catch (webhookError) {
|
||
console.error("Webhook failed:", webhookError);
|
||
alert("Saved, but webhook failed.");
|
||
}
|
||
showReceivedModal = false;
|
||
await fetchPurchaseOrder();
|
||
}
|
||
// Save acknowledged purchase order
|
||
async function saveAcknowledged() {
|
||
const { error } = await supabase
|
||
.from("vb_purchase_orders")
|
||
.update({
|
||
acknowledged: acknowledgedForm.acknowledged === "acknowledged",
|
||
acknowledge_by: acknowledgedForm.acknowledged === "acknowledged" ? currentUserId : null,
|
||
acknowledged_date: acknowledgedForm.acknowledged_date,
|
||
po_status: "acknowledged"
|
||
})
|
||
.eq("id", selectedPO.id);
|
||
|
||
if (error) {
|
||
console.error("Error saving acknowledged:", error);
|
||
alert("Failed to save.");
|
||
return;
|
||
}
|
||
|
||
// ✅ Fire the webhook after successful update:
|
||
try {
|
||
await fetch("https://flow.catalis.app/webhook/vb_acknowledged_new", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
po_id: selectedPO.id,
|
||
acknowledged: acknowledgedForm.acknowledged === "acknowledged",
|
||
acknowledged_by: acknowledgedForm.acknowledged === "acknowledged" ? currentUserId : null,
|
||
acknowledged_date: acknowledgedForm.acknowledged_date
|
||
})
|
||
});
|
||
} catch (webhookError) {
|
||
console.error("Failed to fire webhook:", webhookError);
|
||
alert("Saved, but webhook failed.");
|
||
}
|
||
|
||
showAcknowledgedModal = false;
|
||
await fetchPurchaseOrder();
|
||
}
|
||
//fetch on mount
|
||
onMount(() => {
|
||
fetchPurchaseOrder();
|
||
fetchVendors();
|
||
fetchCurrentUser();
|
||
});
|
||
</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 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>
|
||
<button
|
||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
|
||
on:click={openAddPOModal}
|
||
>
|
||
➕ Add Purchase Order
|
||
</button>
|
||
</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 === "issue_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 === "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 === "po_remark"}
|
||
<td class="px-4 py-2 text-gray-700 max-w-xs whitespace-normal align-top break-words">
|
||
{row[col.key] || "—"}
|
||
</td>
|
||
{:else if col.key === "issue_name"}
|
||
<td class="sticky left-0 px-4 py-2 font-medium text-blue-600 max-w-xs whitespace-normal align-top break-words"
|
||
style="background-color: #f0f8ff; cursor: pointer;">
|
||
{row.issue_name || "—"}
|
||
</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"}
|
||
<td class="px-4 py-2 text-center">
|
||
<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 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||
on:click={() => openApprovalModal(row)}
|
||
disabled={
|
||
row.approved === true ||
|
||
row.approved === false ||
|
||
row.po_status !== 'prepared'
|
||
}
|
||
>
|
||
{#if row.approved === true}
|
||
Approved
|
||
{:else if row.approved === false}
|
||
Rejected
|
||
{:else if row.po_status !== 'prepared'}
|
||
Not Prepared
|
||
{:else}
|
||
Approval
|
||
{/if}
|
||
</button>
|
||
</td>
|
||
{:else if col.key === "acknowledged"}
|
||
<td class="px-4 py-2 text-center">
|
||
<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 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||
on:click={() => openAcknowledgedModal(row)}
|
||
disabled={
|
||
row.approved !== true || row.acknowledged === true || row.acknowledged === false
|
||
}
|
||
>
|
||
{#if row.acknowledged === true}
|
||
Acknowledged
|
||
{:else if row.acknowledged === false}
|
||
Rejected
|
||
{:else if row.approved !== true}
|
||
Pending
|
||
{:else}
|
||
Acknowledge
|
||
{/if}
|
||
</button>
|
||
</td>
|
||
{:else if col.key === "completed"}
|
||
<td class="px-4 py-2 text-center">
|
||
<button
|
||
class="inline-flex items-center gap-1 rounded bg-purple-600 px-3 py-1.5 text-white text-xs font-medium
|
||
hover:bg-purple-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||
on:click={() => openCompletedModal(row)}
|
||
disabled={
|
||
row.acknowledged !== true || row.completed === true || row.completed === false
|
||
}
|
||
>
|
||
{#if row.completed === true}
|
||
Completed
|
||
{:else if row.completed === false}
|
||
Rejected
|
||
{:else if row.acknowledged !== true}
|
||
Pending
|
||
{:else}
|
||
Complete
|
||
{/if}
|
||
</button>
|
||
</td>
|
||
|
||
{:else if col.key === "received"}
|
||
<td class="px-4 py-2 text-center">
|
||
<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 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||
on:click={() => openReceivedModal(row)}
|
||
disabled={
|
||
row.completed !== true || row.received === true || row.received === false
|
||
}
|
||
>
|
||
{#if row.received === true}
|
||
Received
|
||
{:else if row.received === false}
|
||
Rejected
|
||
{:else if row.completed !== true}
|
||
Pending
|
||
{:else}
|
||
Receive
|
||
{/if}
|
||
</button>
|
||
</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={() => openEditModal(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>
|
||
</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;
|
||
}}
|
||
disabled
|
||
>
|
||
<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_price"}
|
||
<div class="mb-4">
|
||
<label
|
||
for={col.key}
|
||
class="block text-sm font-medium text-gray-700 mb-1"
|
||
>
|
||
{col.title}
|
||
</label>
|
||
<input
|
||
name="po_price"
|
||
placeholder="Enter PO Price"
|
||
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 === "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
|
||
name="po_quantity"
|
||
placeholder="Enter PO Quantity"
|
||
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 === "po_total_price"}
|
||
<div class="mb-4">
|
||
<label
|
||
for={col.key}
|
||
class="block text-sm font-medium text-gray-700 mb-1"
|
||
>
|
||
{col.title}
|
||
</label>
|
||
<!-- calculate po quantity * price -->
|
||
<input
|
||
name="po_total_price"
|
||
type="number"
|
||
id={col.key}
|
||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||
readonly
|
||
value={newPurchaseOrders.po_quantity *
|
||
newPurchaseOrders.po_price}
|
||
/>
|
||
</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}
|
||
{#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}
|
||
{#if showApprovalModal}
|
||
<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-md max-h-[90vh] overflow-y-auto p-6 space-y-4">
|
||
<h2 class="text-xl font-bold">Approve Purchase Order</h2>
|
||
|
||
<!-- PO Number -->
|
||
<label>PO Number</label>
|
||
<input
|
||
type="text"
|
||
value={approvalForm.po_number}
|
||
disabled
|
||
class="w-full border p-2 bg-gray-100"
|
||
/>
|
||
|
||
<!-- Approval -->
|
||
<label>Approval</label>
|
||
<select bind:value={approvalForm.approval} class="w-full border p-2">
|
||
<option value="" disabled>Select Approval</option>
|
||
<option value="approve">Approve</option>
|
||
<option value="reject">Reject</option>
|
||
</select>
|
||
{#if approvalForm.approval === 'reject'}
|
||
<label>Reject Comment</label>
|
||
<textarea
|
||
bind:value={approvalForm.reject_comment}
|
||
class="w-full border p-2"
|
||
rows="3"
|
||
placeholder="Enter reason for rejection..."
|
||
></textarea>
|
||
{/if}
|
||
|
||
<!-- Hidden: approved_by -->
|
||
<input type="hidden" value={approvalForm.approved_by} />
|
||
|
||
<!-- Hidden: approved_date -->
|
||
<input type="hidden" value={approvalForm.approved_date} />
|
||
|
||
<!-- Actions -->
|
||
<div class="flex justify-end space-x-2 pt-4">
|
||
<button on:click={saveApproval} class="bg-blue-600 text-white px-4 py-2 rounded">Save</button>
|
||
<button on:click={() => showApprovalModal = false} class="px-4 py-2 rounded border">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
{#if showAcknowledgedModal}
|
||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 overflow-y-auto">
|
||
<div class="min-h-screen flex items-center justify-center p-4">
|
||
<div class="bg-white rounded-lg shadow-lg w-full max-w-md max-h-[90vh] overflow-y-auto p-6 space-y-4">
|
||
<h2 class="text-xl font-bold">Acknowledge Purchase Order</h2>
|
||
|
||
<!-- PO Number -->
|
||
<label>PO Number</label>
|
||
<input
|
||
type="text"
|
||
value={acknowledgedForm.po_number}
|
||
disabled
|
||
class="w-full border p-2 bg-gray-100"
|
||
/>
|
||
|
||
<!-- Acknowledged -->
|
||
<label>Acknowledged</label>
|
||
<select bind:value={acknowledgedForm.acknowledged} class="w-full border p-2">
|
||
<option value="" disabled>Select Acknowledgement</option>
|
||
<option value="acknowledged">Acknowledged</option>
|
||
<option value="reject">Reject</option>
|
||
</select>
|
||
|
||
<!-- Hidden fields -->
|
||
<input type="hidden" value={acknowledgedForm.acknowledged_by} />
|
||
<input type="hidden" value={acknowledgedForm.acknowledged_date} />
|
||
|
||
<!-- Actions -->
|
||
<div class="flex justify-end space-x-2 pt-4">
|
||
<button on:click={saveAcknowledged} class="bg-blue-600 text-white px-4 py-2 rounded">Save</button>
|
||
<button on:click={() => showAcknowledgedModal = false} class="px-4 py-2 rounded border">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
{#if showCompletedModal}
|
||
<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-md max-h-[90vh] overflow-y-auto p-6 space-y-4">
|
||
<h2 class="text-xl font-bold">Complete Purchase Order</h2>
|
||
|
||
<!-- PO Number -->
|
||
<label>PO Number</label>
|
||
<input
|
||
type="text"
|
||
value={completedForm.po_number}
|
||
disabled
|
||
class="w-full border p-2 bg-gray-100"
|
||
/>
|
||
|
||
<!-- Completed -->
|
||
<label>Completion</label>
|
||
<select bind:value={completedForm.completed} class="w-full border p-2">
|
||
<option value="" disabled>Select Status</option>
|
||
<option value="completed">Completed</option>
|
||
<option value="reject">Reject</option>
|
||
</select>
|
||
|
||
<!-- Hidden -->
|
||
<input type="hidden" value={completedForm.completed_by} />
|
||
<input type="hidden" value={completedForm.completed_date} />
|
||
|
||
<!-- Actions -->
|
||
<div class="flex justify-end space-x-2 pt-4">
|
||
<button on:click={saveCompleted} class="bg-blue-600 text-white px-4 py-2 rounded">Save</button>
|
||
<button on:click={() => showCompletedModal = false} class="px-4 py-2 rounded border">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
{#if showReceivedModal}
|
||
<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-md max-h-[90vh] overflow-y-auto p-6 space-y-4">
|
||
<h2 class="text-xl font-bold">Receive Purchase Order</h2>
|
||
|
||
<!-- PO Number -->
|
||
<label>PO Number</label>
|
||
<input
|
||
type="text"
|
||
value={receivedForm.po_number}
|
||
disabled
|
||
class="w-full border p-2 bg-gray-100"
|
||
/>
|
||
|
||
<!-- Received -->
|
||
<label>Received</label>
|
||
<select bind:value={receivedForm.received} class="w-full border p-2">
|
||
<option value="" disabled>Select Status</option>
|
||
<option value="received">Received</option>
|
||
<option value="reject">Reject</option>
|
||
</select>
|
||
|
||
<!-- Hidden -->
|
||
<input type="hidden" value={receivedForm.received_by} />
|
||
<input type="hidden" value={receivedForm.received_date} />
|
||
|
||
<!-- Actions -->
|
||
<div class="flex justify-end space-x-2 pt-4">
|
||
<button on:click={saveReceived} class="bg-blue-600 text-white px-4 py-2 rounded">Save</button>
|
||
<button on:click={() => showReceivedModal = false} class="px-4 py-2 rounded border">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
{#if showEditModal}
|
||
<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">Edit Purchase Order</h2>
|
||
|
||
<!-- PO Number -->
|
||
<label>PO Number</label>
|
||
<input type="text" value={editForm.po_number} disabled class="w-full border p-2 bg-gray-100"/>
|
||
|
||
<!-- Issue Name -->
|
||
<label>Issue Name</label>
|
||
<input type="text" value={editForm.issue_name} disabled class="w-full border p-2 bg-gray-100"/>
|
||
|
||
<!-- Villa ID -->
|
||
<label>Villa ID</label>
|
||
<input type="text" value={editForm.villa_name} disabled class="w-full border p-2 bg-gray-100"/>
|
||
|
||
<!-- PO Status -->
|
||
<label>PO Status</label>
|
||
<input type="text" value={editForm.po_status} disabled class="w-full border p-2 bg-gray-100"/>
|
||
|
||
<!-- Status Toggles -->
|
||
<div class="space-y-2">
|
||
{#each ['prepared','approved','acknowledged','completed','received'] as key}
|
||
<div class="flex items-center gap-2">
|
||
<label class="w-32 capitalize">{key}</label>
|
||
<select bind:value={editForm[key]} class="w-40 border p-2">
|
||
<option value={null}>Unset</option>
|
||
<option value={true}>True</option>
|
||
<option value={false}>False</option>
|
||
</select>
|
||
<button
|
||
type="button"
|
||
class="text-xs text-blue-600 underline"
|
||
on:click={() => editForm[key] = null}
|
||
>Reset</button>
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
|
||
<!-- PO Remark -->
|
||
<label>PO Remark</label>
|
||
<textarea
|
||
bind:value={editForm.po_remark}
|
||
class="w-full border p-2"
|
||
rows="3"
|
||
></textarea>
|
||
|
||
<!-- Actions -->
|
||
<div class="flex justify-end space-x-2 pt-4">
|
||
<button on:click={saveEdit} class="bg-blue-600 text-white px-4 py-2 rounded">Save</button>
|
||
<button on:click={() => showEditModal = false} class="px-4 py-2 rounded border">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
{#if showAddPOModal}
|
||
<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">Add Purchase Order</h2>
|
||
|
||
<!-- PO Type -->
|
||
<label>PO Type</label>
|
||
<select bind:value={addPOForm.po_type} class="w-full border p-2">
|
||
<option value="" disabled>Select PO Type</option>
|
||
<option value="purchase">Purchase</option>
|
||
<option value="project">Project</option>
|
||
<option value="repair">Repair</option>
|
||
</select>
|
||
|
||
<!-- Villa -->
|
||
<label>Villa</label>
|
||
<select bind:value={addPOForm.villa_id} class="w-full border p-2">
|
||
<option value="" disabled>Select Villa</option>
|
||
{#each villaOptions as v}
|
||
<option value={v.id}>{v.villa_name}</option>
|
||
{/each}
|
||
</select>
|
||
|
||
<!-- PO Item -->
|
||
<label>PO Item</label>
|
||
<select bind:value={addPOForm.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>
|
||
|
||
<!-- Quantity -->
|
||
<label>PO Quantity</label>
|
||
<input type="number" bind:value={addPOForm.po_quantity} class="w-full border p-2"/>
|
||
|
||
<!-- Remark -->
|
||
<label>PO Remark</label>
|
||
<textarea bind:value={addPOForm.po_remark} class="w-full border p-2"></textarea>
|
||
|
||
<!-- Requested By -->
|
||
<label>Requested By</label>
|
||
<select bind:value={addPOForm.requested_by} class="w-full border p-2">
|
||
<option value="" disabled>Select Employee</option>
|
||
{#each employeeOptions as e}
|
||
<option value={e.id}>{e.employee_name}</option>
|
||
{/each}
|
||
</select>
|
||
|
||
<!-- Requested Date -->
|
||
<label>Requested Date</label>
|
||
<input type="date" bind:value={addPOForm.requested_date} class="w-full border p-2"/>
|
||
|
||
<!-- Actions -->
|
||
<div class="flex justify-end space-x-2 pt-4">
|
||
<button on:click={saveAddPO} class="bg-blue-600 text-white px-4 py-2 rounded">Save</button>
|
||
<button on:click={() => showAddPOModal = false} class="px-4 py-2 rounded border">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/if} |