penambahan child po item

This commit is contained in:
Aji Setiaji
2025-07-08 17:36:04 +14:00
parent 0836d9457e
commit 34a72c7f71
4 changed files with 1027 additions and 2 deletions

View File

@@ -85,6 +85,14 @@
icon: "📦",
url: "/backoffice/purchaseorder",
roles: ["it", "guest", "accounting", "ga", "office", "hm", "vm"],
sub: [
{
name: "PO Item",
icon: "📋",
url: "/backoffice/purchaseorder/poitem",
roles: ["it", "ga", "office", "hm", "vm", "accounting"],
},
],
},
{
name: "Timesheets",

View File

@@ -1270,7 +1270,7 @@
</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"
class="px-2 py-1 border rounded disabled:opacity-50"
on:click={() => goToPage(currentPage - 1)}
disabled={currentPage === 1}
>
@@ -1290,7 +1290,7 @@
</button>
{/each}
<button
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
class="px-2 py-1 border rounded disabled:opacity-50"
on:click={() => goToPage(currentPage + 1)}
disabled={currentPage === totalPages}
>

View File

@@ -0,0 +1,450 @@
<script lang="ts">
import { supabase } from "$lib/supabaseClient";
import { onMount } from "svelte";
import { writable } from "svelte/store";
type POItem = {
id: number;
item_name: string;
created_at?: Date;
};
type TransportDisplay = {
id: number;
item_name: string;
created_at?: Date;
};
let allRows: POItem[] = [];
type columns = {
key: string;
title: string;
};
const columns: columns[] = [
{ key: "id", title: "ID" },
{ key: "item_name", title: "Item Name" },
{ key: "created_at", title: "Created At" },
{ key: "actions", title: "Actions" },
];
let currentPage = 1;
const rowsPerPage = 10;
let totalItems = 0;
async function fetchItemPo(
searchTerm: string | null = null,
sortColumn: string | null = "created_at",
sortOrder: "asc" | "desc" = "desc",
offset: number = 0,
limit: number = 10,
) {
const fromIndex = offset;
const toIndex = offset + limit - 1;
// Inisialisasi query
let query = supabase
.from("vb_po_item")
.select("*", { count: "exact" })
.order(sortColumn || "created_at", {
ascending: sortOrder === "asc",
})
.range(fromIndex, toIndex); // Ini sudah termasuk offset & limit
// Tambahkan filter pencarian jika ada
if (searchTerm) {
query = query.ilike("item_name", `%${searchTerm}%`);
}
// Jalankan query
const { data, count, error } = await query;
if (error) {
console.error("Error fetching PO items:", error);
return;
}
allRows = data as POItem[];
totalItems = count || 0;
console.log("Fetched PO Items:", allRows);
console.log("Total Items:", totalItems);
}
$: totalPages = Math.ceil(totalItems / rowsPerPage);
function goToPage(page: number) {
if (page < 1 || page > totalPages) return;
currentPage = page;
fetchItemPo(
null,
"created_at",
"desc",
(currentPage - 1) * rowsPerPage,
rowsPerPage,
);
}
function pageRange(totalPages: number, currentPage: number,): (number | string)[] {
const range: (number | string)[] = [];
const maxDisplay = 5;
if (totalPages <= maxDisplay + 2) {
for (let i = 1; i <= totalPages; i++) range.push(i);
} else {
const start = Math.max(2, currentPage - 2);
const end = Math.min(totalPages - 1, currentPage + 2);
range.push(1);
if (start > 2) range.push("...");
for (let i = start; i <= end; i++) {
range.push(i);
}
if (end < totalPages - 1) range.push("...");
range.push(totalPages);
}
return range;
}
function changePage(page: number) {
if (page < 1 || page > totalPages || page === currentPage) return;
currentPage = page;
fetchItemPo(
null,
"created_at",
"desc",
(currentPage - 1) * rowsPerPage,
rowsPerPage,
);
}
onMount(() => {
fetchItemPo();
});
// Initialize the first page
$: currentPage = 1;
let showModal = false;
let isEditing = false;
let currentEditingId: number | null = null;
let newPOItem: POItem = {
id: 0,
item_name: "",
created_at: new Date(),
};
const excludedKeys = ["id", "actions", "created_at"];
const formColumns = columns.filter(
(col) => !excludedKeys.includes(col.key),
);
function openModal(newPOItem?: POItem) {
if (newPOItem) {
isEditing = true;
currentEditingId = newPOItem.id;
newPOItem = { ...newPOItem };
} else {
isEditing = false;
currentEditingId = null;
newPOItem = {
id: 0,
item_name: "",
created_at: new Date(),
};
}
showModal = true;
}
async function savePOItem(event: Event) {
event.preventDefault();
const formData = new FormData(event.target as HTMLFormElement);
// Validate form data
if (!validateForm(formData)) {
console.error("Form validation failed");
return;
}
let newPOItemInsert = {
item_name: newPOItem.item_name,
created_at: new Date(),
};
if (isEditing && currentEditingId) {
const { error } = await supabase
.from("vb_po_item")
.update(newPOItemInsert)
.eq("id", currentEditingId);
if (error) {
alert("Error updating New PO Item: " + error.message);
console.error("Error updating New PO Item:", error);
return;
} else {
alert("New PO Item updated successfully!");
}
} else {
const { error } = await supabase
.from("vb_po_item")
.insert(newPOItemInsert);
if (error) {
alert("Error creating New PO Item: " + error.message);
console.error("Error creating New PO Item:", error);
return;
} else {
alert("New PO Item created successfully!");
}
}
await fetchItemPo();
showModal = false;
}
async function deletePOItem(id: number) {
if (confirm("Are you sure you want to delete this PO Item?")) {
const { error } = await supabase
.from("vb_po_item")
.delete()
.eq("id", id);
if (error) {
console.error("Error deleting issue:", error);
return;
}
await fetchItemPo();
}
}
export let formErrors = writable<{ [key: string]: string }>({});
function validateForm(formData: FormData): boolean {
const errors: { [key: string]: string } = {};
const requiredFields = [
"item_name",
// Add other required fields here if necessary
];
requiredFields.forEach((field) => {
if (!formData.get(field) || formData.get(field) === "") {
errors[field] = `${field.replace(/_/g, " ")} is required.`;
}
});
formErrors.set(errors);
return Object.keys(errors).length === 0;
}
function errorClass(field: string): string {
return $formErrors[field] ? "border-red-500" : "border";
}
</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>
PO Items List
</h2>
<p class="text-sm text-gray-600">
Manage your List Items for Purchase Order
</p>
</div>
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
<input
type="text"
placeholder="🔍 Search by item 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();
fetchItemPo(searchTerm, "created_at", "desc");
}}
/>
<button
class="bg-gray-200 text-gray-700 px-4 py-2 rounded-xl hover:bg-gray-300 text-sm transition"
on:click={() =>
fetchItemPo(null, "created_at", "desc", 0, 10)}
>
🔄 Reset
</button>
<button
class="bg-blue-600 text-white px-4 py-2 rounded-xl hover:bg-blue-700 text-sm transition"
on:click={() => openModal()}
>
New PO Item
</button>
</div>
</div>
<div class="overflow-x-auto rounded-lg shadow mb-4">
<table class="w-full divide-y divide-gray-200 text-sm">
<thead class="bg-gray-100">
<tr>
{#each columns as col}
{#if col.key === "guest_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 allRows as row}
<tr class="hover:bg-gray-50 transition">
{#each columns as col}
{#if col.key === "item_name"}
<td class="px-4 py-2 font-medium text-gray-800">
{row[col.key as keyof POItem]}
</td>
{:else if col.key === "created_at"}
<td class="px-4 py-2">
{row[col.key as keyof POItem]
? new Date(
row[
col.key as keyof POItem
] as string | number | Date,
).toLocaleDateString()
: "N/A"}
</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={() => deletePOItem(row.id)}
>
🗑️ Delete
</button>
</td>
{:else}
<td class="px-4 py-2">
{row[col.key as keyof TransportDisplay]}
</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, totalItems)} of {totalItems} items
</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 pageRange(totalPages, currentPage) as page}
{#if page === "..."}
<span class="px-2">...</span>
{:else}
<button
on:click={() => changePage(page as number)}
class="px-2 py-1 border rounded {page === currentPage
? 'bg-blue-600 text-white'
: ''}"
>
{page}
</button>
{/if}
{/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 bg-black bg-opacity-50 flex items-center justify-center z-50"
>
<div class="bg-white rounded-lg shadow-lg p-6 w-full max-w-md">
<h2 class="text-xl font-semibold mb-4">
{isEditing ? "Edit PO Item" : "New Item Request"}
</h2>
<form on:submit={savePOItem}>
{#each formColumns as col}
<div class="mb-4">
<label
class="block text-sm font-medium mb-1"
for={col.key}
>
{col.title}
</label>
<input
type="text"
id={col.key}
name={col.key}
bind:value={newPOItem[col.key as keyof POItem]}
class={`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${errorClass(
col.key,
)}`}
required
/>
{#if $formErrors[col.key]}
<p class="text-red-500 text-xs mt-1">
{$formErrors[col.key]}
</p>
{/if}
</div>
{/each}
<div class="flex justify-end space-x-2">
<button
type="button"
class="px-4 py-2 bg-gray-200 text-gray-700 rounded-xl hover:bg-gray-300"
on:click={() => (showModal = false)}
>
Cancel
</button>
<button
type="submit"
class="px-4 py-2 bg-blue-600 text-white rounded-xl hover:bg-blue-700"
>
{isEditing ? "Update" : "Create"}
</button>
</div>
</form>
</div>
</div>
{/if}