598 lines
21 KiB
Svelte
598 lines
21 KiB
Svelte
<script lang="ts">
|
||
import { supabase } from "$lib/supabaseClient";
|
||
import { onMount } from "svelte";
|
||
|
||
type Vendor = {
|
||
id: string;
|
||
name: string;
|
||
contact_type: string;
|
||
vendor_status: string;
|
||
vendor_subtype: string;
|
||
address: string;
|
||
contact_comment: string;
|
||
vendor_unik: string;
|
||
created_by: string;
|
||
created_at: string;
|
||
updated_at: string;
|
||
};
|
||
|
||
let allRowsVendor: Vendor[] = [];
|
||
let allRowsContactVendor: ContactVendor[] = [];
|
||
|
||
type columns = {
|
||
key: string;
|
||
title: string;
|
||
};
|
||
|
||
let columns: columns[] = [
|
||
{ key: "name", title: "Name" },
|
||
{ key: "vendor_type", title: "Vendor Type" },
|
||
{ key: "vendor_status", title: "Vendor Status" },
|
||
{ key: "vendor_subtype", title: "Vendor Subtype" },
|
||
{ key: "address", title: "Address" },
|
||
{ key: "vendor_unik", title: "Unique Vendor ID" },
|
||
{ key: "created_by", title: "Created By" },
|
||
{ key: "created_at", title: "Created At" },
|
||
];
|
||
|
||
onMount(async () => {
|
||
const { data: vendorData, error: vendorError } = await supabase
|
||
.from("vendor")
|
||
.select("*")
|
||
.order("created_at", { ascending: false });
|
||
|
||
if (vendorError) {
|
||
console.error("Error fetching vendors:", vendorError);
|
||
} else {
|
||
allRowsVendor = vendorData as Vendor[];
|
||
}
|
||
});
|
||
|
||
let currentPage = 1;
|
||
let itemsPerPage = 10;
|
||
$: totalPages = Math.ceil(allRowsVendor.length / itemsPerPage);
|
||
$: paginatedRows = allRowsVendor.slice(
|
||
(currentPage - 1) * itemsPerPage,
|
||
currentPage * itemsPerPage,
|
||
);
|
||
|
||
function nextPage() {
|
||
if (currentPage < totalPages) {
|
||
currentPage += 1;
|
||
}
|
||
}
|
||
|
||
function previousPage() {
|
||
if (currentPage > 1) {
|
||
currentPage -= 1;
|
||
}
|
||
}
|
||
|
||
function resetPagination() {
|
||
currentPage = 1;
|
||
}
|
||
|
||
let showModal = false;
|
||
let isEditing = false;
|
||
let currentEditingId: string | null = null;
|
||
let newVendor: Record<string, any> = {};
|
||
const excludedKeys = ["id", "created_by", "created_at", "updated_at"];
|
||
$: formColumns = columns.filter((col) => !excludedKeys.includes(col.key));
|
||
|
||
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] : "";
|
||
}
|
||
}
|
||
|
||
async function addVendor() {
|
||
const { data, error } = await supabase
|
||
.from("vendor")
|
||
.insert([newVendor])
|
||
.select();
|
||
|
||
if (error) {
|
||
console.error("Error adding vendor:", error);
|
||
} else {
|
||
allRowsVendor.push(data[0]);
|
||
resetPagination();
|
||
showModal = false;
|
||
}
|
||
}
|
||
|
||
async function updateVendor() {
|
||
const { data, error } = await supabase
|
||
.from("vendor")
|
||
.update(newVendor)
|
||
.eq("id", currentEditingId)
|
||
.select();
|
||
|
||
if (error) {
|
||
console.error("Error updating vendor:", error);
|
||
} else {
|
||
const index = allRowsVendor.findIndex(
|
||
(v) => v.id === currentEditingId,
|
||
);
|
||
if (index !== -1) {
|
||
allRowsVendor[index] = data[0];
|
||
resetPagination();
|
||
showModal = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
async function deleteVendor(vendorId: string) {
|
||
const { error } = await supabase
|
||
.from("vendor")
|
||
.delete()
|
||
.eq("id", vendorId);
|
||
|
||
if (error) {
|
||
console.error("Error deleting vendor:", error);
|
||
} else {
|
||
allRowsVendor = allRowsVendor.filter((v) => v.id !== vendorId);
|
||
resetPagination();
|
||
}
|
||
}
|
||
|
||
type ContactVendor = {
|
||
id: string;
|
||
contact_name: string;
|
||
contact_type: string;
|
||
contact_status: string;
|
||
contact_position: string;
|
||
contact_email: string;
|
||
contact_phone: string;
|
||
contact_phone_mobile: string;
|
||
urutan: number;
|
||
contact_address: string;
|
||
contact_comment: string;
|
||
vendor_id: string;
|
||
created_by: string;
|
||
created_at: string;
|
||
updated_at: string;
|
||
};
|
||
|
||
let columnsContact: columns[] = [
|
||
{ key: "contact_name", title: "Contact Name" },
|
||
{ key: "contact_type", title: "Contact Type" },
|
||
{ key: "contact_status", title: "Contact Status" },
|
||
{ key: "contact_position", title: "Position" },
|
||
{ key: "contact_email", title: "Email" },
|
||
{ key: "contact_phone", title: "Phone" },
|
||
{ key: "contact_phone_mobile", title: "Mobile" },
|
||
{ key: "urutan", title: "Order" },
|
||
];
|
||
|
||
async function fetchContactVendor(vendorId: string) {
|
||
const { data: contactData, error: contactError } = await supabase
|
||
.from("contact_vendor")
|
||
.select("*")
|
||
.eq("vendor_id", vendorId)
|
||
.order("urutan", { ascending: true });
|
||
|
||
if (contactError) {
|
||
console.error("Error fetching contact vendors:", contactError);
|
||
} else {
|
||
allRowsContactVendor = contactData as ContactVendor[];
|
||
}
|
||
}
|
||
|
||
function handleVendorClick(vendorId: string) {
|
||
fetchContactVendor(vendorId);
|
||
}
|
||
|
||
let showModalContact = false;
|
||
let showModalAddEditContact = false;
|
||
let selectedVendorId: string | null = null;
|
||
let isEditingContact = false;
|
||
let newVendorContact: Record<string, any> = {};
|
||
let excludedKeysContact = [
|
||
"id",
|
||
"created_by",
|
||
"created_at",
|
||
"updated_at",
|
||
"vendor_id",
|
||
];
|
||
$: formColumnsContact = columnsContact.filter(
|
||
(col) => !excludedKeysContact.includes(col.key),
|
||
);
|
||
|
||
function openContactModal(vendorId: string) {
|
||
selectedVendorId = vendorId;
|
||
showModalContact = true;
|
||
showModalAddEditContact = true;
|
||
}
|
||
|
||
function closeContactModal() {
|
||
showModalContact = false;
|
||
showModalAddEditContact = false;
|
||
selectedVendorId = null;
|
||
}
|
||
|
||
function openModalAddContact() {
|
||
showModalAddEditContact = true;
|
||
showModalContact = false;
|
||
}
|
||
|
||
async function addContactVendor(contact: ContactVendor) {
|
||
(contact.vendor_id as string) == selectedVendorId;
|
||
const { data, error } = await supabase
|
||
.from("contact_vendor")
|
||
.insert([contact])
|
||
.select();
|
||
|
||
if (error) {
|
||
console.error("Error adding contact vendor:", error);
|
||
} else {
|
||
allRowsContactVendor.push(data[0]);
|
||
closeContactModal();
|
||
}
|
||
}
|
||
|
||
async function updateContactVendor(contact: ContactVendor) {
|
||
const { data, error } = await supabase
|
||
.from("contact_vendor")
|
||
.update(contact)
|
||
.eq("id", contact.id)
|
||
.select();
|
||
|
||
if (error) {
|
||
console.error("Error updating contact vendor:", error);
|
||
} else {
|
||
const index = allRowsContactVendor.findIndex(
|
||
(c) => c.id === contact.id,
|
||
);
|
||
if (index !== -1) {
|
||
allRowsContactVendor[index] = data[0];
|
||
closeContactModal();
|
||
}
|
||
}
|
||
}
|
||
|
||
async function deleteContactVendor(contactId: string) {
|
||
const { error } = await supabase
|
||
.from("contact_vendor")
|
||
.delete()
|
||
.eq("id", contactId);
|
||
|
||
if (error) {
|
||
console.error("Error deleting contact vendor:", error);
|
||
} else {
|
||
allRowsContactVendor = allRowsContactVendor.filter(
|
||
(c) => c.id !== contactId,
|
||
);
|
||
closeContactModal();
|
||
}
|
||
}
|
||
|
||
// Initialize the modal state
|
||
$: showModal = false;
|
||
$: showModalContact = false;
|
||
</script>
|
||
|
||
<!-- Table untuk daftar Vendor -->
|
||
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
|
||
<div>
|
||
<h2 class="text-lg font-semibold text-gray-800">Vendor List</h2>
|
||
<p class="text-sm text-gray-600">Manage your vendor and contact data</p>
|
||
</div>
|
||
<button
|
||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
|
||
on:click={() => {
|
||
showModal = true;
|
||
isEditing = false;
|
||
newVendor = {};
|
||
currentEditingId = null;
|
||
}}
|
||
>
|
||
➕ Add Vendor
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Vendor 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 (col.key)}
|
||
<th
|
||
class="px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||
>{col.title}</th
|
||
>
|
||
{/each}
|
||
<th class="px-4 py-3">Contacts</th>
|
||
<th class="px-4 py-3">Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-gray-200 bg-white">
|
||
{#each paginatedRows as vendor}
|
||
<tr
|
||
class="hover:bg-gray-50 transition"
|
||
on:click={() => handleVendorClick(vendor.id)}
|
||
>
|
||
{#each columns as col}
|
||
<td class="px-4 py-2 text-gray-700"
|
||
>{vendor[col.key as keyof Vendor]}</td
|
||
>
|
||
{/each}
|
||
<!-- Contact Button -->
|
||
<td class="px-4 py-2">
|
||
<button
|
||
class="bg-green-600 text-white px-2 py-1 rounded text-xs hover:bg-green-700"
|
||
on:click|stopPropagation={() => {
|
||
fetchContactVendor(vendor.id);
|
||
showModalContact = true;
|
||
}}>📞 Contacts</button
|
||
>
|
||
</td>
|
||
<td class="px-4 py-2 space-x-2">
|
||
<button
|
||
class="bg-blue-600 text-white px-2 py-1 rounded text-xs hover:bg-blue-700"
|
||
on:click|stopPropagation={() => {
|
||
openModal(vendor);
|
||
}}>✏️ Edit</button
|
||
>
|
||
<button
|
||
class="bg-red-600 text-white px-2 py-1 rounded text-xs hover:bg-red-700"
|
||
on:click|stopPropagation={() =>
|
||
deleteVendor(vendor.id)}>🗑️ Delete</button
|
||
>
|
||
</td>
|
||
</tr>
|
||
{/each}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Pagination -->
|
||
<div class="flex justify-between items-center text-sm mt-2">
|
||
<div>
|
||
Showing {(currentPage - 1) * itemsPerPage + 1}–{Math.min(
|
||
currentPage * itemsPerPage,
|
||
allRowsVendor.length,
|
||
)} of {allRowsVendor.length}
|
||
</div>
|
||
<div class="space-x-2">
|
||
<button
|
||
on:click={previousPage}
|
||
disabled={currentPage === 1}
|
||
class="px-3 py-1 rounded border bg-white hover:bg-gray-100 disabled:opacity-50"
|
||
>Previous</button
|
||
>
|
||
<button
|
||
on:click={nextPage}
|
||
disabled={currentPage === totalPages}
|
||
class="px-3 py-1 rounded border bg-white hover:bg-gray-100 disabled:opacity-50"
|
||
>Next</button
|
||
>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Contact Vendor Table -->
|
||
<!-- {#if allRowsContactVendor.length > 0}
|
||
<div class="mt-6">
|
||
<h3 class="text-md font-semibold mb-2">Contact Vendors</h3>
|
||
<table class="min-w-full border divide-y divide-gray-200 text-sm">
|
||
<thead class="bg-gray-50">
|
||
<tr>
|
||
<th class="px-3 py-2">Name</th>
|
||
<th class="px-3 py-2">Position</th>
|
||
<th class="px-3 py-2">Email</th>
|
||
<th class="px-3 py-2">Phone</th>
|
||
<th class="px-3 py-2">Mobile</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y">
|
||
{#each allRowsContactVendor as contact}
|
||
<tr class="hover:bg-gray-50">
|
||
<td class="px-3 py-2">{contact.contact_name}</td>
|
||
<td class="px-3 py-2">{contact.contact_position}</td>
|
||
<td class="px-3 py-2">{contact.contact_email}</td>
|
||
<td class="px-3 py-2">{contact.contact_phone}</td>
|
||
<td class="px-3 py-2">{contact.contact_phone_mobile}</td
|
||
>
|
||
</tr>
|
||
{/each}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{/if} -->
|
||
|
||
<!-- Modal for Add/Edit Vendor -->
|
||
{#if showModal}
|
||
<div
|
||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||
>
|
||
<div
|
||
class="bg-white p-6 rounded shadow-lg w-[600px] max-h-[90vh] overflow-y-auto space-y-4"
|
||
>
|
||
<h3 class="text-lg font-semibold">
|
||
{isEditing ? "Edit Vendor" : "Add New Vendor"}
|
||
</h3>
|
||
{#each formColumns as col}
|
||
<div class="space-y-1">
|
||
<label class="block text-sm font-medium text-gray-700"
|
||
>{col.title}</label
|
||
>
|
||
<input
|
||
class="w-full border px-3 py-2 rounded"
|
||
type="text"
|
||
bind:value={newVendor[col.key]}
|
||
placeholder={col.title}
|
||
/>
|
||
</div>
|
||
{/each}
|
||
<div class="flex justify-end gap-2 mt-4">
|
||
<button
|
||
class="px-4 py-2 text-sm rounded bg-gray-200 hover:bg-gray-300"
|
||
on:click={() => (showModal = false)}>Cancel</button
|
||
>
|
||
<button
|
||
class="px-4 py-2 text-sm rounded bg-blue-600 text-white hover:bg-blue-700"
|
||
on:click={isEditing ? updateVendor : addVendor}
|
||
>
|
||
Save
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
<!-- Modal Contact ADD and Edit -->
|
||
{#if showModalAddEditContact}
|
||
<div
|
||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||
>
|
||
<div
|
||
class="bg-white p-6 rounded shadow-lg w-[600px] max-h-[90vh] overflow-y-auto space-y-4"
|
||
>
|
||
<h3 class="text-lg font-semibold">
|
||
{isEditingContact
|
||
? "Edit Contact Vendor"
|
||
: "Add New Contact Vendor"}
|
||
</h3>
|
||
{#each formColumnsContact as col}
|
||
<div class="space-y-1">
|
||
<label class="block text-sm font-medium text-gray-700"
|
||
>{col.title}</label
|
||
>
|
||
<input
|
||
class="w-full border px-3 py-2 rounded"
|
||
type="text"
|
||
bind:value={newVendorContact[col.key]}
|
||
placeholder={col.title}
|
||
/>
|
||
</div>
|
||
{/each}
|
||
<div class="flex justify-end gap-2 mt-4">
|
||
<button
|
||
class="px-4 py-2 text-sm rounded bg-gray-200 hover:bg-gray-300"
|
||
on:click={() => (showModalAddEditContact = false)}
|
||
>Cancel</button
|
||
>
|
||
<button
|
||
class="px-4 py-2 text-sm rounded bg-blue-600 text-white hover:bg-blue-700"
|
||
on:click={() =>
|
||
isEditingContact
|
||
? updateContactVendor(
|
||
newVendorContact as ContactVendor,
|
||
)
|
||
: addContactVendor(
|
||
newVendorContact as ContactVendor,
|
||
)}
|
||
>
|
||
Save
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
{#if showModalContact}
|
||
<div
|
||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||
>
|
||
<div
|
||
class="bg-white p-6 rounded-2xl shadow-2xl w-[90vw] max-w-5xl max-h-[90vh] overflow-y-auto space-y-6 transition-all duration-300"
|
||
>
|
||
<!-- Header Modal -->
|
||
<div class="flex justify-between items-center border-b pb-3">
|
||
<h3 class="text-xl font-semibold text-gray-800">
|
||
📇 Contact List
|
||
</h3>
|
||
<div class="flex gap-2 items-center">
|
||
<button
|
||
class="flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded bg-blue-600 text-white hover:bg-blue-700 shadow transition"
|
||
on:click={() => openModalAddContact()}
|
||
>
|
||
➕ Add Contact
|
||
</button>
|
||
<button
|
||
class="text-gray-500 hover:text-red-600 text-2xl leading-none transition"
|
||
on:click={() => (showModalContact = false)}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Body Modal -->
|
||
{#if allRowsContactVendor.length > 0}
|
||
<div class="overflow-x-auto">
|
||
<table
|
||
class="min-w-full text-sm text-left border rounded-md overflow-hidden"
|
||
>
|
||
<thead
|
||
class="bg-gray-100 text-gray-700 uppercase text-xs font-semibold"
|
||
>
|
||
<tr>
|
||
{#each columnsContact as col (col.key)}
|
||
<th class="px-4 py-3 whitespace-nowrap"
|
||
>{col.title}</th
|
||
>
|
||
{/each}
|
||
<th class="px-4 py-3 whitespace-nowrap"
|
||
>Actions</th
|
||
>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="bg-white divide-y divide-gray-200">
|
||
{#each allRowsContactVendor as contact}
|
||
<tr class="hover:bg-gray-50 transition">
|
||
{#each columnsContact as col}
|
||
<td
|
||
class="px-4 py-2 whitespace-nowrap text-gray-800"
|
||
>
|
||
{contact[
|
||
col.key as keyof ContactVendor
|
||
]}
|
||
</td>
|
||
{/each}
|
||
<td class="px-4 py-2 whitespace-nowrap">
|
||
<div class="flex gap-2">
|
||
<button
|
||
class="flex items-center gap-1 bg-blue-500 hover:bg-blue-600 text-white px-2.5 py-1 rounded text-xs transition"
|
||
on:click|stopPropagation={() => {
|
||
isEditingContact = true;
|
||
newVendorContact = {
|
||
...contact,
|
||
};
|
||
}}
|
||
>
|
||
✏️ Edit
|
||
</button>
|
||
<button
|
||
class="flex items-center gap-1 bg-red-500 hover:bg-red-600 text-white px-2.5 py-1 rounded text-xs transition"
|
||
on:click|stopPropagation={() =>
|
||
deleteContactVendor(
|
||
contact.id,
|
||
)}
|
||
>
|
||
🗑️ Delete
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{/each}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{:else}
|
||
<div class="text-center space-y-2 pt-4">
|
||
<p class="text-base font-semibold text-gray-700">
|
||
No Contacts Available
|
||
</p>
|
||
<p class="text-sm text-gray-500">
|
||
There are no contacts available for this vendor.
|
||
</p>
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
{/if}
|