Files
vberp/src/routes/backoffice/vendor/+page.svelte
aji@catalis.app 2ad0f5093d perbaikan data
2025-06-22 12:26:43 +07:00

346 lines
11 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
import { supabase } from "$lib/supabaseClient";
import { onMount } from "svelte";
type Vendor = {
id: string;
name: string;
vendor_type: string;
vendor_address: string;
vendor_status: string;
vendor_subtype: string;
vendor_comment: string;
vendor_unique: string;
created_by: string;
created_at: string;
updated_at: string;
contact_name_primary: string;
contact_pos_primary: string;
contact_email_primary: string;
contact_phone_primary: string;
contact_mobile_primary: string;
contact_name_second: string;
contact_pos_second: string;
contact_email_second: string;
contact_name_tertiary: string;
contact_pos_tertiary: string;
contact_email_tertiary: string;
website: string;
};
const columns = [
{ key: "name", title: "Name" },
{ key: "vendor_type", title: "Vendor Type" },
{ key: "vendor_status", title: "Status" },
{ key: "vendor_subtype", title: "Subtype" },
{ key: "vendor_address", title: "Address" },
{ key: "vendor_unique", title: "Unique ID" },
{ key: "contact_name_primary", title: "Primary Contact Name" },
{ key: "contact_pos_primary", title: "Primary Contact Position" },
{ key: "contact_email_primary", title: "Primary Email" },
{ key: "contact_phone_primary", title: "Primary Phone" },
{ key: "contact_mobile_primary", title: "Primary Mobile" },
{ key: "contact_name_second", title: "Secondary Contact Name" },
{ key: "contact_pos_second", title: "Secondary Contact Position" },
{ key: "contact_email_second", title: "Secondary Email" },
{ key: "contact_name_tertiary", title: "Tertiary Contact Name" },
{ key: "contact_pos_tertiary", title: "Tertiary Contact Position" },
{ key: "contact_email_tertiary", title: "Tertiary Email" },
{ key: "website", title: "Website" },
];
const excludedKeys = ["id", "created_by", "created_at", "updated_at"];
$: formColumns = columns.filter((col) => !excludedKeys.includes(col.key));
let allRowsVendor: Vendor[] = [];
let currentPage = 1;
let offset = 0;
let itemsPerPage = 10;
let totalItems = 0;
let newVendor: Record<string, any> = {};
let showModal = false;
let isEditing = false;
let currentEditingId: string | null = null;
let searchTerm = "";
$: offset = (currentPage - 1) * itemsPerPage;
$: totalPages = Math.ceil(totalItems / itemsPerPage);
async function fetchVendor(search = "", resetPage = false) {
if (resetPage) currentPage = 1;
const { data, error, count } = await supabase
.from("vb_vendor")
.select("*", { count: "exact" })
.order("created_at", { ascending: false })
.range(offset, offset + itemsPerPage - 1)
.ilike("name", `%${search}%`);
if (error) {
console.error("Fetch error:", error);
return;
}
allRowsVendor = data || [];
totalItems = count || 0;
}
function resetPagination() {
currentPage = 1;
offset = 0;
totalItems = 0;
fetchVendor(searchTerm);
showModal = false;
isEditing = false;
currentEditingId = null;
newVendor = {};
}
function nextPage() {
if (currentPage < totalPages) {
currentPage += 1;
fetchVendor(searchTerm);
}
}
function previousPage() {
if (currentPage > 1) {
currentPage -= 1;
fetchVendor(searchTerm);
}
}
function changePage(page: number) {
if (page < 1 || page > totalPages || page === currentPage) return;
currentPage = page;
fetchVendor(searchTerm);
}
async function openModal(vendor?: Vendor) {
showModal = true;
isEditing = !!vendor;
currentEditingId = vendor ? vendor.id : null;
newVendor = {};
for (const col of formColumns) {
newVendor[col.key] = vendor ? vendor[col.key as keyof Vendor] : "";
}
if (!vendor) {
const { data } = await supabase
.from("vb_vendor")
.select("vendor_unique")
.like("vendor_unique", "VEN-%")
.order("vendor_unique", { ascending: false })
.limit(1);
let nextNumber = 5000;
if (data && data.length) {
const last = data[0].vendor_unique;
const num = parseInt(last.replace("VEN-", ""));
if (!isNaN(num)) nextNumber = num + 1;
}
newVendor.vendor_unique = `VEN-${String(nextNumber).padStart(6, "0")}`;
}
}
async function addVendor() {
const { error } = await supabase.from("vb_vendor").insert([newVendor]);
if (error) {
console.error("Add error:", error);
} else {
showModal = false;
resetPagination();
}
}
async function updateVendor() {
const { error } = await supabase
.from("vb_vendor")
.update(newVendor)
.eq("id", currentEditingId);
if (error) {
console.error("Update error:", error);
} else {
showModal = false;
resetPagination();
}
}
async function deleteVendor(id: string) {
const { error } = await supabase
.from("vb_vendor")
.delete()
.eq("id", id);
if (error) {
console.error("Delete error:", error);
} else {
resetPagination();
}
}
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;
}
onMount(() => {
fetchVendor();
});
</script>
<!-- Search + Add -->
<div class="flex justify-between items-center mb-4">
<input
type="text"
class="border px-4 py-2 rounded w-64"
placeholder="🔍 Search vendor..."
on:input={(e) => {
searchTerm = (e.target as HTMLInputElement).value;
fetchVendor(searchTerm, true);
}}
/>
<button
class="bg-blue-600 text-white px-4 py-2 rounded"
on:click={() => openModal()}
>
Add Vendor
</button>
</div>
<!-- Table -->
<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}
<th class="p-2 text-left border">{col.title}</th>
{/each}
<th class="p-2 border">Actions</th>
</tr>
</thead>
<tbody>
{#each allRowsVendor as row}
<tr class="hover:bg-gray-50">
{#each columns as col}
<td class="p-2 border"
>{row[col.key as keyof typeof row]}</td
>
{/each}
<td class="p-2 border">
<button
class="text-blue-600"
on:click={() => openModal(row)}>✏️</button
>
<button
class="text-red-600 ml-2"
on:click={() => deleteVendor(row.id)}>🗑️</button
>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="flex justify-between items-center mt-4 text-sm">
<div>
Page {currentPage} of {totalPages} | Showing
{currentPage === totalPages && totalItems > 0
? totalItems - offset
: itemsPerPage} items | Showing
{offset + 1} to {Math.min(currentPage * itemsPerPage, totalItems)} of {totalItems}
</div>
<div class="flex space-x-1">
<button
on:click={previousPage}
disabled={currentPage <= 1}
class="px-2 py-1 border rounded disabled:opacity-50">Prev</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
on:click={nextPage}
disabled={currentPage >= totalPages}
class="px-2 py-1 border rounded disabled:opacity-50">Next</button
>
</div>
</div>
<!-- Modal -->
{#if showModal}
<div
class="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50"
>
<div
class="bg-white p-6 rounded w-[500px] max-h-[90vh] overflow-y-auto"
>
<h2 class="text-lg font-semibold mb-4">
{isEditing ? "Edit Vendor" : "Add Vendor"}
</h2>
{#each formColumns as col}
<div class="mb-3">
<label class="text-sm">{col.title}</label>
{#if col.key === "vendor_unique"}
<input
class="w-full p-2 border rounded bg-gray-100"
bind:value={newVendor[col.key]}
readonly
/>
{:else}
<input
class="w-full p-2 border rounded"
bind:value={newVendor[col.key]}
/>
{/if}
</div>
{/each}
<div class="flex justify-end mt-4 space-x-2">
<button
class="px-4 py-2 bg-gray-200 rounded"
on:click={() => (showModal = false)}>Cancel</button
>
<button
class="px-4 py-2 bg-blue-600 text-white rounded"
on:click={isEditing ? updateVendor : addVendor}>Save</button
>
</div>
</div>
</div>
{/if}