first commit
This commit is contained in:
597
src/routes/backoffice/vendor/+page.svelte
vendored
Normal file
597
src/routes/backoffice/vendor/+page.svelte
vendored
Normal file
@@ -0,0 +1,597 @@
|
||||
<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}
|
||||
Reference in New Issue
Block a user