project finish
This commit is contained in:
@@ -31,6 +31,7 @@
|
|||||||
need_approval: boolean;
|
need_approval: boolean;
|
||||||
area_of_villa: string;
|
area_of_villa: string;
|
||||||
input_by: string;
|
input_by: string;
|
||||||
|
project_name: string;
|
||||||
issue_number: string;
|
issue_number: string;
|
||||||
issue_id: string;
|
issue_id: string;
|
||||||
report_date: string;
|
report_date: string;
|
||||||
@@ -49,6 +50,7 @@
|
|||||||
|
|
||||||
const columns: columns[] = [
|
const columns: columns[] = [
|
||||||
{ key: "description_of_the_issue", title: "Issue Description" },
|
{ key: "description_of_the_issue", title: "Issue Description" },
|
||||||
|
{ key: "project_name", title: "Project Name" },
|
||||||
{ key: "project_number", title: "Project Number" },
|
{ key: "project_number", title: "Project Number" },
|
||||||
{ key: "priority", title: "Priority" },
|
{ key: "priority", title: "Priority" },
|
||||||
{ key: "add_to_po", title: "Add to PO" },
|
{ key: "add_to_po", title: "Add to PO" },
|
||||||
@@ -67,6 +69,41 @@
|
|||||||
|
|
||||||
let selectedFile: File | null = null;
|
let selectedFile: File | null = null;
|
||||||
let imagePreviewUrl: string | 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) {
|
function handleFileChange(event: Event) {
|
||||||
const input = event.target as HTMLInputElement;
|
const input = event.target as HTMLInputElement;
|
||||||
@@ -103,11 +140,7 @@
|
|||||||
.range(offset, offset + limit - 1);
|
.range(offset, offset + limit - 1);
|
||||||
// Apply filter if provided
|
// Apply filter if provided
|
||||||
if (filter) {
|
if (filter) {
|
||||||
query = query.eq("priority", filter);
|
query = query.eq("villa_name", filter);
|
||||||
}
|
|
||||||
// Apply search term if provided
|
|
||||||
if (searchTerm) {
|
|
||||||
query = query.ilike("description_of_the_issue", `%${searchTerm}%`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch projects
|
// Fetch projects
|
||||||
@@ -149,13 +182,34 @@
|
|||||||
area_of_villa: issue ? issue.area_of_villa : "Unknown",
|
area_of_villa: issue ? issue.area_of_villa : "Unknown",
|
||||||
input_by: project.input_by,
|
input_by: project.input_by,
|
||||||
issue_number: issue ? issue.issue_number : "Unknown",
|
issue_number: issue ? issue.issue_number : "Unknown",
|
||||||
villa_name: issue ? project.villa_data : "Unknown",
|
|
||||||
report_date: issue ? issue.reported_date : "Unknown",
|
report_date: issue ? issue.reported_date : "Unknown",
|
||||||
project_due_date: project.project_due_date,
|
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) {
|
if (issueError) {
|
||||||
console.error("Error fetching available issues:", issueError);
|
console.error("Error fetching available issues:", issueError);
|
||||||
@@ -165,6 +219,9 @@
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
fetchProjects();
|
fetchProjects();
|
||||||
|
fetchPoItems();
|
||||||
|
fetchActiveEmployees();
|
||||||
|
fetchVillas();
|
||||||
});
|
});
|
||||||
|
|
||||||
$: currentPage = 1; // Reset to first page when allRows changes
|
$: currentPage = 1; // Reset to first page when allRows changes
|
||||||
@@ -187,6 +244,7 @@
|
|||||||
"villa_name",
|
"villa_name",
|
||||||
"report_date",
|
"report_date",
|
||||||
"actions",
|
"actions",
|
||||||
|
"project_name",
|
||||||
"add_to_po",
|
"add_to_po",
|
||||||
"issue_number",
|
"issue_number",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
@@ -327,7 +385,37 @@
|
|||||||
const { data } = supabase.storage.from("villabugis").getPublicUrl(path);
|
const { data } = supabase.storage.from("villabugis").getPublicUrl(path);
|
||||||
return data.publicUrl;
|
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) {
|
async function saveProject(event: Event) {
|
||||||
//get session user
|
//get session user
|
||||||
const session = await supabase.auth.getSession();
|
const session = await supabase.auth.getSession();
|
||||||
@@ -423,6 +511,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteProject(id: string) {
|
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
|
const { error } = await supabase
|
||||||
.from("vb_projects")
|
.from("vb_projects")
|
||||||
.delete()
|
.delete()
|
||||||
@@ -430,9 +523,61 @@
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error("Error deleting project:", error);
|
console.error("Error deleting project:", error);
|
||||||
|
alert("Failed to delete project.");
|
||||||
return;
|
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();
|
await fetchProjects();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -450,7 +595,7 @@
|
|||||||
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
|
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
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"
|
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) => {
|
on:input={(e) => {
|
||||||
const searchTerm = (
|
const searchTerm = (
|
||||||
@@ -466,11 +611,10 @@
|
|||||||
fetchProjects(filter, null, null, "desc");
|
fetchProjects(filter, null, null, "desc");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">Filter by Priority</option>
|
<option value="">All Villas</option>
|
||||||
<option value="High">Critical</option>
|
{#each villas as villa}
|
||||||
<option value="High">High</option>
|
<option value={villa.villa_name}>{villa.villa_name}</option>
|
||||||
<option value="Medium">Medium</option>
|
{/each}
|
||||||
<option value="Low">Low</option>
|
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
class="bg-gray-200 text-gray-700 px-4 py-2 rounded-xl hover:bg-gray-300 text-sm transition"
|
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>
|
</td>
|
||||||
{:else if col.key === "actions"}
|
{:else if col.key === "actions"}
|
||||||
<td class="px-4 py-2">
|
<td class="px-4 py-2">
|
||||||
<button
|
<td class="px-4 py-2">
|
||||||
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"
|
<button
|
||||||
on:click={() => openModal(row)}
|
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'}"
|
||||||
✏️ Edit
|
on:click={() => openModal(row)}
|
||||||
</button>
|
disabled={row.purchase_order_exists}
|
||||||
<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"
|
✏️ Edit
|
||||||
on:click={() => deleteProject(row.id)}
|
</button>
|
||||||
>
|
<button
|
||||||
🗑️ Delete
|
class="inline-flex items-center gap-1 rounded px-3 py-1.5 text-xs font-medium
|
||||||
</button>
|
{row.purchase_order_exists ? 'bg-gray-300 text-gray-500 cursor-not-allowed' : 'bg-red-600 text-white hover:bg-red-700'}"
|
||||||
</td>
|
on:click={() => deleteProject(row.id)}
|
||||||
|
disabled={row.purchase_order_exists}
|
||||||
|
>
|
||||||
|
🗑️ Delete
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
{:else if col.key === "add_to_po"}
|
{:else if col.key === "add_to_po"}
|
||||||
<td class="px-4 py-2 text-center">
|
<td class="px-4 py-2 text-center">
|
||||||
<input
|
{#if row.purchase_order_exists}
|
||||||
type="checkbox"
|
<button class="bg-gray-300 text-gray-500 px-3 py-1.5 rounded cursor-not-allowed" disabled>
|
||||||
checked={row.add_to_po}
|
PO Created
|
||||||
on:change={async (e) => {
|
</button>
|
||||||
const isChecked = (
|
{:else}
|
||||||
e.target as HTMLInputElement
|
<button
|
||||||
).checked;
|
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"
|
||||||
row.add_to_po = isChecked;
|
on:click={() => openPoModal(row)}
|
||||||
|
disabled={addToPoInProgress.has(row.id)}
|
||||||
if (isChecked) {
|
>
|
||||||
// map to project
|
➕ Add to PO
|
||||||
const project: Project = {
|
</button>
|
||||||
id: row.id,
|
{/if}
|
||||||
issue_id: row.issue_id,
|
</td>
|
||||||
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>
|
|
||||||
{:else if col.key === "need_approval"}
|
{:else if col.key === "need_approval"}
|
||||||
<td class="px-4 py-2">
|
<td class="px-4 py-2">
|
||||||
{#if row[col.key as keyof Projects]}
|
{#if row[col.key as keyof Projects]}
|
||||||
@@ -814,3 +933,45 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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