Files
vberp/src/routes/backoffice/vendor/+page.svelte
Aji Setiaji 8d984635af first commit
2025-05-27 21:43:01 +07:00

598 lines
21 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;
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}