project finish
This commit is contained in:
@@ -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}
|
||||
Reference in New Issue
Block a user