346 lines
11 KiB
Svelte
346 lines
11 KiB
Svelte
<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}
|