refactor vendor page, insert sesuai clickup, modal nested contact dihilangi
This commit is contained in:
755
src/routes/backoffice/vendor/+page.svelte
vendored
755
src/routes/backoffice/vendor/+page.svelte
vendored
@@ -1,667 +1,286 @@
|
||||
<script lang="ts">
|
||||
import { supabase } from "$lib/supabaseClient";
|
||||
import { onMount } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
type Vendor = {
|
||||
id: string;
|
||||
name: string;
|
||||
contact_type: string;
|
||||
vendor_type: string;
|
||||
vendor_address: string;
|
||||
vendor_status: string;
|
||||
vendor_subtype: string;
|
||||
address: string;
|
||||
contact_comment: string;
|
||||
vendor_unik: 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;
|
||||
};
|
||||
|
||||
let allRowsVendor: Vendor[] = [];
|
||||
let allRowsContactVendor: ContactVendor[] = [];
|
||||
let offset = 0;
|
||||
let limit = 10;
|
||||
let currentPage = 1;
|
||||
let itemsPerPage = 10;
|
||||
$: offset = (currentPage - 1) * itemsPerPage;
|
||||
$: totalPages = Math.ceil(totalItems / itemsPerPage);
|
||||
let totalItems = 0;
|
||||
export let formErrors = writable<{ [key: string]: string }>({});
|
||||
let newVendor: Record<string, any> = {};
|
||||
let showModal = false;
|
||||
let isEditing = false;
|
||||
let currentEditingId: string | null = null;
|
||||
let searchTerm = "";
|
||||
|
||||
type columns = {
|
||||
key: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
let columns: columns[] = [
|
||||
const 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: "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" },
|
||||
{ key: "created_at", title: "Created At" },
|
||||
];
|
||||
const excludedKeys = ["id", "created_by", "created_at", "updated_at"];
|
||||
$: formColumns = columns.filter((col) => !excludedKeys.includes(col.key));
|
||||
|
||||
async function fetchVendor(
|
||||
searchTerm: string = "",
|
||||
orderBy: string = "created_at",
|
||||
orderDirection: "asc" | "desc" = "desc",
|
||||
limit: number = 10,
|
||||
offset: number = 0,
|
||||
) {
|
||||
let query = supabase
|
||||
async function fetchVendor(search = "", resetPage = false) {
|
||||
if (resetPage) currentPage = 1;
|
||||
|
||||
const { data, error, count } = await supabase
|
||||
.from("vb_vendor")
|
||||
.select("*", { count: "exact" })
|
||||
.order(orderBy, { ascending: orderDirection === "asc" })
|
||||
.range(offset, offset + limit - 1);
|
||||
|
||||
if (searchTerm) {
|
||||
query = query.ilike("name", `%${searchTerm}%`);
|
||||
}
|
||||
|
||||
const { data, error, count } = await query;
|
||||
.order("created_at", { ascending: false })
|
||||
.range(offset, offset + itemsPerPage - 1)
|
||||
.ilike("name", `%${search}%`);
|
||||
|
||||
if (error) {
|
||||
console.error("Error fetching vendors:", error);
|
||||
return [];
|
||||
console.error("Fetch error:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
allRowsVendor = data as Vendor[];
|
||||
|
||||
const { count: total } = await supabase
|
||||
.from("vb_vendor")
|
||||
.select("*", { count: "exact" })
|
||||
.ilike("name", `%${searchTerm}%`);
|
||||
|
||||
totalItems = total || 0;
|
||||
offset = Math.floor(totalItems / limit) * limit;
|
||||
allRowsVendor = data || [];
|
||||
totalItems = count || 0;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchVendor();
|
||||
});
|
||||
|
||||
let currentPage = offset + 1;
|
||||
let itemsPerPage = limit;
|
||||
let totalPages = Math.ceil(totalItems / itemsPerPage);
|
||||
function resetPagination() {
|
||||
currentPage = 1;
|
||||
fetchVendor(searchTerm);
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
if (currentPage < totalPages) {
|
||||
currentPage += 1;
|
||||
fetchVendor(searchTerm);
|
||||
}
|
||||
}
|
||||
|
||||
function previousPage() {
|
||||
if (currentPage > 1) {
|
||||
currentPage -= 1;
|
||||
fetchVendor(searchTerm);
|
||||
}
|
||||
}
|
||||
|
||||
function resetPagination() {
|
||||
currentPage = 1;
|
||||
function changePage(page: number) {
|
||||
currentPage = page;
|
||||
fetchVendor(searchTerm);
|
||||
}
|
||||
|
||||
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) {
|
||||
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 { data, error } = await supabase
|
||||
.from("vb_vendor")
|
||||
.insert([newVendor])
|
||||
.select();
|
||||
|
||||
const { error } = await supabase.from("vb_vendor").insert([newVendor]);
|
||||
if (error) {
|
||||
console.error("Error adding vendor:", error);
|
||||
console.error("Add error:", error);
|
||||
} else {
|
||||
allRowsVendor.push(data[0]);
|
||||
resetPagination();
|
||||
showModal = false;
|
||||
resetPagination();
|
||||
}
|
||||
}
|
||||
|
||||
async function updateVendor() {
|
||||
const { data, error } = await supabase
|
||||
.from("vb_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("vb_vendor")
|
||||
.delete()
|
||||
.eq("id", vendorId);
|
||||
|
||||
.update(newVendor)
|
||||
.eq("id", currentEditingId);
|
||||
if (error) {
|
||||
console.error("Error deleting vendor:", error);
|
||||
console.error("Update error:", error);
|
||||
} else {
|
||||
allRowsVendor = allRowsVendor.filter((v) => v.id !== vendorId);
|
||||
showModal = false;
|
||||
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);
|
||||
async function deleteVendor(id: string) {
|
||||
const { error } = await supabase.from("vb_vendor").delete().eq("id", id);
|
||||
if (error) {
|
||||
console.error("Delete error:", error);
|
||||
} else {
|
||||
allRowsContactVendor = contactData as ContactVendor[];
|
||||
resetPagination();
|
||||
}
|
||||
}
|
||||
|
||||
function handleVendorClick(vendorId: string) {
|
||||
fetchContactVendor(vendorId);
|
||||
}
|
||||
function pageRange(totalPages: number, currentPage: number): (number | string)[] {
|
||||
const range: (number | string)[] = [];
|
||||
const maxDisplay = 5;
|
||||
|
||||
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);
|
||||
if (totalPages <= maxDisplay + 2) {
|
||||
for (let i = 1; i <= totalPages; i++) range.push(i);
|
||||
} else {
|
||||
allRowsContactVendor.push(data[0]);
|
||||
closeContactModal();
|
||||
}
|
||||
}
|
||||
const start = Math.max(2, currentPage - 2);
|
||||
const end = Math.min(totalPages - 1, currentPage + 2);
|
||||
|
||||
async function updateContactVendor(contact: ContactVendor) {
|
||||
const { data, error } = await supabase
|
||||
.from("contact_vendor")
|
||||
.update(contact)
|
||||
.eq("id", contact.id)
|
||||
.select();
|
||||
range.push(1);
|
||||
if (start > 2) range.push("...");
|
||||
|
||||
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();
|
||||
for (let i = start; i <= end; i++) {
|
||||
range.push(i);
|
||||
}
|
||||
|
||||
if (end < totalPages - 1) range.push("...");
|
||||
range.push(totalPages);
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
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;
|
||||
onMount(() => {
|
||||
fetchVendor();
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Table untuk daftar Vendor -->
|
||||
<div>
|
||||
<div
|
||||
class="p-6 bg-white shadow-md rounded-2xl mb-4 flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4"
|
||||
>
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-gray-800">🏡 Vendor List</h2>
|
||||
<p class="text-sm text-gray-500">
|
||||
Manage your vendors and their contact information here.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="🔍 Search by name..."
|
||||
class="border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:outline-none px-4 py-2 rounded-xl text-sm w-64 transition"
|
||||
on:input={(e) => {
|
||||
const searchTerm = (
|
||||
e.target as HTMLInputElement
|
||||
).value.toLowerCase();
|
||||
fetchVendor(searchTerm, "created_at", "desc", limit, 0);
|
||||
}}
|
||||
/>
|
||||
<!-- filter -->
|
||||
<select
|
||||
class="border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:outline-none px-4 py-2 rounded-xl text-sm w-48 transition"
|
||||
on:change={(e) => {
|
||||
const value = (e.target as HTMLSelectElement).value;
|
||||
fetchVendor("", value, "desc", limit, 0);
|
||||
}}
|
||||
>
|
||||
<option value="created_at">Sort by Created At</option>
|
||||
<option value="name">Sort by Name</option>
|
||||
<option value="vendor_type">Sort by Vendor Type</option>
|
||||
<option value="vendor_status">Sort by Vendor Status</option>
|
||||
<option value="vendor_subtype">Sort by Vendor Subtype</option>
|
||||
</select>
|
||||
<!-- button reset -->
|
||||
<button
|
||||
class="bg-gray-200 text-gray-700 px-4 py-2 rounded hover:bg-gray-300 text-sm"
|
||||
on:click={() => {
|
||||
fetchVendor();
|
||||
resetPagination();
|
||||
}}
|
||||
>
|
||||
🔄 Reset
|
||||
</button>
|
||||
<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>
|
||||
</div>
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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 allRowsVendor 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>
|
||||
<!-- Table -->
|
||||
<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}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<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]}</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>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="flex justify-between items-center text-sm mt-2">
|
||||
<div class="flex justify-between items-center mt-4 text-sm">
|
||||
<div>
|
||||
Showing {(currentPage - 1) * itemsPerPage + 1}–{Math.min(
|
||||
currentPage * itemsPerPage,
|
||||
allRowsVendor.length,
|
||||
)} of {allRowsVendor.length}
|
||||
Showing {(currentPage - 1) * itemsPerPage + 1}–
|
||||
{Math.min(currentPage * itemsPerPage, totalItems)} of {totalItems}
|
||||
</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 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)}
|
||||
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>
|
||||
|
||||
<!-- 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 -->
|
||||
<!-- Modal -->
|
||||
{#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 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>
|
||||
{/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>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
43
yarn.lock
43
yarn.lock
@@ -61,14 +61,6 @@
|
||||
resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz"
|
||||
integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==
|
||||
|
||||
"@jridgewell/source-map@^0.3.3":
|
||||
version "0.3.6"
|
||||
resolved "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz"
|
||||
integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==
|
||||
dependencies:
|
||||
"@jridgewell/gen-mapping" "^0.3.5"
|
||||
"@jridgewell/trace-mapping" "^0.3.25"
|
||||
|
||||
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15", "@jridgewell/sourcemap-codec@^1.5.0":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz"
|
||||
@@ -335,7 +327,7 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
acorn@^8.12.1, acorn@^8.14.0, acorn@^8.14.1, acorn@^8.9.0:
|
||||
acorn@^8.12.1, acorn@^8.14.1, acorn@^8.9.0:
|
||||
version "8.14.1"
|
||||
resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz"
|
||||
integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==
|
||||
@@ -350,11 +342,6 @@ axobject-query@^4.1.0:
|
||||
resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz"
|
||||
integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==
|
||||
|
||||
buffer-from@^1.0.0:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz"
|
||||
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
|
||||
|
||||
chokidar@^4.0.1:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz"
|
||||
@@ -372,11 +359,6 @@ clsx@^2.1.1:
|
||||
resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz"
|
||||
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
|
||||
|
||||
commander@^2.20.0:
|
||||
version "2.20.3"
|
||||
resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz"
|
||||
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
|
||||
|
||||
commondir@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz"
|
||||
@@ -693,19 +675,6 @@ source-map-js@^1.2.1:
|
||||
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz"
|
||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||
|
||||
source-map-support@~0.5.20:
|
||||
version "0.5.21"
|
||||
resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz"
|
||||
integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
|
||||
dependencies:
|
||||
buffer-from "^1.0.0"
|
||||
source-map "^0.6.0"
|
||||
|
||||
source-map@^0.6.0:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz"
|
||||
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
||||
|
||||
supports-preserve-symlinks-flag@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
|
||||
@@ -784,16 +753,6 @@ tar@^7.4.3:
|
||||
mkdirp "^3.0.1"
|
||||
yallist "^5.0.0"
|
||||
|
||||
terser@^5.16.0:
|
||||
version "5.40.0"
|
||||
resolved "https://registry.npmjs.org/terser/-/terser-5.40.0.tgz"
|
||||
integrity sha512-cfeKl/jjwSR5ar7d0FGmave9hFGJT8obyo0z+CrQOylLDbk7X81nPU6vq9VORa5jU30SkDnT2FXjLbR8HLP+xA==
|
||||
dependencies:
|
||||
"@jridgewell/source-map" "^0.3.3"
|
||||
acorn "^8.14.0"
|
||||
commander "^2.20.0"
|
||||
source-map-support "~0.5.20"
|
||||
|
||||
tinyglobby@^0.2.13:
|
||||
version "0.2.13"
|
||||
resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz"
|
||||
|
||||
Reference in New Issue
Block a user