project finish

This commit is contained in:
2025-06-30 01:26:02 +08:00
parent 119f53f0e8
commit e7f2ae73be

View File

@@ -31,6 +31,7 @@
need_approval: boolean;
area_of_villa: string;
input_by: string;
project_name: string;
issue_number: string;
issue_id: string;
report_date: string;
@@ -49,6 +50,7 @@
const columns: columns[] = [
{ key: "description_of_the_issue", title: "Issue Description" },
{ key: "project_name", title: "Project Name" },
{ key: "project_number", title: "Project Number" },
{ key: "priority", title: "Priority" },
{ key: "add_to_po", title: "Add to PO" },
@@ -67,6 +69,41 @@
let selectedFile: File | null = null;
let imagePreviewUrl: string | null = null;
let showPoModal = false;
let newPurchaseOrder: Record<string, any> = {};
let poItems: any[] = [];
let addToPoInProgress: Set<string> = new Set();
let employees: any[] = [];
let villas: any[] = [];
async function fetchVillas() {
const { data, error } = await supabase
.from("vb_villas")
.select("villa_name")
.eq("villa_status", "Active");
if (error) console.error(error);
else villas = data;
}
async function fetchActiveEmployees() {
const { data, error } = await supabase
.from("vb_employee")
.select("id, employee_name, employee_status")
.eq("employee_status", "Active");
if (error) {
console.error("Error fetching employees:", error);
} else {
employees = data;
}
}
async function fetchPoItems() {
const { data, error } = await supabase.from("vb_po_item").select("*");
if (error) console.error("Error fetching PO items", error);
else poItems = data;
}
function handleFileChange(event: Event) {
const input = event.target as HTMLInputElement;
@@ -103,11 +140,7 @@
.range(offset, offset + limit - 1);
// Apply filter if provided
if (filter) {
query = query.eq("priority", filter);
}
// Apply search term if provided
if (searchTerm) {
query = query.ilike("description_of_the_issue", `%${searchTerm}%`);
query = query.eq("villa_name", filter);
}
// Fetch projects
@@ -149,13 +182,34 @@
area_of_villa: issue ? issue.area_of_villa : "Unknown",
input_by: project.input_by,
issue_number: issue ? issue.issue_number : "Unknown",
villa_name: issue ? project.villa_data : "Unknown",
report_date: issue ? issue.reported_date : "Unknown",
project_due_date: project.project_due_date,
};
});
const projectIds = allRows.map(row => row.id);
// hanya valid uuid
const { data: existingPOs, error: poError } = await supabase
.from("vb_purchase_orders")
.select("project_id")
.in("project_id", projectIds);
if (poError) {
console.error("Error fetching purchase orders:", poError);
}
const poProjectIds = new Set(existingPOs?.map(po => po.project_id));
// Add purchase_order_exists flag
allRows = allRows.map(row => ({
...row,
purchase_order_exists: poProjectIds.has(row.id)
}));
if (searchTerm) {
allRows = allRows.filter(row =>
row.description_of_the_issue?.toLowerCase().includes(searchTerm) ||
row.project_name?.toLowerCase().includes(searchTerm)
);
}
if (issueError) {
console.error("Error fetching available issues:", issueError);
@@ -165,6 +219,9 @@
onMount(() => {
fetchProjects();
fetchPoItems();
fetchActiveEmployees();
fetchVillas();
});
$: currentPage = 1; // Reset to first page when allRows changes
@@ -187,6 +244,7 @@
"villa_name",
"report_date",
"actions",
"project_name",
"add_to_po",
"issue_number",
"updated_at",
@@ -327,7 +385,37 @@
const { data } = supabase.storage.from("villabugis").getPublicUrl(path);
return data.publicUrl;
}
async function openPoModal(row) {
const { data, error } = await supabase
.from("vb_purchase_orders")
.select("id")
.eq("project_id", row.id)
.maybeSingle();
if (data) {
alert("Purchase Order for this project already exists.");
return;
}
newPurchaseOrder = {
po_type: "project",
po_status: "requested",
project_id: row.id,
issue_id: row.issue_id,
villa_id: row.villa_id || "unknown",
po_item: "",
po_quantity: 1,
requested_by: ""
};
if (!row.villa_id) {
// Optional: fetch villa_id from vb_issues if missing
const { data, error } = await supabase.from("vb_issues").select("villa_id").eq("id", row.issue_id).single();
if (data) newPurchaseOrder.villa_id = data.villa_id;
}
showPoModal = true;
addToPoInProgress.add(row.id);
}
async function saveProject(event: Event) {
//get session user
const session = await supabase.auth.getSession();
@@ -423,6 +511,11 @@
}
async function deleteProject(id: string) {
const confirmed = confirm("Are you sure you want to delete this project? This action cannot be undone.");
if (!confirmed) {
return;
}
const { error } = await supabase
.from("vb_projects")
.delete()
@@ -430,9 +523,61 @@
if (error) {
console.error("Error deleting project:", error);
alert("Failed to delete project.");
return;
}
alert("Project deleted successfully.");
await fetchProjects();
}
async function savePurchaseOrder() {
const { error } = await supabase.from("vb_purchase_orders").insert({
po_type: newPurchaseOrder.po_type,
po_status: newPurchaseOrder.po_status,
project_id: newPurchaseOrder.project_id,
issue_id: newPurchaseOrder.issue_id,
villa_id: newPurchaseOrder.villa_id,
po_item: newPurchaseOrder.po_item,
po_quantity: newPurchaseOrder.po_quantity,
requested_by: newPurchaseOrder.requested_by
});
if (error) {
console.error("Error saving purchase order:", error);
alert("Failed to save purchase order");
return;
}
try {
const webhookResponse = await fetch(
"https://flow.catalis.app/webhook-test/vb_project_purchase",
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
po_type: newPurchaseOrder.po_type,
po_status: newPurchaseOrder.po_status,
project_id: newPurchaseOrder.project_id,
issue_id: newPurchaseOrder.issue_id,
villa_id: newPurchaseOrder.villa_id,
po_item: newPurchaseOrder.po_item,
po_quantity: newPurchaseOrder.po_quantity,
requested_by: newPurchaseOrder.requested_by
})
}
);
if (!webhookResponse.ok) {
console.error("Webhook failed:", webhookResponse.statusText);
}
} catch (webhookError) {
console.error("Webhook error:", webhookError);
}
alert("Purchase order added");
showPoModal = false;
addToPoInProgress.delete(newPurchaseOrder.project_id); // clear state
await fetchProjects();
}
</script>
@@ -450,7 +595,7 @@
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
<input
type="text"
placeholder="🔍 Search by name..."
placeholder="🔍 Search by Issue..."
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 = (
@@ -466,11 +611,10 @@
fetchProjects(filter, null, null, "desc");
}}
>
<option value="">Filter by Priority</option>
<option value="High">Critical</option>
<option value="High">High</option>
<option value="Medium">Medium</option>
<option value="Low">Low</option>
<option value="">All Villas</option>
{#each villas as villa}
<option value={villa.villa_name}>{villa.villa_name}</option>
{/each}
</select>
<button
class="bg-gray-200 text-gray-700 px-4 py-2 rounded-xl hover:bg-gray-300 text-sm transition"
@@ -516,65 +660,40 @@
</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>
<td class="px-4 py-2">
<button
class="inline-flex items-center gap-1 rounded px-3 py-1.5 text-xs font-medium
{row.purchase_order_exists ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-blue-600 text-white hover:bg-blue-700'}"
on:click={() => openModal(row)}
disabled={row.purchase_order_exists}
>
✏️ Edit
</button>
<button
class="inline-flex items-center gap-1 rounded px-3 py-1.5 text-xs font-medium
{row.purchase_order_exists ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-red-600 text-white hover:bg-red-700'}"
on:click={() => deleteProject(row.id)}
disabled={row.purchase_order_exists}
>
🗑️ Delete
</button>
</td>
{:else if col.key === "add_to_po"}
<td class="px-4 py-2 text-center">
<input
type="checkbox"
checked={row.add_to_po}
on:change={async (e) => {
const isChecked = (
e.target as HTMLInputElement
).checked;
row.add_to_po = isChecked;
if (isChecked) {
// map to project
const project: Project = {
id: row.id,
issue_id: row.issue_id,
project_number:
row.issue_number,
add_to_po: isChecked,
input_by: row.input_by,
project_due_date:
row.project_due_date,
picture_link:
row.picture_link,
};
currentEditingId = row.id;
await addToPo(project);
} else {
// uncheck
const { data, error } =
await supabase
.from("vb_projects")
.update({
add_to_po: false,
})
.eq("id", row.id);
if (error) {
console.error(
"Error updating project:",
error,
);
}
}
}}
/>
</td>
<td class="px-4 py-2 text-center">
{#if row.purchase_order_exists}
<button class="bg-gray-300 text-gray-500 px-3 py-1.5 rounded cursor-not-allowed" disabled>
PO Created
</button>
{:else}
<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-300"
on:click={() => openPoModal(row)}
disabled={addToPoInProgress.has(row.id)}
>
Add to PO
</button>
{/if}
</td>
{:else if col.key === "need_approval"}
<td class="px-4 py-2">
{#if row[col.key as keyof Projects]}
@@ -814,3 +933,45 @@
</div>
</div>
{/if}
{#if showPoModal}
<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-[400px]">
<h3 class="text-lg font-semibold mb-4">Add to Purchase Order</h3>
<form on:submit|preventDefault={savePurchaseOrder}>
<!-- Hidden fields -->
<input type="hidden" name="po_type" value={newPurchaseOrder.po_type} />
<input type="hidden" name="po_status" value={newPurchaseOrder.po_status} />
<input type="hidden" name="issue_id" value={newPurchaseOrder.issue_id} />
<input type="hidden" name="villa_id" value={newPurchaseOrder.villa_id} />
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Item</label>
<select bind:value={newPurchaseOrder.po_item} required class="w-full border px-2 py-2 rounded">
<option value="">Select Item</option>
{#each poItems as item}
<option value={item.item_name}>{item.item_name}</option>
{/each}
</select>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Quantity</label>
<input type="number" bind:value={newPurchaseOrder.po_quantity} min="1" class="w-full border px-2 py-2 rounded" required/>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Requested By</label>
<select bind:value={newPurchaseOrder.requested_by} required class="w-full border px-2 py-2 rounded">
<option value="">Select Employee</option>
{#each employees as emp}
<option value={emp.id}>{emp.employee_name}</option>
{/each}
</select>
</div>
<div class="flex justify-end space-x-2">
<button type="button" on:click={() => showPoModal = false} class="px-4 py-2 bg-gray-200 rounded">Cancel</button>
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded">Submit</button>
</div>
</form>
</div>
</div>
{/if}