penamabahan menu approval untill received
This commit is contained in:
@@ -1,5 +1,21 @@
|
||||
<script>
|
||||
let menuItems = [
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
type SubMenuItem = {
|
||||
name: string;
|
||||
icon: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
type MenuItem = {
|
||||
name: string;
|
||||
icon: string;
|
||||
url: string;
|
||||
sub?: SubMenuItem[];
|
||||
};
|
||||
|
||||
let menuItems: MenuItem[] = [
|
||||
{ name: "Beranda", icon: "🏠", url: "/" },
|
||||
{ name: "Profile", icon: "🧑", url: "/profile" },
|
||||
{ name: "Issues", icon: "📂", url: "/backoffice/issue" },
|
||||
@@ -7,8 +23,30 @@
|
||||
{ name: "Projects", icon: "📂", url: "/backoffice/project" },
|
||||
{
|
||||
name: "Purchase Orders",
|
||||
icon: "📂",
|
||||
icon: "📦",
|
||||
url: "/backoffice/purchaseorder",
|
||||
sub: [
|
||||
{
|
||||
name: "Approval",
|
||||
icon: "✅",
|
||||
url: "/backoffice/purchaseorder/approval",
|
||||
},
|
||||
{
|
||||
name: "Acknowledged",
|
||||
icon: "📋",
|
||||
url: "/backoffice/purchaseorder/acknowledged",
|
||||
},
|
||||
{
|
||||
name: "Complete",
|
||||
icon: "✔️",
|
||||
url: "/backoffice/purchaseorder/complete",
|
||||
},
|
||||
{
|
||||
name: "Received",
|
||||
icon: "📥",
|
||||
url: "/backoffice/purchaseorder/received",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ name: "Timesheets", icon: "📂", url: "/backoffice/timesheets" },
|
||||
{
|
||||
@@ -20,15 +58,39 @@
|
||||
{ name: "Inventories", icon: "📂", url: "/backoffice/inventories" },
|
||||
{ name: "Vendor", icon: "📂", url: "/backoffice/vendor" },
|
||||
{ name: "Booking", icon: "📂", url: "/backoffice/booking" },
|
||||
{ name: "Users", icon: "👤", url: "/backoffice/users" },
|
||||
{ name: "Users", icon: "👤", url: "/backoffice/account" },
|
||||
{ name: "Logout", icon: "🚪", url: "/logout" },
|
||||
];
|
||||
|
||||
let active = "Purchase Orders";
|
||||
let activeUrl = "/";
|
||||
let openMenus: Record<string, boolean> = {};
|
||||
|
||||
onMount(() => {
|
||||
activeUrl = window.location.pathname;
|
||||
});
|
||||
|
||||
function handleMenuClick(item: MenuItem) {
|
||||
console.log(`Menu item clicked: ${item.name}`);
|
||||
console.log(`URL: ${item.url}`);
|
||||
|
||||
if (item.sub) {
|
||||
goto(item.url);
|
||||
activeUrl = item.url;
|
||||
openMenus[item.name] = !openMenus[item.name];
|
||||
} else {
|
||||
activeUrl = item.url;
|
||||
goto(item.url);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubClick(sub: SubMenuItem) {
|
||||
activeUrl = sub.url;
|
||||
goto(sub.url);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-64 h-screen bg-white border-r shadow-sm">
|
||||
<div class=" p-8 border-b">
|
||||
<div class="p-8 border-b">
|
||||
<h1 class="text-xl font-semibold text-gray-800">Backoffice</h1>
|
||||
<p class="text-sm text-gray-500">Manage your application</p>
|
||||
</div>
|
||||
@@ -38,17 +100,39 @@
|
||||
<a
|
||||
href={item.url}
|
||||
class={`flex items-center gap-2 px-4 py-2 rounded transition-colors duration-150
|
||||
${
|
||||
active === item.name
|
||||
? "bg-blue-100 text-blue-600 font-semibold"
|
||||
: "text-gray-700 hover:bg-blue-50"
|
||||
}`}
|
||||
on:click={() => (active = item.name)}
|
||||
${activeUrl === item.url ? "bg-blue-100 text-blue-600 font-semibold" : "text-gray-700 hover:bg-blue-50"}
|
||||
${item.sub ? "justify-between" : ""}
|
||||
`}
|
||||
on:click|preventDefault={() => handleMenuClick(item)}
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="text-xl">{item.icon}</span>
|
||||
<span class="truncate">{item.name}</span>
|
||||
</span>
|
||||
{#if item.sub}
|
||||
<span>{openMenus[item.name] ? "▲" : "▼"}</span>
|
||||
{/if}
|
||||
</a>
|
||||
{#if item.sub && openMenus[item.name]}
|
||||
<ul class="ml-8 mt-1 space-y-1">
|
||||
{#each item.sub as sub}
|
||||
<li>
|
||||
<a
|
||||
href={sub.url}
|
||||
class={`flex items-center gap-2 px-3 py-1 rounded transition-colors duration-150
|
||||
${activeUrl === sub.url ? "bg-blue-50 text-blue-600 font-semibold" : "text-gray-700 hover:bg-blue-50"}
|
||||
`}
|
||||
on:click|preventDefault={() =>
|
||||
handleSubClick(sub)}
|
||||
>
|
||||
<span class="text-lg">{sub.icon}</span>
|
||||
<span class="truncate">{sub.name}</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
662
src/routes/backoffice/account/+page.svelte
Normal file
662
src/routes/backoffice/account/+page.svelte
Normal file
@@ -0,0 +1,662 @@
|
||||
<script lang="ts">
|
||||
import { supabase } from "$lib/supabaseClient";
|
||||
import { onMount } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
const roleUser = [
|
||||
{ label: "Admin", value: "admin" },
|
||||
{ label: "User", value: "user" },
|
||||
{ label: "Guest", value: "guest" },
|
||||
{ label: "Super Admin", value: "superadmin" },
|
||||
{ label: "Manager", value: "manager" },
|
||||
{ label: "Staff", value: "staff" },
|
||||
{ label: "Accountant", value: "accountant" },
|
||||
{ label: "Project Manager", value: "projectmanager" },
|
||||
{ label: "HR", value: "hr" },
|
||||
{ label: "Support", value: "support" },
|
||||
{ label: "Office Manager", value: "officemanager" },
|
||||
{ label: "Operation Manager", value: "operationmanager" },
|
||||
{ label: "Purchasing", value: "purchasing" },
|
||||
{ label: "Finance", value: "finance" },
|
||||
{ label: "Villa Manager", value: "villamanager" },
|
||||
];
|
||||
|
||||
type User = {
|
||||
id: string;
|
||||
email: string;
|
||||
password: string; // Note: Passwords should not be stored in plain text
|
||||
role: string;
|
||||
nip: string;
|
||||
full_name: string;
|
||||
phone: string;
|
||||
address: string;
|
||||
profile_picture: string;
|
||||
last_login: string;
|
||||
is_active: boolean;
|
||||
is_verified: boolean;
|
||||
is_deleted: boolean;
|
||||
last_updated: string;
|
||||
last_updated_by: string;
|
||||
created_by: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
let allRows: User[] = [];
|
||||
let offset = 0;
|
||||
let limit = 10;
|
||||
let totalItems = 0;
|
||||
export let formErrors = writable<{ [key: string]: string }>({});
|
||||
|
||||
type columns = {
|
||||
key: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
let columns: columns[] = [
|
||||
{ key: "no", title: "No. " },
|
||||
{ key: "email", title: "Email" },
|
||||
{ key: "password", title: "Password" },
|
||||
{ key: "role", title: "Role" },
|
||||
{ key: "nip", title: "NIP" },
|
||||
{ key: "full_name", title: "Full Name" },
|
||||
{ key: "phone", title: "Phone" },
|
||||
{ key: "address", title: "Address" },
|
||||
{ key: "profile_picture", title: "Profile Picture" },
|
||||
{ key: "last_login", title: "Last Login" },
|
||||
{ key: "is_active", title: "Active" },
|
||||
{ key: "is_verified", title: "Verified" },
|
||||
{ key: "last_updated", title: "Last Updated" },
|
||||
{ key: "last_updated_by", title: "Last Updated By" },
|
||||
{ key: "created_by", title: "Created By" },
|
||||
{ key: "created_at", title: "Created At" },
|
||||
];
|
||||
|
||||
async function fetchData(
|
||||
filter: string = "",
|
||||
sortBy: string = "created_at",
|
||||
sortOrder: "asc" | "desc" = "desc",
|
||||
search: string = "",
|
||||
offset: number = 0,
|
||||
limit: number = 10,
|
||||
) {
|
||||
try {
|
||||
const { data, error, count } = await supabase
|
||||
.from("users")
|
||||
.select("*", { count: "exact" })
|
||||
.ilike("email", `%${search}%`)
|
||||
.or(`full_name.ilike.%${search}%,nip.ilike.%${search}%`)
|
||||
.order(sortBy, { ascending: sortOrder === "asc" })
|
||||
.range(offset, offset + limit - 1);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
allRows = data as User[];
|
||||
totalItems = count || 0;
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchData();
|
||||
});
|
||||
|
||||
let currentPage = offset + 1;
|
||||
let rowsPerPage = 10;
|
||||
$: totalPages = Math.ceil(totalItems / rowsPerPage);
|
||||
|
||||
function nextPage() {
|
||||
if (currentPage < totalPages) {
|
||||
currentPage += 1;
|
||||
offset = (currentPage - 1) * rowsPerPage;
|
||||
fetchData("", "created_at", "desc", "", offset, rowsPerPage);
|
||||
}
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
if (currentPage > 1) {
|
||||
currentPage -= 1;
|
||||
offset = (currentPage - 1) * rowsPerPage;
|
||||
fetchData("", "created_at", "desc", "", offset, rowsPerPage);
|
||||
}
|
||||
}
|
||||
|
||||
function goToPage(page: number) {
|
||||
if (page >= 1 && page <= totalPages) {
|
||||
currentPage = page;
|
||||
offset = (currentPage - 1) * rowsPerPage;
|
||||
fetchData("", "created_at", "desc", "", offset, rowsPerPage);
|
||||
}
|
||||
}
|
||||
|
||||
let showModal = false;
|
||||
let isEditing = false;
|
||||
let currentEditingId: string | null = null;
|
||||
let newUser: Record<string, any> = {};
|
||||
const excludedKeys = [
|
||||
"id",
|
||||
"last_updated",
|
||||
"last_updated_by",
|
||||
"created_by",
|
||||
"created_at",
|
||||
"last_login",
|
||||
"deleted",
|
||||
"no",
|
||||
];
|
||||
|
||||
const formColumns = columns.filter(
|
||||
(col) => !excludedKeys.includes(col.key),
|
||||
);
|
||||
|
||||
let selectedFile: File | null = null;
|
||||
let imagePreviewUrl: string | null = null;
|
||||
|
||||
function handleFileChange(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) {
|
||||
selectedFile = input.files[0];
|
||||
imagePreviewUrl = URL.createObjectURL(selectedFile);
|
||||
}
|
||||
}
|
||||
|
||||
// function get public URL for image supabase
|
||||
async function getPublicUrl(path: string): Promise<string> {
|
||||
const { data } = supabase.storage.from("villabugis").getPublicUrl(path);
|
||||
return data.publicUrl;
|
||||
}
|
||||
|
||||
function openModal(user?: Record<string, any>) {
|
||||
if (user) {
|
||||
isEditing = true;
|
||||
currentEditingId = user.id;
|
||||
newUser = { ...user };
|
||||
} else {
|
||||
isEditing = false;
|
||||
currentEditingId = null;
|
||||
newUser = {};
|
||||
}
|
||||
showModal = true;
|
||||
}
|
||||
|
||||
function validateForm(formData: FormData): boolean {
|
||||
const errors: { [key: string]: string } = {};
|
||||
const requiredFields = [
|
||||
"email",
|
||||
"role",
|
||||
"nip",
|
||||
"full_name",
|
||||
"phone",
|
||||
"address",
|
||||
];
|
||||
|
||||
requiredFields.forEach((field) => {
|
||||
if (!formData.get(field) || formData.get(field) === "") {
|
||||
errors[field] = `${field.replace(/_/g, " ")} is required.`;
|
||||
}
|
||||
});
|
||||
|
||||
formErrors.set(errors);
|
||||
return Object.keys(errors).length === 0;
|
||||
}
|
||||
|
||||
function errorClass(field: string): string {
|
||||
return $formErrors[field] ? "border-red-500" : "border";
|
||||
}
|
||||
|
||||
async function saveUser(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.target as HTMLFormElement);
|
||||
if (!validateForm(formData)) {
|
||||
console.error("Form validation failed:", $formErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedFile) {
|
||||
// Upload file to Supabase Storage
|
||||
const { data, error } = await supabase.storage
|
||||
.from("villabugis")
|
||||
.upload(`profile_pictures/${selectedFile.name}`, selectedFile);
|
||||
|
||||
if (error) {
|
||||
console.error("Error uploading file:", error);
|
||||
return;
|
||||
}
|
||||
newUser.profile_picture = data.path; // Store the path in newUser
|
||||
}
|
||||
|
||||
try {
|
||||
if (isEditing && currentEditingId) {
|
||||
// Update user in 'users' table
|
||||
const { error } = await supabase
|
||||
.from("users")
|
||||
.update(newUser)
|
||||
.eq("id", currentEditingId);
|
||||
|
||||
if (error) throw error;
|
||||
} else {
|
||||
// 1. Create user in Supabase Auth
|
||||
const { data: authUser, error: authError } =
|
||||
await supabase.auth.signUp({
|
||||
email: newUser.email,
|
||||
password: newUser.nip || "defaultPassword123", // Use NIP as password or fallback
|
||||
});
|
||||
|
||||
if (authError) throw authError;
|
||||
|
||||
// 2. Insert user in 'users' table with auth user id
|
||||
const { error } = await supabase
|
||||
.from("users")
|
||||
.insert([{ ...newUser, id: authUser.user?.id }]);
|
||||
if (error) throw error;
|
||||
}
|
||||
showModal = false;
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
console.error("Error saving user:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteUser(id: string) {
|
||||
if (confirm("Are you sure you want to delete this user?")) {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from("users")
|
||||
.delete()
|
||||
.eq("id", id);
|
||||
if (error) throw error;
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
console.error("Error deleting user:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
showModal = false;
|
||||
isEditing = false;
|
||||
currentEditingId = null;
|
||||
newUser = {};
|
||||
formErrors.set({});
|
||||
}
|
||||
|
||||
$: if (showModal) {
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
document.body.style.overflow = "auto";
|
||||
}
|
||||
|
||||
$: if (isEditing && currentEditingId) {
|
||||
const user = allRows.find((u) => u.id === currentEditingId);
|
||||
if (user) {
|
||||
newUser = { ...user };
|
||||
}
|
||||
}
|
||||
|
||||
$: if (!isEditing) {
|
||||
newUser = {
|
||||
email: "",
|
||||
role: "user",
|
||||
nip: "",
|
||||
full_name: "",
|
||||
phone: "",
|
||||
address: "",
|
||||
profile_picture: "",
|
||||
is_active: true,
|
||||
is_verified: false,
|
||||
is_deleted: false,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<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">👤 User List</h2>
|
||||
<p class="text-sm text-gray-500">
|
||||
Manage and view all users in the system.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="🔍 Search by email, name, or NIP..."
|
||||
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;
|
||||
fetchData(
|
||||
"",
|
||||
"created_at",
|
||||
"desc",
|
||||
searchTerm,
|
||||
0,
|
||||
rowsPerPage,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
class="bg-gray-200 text-gray-700 px-4 py-2 rounded-xl hover:bg-gray-300 text-sm transition"
|
||||
on:click={() =>
|
||||
fetchData("", "created_at", "desc", "", 0, rowsPerPage)}
|
||||
>
|
||||
🔄 Reset
|
||||
</button>
|
||||
<button
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
|
||||
on:click={() => openModal()}
|
||||
>
|
||||
➕ Add User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<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="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 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||||
>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{#if allRows.length === 0}
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
colspan={columns.length + 1}
|
||||
class="px-4 py-12 text-center text-gray-400 bg-gray-50"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col items-center justify-center space-y-2"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-10 w-10 text-gray-300 mb-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6 6 0 10-12 0v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-lg font-semibold"
|
||||
>No users found</span
|
||||
>
|
||||
<span class="text-sm text-gray-400"
|
||||
>Try adjusting your search or add a new
|
||||
user.</span
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
{:else}
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
{#each allRows as row, i}
|
||||
<tr class="hover:bg-gray-50 transition">
|
||||
{#each columns as col}
|
||||
<td class="px-4 py-2 text-gray-700">
|
||||
{#if col.key === "no"}
|
||||
{offset + i + 1}
|
||||
{:else if col.key === "is_active" || col.key === "is_verified" || col.key === "is_deleted"}
|
||||
{(row as Record<string, any>)[col.key]
|
||||
? "✅"
|
||||
: "❌"}
|
||||
{:else if col.key === "profile_picture"}
|
||||
{#if row.profile_picture}
|
||||
{#if typeof row[col.key] === "string" && row[col.key]}
|
||||
{#await getPublicUrl(row[col.key] as string) then publicUrl}
|
||||
<a
|
||||
href={publicUrl}
|
||||
target="_blank"
|
||||
class="text-blue-600 hover:underline"
|
||||
>View Picture</a
|
||||
>
|
||||
{:catch}
|
||||
<span class="text-red-500"
|
||||
>Error loading image</span
|
||||
>
|
||||
{/await}
|
||||
{:else}
|
||||
No Picture
|
||||
{/if}
|
||||
{/if}
|
||||
{:else}
|
||||
{(row as Record<string, any>)[col.key]}
|
||||
{/if}
|
||||
</td>
|
||||
{/each}
|
||||
<td class="px-4 py-2">
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-blue-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-blue-700"
|
||||
on:click={() => openModal(row)}
|
||||
>
|
||||
✏️ Edit
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-red-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-red-700"
|
||||
on:click={() => deleteUser(row.id)}
|
||||
>
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
{/if}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination controls -->
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div>
|
||||
Showing {offset + 1}–{Math.min(offset + rowsPerPage, totalItems)} of
|
||||
{totalItems}
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<button
|
||||
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
|
||||
on:click={prevPage}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
{#each Array(totalPages)
|
||||
.fill(0)
|
||||
.map((_, i) => i + 1) as page}
|
||||
<button
|
||||
class="px-3 py-1 rounded border text-sm
|
||||
{currentPage === page
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'bg-white border-gray-300 hover:bg-gray-100'}"
|
||||
on:click={() => goToPage(page)}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
{/each}
|
||||
<button
|
||||
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
|
||||
on:click={nextPage}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
{#if showModal}
|
||||
<div
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||
>
|
||||
<form
|
||||
on:submit|preventDefault={saveUser}
|
||||
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 User" : "Add New User"}
|
||||
</h3>
|
||||
{#each formColumns as col}
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700">
|
||||
{col.title}
|
||||
</label>
|
||||
{#if col.key === "role"}
|
||||
<select
|
||||
name="role"
|
||||
class="w-full border px-3 py-2 rounded {errorClass(
|
||||
'role',
|
||||
)}"
|
||||
bind:value={newUser.role}
|
||||
>
|
||||
<option value="" disabled>Select Role</option>
|
||||
{#each roleUser as role}
|
||||
<option value={role.value}>{role.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.role}
|
||||
<p class="text-red-500 text-xs">
|
||||
{$formErrors.role}
|
||||
</p>
|
||||
{/if}
|
||||
{:else if col.key === "email"}
|
||||
<input
|
||||
name="email"
|
||||
class="w-full border px-3 py-2 rounded {errorClass(
|
||||
'email',
|
||||
)}"
|
||||
type="email"
|
||||
bind:value={newUser.email}
|
||||
placeholder="Email"
|
||||
/>
|
||||
{#if $formErrors.email}
|
||||
<p class="text-red-500 text-xs">
|
||||
{$formErrors.email}
|
||||
</p>
|
||||
{/if}
|
||||
{:else if col.key === "password"}
|
||||
<input
|
||||
name="password"
|
||||
class="w-full border px-3 py-2 rounded {errorClass(
|
||||
'password',
|
||||
)}"
|
||||
type="password"
|
||||
bind:value={newUser.password}
|
||||
placeholder="Password"
|
||||
/>
|
||||
{#if $formErrors.password}
|
||||
<p class="text-red-500 text-xs">
|
||||
{$formErrors.password}
|
||||
</p>
|
||||
{/if}
|
||||
{:else if col.key === "phone"}
|
||||
<input
|
||||
name="phone"
|
||||
class="w-full border px-3 py-2 rounded {errorClass(
|
||||
'phone',
|
||||
)}"
|
||||
type="tel"
|
||||
bind:value={newUser.phone}
|
||||
placeholder="Phone Number"
|
||||
/>
|
||||
{#if $formErrors.phone}
|
||||
<p class="text-red-500 text-xs">
|
||||
{$formErrors.phone}
|
||||
</p>
|
||||
{/if}
|
||||
{:else if col.key === "profile_picture"}
|
||||
<div class="space-y-1">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="w-full border px-3 py-2 rounded"
|
||||
on:change={handleFileChange}
|
||||
/>
|
||||
{#if imagePreviewUrl}
|
||||
<img
|
||||
src={imagePreviewUrl}
|
||||
alt="Preview"
|
||||
class="mt-2 max-h-48 rounded border"
|
||||
/>
|
||||
<p class="text-xs text-gray-500">
|
||||
Preview of selected image
|
||||
</p>
|
||||
{:else if newUser.profile_picture}
|
||||
{#await getPublicUrl(newUser.profile_picture) then publicUrl}
|
||||
<img
|
||||
src={publicUrl}
|
||||
alt="Profile Picture"
|
||||
class="mt-2 max-h-48 rounded border"
|
||||
/>
|
||||
{:catch}
|
||||
<span class="text-red-500"
|
||||
>Error loading image</span
|
||||
>
|
||||
{/await}
|
||||
{:else}
|
||||
<span class="text-gray-400">No Image</span>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if col.key === "is_active" || col.key === "is_verified" || col.key === "is_deleted"}
|
||||
<select
|
||||
name={col.key}
|
||||
class="w-full border px-3 py-2 rounded"
|
||||
bind:value={newUser[col.key]}
|
||||
>
|
||||
<option value={true}>Yes</option>
|
||||
<option value={false}>No</option>
|
||||
</select>
|
||||
{:else}
|
||||
<input
|
||||
name={col.key}
|
||||
class="w-full border px-3 py-2 rounded {errorClass(
|
||||
col.key,
|
||||
)}"
|
||||
type="text"
|
||||
bind:value={newUser[col.key]}
|
||||
placeholder={col.title}
|
||||
/>
|
||||
{#if $formErrors[col.key]}
|
||||
<p class="text-red-500 text-xs">
|
||||
{$formErrors[col.key]}
|
||||
</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</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"
|
||||
type="button"
|
||||
on:click={closeModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-2 text-sm rounded bg-blue-600 text-white hover:bg-blue-700"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -130,6 +130,7 @@
|
||||
type Issue = {
|
||||
id: string;
|
||||
name: string;
|
||||
villa_id: string;
|
||||
villa_name: string;
|
||||
area_of_villa: string;
|
||||
priority: string;
|
||||
@@ -148,11 +149,12 @@
|
||||
follow_up: boolean;
|
||||
need_approval: boolean;
|
||||
created_at: string;
|
||||
// Optional field to hold the name of the villa
|
||||
};
|
||||
|
||||
type issueInsert = {
|
||||
name: string;
|
||||
villa_name: string;
|
||||
villa_id: string;
|
||||
area_of_villa: string;
|
||||
priority: string;
|
||||
issue_type: string;
|
||||
@@ -244,7 +246,7 @@
|
||||
}
|
||||
|
||||
// Ambil semua villa_id unik dari issues
|
||||
const villaIds = [...new Set(issues.map((i: Issue) => i.villa_name))];
|
||||
const villaIds = [...new Set(issues.map((i: Issue) => i.villa_id))];
|
||||
|
||||
const { data: villas, error: villaError } = await supabase
|
||||
.from("villas")
|
||||
@@ -260,7 +262,7 @@
|
||||
allRows = issues.map((issue: Issue) => ({
|
||||
...issue,
|
||||
villa_name:
|
||||
villas.find((v) => v.id === issue.villa_name).name || null,
|
||||
villas.find((v) => v.id === issue.villa_id).name || null,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -268,10 +270,6 @@
|
||||
let rowsPerPage = limit;
|
||||
$: totalPages = Math.ceil(totalItems / rowsPerPage);
|
||||
|
||||
function editIssue(id: number) {
|
||||
alert(`Edit issue with ID ${id}`);
|
||||
}
|
||||
|
||||
function goToPage(page: number) {
|
||||
if (page >= 1 && page <= totalPages) currentPage = page;
|
||||
}
|
||||
@@ -352,7 +350,7 @@
|
||||
} else {
|
||||
const issueInsert: issueInsert = {
|
||||
name: formData.get("name") as string,
|
||||
villa_name: formData.get("villa_name") as string,
|
||||
villa_id: formData.get("villa_id") as string,
|
||||
area_of_villa: formData.get("area_of_villa") as string,
|
||||
priority: formData.get("priority") as string,
|
||||
issue_type: formData.get("issue_type") as string,
|
||||
@@ -429,7 +427,7 @@
|
||||
"name",
|
||||
"description_of_the_issue",
|
||||
"issue_source",
|
||||
"villa_name",
|
||||
"villa_id",
|
||||
"reported_date",
|
||||
"reported_by",
|
||||
"priority",
|
||||
@@ -1029,15 +1027,15 @@
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if col.key === "villa_name"}
|
||||
{:else if col.key === "villa_id"}
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-gray-700"
|
||||
>Villa Name</label
|
||||
>
|
||||
<select
|
||||
name="villa_name"
|
||||
name="villa_id"
|
||||
class="w-full border px-3 py-2 rounded {errorClass(
|
||||
'villa_name',
|
||||
'villa_id',
|
||||
)}"
|
||||
bind:value={newIssue[col.key as keyof Issue]}
|
||||
>
|
||||
@@ -1048,9 +1046,9 @@
|
||||
<option value={villa.id}>{villa.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if $formErrors.villa_name}
|
||||
{#if $formErrors.villa_id}
|
||||
<p class="text-red-500 text-xs">
|
||||
{$formErrors.villa_name}
|
||||
{$formErrors.villa_id}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
input_by: string;
|
||||
project_due_date: string;
|
||||
picture_link: string;
|
||||
villa_data?: string; // Optional, if not always present
|
||||
};
|
||||
|
||||
type insetProject = {
|
||||
@@ -31,7 +32,6 @@
|
||||
input_by: string;
|
||||
issue_number: string;
|
||||
issue_id: string;
|
||||
villa_name: string;
|
||||
report_date: string;
|
||||
project_due_date: string;
|
||||
};
|
||||
@@ -82,12 +82,34 @@
|
||||
if (page >= 1 && page <= totalPages) currentPage = page;
|
||||
}
|
||||
|
||||
async function fetchProjects() {
|
||||
// Fetch all projects
|
||||
const { data, error } = await supabase
|
||||
.from("projects")
|
||||
.select("*")
|
||||
.order("id", { ascending: false });
|
||||
async function fetchProjects(
|
||||
filter: string | null = null,
|
||||
searchTerm: string | null = null,
|
||||
sortBy: string | null = null,
|
||||
sortOrder: "asc" | "desc" = "asc",
|
||||
offset: number = 0,
|
||||
limit: number = 10,
|
||||
) {
|
||||
let query = supabase
|
||||
.from("projects_data")
|
||||
.select("*", { count: "exact" })
|
||||
.order(sortBy || "created_at", { ascending: sortOrder === "asc" })
|
||||
.range(offset, offset + limit - 1);
|
||||
// Apply filter if provided
|
||||
if (filter) {
|
||||
query = query.eq("priority", filter);
|
||||
}
|
||||
// Apply search term if provided
|
||||
if (searchTerm) {
|
||||
query = query.ilike("issue_name", `%${searchTerm}%`);
|
||||
}
|
||||
|
||||
// Fetch projects
|
||||
const { data, error } = await query;
|
||||
if (error) {
|
||||
console.error("Error fetching projects:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
// ambil issue_id dari projects kemudian ambil data issue yang sesuai
|
||||
const issueIds = data?.map((project: Project) => project.issue_id);
|
||||
@@ -103,6 +125,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
allRows = []; // Reset allRows before populating
|
||||
|
||||
// Set allRows to the combined data
|
||||
allRows = data.map((project: Project) => {
|
||||
const issue = issueData.find(
|
||||
@@ -122,7 +146,7 @@
|
||||
area_of_villa: issue ? issue.area_of_villa : "Unknown",
|
||||
input_by: project.input_by,
|
||||
issue_number: issue ? issue.issue_number : "Unknown",
|
||||
villa_name: issue ? issue.villa_name : "Unknown",
|
||||
villa_name: issue ? project.villa_data : "Unknown",
|
||||
report_date: issue ? issue.reported_date : "Unknown",
|
||||
project_due_date: project.project_due_date,
|
||||
};
|
||||
@@ -357,21 +381,54 @@
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<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-lg font-semibold text-gray-800">Project List</h2>
|
||||
<h2 class="text-lg font-semibold text-gray-800">📋 Project List</h2>
|
||||
<p class="text-sm text-gray-600">
|
||||
Manage your projects here. You can add, edit, or delete
|
||||
projects.
|
||||
Manage your projects and tasks efficiently.
|
||||
</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();
|
||||
fetchProjects(null, searchTerm, "created_at", "desc");
|
||||
}}
|
||||
/>
|
||||
<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 filter = (e.target as HTMLSelectElement).value;
|
||||
fetchProjects(filter, null, null, "desc");
|
||||
}}
|
||||
>
|
||||
<option value="">Filter by Priority</option>
|
||||
<option value="High">High</option>
|
||||
<option value="Medium">Medium</option>
|
||||
<option value="Low">Low</option>
|
||||
</select>
|
||||
<button
|
||||
class="bg-gray-200 text-gray-700 px-4 py-2 rounded-xl hover:bg-gray-300 text-sm transition"
|
||||
on:click={() =>
|
||||
fetchProjects(null, null, "created_at", "desc", 0, 10)}
|
||||
>
|
||||
🔄 Reset
|
||||
</button>
|
||||
<button
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
|
||||
on:click={() => openModal()}
|
||||
>
|
||||
➕ Add Projects
|
||||
➕ Add Project
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
|
||||
@@ -122,12 +122,28 @@
|
||||
if (page >= 1 && page <= totalPages) currentPage = page;
|
||||
}
|
||||
|
||||
async function fetchPurchaseOrder() {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.select("*")
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
async function fetchPurchaseOrder(
|
||||
filter: string | null = null,
|
||||
search: string | null = null,
|
||||
sort: string | null = null,
|
||||
order: "asc" | "desc" = "desc",
|
||||
offset: number = 0,
|
||||
limit: number = 1000,
|
||||
) {
|
||||
let query = supabase
|
||||
.from("purchaseorder_data")
|
||||
.select(
|
||||
"id, purchase_order_number, villa_data, issue_id, prepared_date, po_type, po_quantity, po_status, approved_vendor, acknowledged, acknowledge_by, approved_price, approved_quantity, total_approved_order_amount, approval, completed_status, received, received_by, input_by, approved_by, created_at",
|
||||
)
|
||||
.order(sort || "created_at", { ascending: order === "asc" })
|
||||
.range(offset, offset + limit - 1);
|
||||
if (filter) {
|
||||
query = query.eq("po_type", filter);
|
||||
}
|
||||
if (search) {
|
||||
query = query.ilike("purchase_order_number", `%${search}%`);
|
||||
}
|
||||
const { data, error } = await query;
|
||||
if (error) {
|
||||
console.error("Error fetching purchase orders:", error);
|
||||
return;
|
||||
@@ -158,7 +174,7 @@
|
||||
// masukkan villa name dan issue name ke dalam data
|
||||
allRows = data.map((row) => {
|
||||
const issue = issues.find((issue) => issue.id === row.issue_id);
|
||||
const villa = villas.find((villa) => villa.id === issue.villa_name);
|
||||
const villa = villas.find((villa) => villa.id === issue.villa_id);
|
||||
const vendor = vendors.find(
|
||||
(vendor) => vendor.id === row.approved_vendor,
|
||||
);
|
||||
@@ -166,7 +182,7 @@
|
||||
return {
|
||||
...row,
|
||||
name: issue ? issue.name : "Unknown Issue",
|
||||
villa_name: villa ? villa.name : "Unknown Villa",
|
||||
villa_name: row.villa_data,
|
||||
priority: issue ? issue.priority : "Unknown Priority",
|
||||
approved_vendor: vendor
|
||||
? vendor.name
|
||||
@@ -476,9 +492,14 @@
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<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-lg font-semibold text-gray-800">
|
||||
<h2
|
||||
class="text-lg font-semibold text-gray-800 flex items-center gap-2"
|
||||
>
|
||||
<span>📦</span>
|
||||
Purchase Order List
|
||||
</h2>
|
||||
<p class="text-sm text-gray-600">
|
||||
@@ -486,6 +507,29 @@
|
||||
delete purchase orders as needed.
|
||||
</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();
|
||||
fetchPurchaseOrder(null, searchTerm, "created_at", "desc");
|
||||
}}
|
||||
/>
|
||||
<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 filter = (e.target as HTMLSelectElement).value;
|
||||
fetchPurchaseOrder(filter, null, null, "desc");
|
||||
}}
|
||||
>
|
||||
<option value="">All Issues</option>
|
||||
<option value="PROJECT">Project Issues</option>
|
||||
<option value="PURCHASE_ORDER">Purchase Order Issues</option>
|
||||
</select>
|
||||
<button
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
|
||||
on:click={() => openModal()}
|
||||
@@ -493,6 +537,7 @@
|
||||
➕ Add Purchase Order
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
@@ -526,33 +571,6 @@
|
||||
>
|
||||
{row[col.key as keyof PurchaseOrderDisplay]}
|
||||
</td>
|
||||
{:else if col.key === "po_status"}
|
||||
<td class="px-4 py-2">
|
||||
<select
|
||||
bind:value={
|
||||
row[
|
||||
col.key as keyof PurchaseOrderDisplay
|
||||
]
|
||||
}
|
||||
class="w-full p-3 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
on:change={(e: Event) => {
|
||||
updatePurchaseOrderStatus(
|
||||
e,
|
||||
row.id,
|
||||
row,
|
||||
);
|
||||
}}
|
||||
>
|
||||
{#each statusOptions as option}
|
||||
<option
|
||||
value={option.value}
|
||||
style="background-color: {option.color};"
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</td>
|
||||
{:else if col.key === "approval"}
|
||||
<td class="px-4 py-2">
|
||||
<select
|
||||
@@ -569,9 +587,10 @@
|
||||
row,
|
||||
);
|
||||
}}
|
||||
disabled
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>SELECT APPROVAL</option
|
||||
>ON PROSES</option
|
||||
>
|
||||
<option value="APPROVED"
|
||||
>APPROVED</option
|
||||
@@ -600,6 +619,7 @@
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled
|
||||
/>
|
||||
</td>
|
||||
{:else if col.key === "received"}
|
||||
@@ -621,6 +641,7 @@
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled
|
||||
/>
|
||||
</td>
|
||||
{:else if col.key === "completed_status"}
|
||||
@@ -645,9 +666,10 @@
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>SELECT COMPLETE</option
|
||||
>ON PROSES</option
|
||||
>
|
||||
<option value="APPROVED"
|
||||
>RECEIVED COMPLETE</option
|
||||
|
||||
949
src/routes/backoffice/purchaseorder/acknowledged/+page.svelte
Normal file
949
src/routes/backoffice/purchaseorder/acknowledged/+page.svelte
Normal file
@@ -0,0 +1,949 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import Select from "svelte-select";
|
||||
import { supabase } from "$lib/supabaseClient";
|
||||
|
||||
type PurchaseOrderInsert = {
|
||||
issue_id: string;
|
||||
prepared_date: string;
|
||||
po_type: string;
|
||||
po_quantity: number;
|
||||
po_status: string;
|
||||
approved_vendor: string;
|
||||
acknowledge_by: string;
|
||||
approved_by: string;
|
||||
approved_price: number;
|
||||
completed_status: string;
|
||||
};
|
||||
|
||||
let purchaseOrderInsert: PurchaseOrderInsert = {
|
||||
issue_id: "",
|
||||
prepared_date: "",
|
||||
po_type: "",
|
||||
po_quantity: 0,
|
||||
po_status: "REQUESTED",
|
||||
approved_vendor: "",
|
||||
acknowledge_by: "",
|
||||
approved_by: "",
|
||||
approved_price: 0,
|
||||
completed_status: "",
|
||||
};
|
||||
|
||||
type PurchaseOrders = {
|
||||
id: string;
|
||||
purchase_order_number: string;
|
||||
prepared_date: string;
|
||||
po_type: string;
|
||||
po_quantity: number;
|
||||
po_status: string;
|
||||
approved_vendor: string;
|
||||
acknowledged: boolean;
|
||||
approved_vendor_id: string;
|
||||
acknowledge_by: string;
|
||||
approved_price: number;
|
||||
approved_quantity: number;
|
||||
total_approved_order_amount: number;
|
||||
approval: string;
|
||||
completed_status: string;
|
||||
received: boolean;
|
||||
received_by: string;
|
||||
input_by: string;
|
||||
issue_id: string;
|
||||
approved_by: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
type PurchaseOrderDisplay = {
|
||||
id: string;
|
||||
name: string;
|
||||
purchase_order_number: string;
|
||||
villa_name: string;
|
||||
priority: string;
|
||||
prepared_date: string;
|
||||
po_type: string;
|
||||
po_quantity: number;
|
||||
po_status: string;
|
||||
approved_vendor: string;
|
||||
acknowledged: boolean;
|
||||
acknowledge_by: string;
|
||||
approved_by: string;
|
||||
approved_price: number;
|
||||
approved_quantity: number;
|
||||
total_approved_order_amount: number;
|
||||
approval: string;
|
||||
completed_status: string;
|
||||
received: boolean;
|
||||
received_by: string;
|
||||
};
|
||||
|
||||
let allRows: PurchaseOrderDisplay[] = [];
|
||||
|
||||
type columns = {
|
||||
key: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
const columns: columns[] = [
|
||||
{ key: "name", title: "Name" },
|
||||
{ key: "purchase_order_number", title: "Purchase Order Number" },
|
||||
{ key: "po_quantity", title: "PO Quantity" },
|
||||
{ key: "po_status", title: "PO Status" },
|
||||
{ key: "approved_vendor", title: "Approved Vendor" },
|
||||
{ key: "approved_price", title: "Approved Price" },
|
||||
{ key: "approved_quantity", title: "Approved Quantity" },
|
||||
{
|
||||
key: "total_approved_order_amount",
|
||||
title: "Total Approved Order Amount",
|
||||
},
|
||||
{ key: "acknowledged", title: "Acknowledged" },
|
||||
{ key: "acknowledge_by", title: "Acknowledge By" },
|
||||
{ key: "created_at", title: "Created At" },
|
||||
{ key: "actions", title: "Actions" }, // For edit/delete buttons
|
||||
];
|
||||
|
||||
let currentPage = 1;
|
||||
let rowsPerPage = 10;
|
||||
$: totalPages = Math.ceil(allRows.length / rowsPerPage);
|
||||
$: paginatedRows = allRows.slice(
|
||||
(currentPage - 1) * rowsPerPage,
|
||||
currentPage * rowsPerPage,
|
||||
);
|
||||
|
||||
function goToPage(page: number) {
|
||||
if (page >= 1 && page <= totalPages) currentPage = page;
|
||||
}
|
||||
|
||||
async function fetchPurchaseOrder(
|
||||
filter: string | null = null,
|
||||
search: string | null = null,
|
||||
sort: string | null = null,
|
||||
order: "asc" | "desc" = "desc",
|
||||
offset: number = 0,
|
||||
limit: number = 1000,
|
||||
) {
|
||||
let query = supabase
|
||||
.from("purchaseorder_data")
|
||||
.select(
|
||||
"id, purchase_order_number, villa_data, issue_id, prepared_date, po_type, po_quantity, po_status, approved_vendor, acknowledged, acknowledge_by, approved_price, approved_quantity, total_approved_order_amount, approval, completed_status, received, received_by, input_by, approved_by, created_at",
|
||||
)
|
||||
.order(sort || "created_at", { ascending: order === "asc" })
|
||||
.range(offset, offset + limit - 1);
|
||||
if (filter) {
|
||||
query = query.eq("po_type", filter);
|
||||
}
|
||||
if (search) {
|
||||
query = query.ilike("purchase_order_number", `%${search}%`);
|
||||
}
|
||||
const { data, error } = await query;
|
||||
if (error) {
|
||||
console.error("Error fetching purchase orders:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
// fetch issue and villa names
|
||||
const issueIds = data.map((row) => row.issue_id);
|
||||
const { data: issues, error: issueError } = await supabase
|
||||
.from("issues")
|
||||
.select("*")
|
||||
.in("id", issueIds);
|
||||
|
||||
if (issueError) {
|
||||
console.error("Error fetching issues:", issueError);
|
||||
return;
|
||||
}
|
||||
|
||||
const villaIds = issues.map((row) => row.villa_name).filter(Boolean);
|
||||
const { data: villas, error: villaError } = await supabase
|
||||
.from("villas")
|
||||
.select("id, name")
|
||||
.in("id", villaIds);
|
||||
if (villaError) {
|
||||
console.error("Error fetching villas:", villaError);
|
||||
return;
|
||||
}
|
||||
|
||||
// masukkan villa name dan issue name ke dalam data
|
||||
allRows = data.map((row) => {
|
||||
const issue = issues.find((issue) => issue.id === row.issue_id);
|
||||
const villa = villas.find((villa) => villa.id === issue.villa_id);
|
||||
const vendor = vendors.find(
|
||||
(vendor) => vendor.id === row.approved_vendor,
|
||||
);
|
||||
|
||||
return {
|
||||
...row,
|
||||
name: issue ? issue.name : "Unknown Issue",
|
||||
villa_name: row.villa_data,
|
||||
priority: issue ? issue.priority : "Unknown Priority",
|
||||
approved_vendor: vendor
|
||||
? vendor.name
|
||||
: "Unknown Approved Vendor",
|
||||
approval: row.approval || "",
|
||||
completed_status: row.completed_status || "",
|
||||
} as PurchaseOrderDisplay;
|
||||
});
|
||||
}
|
||||
|
||||
//fetch all issues
|
||||
async function fetchIssues() {
|
||||
const { data, error } = await supabase
|
||||
.from("issues")
|
||||
.select("id, name");
|
||||
|
||||
if (error) {
|
||||
console.error("Error fetching issues:", error);
|
||||
return [];
|
||||
}
|
||||
|
||||
issues = data.map((issue) => ({
|
||||
id: issue.id,
|
||||
name: issue.name,
|
||||
}));
|
||||
}
|
||||
|
||||
async function fetchVendors() {
|
||||
const { data, error } = await supabase
|
||||
.from("vendor")
|
||||
.select("id, name");
|
||||
|
||||
if (error) {
|
||||
console.error("Error fetching vendors:", error);
|
||||
return [];
|
||||
}
|
||||
|
||||
vendors = data.map((vendor) => ({
|
||||
id: vendor.id,
|
||||
name: vendor.name,
|
||||
}));
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchPurchaseOrder();
|
||||
fetchVendors();
|
||||
});
|
||||
|
||||
$: currentPage = 1; // Reset to first page when allRows changes
|
||||
|
||||
let showModal = false;
|
||||
let isEditing = false;
|
||||
let currentEditingId: string | null = null;
|
||||
let newPurchaseOrders: Record<string, any> = {};
|
||||
let vendors: { id: string; name: string }[] = [];
|
||||
let issues: { id: string; name: string }[] = [];
|
||||
const excludedKeys = [
|
||||
"id",
|
||||
"priority",
|
||||
"villa_name",
|
||||
"purchase_order_number",
|
||||
"issue_id",
|
||||
"number_project",
|
||||
"input_by",
|
||||
"created_at",
|
||||
"actions",
|
||||
"acknowledged",
|
||||
"acknowledge_by",
|
||||
"approval",
|
||||
"completed_status",
|
||||
"received",
|
||||
"received_by",
|
||||
"approved_quantity",
|
||||
"total_approved_order_amount",
|
||||
"approved_by",
|
||||
"name",
|
||||
"po_status",
|
||||
];
|
||||
const formColumns = columns.filter(
|
||||
(col) => !excludedKeys.includes(col.key),
|
||||
);
|
||||
|
||||
async function openModal(purchase: PurchaseOrderDisplay | null = null) {
|
||||
await fetchIssues();
|
||||
if (purchase) {
|
||||
isEditing = true;
|
||||
currentEditingId = purchase.id;
|
||||
newPurchaseOrders = { ...purchase };
|
||||
} else {
|
||||
isEditing = false;
|
||||
currentEditingId = null;
|
||||
newPurchaseOrders = {};
|
||||
}
|
||||
showModal = true;
|
||||
}
|
||||
|
||||
async function saveProject() {
|
||||
purchaseOrderInsert = {
|
||||
issue_id: newPurchaseOrders.issue_id || "",
|
||||
prepared_date: newPurchaseOrders.prepared_date || "",
|
||||
po_type: newPurchaseOrders.po_type || "",
|
||||
po_quantity: newPurchaseOrders.po_quantity || 0,
|
||||
po_status: newPurchaseOrders.po_status || "REQUESTED",
|
||||
approved_vendor: newPurchaseOrders.approved_vendor || "",
|
||||
acknowledge_by: newPurchaseOrders.acknowledge_by || "",
|
||||
approved_price: newPurchaseOrders.approved_price || "",
|
||||
approved_by: newPurchaseOrders.approved_by || "",
|
||||
completed_status: newPurchaseOrders.completed_status || "",
|
||||
};
|
||||
|
||||
if (isEditing && currentEditingId) {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update(purchaseOrderInsert)
|
||||
.eq("id", currentEditingId);
|
||||
|
||||
if (error) {
|
||||
console.error("Error updating purchase order:", error);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.insert(purchaseOrderInsert);
|
||||
|
||||
if (error) {
|
||||
console.error("Error inserting purchase order:", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
showModal = false;
|
||||
}
|
||||
|
||||
async function deleteProject(id: string) {
|
||||
const { error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.delete()
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error deleting project:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
const statusOptions = [
|
||||
{ label: "Requested", value: "REQUESTED", color: "#e5e7eb" },
|
||||
{ label: "Prepared", value: "PREPARED", color: "#93c5fd" },
|
||||
{ label: "Approved", value: "APPROVED", color: "#34d399" },
|
||||
{ label: "Acknowledged", value: "ACKNOWLEDGE", color: "#60a5fa" },
|
||||
{
|
||||
label: "Received - Incomplete",
|
||||
value: "RECEIVE - INCOMPLETE",
|
||||
color: "#fb923c",
|
||||
},
|
||||
{
|
||||
label: "Received - Completed",
|
||||
value: "RECEIVE COMPLETED",
|
||||
color: "#10b981",
|
||||
},
|
||||
{ label: "Canceled", value: "CANCELED", color: "#f87171" },
|
||||
];
|
||||
|
||||
function getStatusOption(value: string) {
|
||||
return statusOptions.find((option) => option.value === value) ?? null;
|
||||
}
|
||||
|
||||
//validate input fields purchase order
|
||||
function validateInput() {
|
||||
const requiredFields = [
|
||||
"prepared_date",
|
||||
"po_type",
|
||||
"po_quantity",
|
||||
"approved_vendor",
|
||||
];
|
||||
for (const field of requiredFields) {
|
||||
if (!newPurchaseOrders[field]) {
|
||||
alert(`Please fill in the ${field} field.`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateInputApproval() {
|
||||
const requiredFields = ["approved_price"];
|
||||
for (const field of requiredFields) {
|
||||
if (!newPurchaseOrders[field]) {
|
||||
alert(`Please fill in the ${field} field.`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function updatePurchaseOrderStatus(
|
||||
e: Event,
|
||||
id: string,
|
||||
row: PurchaseOrderDisplay,
|
||||
) {
|
||||
const selectedOption = (e.target as HTMLSelectElement)?.value ?? "";
|
||||
const option = getStatusOption(selectedOption);
|
||||
|
||||
newPurchaseOrders = {
|
||||
...row,
|
||||
po_status: option?.value || row.po_status,
|
||||
};
|
||||
|
||||
if (option?.value === "APPROVED") {
|
||||
if (!validateInput()) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ po_status: newPurchaseOrders.po_status })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error updating purchase order status:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
async function acknowledgedOk(id: string, status: boolean) {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ acknowledged: status })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error acknowledging purchase order:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
async function receivedOk(id: string, status: boolean) {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ receivedOk: status })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error acknowledging purchase order:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
async function updatePurchaseOrderApprovalStatus(
|
||||
e: Event,
|
||||
id: string,
|
||||
row: PurchaseOrderDisplay,
|
||||
) {
|
||||
const selectedOption = (e.target as HTMLSelectElement)?.value ?? "";
|
||||
const option = getStatusOption(selectedOption);
|
||||
|
||||
newPurchaseOrders = {
|
||||
...row,
|
||||
approval: option?.value || row.approval,
|
||||
};
|
||||
|
||||
if (option?.value === "APPROVED") {
|
||||
if (!validateInputApproval()) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ approval: newPurchaseOrders.approval })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error updating purchase order status:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
async function completedStatusOk(id: string, status: string) {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ completed_status: status })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error acknowledging purchase order:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
</script>
|
||||
|
||||
<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-lg font-semibold text-gray-800 flex items-center gap-2"
|
||||
>
|
||||
<span>📦</span>
|
||||
Purchase Order Acknowledged List
|
||||
</h2>
|
||||
<p class="text-sm text-gray-600">
|
||||
Manage your purchase orders efficiently. You can add, edit, or
|
||||
delete purchase orders as needed.
|
||||
</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();
|
||||
fetchPurchaseOrder(null, searchTerm, "created_at", "desc");
|
||||
}}
|
||||
/>
|
||||
<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 filter = (e.target as HTMLSelectElement).value;
|
||||
fetchPurchaseOrder(filter, null, null, "desc");
|
||||
}}
|
||||
>
|
||||
<option value="">All Issues</option>
|
||||
<option value="PROJECT">Project Issues</option>
|
||||
<option value="PURCHASE_ORDER">Purchase Order Issues</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<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}
|
||||
{#if col.key === "name"}
|
||||
<th
|
||||
class="sticky left-0 px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||||
style="background-color: #f0f8ff; z-index: 10;"
|
||||
>
|
||||
{col.title}
|
||||
</th>
|
||||
{:else}
|
||||
<th
|
||||
class="px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||||
>
|
||||
{col.title}
|
||||
</th>
|
||||
{/if}
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
{#each paginatedRows as row}
|
||||
<tr class="hover:bg-gray-50 transition">
|
||||
{#each columns as col}
|
||||
{#if col.key === "name"}
|
||||
<td
|
||||
class="sticky left-0 px-4 py-2 font-medium text-blue-600"
|
||||
style="background-color: #f0f8ff; cursor: pointer;"
|
||||
>
|
||||
{row[col.key as keyof PurchaseOrderDisplay]}
|
||||
</td>
|
||||
{:else if col.key === "approval"}
|
||||
<td class="px-4 py-2">
|
||||
<select
|
||||
bind:value={
|
||||
row[
|
||||
col.key as keyof PurchaseOrderDisplay
|
||||
]
|
||||
}
|
||||
class="w-full p-3 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500 text-gray-900"
|
||||
on:change={(e: Event) => {
|
||||
updatePurchaseOrderApprovalStatus(
|
||||
e,
|
||||
row.id,
|
||||
row,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>SELECT APPROVAL</option
|
||||
>
|
||||
<option value="APPROVED"
|
||||
>APPROVED</option
|
||||
>
|
||||
<option value="REJECTED"
|
||||
>REJECTED</option
|
||||
>
|
||||
</select>
|
||||
</td>
|
||||
{:else if col.key === "acknowledged"}
|
||||
<td class="px-4 py-2 text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={row.acknowledged}
|
||||
on:change={async (e) => {
|
||||
const isChecked = (
|
||||
e.target as HTMLInputElement
|
||||
).checked;
|
||||
row.acknowledged = isChecked;
|
||||
|
||||
if (isChecked) {
|
||||
// map to project
|
||||
await acknowledgedOk(
|
||||
row.id,
|
||||
isChecked,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
{:else if col.key === "received"}
|
||||
<td class="px-4 py-2 text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={row.received}
|
||||
on:change={async (e) => {
|
||||
const isChecked = (
|
||||
e.target as HTMLInputElement
|
||||
).checked;
|
||||
row.received = isChecked;
|
||||
|
||||
if (isChecked) {
|
||||
// map to project
|
||||
await receivedOk(
|
||||
row.id,
|
||||
isChecked,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
{:else if col.key === "completed_status"}
|
||||
<td class="px-4 py-2">
|
||||
<select
|
||||
bind:value={
|
||||
row[
|
||||
col.key as keyof PurchaseOrderDisplay
|
||||
]
|
||||
}
|
||||
class="w-full p-3 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500 text-gray-900"
|
||||
on:change={async (e) => {
|
||||
const isValue = (
|
||||
e.target as HTMLInputElement
|
||||
).value;
|
||||
|
||||
if (isValue) {
|
||||
// map to project
|
||||
await completedStatusOk(
|
||||
row.id,
|
||||
isValue,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>SELECT COMPLETE</option
|
||||
>
|
||||
<option value="APPROVED"
|
||||
>RECEIVED COMPLETE</option
|
||||
>
|
||||
<option value="REJECTED"
|
||||
>COMPLETE INCOMPLETE</option
|
||||
>
|
||||
</select>
|
||||
</td>
|
||||
{:else if col.key === "actions"}
|
||||
<td class="px-4 py-2">
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-blue-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-blue-700"
|
||||
on:click={() => openModal(row)}
|
||||
>
|
||||
✏️ Edit
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-red-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-red-700"
|
||||
on:click={() => deleteProject(row.id)}
|
||||
>
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</td>
|
||||
{:else if col.key === "move_issue"}
|
||||
<td class="px-4 py-2">
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-green-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-green-700"
|
||||
on:click={() =>
|
||||
alert(
|
||||
`Move issue ${row.id} to project`,
|
||||
)}
|
||||
>
|
||||
➡️ PROJECT
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-yellow-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-yellow-700"
|
||||
on:click={() =>
|
||||
alert(
|
||||
`Move issue ${row.id} to another area`,
|
||||
)}
|
||||
>
|
||||
➡️ PURCHASE ORDER
|
||||
</button>
|
||||
</td>
|
||||
{:else}
|
||||
<td class="px-4 py-2 text-gray-700"
|
||||
>{row[
|
||||
col.key as keyof PurchaseOrderDisplay
|
||||
]}</td
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination controls -->
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div>
|
||||
Showing {(currentPage - 1) * rowsPerPage + 1}–
|
||||
{Math.min(currentPage * rowsPerPage, allRows.length)} of {allRows.length}
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<button
|
||||
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
|
||||
on:click={() => goToPage(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
{#each Array(totalPages)
|
||||
.fill(0)
|
||||
.map((_, i) => i + 1) as page}
|
||||
<button
|
||||
class="px-3 py-1 rounded border text-sm
|
||||
{currentPage === page
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'bg-white border-gray-300 hover:bg-gray-100'}"
|
||||
on:click={() => goToPage(page)}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
{/each}
|
||||
<button
|
||||
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
|
||||
on:click={() => goToPage(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showModal}
|
||||
<div
|
||||
class="fixed inset-0 flex items-center justify-center bg-gray-800 bg-opacity-50 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 mb-4">
|
||||
{isEditing ? "Edit Project" : "Add Project"}
|
||||
</h3>
|
||||
<form on:submit|preventDefault={saveProject}>
|
||||
<!-- choose issuess -->
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for="issue_id"
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Choose Issue
|
||||
</label>
|
||||
<select
|
||||
id="issue_id"
|
||||
bind:value={newPurchaseOrders.issue_id}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
on:change={(e: Event) => {
|
||||
const selectedIssue =
|
||||
(e.target as HTMLSelectElement)?.value ?? "";
|
||||
newPurchaseOrders.issue_id = selectedIssue;
|
||||
}}
|
||||
>
|
||||
<option value="">Select Issue</option>
|
||||
{#each issues as issue}
|
||||
<option value={issue.id}>{issue.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{#each formColumns as col}
|
||||
{#if col.key === "po_status"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<select
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
on:change={(e: Event) => {
|
||||
const selectedOption =
|
||||
(e.target as HTMLSelectElement)
|
||||
?.value ?? "";
|
||||
const option =
|
||||
getStatusOption(selectedOption);
|
||||
if (
|
||||
option?.value === "APPROVED" &&
|
||||
!validateInput()
|
||||
) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#each statusOptions as option}
|
||||
<option
|
||||
value={option.value}
|
||||
style="background-color: {option.color};"
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{:else if col.key === "po_type"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<select
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Select PO Type</option>
|
||||
<option value="Regular">Regular</option>
|
||||
<option value="Urgent">Urgent</option>
|
||||
</select>
|
||||
</div>
|
||||
{:else if col.key === "prepared_date"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{:else if col.key === "po_quantity"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{:else if col.key === "approved_price"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{:else if col.key === "approved_vendor"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<select
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
on:change={(e: Event) => {
|
||||
const selectedVendor =
|
||||
(e.target as HTMLSelectElement)
|
||||
?.value ?? "";
|
||||
newPurchaseOrders[col.key] = selectedVendor;
|
||||
}}
|
||||
>
|
||||
<option value="">Select Vendor</option>
|
||||
{#each vendors as vendor}
|
||||
<option value={vendor.id}>
|
||||
{vendor.name}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
<div class="flex justify-end space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300"
|
||||
on:click={() => (showModal = false)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
949
src/routes/backoffice/purchaseorder/approval/+page.svelte
Normal file
949
src/routes/backoffice/purchaseorder/approval/+page.svelte
Normal file
@@ -0,0 +1,949 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import Select from "svelte-select";
|
||||
import { supabase } from "$lib/supabaseClient";
|
||||
|
||||
type PurchaseOrderInsert = {
|
||||
issue_id: string;
|
||||
prepared_date: string;
|
||||
po_type: string;
|
||||
po_quantity: number;
|
||||
po_status: string;
|
||||
approved_vendor: string;
|
||||
acknowledge_by: string;
|
||||
approved_by: string;
|
||||
approved_price: number;
|
||||
completed_status: string;
|
||||
};
|
||||
|
||||
let purchaseOrderInsert: PurchaseOrderInsert = {
|
||||
issue_id: "",
|
||||
prepared_date: "",
|
||||
po_type: "",
|
||||
po_quantity: 0,
|
||||
po_status: "REQUESTED",
|
||||
approved_vendor: "",
|
||||
acknowledge_by: "",
|
||||
approved_by: "",
|
||||
approved_price: 0,
|
||||
completed_status: "",
|
||||
};
|
||||
|
||||
type PurchaseOrders = {
|
||||
id: string;
|
||||
purchase_order_number: string;
|
||||
prepared_date: string;
|
||||
po_type: string;
|
||||
po_quantity: number;
|
||||
po_status: string;
|
||||
approved_vendor: string;
|
||||
acknowledged: boolean;
|
||||
approved_vendor_id: string;
|
||||
acknowledge_by: string;
|
||||
approved_price: number;
|
||||
approved_quantity: number;
|
||||
total_approved_order_amount: number;
|
||||
approval: string;
|
||||
completed_status: string;
|
||||
received: boolean;
|
||||
received_by: string;
|
||||
input_by: string;
|
||||
issue_id: string;
|
||||
approved_by: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
type PurchaseOrderDisplay = {
|
||||
id: string;
|
||||
name: string;
|
||||
purchase_order_number: string;
|
||||
villa_name: string;
|
||||
priority: string;
|
||||
prepared_date: string;
|
||||
po_type: string;
|
||||
po_quantity: number;
|
||||
po_status: string;
|
||||
approved_vendor: string;
|
||||
acknowledged: boolean;
|
||||
acknowledge_by: string;
|
||||
approved_by: string;
|
||||
approved_price: number;
|
||||
approved_quantity: number;
|
||||
total_approved_order_amount: number;
|
||||
approval: string;
|
||||
completed_status: string;
|
||||
received: boolean;
|
||||
received_by: string;
|
||||
};
|
||||
|
||||
let allRows: PurchaseOrderDisplay[] = [];
|
||||
|
||||
type columns = {
|
||||
key: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
const columns: columns[] = [
|
||||
{ key: "name", title: "Name" },
|
||||
{ key: "purchase_order_number", title: "Purchase Order Number" },
|
||||
{ key: "po_quantity", title: "PO Quantity" },
|
||||
{ key: "po_status", title: "PO Status" },
|
||||
{ key: "approved_vendor", title: "Approved Vendor" },
|
||||
{ key: "approved_price", title: "Approved Price" },
|
||||
{ key: "approved_quantity", title: "Approved Quantity" },
|
||||
{
|
||||
key: "total_approved_order_amount",
|
||||
title: "Total Approved Order Amount",
|
||||
},
|
||||
{ key: "approval", title: "Approval" },
|
||||
{ key: "approved_by", title: "Approved By" },
|
||||
{ key: "created_at", title: "Created At" },
|
||||
{ key: "actions", title: "Actions" }, // For edit/delete buttons
|
||||
];
|
||||
|
||||
let currentPage = 1;
|
||||
let rowsPerPage = 10;
|
||||
$: totalPages = Math.ceil(allRows.length / rowsPerPage);
|
||||
$: paginatedRows = allRows.slice(
|
||||
(currentPage - 1) * rowsPerPage,
|
||||
currentPage * rowsPerPage,
|
||||
);
|
||||
|
||||
function goToPage(page: number) {
|
||||
if (page >= 1 && page <= totalPages) currentPage = page;
|
||||
}
|
||||
|
||||
async function fetchPurchaseOrder(
|
||||
filter: string | null = null,
|
||||
search: string | null = null,
|
||||
sort: string | null = null,
|
||||
order: "asc" | "desc" = "desc",
|
||||
offset: number = 0,
|
||||
limit: number = 1000,
|
||||
) {
|
||||
let query = supabase
|
||||
.from("purchaseorder_data")
|
||||
.select(
|
||||
"id, purchase_order_number, villa_data, issue_id, prepared_date, po_type, po_quantity, po_status, approved_vendor, acknowledged, acknowledge_by, approved_price, approved_quantity, total_approved_order_amount, approval, completed_status, received, received_by, input_by, approved_by, created_at",
|
||||
)
|
||||
.order(sort || "created_at", { ascending: order === "asc" })
|
||||
.range(offset, offset + limit - 1);
|
||||
if (filter) {
|
||||
query = query.eq("po_type", filter);
|
||||
}
|
||||
if (search) {
|
||||
query = query.ilike("purchase_order_number", `%${search}%`);
|
||||
}
|
||||
const { data, error } = await query;
|
||||
if (error) {
|
||||
console.error("Error fetching purchase orders:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
// fetch issue and villa names
|
||||
const issueIds = data.map((row) => row.issue_id);
|
||||
const { data: issues, error: issueError } = await supabase
|
||||
.from("issues")
|
||||
.select("*")
|
||||
.in("id", issueIds);
|
||||
|
||||
if (issueError) {
|
||||
console.error("Error fetching issues:", issueError);
|
||||
return;
|
||||
}
|
||||
|
||||
const villaIds = issues.map((row) => row.villa_name).filter(Boolean);
|
||||
const { data: villas, error: villaError } = await supabase
|
||||
.from("villas")
|
||||
.select("id, name")
|
||||
.in("id", villaIds);
|
||||
if (villaError) {
|
||||
console.error("Error fetching villas:", villaError);
|
||||
return;
|
||||
}
|
||||
|
||||
// masukkan villa name dan issue name ke dalam data
|
||||
allRows = data.map((row) => {
|
||||
const issue = issues.find((issue) => issue.id === row.issue_id);
|
||||
const villa = villas.find((villa) => villa.id === issue.villa_id);
|
||||
const vendor = vendors.find(
|
||||
(vendor) => vendor.id === row.approved_vendor,
|
||||
);
|
||||
|
||||
return {
|
||||
...row,
|
||||
name: issue ? issue.name : "Unknown Issue",
|
||||
villa_name: row.villa_data,
|
||||
priority: issue ? issue.priority : "Unknown Priority",
|
||||
approved_vendor: vendor
|
||||
? vendor.name
|
||||
: "Unknown Approved Vendor",
|
||||
approval: row.approval || "",
|
||||
completed_status: row.completed_status || "",
|
||||
} as PurchaseOrderDisplay;
|
||||
});
|
||||
}
|
||||
|
||||
//fetch all issues
|
||||
async function fetchIssues() {
|
||||
const { data, error } = await supabase
|
||||
.from("issues")
|
||||
.select("id, name");
|
||||
|
||||
if (error) {
|
||||
console.error("Error fetching issues:", error);
|
||||
return [];
|
||||
}
|
||||
|
||||
issues = data.map((issue) => ({
|
||||
id: issue.id,
|
||||
name: issue.name,
|
||||
}));
|
||||
}
|
||||
|
||||
async function fetchVendors() {
|
||||
const { data, error } = await supabase
|
||||
.from("vendor")
|
||||
.select("id, name");
|
||||
|
||||
if (error) {
|
||||
console.error("Error fetching vendors:", error);
|
||||
return [];
|
||||
}
|
||||
|
||||
vendors = data.map((vendor) => ({
|
||||
id: vendor.id,
|
||||
name: vendor.name,
|
||||
}));
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchPurchaseOrder();
|
||||
fetchVendors();
|
||||
});
|
||||
|
||||
$: currentPage = 1; // Reset to first page when allRows changes
|
||||
|
||||
let showModal = false;
|
||||
let isEditing = false;
|
||||
let currentEditingId: string | null = null;
|
||||
let newPurchaseOrders: Record<string, any> = {};
|
||||
let vendors: { id: string; name: string }[] = [];
|
||||
let issues: { id: string; name: string }[] = [];
|
||||
const excludedKeys = [
|
||||
"id",
|
||||
"priority",
|
||||
"villa_name",
|
||||
"purchase_order_number",
|
||||
"issue_id",
|
||||
"number_project",
|
||||
"input_by",
|
||||
"created_at",
|
||||
"actions",
|
||||
"acknowledged",
|
||||
"acknowledge_by",
|
||||
"approval",
|
||||
"completed_status",
|
||||
"received",
|
||||
"received_by",
|
||||
"approved_quantity",
|
||||
"total_approved_order_amount",
|
||||
"approved_by",
|
||||
"name",
|
||||
"po_status",
|
||||
];
|
||||
const formColumns = columns.filter(
|
||||
(col) => !excludedKeys.includes(col.key),
|
||||
);
|
||||
|
||||
async function openModal(purchase: PurchaseOrderDisplay | null = null) {
|
||||
await fetchIssues();
|
||||
if (purchase) {
|
||||
isEditing = true;
|
||||
currentEditingId = purchase.id;
|
||||
newPurchaseOrders = { ...purchase };
|
||||
} else {
|
||||
isEditing = false;
|
||||
currentEditingId = null;
|
||||
newPurchaseOrders = {};
|
||||
}
|
||||
showModal = true;
|
||||
}
|
||||
|
||||
async function saveProject() {
|
||||
purchaseOrderInsert = {
|
||||
issue_id: newPurchaseOrders.issue_id || "",
|
||||
prepared_date: newPurchaseOrders.prepared_date || "",
|
||||
po_type: newPurchaseOrders.po_type || "",
|
||||
po_quantity: newPurchaseOrders.po_quantity || 0,
|
||||
po_status: newPurchaseOrders.po_status || "REQUESTED",
|
||||
approved_vendor: newPurchaseOrders.approved_vendor || "",
|
||||
acknowledge_by: newPurchaseOrders.acknowledge_by || "",
|
||||
approved_price: newPurchaseOrders.approved_price || "",
|
||||
approved_by: newPurchaseOrders.approved_by || "",
|
||||
completed_status: newPurchaseOrders.completed_status || "",
|
||||
};
|
||||
|
||||
if (isEditing && currentEditingId) {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update(purchaseOrderInsert)
|
||||
.eq("id", currentEditingId);
|
||||
|
||||
if (error) {
|
||||
console.error("Error updating purchase order:", error);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.insert(purchaseOrderInsert);
|
||||
|
||||
if (error) {
|
||||
console.error("Error inserting purchase order:", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
showModal = false;
|
||||
}
|
||||
|
||||
async function deleteProject(id: string) {
|
||||
const { error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.delete()
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error deleting project:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
const statusOptions = [
|
||||
{ label: "Requested", value: "REQUESTED", color: "#e5e7eb" },
|
||||
{ label: "Prepared", value: "PREPARED", color: "#93c5fd" },
|
||||
{ label: "Approved", value: "APPROVED", color: "#34d399" },
|
||||
{ label: "Acknowledged", value: "ACKNOWLEDGE", color: "#60a5fa" },
|
||||
{
|
||||
label: "Received - Incomplete",
|
||||
value: "RECEIVE - INCOMPLETE",
|
||||
color: "#fb923c",
|
||||
},
|
||||
{
|
||||
label: "Received - Completed",
|
||||
value: "RECEIVE COMPLETED",
|
||||
color: "#10b981",
|
||||
},
|
||||
{ label: "Canceled", value: "CANCELED", color: "#f87171" },
|
||||
];
|
||||
|
||||
function getStatusOption(value: string) {
|
||||
return statusOptions.find((option) => option.value === value) ?? null;
|
||||
}
|
||||
|
||||
//validate input fields purchase order
|
||||
function validateInput() {
|
||||
const requiredFields = [
|
||||
"prepared_date",
|
||||
"po_type",
|
||||
"po_quantity",
|
||||
"approved_vendor",
|
||||
];
|
||||
for (const field of requiredFields) {
|
||||
if (!newPurchaseOrders[field]) {
|
||||
alert(`Please fill in the ${field} field.`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateInputApproval() {
|
||||
const requiredFields = ["approved_price"];
|
||||
for (const field of requiredFields) {
|
||||
if (!newPurchaseOrders[field]) {
|
||||
alert(`Please fill in the ${field} field.`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function updatePurchaseOrderStatus(
|
||||
e: Event,
|
||||
id: string,
|
||||
row: PurchaseOrderDisplay,
|
||||
) {
|
||||
const selectedOption = (e.target as HTMLSelectElement)?.value ?? "";
|
||||
const option = getStatusOption(selectedOption);
|
||||
|
||||
newPurchaseOrders = {
|
||||
...row,
|
||||
po_status: option?.value || row.po_status,
|
||||
};
|
||||
|
||||
if (option?.value === "APPROVED") {
|
||||
if (!validateInput()) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ po_status: newPurchaseOrders.po_status })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error updating purchase order status:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
async function acknowledgedOk(id: string, status: boolean) {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ acknowledged: status })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error acknowledging purchase order:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
async function receivedOk(id: string, status: boolean) {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ receivedOk: status })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error acknowledging purchase order:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
async function updatePurchaseOrderApprovalStatus(
|
||||
e: Event,
|
||||
id: string,
|
||||
row: PurchaseOrderDisplay,
|
||||
) {
|
||||
const selectedOption = (e.target as HTMLSelectElement)?.value ?? "";
|
||||
const option = getStatusOption(selectedOption);
|
||||
|
||||
newPurchaseOrders = {
|
||||
...row,
|
||||
approval: option?.value || row.approval,
|
||||
};
|
||||
|
||||
if (option?.value === "APPROVED") {
|
||||
if (!validateInputApproval()) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ approval: newPurchaseOrders.approval })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error updating purchase order status:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
async function completedStatusOk(id: string, status: string) {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ completed_status: status })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error acknowledging purchase order:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
</script>
|
||||
|
||||
<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-lg font-semibold text-gray-800 flex items-center gap-2"
|
||||
>
|
||||
<span>📦</span>
|
||||
Purchase Order List
|
||||
</h2>
|
||||
<p class="text-sm text-gray-600">
|
||||
Manage your purchase orders efficiently. You can add, edit, or
|
||||
delete purchase orders as needed.
|
||||
</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();
|
||||
fetchPurchaseOrder(null, searchTerm, "created_at", "desc");
|
||||
}}
|
||||
/>
|
||||
<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 filter = (e.target as HTMLSelectElement).value;
|
||||
fetchPurchaseOrder(filter, null, null, "desc");
|
||||
}}
|
||||
>
|
||||
<option value="">All Issues</option>
|
||||
<option value="PROJECT">Project Issues</option>
|
||||
<option value="PURCHASE_ORDER">Purchase Order Issues</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<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}
|
||||
{#if col.key === "name"}
|
||||
<th
|
||||
class="sticky left-0 px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||||
style="background-color: #f0f8ff; z-index: 10;"
|
||||
>
|
||||
{col.title}
|
||||
</th>
|
||||
{:else}
|
||||
<th
|
||||
class="px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||||
>
|
||||
{col.title}
|
||||
</th>
|
||||
{/if}
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
{#each paginatedRows as row}
|
||||
<tr class="hover:bg-gray-50 transition">
|
||||
{#each columns as col}
|
||||
{#if col.key === "name"}
|
||||
<td
|
||||
class="sticky left-0 px-4 py-2 font-medium text-blue-600"
|
||||
style="background-color: #f0f8ff; cursor: pointer;"
|
||||
>
|
||||
{row[col.key as keyof PurchaseOrderDisplay]}
|
||||
</td>
|
||||
{:else if col.key === "approval"}
|
||||
<td class="px-4 py-2">
|
||||
<select
|
||||
bind:value={
|
||||
row[
|
||||
col.key as keyof PurchaseOrderDisplay
|
||||
]
|
||||
}
|
||||
class="w-full p-3 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500 text-gray-900"
|
||||
on:change={(e: Event) => {
|
||||
updatePurchaseOrderApprovalStatus(
|
||||
e,
|
||||
row.id,
|
||||
row,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>SELECT APPROVAL</option
|
||||
>
|
||||
<option value="APPROVED"
|
||||
>APPROVED</option
|
||||
>
|
||||
<option value="REJECTED"
|
||||
>REJECTED</option
|
||||
>
|
||||
</select>
|
||||
</td>
|
||||
{:else if col.key === "acknowledged"}
|
||||
<td class="px-4 py-2 text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={row.acknowledged}
|
||||
on:change={async (e) => {
|
||||
const isChecked = (
|
||||
e.target as HTMLInputElement
|
||||
).checked;
|
||||
row.acknowledged = isChecked;
|
||||
|
||||
if (isChecked) {
|
||||
// map to project
|
||||
await acknowledgedOk(
|
||||
row.id,
|
||||
isChecked,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
{:else if col.key === "received"}
|
||||
<td class="px-4 py-2 text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={row.received}
|
||||
on:change={async (e) => {
|
||||
const isChecked = (
|
||||
e.target as HTMLInputElement
|
||||
).checked;
|
||||
row.received = isChecked;
|
||||
|
||||
if (isChecked) {
|
||||
// map to project
|
||||
await receivedOk(
|
||||
row.id,
|
||||
isChecked,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
{:else if col.key === "completed_status"}
|
||||
<td class="px-4 py-2">
|
||||
<select
|
||||
bind:value={
|
||||
row[
|
||||
col.key as keyof PurchaseOrderDisplay
|
||||
]
|
||||
}
|
||||
class="w-full p-3 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500 text-gray-900"
|
||||
on:change={async (e) => {
|
||||
const isValue = (
|
||||
e.target as HTMLInputElement
|
||||
).value;
|
||||
|
||||
if (isValue) {
|
||||
// map to project
|
||||
await completedStatusOk(
|
||||
row.id,
|
||||
isValue,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>SELECT COMPLETE</option
|
||||
>
|
||||
<option value="APPROVED"
|
||||
>RECEIVED COMPLETE</option
|
||||
>
|
||||
<option value="REJECTED"
|
||||
>COMPLETE INCOMPLETE</option
|
||||
>
|
||||
</select>
|
||||
</td>
|
||||
{:else if col.key === "actions"}
|
||||
<td class="px-4 py-2">
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-blue-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-blue-700"
|
||||
on:click={() => openModal(row)}
|
||||
>
|
||||
✏️ Edit
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-red-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-red-700"
|
||||
on:click={() => deleteProject(row.id)}
|
||||
>
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</td>
|
||||
{:else if col.key === "move_issue"}
|
||||
<td class="px-4 py-2">
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-green-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-green-700"
|
||||
on:click={() =>
|
||||
alert(
|
||||
`Move issue ${row.id} to project`,
|
||||
)}
|
||||
>
|
||||
➡️ PROJECT
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-yellow-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-yellow-700"
|
||||
on:click={() =>
|
||||
alert(
|
||||
`Move issue ${row.id} to another area`,
|
||||
)}
|
||||
>
|
||||
➡️ PURCHASE ORDER
|
||||
</button>
|
||||
</td>
|
||||
{:else}
|
||||
<td class="px-4 py-2 text-gray-700"
|
||||
>{row[
|
||||
col.key as keyof PurchaseOrderDisplay
|
||||
]}</td
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination controls -->
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div>
|
||||
Showing {(currentPage - 1) * rowsPerPage + 1}–
|
||||
{Math.min(currentPage * rowsPerPage, allRows.length)} of {allRows.length}
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<button
|
||||
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
|
||||
on:click={() => goToPage(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
{#each Array(totalPages)
|
||||
.fill(0)
|
||||
.map((_, i) => i + 1) as page}
|
||||
<button
|
||||
class="px-3 py-1 rounded border text-sm
|
||||
{currentPage === page
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'bg-white border-gray-300 hover:bg-gray-100'}"
|
||||
on:click={() => goToPage(page)}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
{/each}
|
||||
<button
|
||||
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
|
||||
on:click={() => goToPage(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showModal}
|
||||
<div
|
||||
class="fixed inset-0 flex items-center justify-center bg-gray-800 bg-opacity-50 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 mb-4">
|
||||
{isEditing ? "Edit Project" : "Add Project"}
|
||||
</h3>
|
||||
<form on:submit|preventDefault={saveProject}>
|
||||
<!-- choose issuess -->
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for="issue_id"
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Choose Issue
|
||||
</label>
|
||||
<select
|
||||
id="issue_id"
|
||||
bind:value={newPurchaseOrders.issue_id}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
on:change={(e: Event) => {
|
||||
const selectedIssue =
|
||||
(e.target as HTMLSelectElement)?.value ?? "";
|
||||
newPurchaseOrders.issue_id = selectedIssue;
|
||||
}}
|
||||
>
|
||||
<option value="">Select Issue</option>
|
||||
{#each issues as issue}
|
||||
<option value={issue.id}>{issue.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{#each formColumns as col}
|
||||
{#if col.key === "po_status"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<select
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
on:change={(e: Event) => {
|
||||
const selectedOption =
|
||||
(e.target as HTMLSelectElement)
|
||||
?.value ?? "";
|
||||
const option =
|
||||
getStatusOption(selectedOption);
|
||||
if (
|
||||
option?.value === "APPROVED" &&
|
||||
!validateInput()
|
||||
) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#each statusOptions as option}
|
||||
<option
|
||||
value={option.value}
|
||||
style="background-color: {option.color};"
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{:else if col.key === "po_type"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<select
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Select PO Type</option>
|
||||
<option value="Regular">Regular</option>
|
||||
<option value="Urgent">Urgent</option>
|
||||
</select>
|
||||
</div>
|
||||
{:else if col.key === "prepared_date"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{:else if col.key === "po_quantity"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{:else if col.key === "approved_price"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{:else if col.key === "approved_vendor"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<select
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
on:change={(e: Event) => {
|
||||
const selectedVendor =
|
||||
(e.target as HTMLSelectElement)
|
||||
?.value ?? "";
|
||||
newPurchaseOrders[col.key] = selectedVendor;
|
||||
}}
|
||||
>
|
||||
<option value="">Select Vendor</option>
|
||||
{#each vendors as vendor}
|
||||
<option value={vendor.id}>
|
||||
{vendor.name}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
<div class="flex justify-end space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300"
|
||||
on:click={() => (showModal = false)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
949
src/routes/backoffice/purchaseorder/complete/+page.svelte
Normal file
949
src/routes/backoffice/purchaseorder/complete/+page.svelte
Normal file
@@ -0,0 +1,949 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import Select from "svelte-select";
|
||||
import { supabase } from "$lib/supabaseClient";
|
||||
|
||||
type PurchaseOrderInsert = {
|
||||
issue_id: string;
|
||||
prepared_date: string;
|
||||
po_type: string;
|
||||
po_quantity: number;
|
||||
po_status: string;
|
||||
approved_vendor: string;
|
||||
acknowledge_by: string;
|
||||
approved_by: string;
|
||||
approved_price: number;
|
||||
completed_status: string;
|
||||
};
|
||||
|
||||
let purchaseOrderInsert: PurchaseOrderInsert = {
|
||||
issue_id: "",
|
||||
prepared_date: "",
|
||||
po_type: "",
|
||||
po_quantity: 0,
|
||||
po_status: "REQUESTED",
|
||||
approved_vendor: "",
|
||||
acknowledge_by: "",
|
||||
approved_by: "",
|
||||
approved_price: 0,
|
||||
completed_status: "",
|
||||
};
|
||||
|
||||
type PurchaseOrders = {
|
||||
id: string;
|
||||
purchase_order_number: string;
|
||||
prepared_date: string;
|
||||
po_type: string;
|
||||
po_quantity: number;
|
||||
po_status: string;
|
||||
approved_vendor: string;
|
||||
acknowledged: boolean;
|
||||
approved_vendor_id: string;
|
||||
acknowledge_by: string;
|
||||
approved_price: number;
|
||||
approved_quantity: number;
|
||||
total_approved_order_amount: number;
|
||||
approval: string;
|
||||
completed_status: string;
|
||||
received: boolean;
|
||||
received_by: string;
|
||||
input_by: string;
|
||||
issue_id: string;
|
||||
approved_by: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
type PurchaseOrderDisplay = {
|
||||
id: string;
|
||||
name: string;
|
||||
purchase_order_number: string;
|
||||
villa_name: string;
|
||||
priority: string;
|
||||
prepared_date: string;
|
||||
po_type: string;
|
||||
po_quantity: number;
|
||||
po_status: string;
|
||||
approved_vendor: string;
|
||||
acknowledged: boolean;
|
||||
acknowledge_by: string;
|
||||
approved_by: string;
|
||||
approved_price: number;
|
||||
approved_quantity: number;
|
||||
total_approved_order_amount: number;
|
||||
approval: string;
|
||||
completed_status: string;
|
||||
received: boolean;
|
||||
received_by: string;
|
||||
};
|
||||
|
||||
let allRows: PurchaseOrderDisplay[] = [];
|
||||
|
||||
type columns = {
|
||||
key: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
const columns: columns[] = [
|
||||
{ key: "name", title: "Name" },
|
||||
{ key: "purchase_order_number", title: "Purchase Order Number" },
|
||||
{ key: "po_quantity", title: "PO Quantity" },
|
||||
{ key: "po_status", title: "PO Status" },
|
||||
{ key: "approved_vendor", title: "Approved Vendor" },
|
||||
{ key: "approved_price", title: "Approved Price" },
|
||||
{ key: "approved_quantity", title: "Approved Quantity" },
|
||||
{
|
||||
key: "total_approved_order_amount",
|
||||
title: "Total Approved Order Amount",
|
||||
},
|
||||
{ key: "completed_status", title: "Completed Status" },
|
||||
{ key: "acknowledge_by", title: "Acknowledged By" },
|
||||
{ key: "created_at", title: "Created At" },
|
||||
{ key: "actions", title: "Actions" }, // For edit/delete buttons
|
||||
];
|
||||
|
||||
let currentPage = 1;
|
||||
let rowsPerPage = 10;
|
||||
$: totalPages = Math.ceil(allRows.length / rowsPerPage);
|
||||
$: paginatedRows = allRows.slice(
|
||||
(currentPage - 1) * rowsPerPage,
|
||||
currentPage * rowsPerPage,
|
||||
);
|
||||
|
||||
function goToPage(page: number) {
|
||||
if (page >= 1 && page <= totalPages) currentPage = page;
|
||||
}
|
||||
|
||||
async function fetchPurchaseOrder(
|
||||
filter: string | null = null,
|
||||
search: string | null = null,
|
||||
sort: string | null = null,
|
||||
order: "asc" | "desc" = "desc",
|
||||
offset: number = 0,
|
||||
limit: number = 1000,
|
||||
) {
|
||||
let query = supabase
|
||||
.from("purchaseorder_data")
|
||||
.select(
|
||||
"id, purchase_order_number, villa_data, issue_id, prepared_date, po_type, po_quantity, po_status, approved_vendor, acknowledged, acknowledge_by, approved_price, approved_quantity, total_approved_order_amount, approval, completed_status, received, received_by, input_by, approved_by, created_at",
|
||||
)
|
||||
.order(sort || "created_at", { ascending: order === "asc" })
|
||||
.range(offset, offset + limit - 1);
|
||||
if (filter) {
|
||||
query = query.eq("po_type", filter);
|
||||
}
|
||||
if (search) {
|
||||
query = query.ilike("purchase_order_number", `%${search}%`);
|
||||
}
|
||||
const { data, error } = await query;
|
||||
if (error) {
|
||||
console.error("Error fetching purchase orders:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
// fetch issue and villa names
|
||||
const issueIds = data.map((row) => row.issue_id);
|
||||
const { data: issues, error: issueError } = await supabase
|
||||
.from("issues")
|
||||
.select("*")
|
||||
.in("id", issueIds);
|
||||
|
||||
if (issueError) {
|
||||
console.error("Error fetching issues:", issueError);
|
||||
return;
|
||||
}
|
||||
|
||||
const villaIds = issues.map((row) => row.villa_name).filter(Boolean);
|
||||
const { data: villas, error: villaError } = await supabase
|
||||
.from("villas")
|
||||
.select("id, name")
|
||||
.in("id", villaIds);
|
||||
if (villaError) {
|
||||
console.error("Error fetching villas:", villaError);
|
||||
return;
|
||||
}
|
||||
|
||||
// masukkan villa name dan issue name ke dalam data
|
||||
allRows = data.map((row) => {
|
||||
const issue = issues.find((issue) => issue.id === row.issue_id);
|
||||
const villa = villas.find((villa) => villa.id === issue.villa_id);
|
||||
const vendor = vendors.find(
|
||||
(vendor) => vendor.id === row.approved_vendor,
|
||||
);
|
||||
|
||||
return {
|
||||
...row,
|
||||
name: issue ? issue.name : "Unknown Issue",
|
||||
villa_name: row.villa_data,
|
||||
priority: issue ? issue.priority : "Unknown Priority",
|
||||
approved_vendor: vendor
|
||||
? vendor.name
|
||||
: "Unknown Approved Vendor",
|
||||
approval: row.approval || "",
|
||||
completed_status: row.completed_status || "",
|
||||
} as PurchaseOrderDisplay;
|
||||
});
|
||||
}
|
||||
|
||||
//fetch all issues
|
||||
async function fetchIssues() {
|
||||
const { data, error } = await supabase
|
||||
.from("issues")
|
||||
.select("id, name");
|
||||
|
||||
if (error) {
|
||||
console.error("Error fetching issues:", error);
|
||||
return [];
|
||||
}
|
||||
|
||||
issues = data.map((issue) => ({
|
||||
id: issue.id,
|
||||
name: issue.name,
|
||||
}));
|
||||
}
|
||||
|
||||
async function fetchVendors() {
|
||||
const { data, error } = await supabase
|
||||
.from("vendor")
|
||||
.select("id, name");
|
||||
|
||||
if (error) {
|
||||
console.error("Error fetching vendors:", error);
|
||||
return [];
|
||||
}
|
||||
|
||||
vendors = data.map((vendor) => ({
|
||||
id: vendor.id,
|
||||
name: vendor.name,
|
||||
}));
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchPurchaseOrder();
|
||||
fetchVendors();
|
||||
});
|
||||
|
||||
$: currentPage = 1; // Reset to first page when allRows changes
|
||||
|
||||
let showModal = false;
|
||||
let isEditing = false;
|
||||
let currentEditingId: string | null = null;
|
||||
let newPurchaseOrders: Record<string, any> = {};
|
||||
let vendors: { id: string; name: string }[] = [];
|
||||
let issues: { id: string; name: string }[] = [];
|
||||
const excludedKeys = [
|
||||
"id",
|
||||
"priority",
|
||||
"villa_name",
|
||||
"purchase_order_number",
|
||||
"issue_id",
|
||||
"number_project",
|
||||
"input_by",
|
||||
"created_at",
|
||||
"actions",
|
||||
"acknowledged",
|
||||
"acknowledge_by",
|
||||
"approval",
|
||||
"completed_status",
|
||||
"received",
|
||||
"received_by",
|
||||
"approved_quantity",
|
||||
"total_approved_order_amount",
|
||||
"approved_by",
|
||||
"name",
|
||||
"po_status",
|
||||
];
|
||||
const formColumns = columns.filter(
|
||||
(col) => !excludedKeys.includes(col.key),
|
||||
);
|
||||
|
||||
async function openModal(purchase: PurchaseOrderDisplay | null = null) {
|
||||
await fetchIssues();
|
||||
if (purchase) {
|
||||
isEditing = true;
|
||||
currentEditingId = purchase.id;
|
||||
newPurchaseOrders = { ...purchase };
|
||||
} else {
|
||||
isEditing = false;
|
||||
currentEditingId = null;
|
||||
newPurchaseOrders = {};
|
||||
}
|
||||
showModal = true;
|
||||
}
|
||||
|
||||
async function saveProject() {
|
||||
purchaseOrderInsert = {
|
||||
issue_id: newPurchaseOrders.issue_id || "",
|
||||
prepared_date: newPurchaseOrders.prepared_date || "",
|
||||
po_type: newPurchaseOrders.po_type || "",
|
||||
po_quantity: newPurchaseOrders.po_quantity || 0,
|
||||
po_status: newPurchaseOrders.po_status || "REQUESTED",
|
||||
approved_vendor: newPurchaseOrders.approved_vendor || "",
|
||||
acknowledge_by: newPurchaseOrders.acknowledge_by || "",
|
||||
approved_price: newPurchaseOrders.approved_price || "",
|
||||
approved_by: newPurchaseOrders.approved_by || "",
|
||||
completed_status: newPurchaseOrders.completed_status || "",
|
||||
};
|
||||
|
||||
if (isEditing && currentEditingId) {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update(purchaseOrderInsert)
|
||||
.eq("id", currentEditingId);
|
||||
|
||||
if (error) {
|
||||
console.error("Error updating purchase order:", error);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.insert(purchaseOrderInsert);
|
||||
|
||||
if (error) {
|
||||
console.error("Error inserting purchase order:", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
showModal = false;
|
||||
}
|
||||
|
||||
async function deleteProject(id: string) {
|
||||
const { error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.delete()
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error deleting project:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
const statusOptions = [
|
||||
{ label: "Requested", value: "REQUESTED", color: "#e5e7eb" },
|
||||
{ label: "Prepared", value: "PREPARED", color: "#93c5fd" },
|
||||
{ label: "Approved", value: "APPROVED", color: "#34d399" },
|
||||
{ label: "Acknowledged", value: "ACKNOWLEDGE", color: "#60a5fa" },
|
||||
{
|
||||
label: "Received - Incomplete",
|
||||
value: "RECEIVE - INCOMPLETE",
|
||||
color: "#fb923c",
|
||||
},
|
||||
{
|
||||
label: "Received - Completed",
|
||||
value: "RECEIVE COMPLETED",
|
||||
color: "#10b981",
|
||||
},
|
||||
{ label: "Canceled", value: "CANCELED", color: "#f87171" },
|
||||
];
|
||||
|
||||
function getStatusOption(value: string) {
|
||||
return statusOptions.find((option) => option.value === value) ?? null;
|
||||
}
|
||||
|
||||
//validate input fields purchase order
|
||||
function validateInput() {
|
||||
const requiredFields = [
|
||||
"prepared_date",
|
||||
"po_type",
|
||||
"po_quantity",
|
||||
"approved_vendor",
|
||||
];
|
||||
for (const field of requiredFields) {
|
||||
if (!newPurchaseOrders[field]) {
|
||||
alert(`Please fill in the ${field} field.`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateInputApproval() {
|
||||
const requiredFields = ["approved_price"];
|
||||
for (const field of requiredFields) {
|
||||
if (!newPurchaseOrders[field]) {
|
||||
alert(`Please fill in the ${field} field.`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function updatePurchaseOrderStatus(
|
||||
e: Event,
|
||||
id: string,
|
||||
row: PurchaseOrderDisplay,
|
||||
) {
|
||||
const selectedOption = (e.target as HTMLSelectElement)?.value ?? "";
|
||||
const option = getStatusOption(selectedOption);
|
||||
|
||||
newPurchaseOrders = {
|
||||
...row,
|
||||
po_status: option?.value || row.po_status,
|
||||
};
|
||||
|
||||
if (option?.value === "APPROVED") {
|
||||
if (!validateInput()) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ po_status: newPurchaseOrders.po_status })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error updating purchase order status:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
async function acknowledgedOk(id: string, status: boolean) {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ acknowledged: status })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error acknowledging purchase order:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
async function receivedOk(id: string, status: boolean) {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ receivedOk: status })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error acknowledging purchase order:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
async function updatePurchaseOrderApprovalStatus(
|
||||
e: Event,
|
||||
id: string,
|
||||
row: PurchaseOrderDisplay,
|
||||
) {
|
||||
const selectedOption = (e.target as HTMLSelectElement)?.value ?? "";
|
||||
const option = getStatusOption(selectedOption);
|
||||
|
||||
newPurchaseOrders = {
|
||||
...row,
|
||||
approval: option?.value || row.approval,
|
||||
};
|
||||
|
||||
if (option?.value === "APPROVED") {
|
||||
if (!validateInputApproval()) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ approval: newPurchaseOrders.approval })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error updating purchase order status:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
async function completedStatusOk(id: string, status: string) {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ completed_status: status })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error acknowledging purchase order:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
</script>
|
||||
|
||||
<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-lg font-semibold text-gray-800 flex items-center gap-2"
|
||||
>
|
||||
<span>📦</span>
|
||||
Purchase Order Complete List
|
||||
</h2>
|
||||
<p class="text-sm text-gray-600">
|
||||
Manage your purchase orders efficiently. You can add, edit, or
|
||||
delete purchase orders as needed.
|
||||
</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();
|
||||
fetchPurchaseOrder(null, searchTerm, "created_at", "desc");
|
||||
}}
|
||||
/>
|
||||
<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 filter = (e.target as HTMLSelectElement).value;
|
||||
fetchPurchaseOrder(filter, null, null, "desc");
|
||||
}}
|
||||
>
|
||||
<option value="">All Issues</option>
|
||||
<option value="PROJECT">Project Issues</option>
|
||||
<option value="PURCHASE_ORDER">Purchase Order Issues</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<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}
|
||||
{#if col.key === "name"}
|
||||
<th
|
||||
class="sticky left-0 px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||||
style="background-color: #f0f8ff; z-index: 10;"
|
||||
>
|
||||
{col.title}
|
||||
</th>
|
||||
{:else}
|
||||
<th
|
||||
class="px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||||
>
|
||||
{col.title}
|
||||
</th>
|
||||
{/if}
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
{#each paginatedRows as row}
|
||||
<tr class="hover:bg-gray-50 transition">
|
||||
{#each columns as col}
|
||||
{#if col.key === "name"}
|
||||
<td
|
||||
class="sticky left-0 px-4 py-2 font-medium text-blue-600"
|
||||
style="background-color: #f0f8ff; cursor: pointer;"
|
||||
>
|
||||
{row[col.key as keyof PurchaseOrderDisplay]}
|
||||
</td>
|
||||
{:else if col.key === "approval"}
|
||||
<td class="px-4 py-2">
|
||||
<select
|
||||
bind:value={
|
||||
row[
|
||||
col.key as keyof PurchaseOrderDisplay
|
||||
]
|
||||
}
|
||||
class="w-full p-3 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500 text-gray-900"
|
||||
on:change={(e: Event) => {
|
||||
updatePurchaseOrderApprovalStatus(
|
||||
e,
|
||||
row.id,
|
||||
row,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>SELECT APPROVAL</option
|
||||
>
|
||||
<option value="APPROVED"
|
||||
>APPROVED</option
|
||||
>
|
||||
<option value="REJECTED"
|
||||
>REJECTED</option
|
||||
>
|
||||
</select>
|
||||
</td>
|
||||
{:else if col.key === "acknowledged"}
|
||||
<td class="px-4 py-2 text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={row.acknowledged}
|
||||
on:change={async (e) => {
|
||||
const isChecked = (
|
||||
e.target as HTMLInputElement
|
||||
).checked;
|
||||
row.acknowledged = isChecked;
|
||||
|
||||
if (isChecked) {
|
||||
// map to project
|
||||
await acknowledgedOk(
|
||||
row.id,
|
||||
isChecked,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
{:else if col.key === "received"}
|
||||
<td class="px-4 py-2 text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={row.received}
|
||||
on:change={async (e) => {
|
||||
const isChecked = (
|
||||
e.target as HTMLInputElement
|
||||
).checked;
|
||||
row.received = isChecked;
|
||||
|
||||
if (isChecked) {
|
||||
// map to project
|
||||
await receivedOk(
|
||||
row.id,
|
||||
isChecked,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
{:else if col.key === "completed_status"}
|
||||
<td class="px-4 py-2">
|
||||
<select
|
||||
bind:value={
|
||||
row[
|
||||
col.key as keyof PurchaseOrderDisplay
|
||||
]
|
||||
}
|
||||
class="w-full p-3 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500 text-gray-900"
|
||||
on:change={async (e) => {
|
||||
const isValue = (
|
||||
e.target as HTMLInputElement
|
||||
).value;
|
||||
|
||||
if (isValue) {
|
||||
// map to project
|
||||
await completedStatusOk(
|
||||
row.id,
|
||||
isValue,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>SELECT COMPLETE</option
|
||||
>
|
||||
<option value="APPROVED"
|
||||
>RECEIVED COMPLETE</option
|
||||
>
|
||||
<option value="REJECTED"
|
||||
>COMPLETE INCOMPLETE</option
|
||||
>
|
||||
</select>
|
||||
</td>
|
||||
{:else if col.key === "actions"}
|
||||
<td class="px-4 py-2">
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-blue-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-blue-700"
|
||||
on:click={() => openModal(row)}
|
||||
>
|
||||
✏️ Edit
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-red-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-red-700"
|
||||
on:click={() => deleteProject(row.id)}
|
||||
>
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</td>
|
||||
{:else if col.key === "move_issue"}
|
||||
<td class="px-4 py-2">
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-green-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-green-700"
|
||||
on:click={() =>
|
||||
alert(
|
||||
`Move issue ${row.id} to project`,
|
||||
)}
|
||||
>
|
||||
➡️ PROJECT
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-yellow-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-yellow-700"
|
||||
on:click={() =>
|
||||
alert(
|
||||
`Move issue ${row.id} to another area`,
|
||||
)}
|
||||
>
|
||||
➡️ PURCHASE ORDER
|
||||
</button>
|
||||
</td>
|
||||
{:else}
|
||||
<td class="px-4 py-2 text-gray-700"
|
||||
>{row[
|
||||
col.key as keyof PurchaseOrderDisplay
|
||||
]}</td
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination controls -->
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div>
|
||||
Showing {(currentPage - 1) * rowsPerPage + 1}–
|
||||
{Math.min(currentPage * rowsPerPage, allRows.length)} of {allRows.length}
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<button
|
||||
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
|
||||
on:click={() => goToPage(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
{#each Array(totalPages)
|
||||
.fill(0)
|
||||
.map((_, i) => i + 1) as page}
|
||||
<button
|
||||
class="px-3 py-1 rounded border text-sm
|
||||
{currentPage === page
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'bg-white border-gray-300 hover:bg-gray-100'}"
|
||||
on:click={() => goToPage(page)}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
{/each}
|
||||
<button
|
||||
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
|
||||
on:click={() => goToPage(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showModal}
|
||||
<div
|
||||
class="fixed inset-0 flex items-center justify-center bg-gray-800 bg-opacity-50 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 mb-4">
|
||||
{isEditing ? "Edit Project" : "Add Project"}
|
||||
</h3>
|
||||
<form on:submit|preventDefault={saveProject}>
|
||||
<!-- choose issuess -->
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for="issue_id"
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Choose Issue
|
||||
</label>
|
||||
<select
|
||||
id="issue_id"
|
||||
bind:value={newPurchaseOrders.issue_id}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
on:change={(e: Event) => {
|
||||
const selectedIssue =
|
||||
(e.target as HTMLSelectElement)?.value ?? "";
|
||||
newPurchaseOrders.issue_id = selectedIssue;
|
||||
}}
|
||||
>
|
||||
<option value="">Select Issue</option>
|
||||
{#each issues as issue}
|
||||
<option value={issue.id}>{issue.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{#each formColumns as col}
|
||||
{#if col.key === "po_status"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<select
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
on:change={(e: Event) => {
|
||||
const selectedOption =
|
||||
(e.target as HTMLSelectElement)
|
||||
?.value ?? "";
|
||||
const option =
|
||||
getStatusOption(selectedOption);
|
||||
if (
|
||||
option?.value === "APPROVED" &&
|
||||
!validateInput()
|
||||
) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#each statusOptions as option}
|
||||
<option
|
||||
value={option.value}
|
||||
style="background-color: {option.color};"
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{:else if col.key === "po_type"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<select
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Select PO Type</option>
|
||||
<option value="Regular">Regular</option>
|
||||
<option value="Urgent">Urgent</option>
|
||||
</select>
|
||||
</div>
|
||||
{:else if col.key === "prepared_date"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{:else if col.key === "po_quantity"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{:else if col.key === "approved_price"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{:else if col.key === "approved_vendor"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<select
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
on:change={(e: Event) => {
|
||||
const selectedVendor =
|
||||
(e.target as HTMLSelectElement)
|
||||
?.value ?? "";
|
||||
newPurchaseOrders[col.key] = selectedVendor;
|
||||
}}
|
||||
>
|
||||
<option value="">Select Vendor</option>
|
||||
{#each vendors as vendor}
|
||||
<option value={vendor.id}>
|
||||
{vendor.name}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
<div class="flex justify-end space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300"
|
||||
on:click={() => (showModal = false)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
949
src/routes/backoffice/purchaseorder/received/+page.svelte
Normal file
949
src/routes/backoffice/purchaseorder/received/+page.svelte
Normal file
@@ -0,0 +1,949 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import Select from "svelte-select";
|
||||
import { supabase } from "$lib/supabaseClient";
|
||||
|
||||
type PurchaseOrderInsert = {
|
||||
issue_id: string;
|
||||
prepared_date: string;
|
||||
po_type: string;
|
||||
po_quantity: number;
|
||||
po_status: string;
|
||||
approved_vendor: string;
|
||||
acknowledge_by: string;
|
||||
approved_by: string;
|
||||
approved_price: number;
|
||||
completed_status: string;
|
||||
};
|
||||
|
||||
let purchaseOrderInsert: PurchaseOrderInsert = {
|
||||
issue_id: "",
|
||||
prepared_date: "",
|
||||
po_type: "",
|
||||
po_quantity: 0,
|
||||
po_status: "REQUESTED",
|
||||
approved_vendor: "",
|
||||
acknowledge_by: "",
|
||||
approved_by: "",
|
||||
approved_price: 0,
|
||||
completed_status: "",
|
||||
};
|
||||
|
||||
type PurchaseOrders = {
|
||||
id: string;
|
||||
purchase_order_number: string;
|
||||
prepared_date: string;
|
||||
po_type: string;
|
||||
po_quantity: number;
|
||||
po_status: string;
|
||||
approved_vendor: string;
|
||||
acknowledged: boolean;
|
||||
approved_vendor_id: string;
|
||||
acknowledge_by: string;
|
||||
approved_price: number;
|
||||
approved_quantity: number;
|
||||
total_approved_order_amount: number;
|
||||
approval: string;
|
||||
completed_status: string;
|
||||
received: boolean;
|
||||
received_by: string;
|
||||
input_by: string;
|
||||
issue_id: string;
|
||||
approved_by: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
type PurchaseOrderDisplay = {
|
||||
id: string;
|
||||
name: string;
|
||||
purchase_order_number: string;
|
||||
villa_name: string;
|
||||
priority: string;
|
||||
prepared_date: string;
|
||||
po_type: string;
|
||||
po_quantity: number;
|
||||
po_status: string;
|
||||
approved_vendor: string;
|
||||
acknowledged: boolean;
|
||||
acknowledge_by: string;
|
||||
approved_by: string;
|
||||
approved_price: number;
|
||||
approved_quantity: number;
|
||||
total_approved_order_amount: number;
|
||||
approval: string;
|
||||
completed_status: string;
|
||||
received: boolean;
|
||||
received_by: string;
|
||||
};
|
||||
|
||||
let allRows: PurchaseOrderDisplay[] = [];
|
||||
|
||||
type columns = {
|
||||
key: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
const columns: columns[] = [
|
||||
{ key: "name", title: "Name" },
|
||||
{ key: "purchase_order_number", title: "Purchase Order Number" },
|
||||
{ key: "po_quantity", title: "PO Quantity" },
|
||||
{ key: "po_status", title: "PO Status" },
|
||||
{ key: "approved_vendor", title: "Approved Vendor" },
|
||||
{ key: "approved_price", title: "Approved Price" },
|
||||
{ key: "approved_quantity", title: "Approved Quantity" },
|
||||
{
|
||||
key: "total_approved_order_amount",
|
||||
title: "Total Approved Order Amount",
|
||||
},
|
||||
{ key: "received", title: "Received" },
|
||||
{ key: "received_by", title: "Received By" },
|
||||
{ key: "created_at", title: "Created At" },
|
||||
{ key: "actions", title: "Actions" }, // For edit/delete buttons
|
||||
];
|
||||
|
||||
let currentPage = 1;
|
||||
let rowsPerPage = 10;
|
||||
$: totalPages = Math.ceil(allRows.length / rowsPerPage);
|
||||
$: paginatedRows = allRows.slice(
|
||||
(currentPage - 1) * rowsPerPage,
|
||||
currentPage * rowsPerPage,
|
||||
);
|
||||
|
||||
function goToPage(page: number) {
|
||||
if (page >= 1 && page <= totalPages) currentPage = page;
|
||||
}
|
||||
|
||||
async function fetchPurchaseOrder(
|
||||
filter: string | null = null,
|
||||
search: string | null = null,
|
||||
sort: string | null = null,
|
||||
order: "asc" | "desc" = "desc",
|
||||
offset: number = 0,
|
||||
limit: number = 1000,
|
||||
) {
|
||||
let query = supabase
|
||||
.from("purchaseorder_data")
|
||||
.select(
|
||||
"id, purchase_order_number, villa_data, issue_id, prepared_date, po_type, po_quantity, po_status, approved_vendor, acknowledged, acknowledge_by, approved_price, approved_quantity, total_approved_order_amount, approval, completed_status, received, received_by, input_by, approved_by, created_at",
|
||||
)
|
||||
.order(sort || "created_at", { ascending: order === "asc" })
|
||||
.range(offset, offset + limit - 1);
|
||||
if (filter) {
|
||||
query = query.eq("po_type", filter);
|
||||
}
|
||||
if (search) {
|
||||
query = query.ilike("purchase_order_number", `%${search}%`);
|
||||
}
|
||||
const { data, error } = await query;
|
||||
if (error) {
|
||||
console.error("Error fetching purchase orders:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
// fetch issue and villa names
|
||||
const issueIds = data.map((row) => row.issue_id);
|
||||
const { data: issues, error: issueError } = await supabase
|
||||
.from("issues")
|
||||
.select("*")
|
||||
.in("id", issueIds);
|
||||
|
||||
if (issueError) {
|
||||
console.error("Error fetching issues:", issueError);
|
||||
return;
|
||||
}
|
||||
|
||||
const villaIds = issues.map((row) => row.villa_name).filter(Boolean);
|
||||
const { data: villas, error: villaError } = await supabase
|
||||
.from("villas")
|
||||
.select("id, name")
|
||||
.in("id", villaIds);
|
||||
if (villaError) {
|
||||
console.error("Error fetching villas:", villaError);
|
||||
return;
|
||||
}
|
||||
|
||||
// masukkan villa name dan issue name ke dalam data
|
||||
allRows = data.map((row) => {
|
||||
const issue = issues.find((issue) => issue.id === row.issue_id);
|
||||
const villa = villas.find((villa) => villa.id === issue.villa_id);
|
||||
const vendor = vendors.find(
|
||||
(vendor) => vendor.id === row.approved_vendor,
|
||||
);
|
||||
|
||||
return {
|
||||
...row,
|
||||
name: issue ? issue.name : "Unknown Issue",
|
||||
villa_name: row.villa_data,
|
||||
priority: issue ? issue.priority : "Unknown Priority",
|
||||
approved_vendor: vendor
|
||||
? vendor.name
|
||||
: "Unknown Approved Vendor",
|
||||
approval: row.approval || "",
|
||||
completed_status: row.completed_status || "",
|
||||
} as PurchaseOrderDisplay;
|
||||
});
|
||||
}
|
||||
|
||||
//fetch all issues
|
||||
async function fetchIssues() {
|
||||
const { data, error } = await supabase
|
||||
.from("issues")
|
||||
.select("id, name");
|
||||
|
||||
if (error) {
|
||||
console.error("Error fetching issues:", error);
|
||||
return [];
|
||||
}
|
||||
|
||||
issues = data.map((issue) => ({
|
||||
id: issue.id,
|
||||
name: issue.name,
|
||||
}));
|
||||
}
|
||||
|
||||
async function fetchVendors() {
|
||||
const { data, error } = await supabase
|
||||
.from("vendor")
|
||||
.select("id, name");
|
||||
|
||||
if (error) {
|
||||
console.error("Error fetching vendors:", error);
|
||||
return [];
|
||||
}
|
||||
|
||||
vendors = data.map((vendor) => ({
|
||||
id: vendor.id,
|
||||
name: vendor.name,
|
||||
}));
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchPurchaseOrder();
|
||||
fetchVendors();
|
||||
});
|
||||
|
||||
$: currentPage = 1; // Reset to first page when allRows changes
|
||||
|
||||
let showModal = false;
|
||||
let isEditing = false;
|
||||
let currentEditingId: string | null = null;
|
||||
let newPurchaseOrders: Record<string, any> = {};
|
||||
let vendors: { id: string; name: string }[] = [];
|
||||
let issues: { id: string; name: string }[] = [];
|
||||
const excludedKeys = [
|
||||
"id",
|
||||
"priority",
|
||||
"villa_name",
|
||||
"purchase_order_number",
|
||||
"issue_id",
|
||||
"number_project",
|
||||
"input_by",
|
||||
"created_at",
|
||||
"actions",
|
||||
"acknowledged",
|
||||
"acknowledge_by",
|
||||
"approval",
|
||||
"completed_status",
|
||||
"received",
|
||||
"received_by",
|
||||
"approved_quantity",
|
||||
"total_approved_order_amount",
|
||||
"approved_by",
|
||||
"name",
|
||||
"po_status",
|
||||
];
|
||||
const formColumns = columns.filter(
|
||||
(col) => !excludedKeys.includes(col.key),
|
||||
);
|
||||
|
||||
async function openModal(purchase: PurchaseOrderDisplay | null = null) {
|
||||
await fetchIssues();
|
||||
if (purchase) {
|
||||
isEditing = true;
|
||||
currentEditingId = purchase.id;
|
||||
newPurchaseOrders = { ...purchase };
|
||||
} else {
|
||||
isEditing = false;
|
||||
currentEditingId = null;
|
||||
newPurchaseOrders = {};
|
||||
}
|
||||
showModal = true;
|
||||
}
|
||||
|
||||
async function saveProject() {
|
||||
purchaseOrderInsert = {
|
||||
issue_id: newPurchaseOrders.issue_id || "",
|
||||
prepared_date: newPurchaseOrders.prepared_date || "",
|
||||
po_type: newPurchaseOrders.po_type || "",
|
||||
po_quantity: newPurchaseOrders.po_quantity || 0,
|
||||
po_status: newPurchaseOrders.po_status || "REQUESTED",
|
||||
approved_vendor: newPurchaseOrders.approved_vendor || "",
|
||||
acknowledge_by: newPurchaseOrders.acknowledge_by || "",
|
||||
approved_price: newPurchaseOrders.approved_price || "",
|
||||
approved_by: newPurchaseOrders.approved_by || "",
|
||||
completed_status: newPurchaseOrders.completed_status || "",
|
||||
};
|
||||
|
||||
if (isEditing && currentEditingId) {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update(purchaseOrderInsert)
|
||||
.eq("id", currentEditingId);
|
||||
|
||||
if (error) {
|
||||
console.error("Error updating purchase order:", error);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.insert(purchaseOrderInsert);
|
||||
|
||||
if (error) {
|
||||
console.error("Error inserting purchase order:", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
showModal = false;
|
||||
}
|
||||
|
||||
async function deleteProject(id: string) {
|
||||
const { error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.delete()
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error deleting project:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
const statusOptions = [
|
||||
{ label: "Requested", value: "REQUESTED", color: "#e5e7eb" },
|
||||
{ label: "Prepared", value: "PREPARED", color: "#93c5fd" },
|
||||
{ label: "Approved", value: "APPROVED", color: "#34d399" },
|
||||
{ label: "Acknowledged", value: "ACKNOWLEDGE", color: "#60a5fa" },
|
||||
{
|
||||
label: "Received - Incomplete",
|
||||
value: "RECEIVE - INCOMPLETE",
|
||||
color: "#fb923c",
|
||||
},
|
||||
{
|
||||
label: "Received - Completed",
|
||||
value: "RECEIVE COMPLETED",
|
||||
color: "#10b981",
|
||||
},
|
||||
{ label: "Canceled", value: "CANCELED", color: "#f87171" },
|
||||
];
|
||||
|
||||
function getStatusOption(value: string) {
|
||||
return statusOptions.find((option) => option.value === value) ?? null;
|
||||
}
|
||||
|
||||
//validate input fields purchase order
|
||||
function validateInput() {
|
||||
const requiredFields = [
|
||||
"prepared_date",
|
||||
"po_type",
|
||||
"po_quantity",
|
||||
"approved_vendor",
|
||||
];
|
||||
for (const field of requiredFields) {
|
||||
if (!newPurchaseOrders[field]) {
|
||||
alert(`Please fill in the ${field} field.`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateInputApproval() {
|
||||
const requiredFields = ["approved_price"];
|
||||
for (const field of requiredFields) {
|
||||
if (!newPurchaseOrders[field]) {
|
||||
alert(`Please fill in the ${field} field.`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function updatePurchaseOrderStatus(
|
||||
e: Event,
|
||||
id: string,
|
||||
row: PurchaseOrderDisplay,
|
||||
) {
|
||||
const selectedOption = (e.target as HTMLSelectElement)?.value ?? "";
|
||||
const option = getStatusOption(selectedOption);
|
||||
|
||||
newPurchaseOrders = {
|
||||
...row,
|
||||
po_status: option?.value || row.po_status,
|
||||
};
|
||||
|
||||
if (option?.value === "APPROVED") {
|
||||
if (!validateInput()) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ po_status: newPurchaseOrders.po_status })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error updating purchase order status:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
async function acknowledgedOk(id: string, status: boolean) {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ acknowledged: status })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error acknowledging purchase order:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
async function receivedOk(id: string, status: boolean) {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ receivedOk: status })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error acknowledging purchase order:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
async function updatePurchaseOrderApprovalStatus(
|
||||
e: Event,
|
||||
id: string,
|
||||
row: PurchaseOrderDisplay,
|
||||
) {
|
||||
const selectedOption = (e.target as HTMLSelectElement)?.value ?? "";
|
||||
const option = getStatusOption(selectedOption);
|
||||
|
||||
newPurchaseOrders = {
|
||||
...row,
|
||||
approval: option?.value || row.approval,
|
||||
};
|
||||
|
||||
if (option?.value === "APPROVED") {
|
||||
if (!validateInputApproval()) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ approval: newPurchaseOrders.approval })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error updating purchase order status:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
|
||||
async function completedStatusOk(id: string, status: string) {
|
||||
const { data, error } = await supabase
|
||||
.from("purchase_orders")
|
||||
.update({ completed_status: status })
|
||||
.eq("id", id);
|
||||
|
||||
if (error) {
|
||||
console.error("Error acknowledging purchase order:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchPurchaseOrder();
|
||||
}
|
||||
</script>
|
||||
|
||||
<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-lg font-semibold text-gray-800 flex items-center gap-2"
|
||||
>
|
||||
<span>📦</span>
|
||||
Purchase Order Received List
|
||||
</h2>
|
||||
<p class="text-sm text-gray-600">
|
||||
Manage your purchase orders efficiently. You can add, edit, or
|
||||
delete purchase orders as needed.
|
||||
</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();
|
||||
fetchPurchaseOrder(null, searchTerm, "created_at", "desc");
|
||||
}}
|
||||
/>
|
||||
<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 filter = (e.target as HTMLSelectElement).value;
|
||||
fetchPurchaseOrder(filter, null, null, "desc");
|
||||
}}
|
||||
>
|
||||
<option value="">All Issues</option>
|
||||
<option value="PROJECT">Project Issues</option>
|
||||
<option value="PURCHASE_ORDER">Purchase Order Issues</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<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}
|
||||
{#if col.key === "name"}
|
||||
<th
|
||||
class="sticky left-0 px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||||
style="background-color: #f0f8ff; z-index: 10;"
|
||||
>
|
||||
{col.title}
|
||||
</th>
|
||||
{:else}
|
||||
<th
|
||||
class="px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||||
>
|
||||
{col.title}
|
||||
</th>
|
||||
{/if}
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
{#each paginatedRows as row}
|
||||
<tr class="hover:bg-gray-50 transition">
|
||||
{#each columns as col}
|
||||
{#if col.key === "name"}
|
||||
<td
|
||||
class="sticky left-0 px-4 py-2 font-medium text-blue-600"
|
||||
style="background-color: #f0f8ff; cursor: pointer;"
|
||||
>
|
||||
{row[col.key as keyof PurchaseOrderDisplay]}
|
||||
</td>
|
||||
{:else if col.key === "approval"}
|
||||
<td class="px-4 py-2">
|
||||
<select
|
||||
bind:value={
|
||||
row[
|
||||
col.key as keyof PurchaseOrderDisplay
|
||||
]
|
||||
}
|
||||
class="w-full p-3 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500 text-gray-900"
|
||||
on:change={(e: Event) => {
|
||||
updatePurchaseOrderApprovalStatus(
|
||||
e,
|
||||
row.id,
|
||||
row,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>SELECT APPROVAL</option
|
||||
>
|
||||
<option value="APPROVED"
|
||||
>APPROVED</option
|
||||
>
|
||||
<option value="REJECTED"
|
||||
>REJECTED</option
|
||||
>
|
||||
</select>
|
||||
</td>
|
||||
{:else if col.key === "acknowledged"}
|
||||
<td class="px-4 py-2 text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={row.acknowledged}
|
||||
on:change={async (e) => {
|
||||
const isChecked = (
|
||||
e.target as HTMLInputElement
|
||||
).checked;
|
||||
row.acknowledged = isChecked;
|
||||
|
||||
if (isChecked) {
|
||||
// map to project
|
||||
await acknowledgedOk(
|
||||
row.id,
|
||||
isChecked,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
{:else if col.key === "received"}
|
||||
<td class="px-4 py-2 text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={row.received}
|
||||
on:change={async (e) => {
|
||||
const isChecked = (
|
||||
e.target as HTMLInputElement
|
||||
).checked;
|
||||
row.received = isChecked;
|
||||
|
||||
if (isChecked) {
|
||||
// map to project
|
||||
await receivedOk(
|
||||
row.id,
|
||||
isChecked,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
{:else if col.key === "completed_status"}
|
||||
<td class="px-4 py-2">
|
||||
<select
|
||||
bind:value={
|
||||
row[
|
||||
col.key as keyof PurchaseOrderDisplay
|
||||
]
|
||||
}
|
||||
class="w-full p-3 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500 text-gray-900"
|
||||
on:change={async (e) => {
|
||||
const isValue = (
|
||||
e.target as HTMLInputElement
|
||||
).value;
|
||||
|
||||
if (isValue) {
|
||||
// map to project
|
||||
await completedStatusOk(
|
||||
row.id,
|
||||
isValue,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="" disabled selected
|
||||
>SELECT COMPLETE</option
|
||||
>
|
||||
<option value="APPROVED"
|
||||
>RECEIVED COMPLETE</option
|
||||
>
|
||||
<option value="REJECTED"
|
||||
>COMPLETE INCOMPLETE</option
|
||||
>
|
||||
</select>
|
||||
</td>
|
||||
{:else if col.key === "actions"}
|
||||
<td class="px-4 py-2">
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-blue-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-blue-700"
|
||||
on:click={() => openModal(row)}
|
||||
>
|
||||
✏️ Edit
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-red-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-red-700"
|
||||
on:click={() => deleteProject(row.id)}
|
||||
>
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</td>
|
||||
{:else if col.key === "move_issue"}
|
||||
<td class="px-4 py-2">
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-green-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-green-700"
|
||||
on:click={() =>
|
||||
alert(
|
||||
`Move issue ${row.id} to project`,
|
||||
)}
|
||||
>
|
||||
➡️ PROJECT
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center gap-1 rounded bg-yellow-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-yellow-700"
|
||||
on:click={() =>
|
||||
alert(
|
||||
`Move issue ${row.id} to another area`,
|
||||
)}
|
||||
>
|
||||
➡️ PURCHASE ORDER
|
||||
</button>
|
||||
</td>
|
||||
{:else}
|
||||
<td class="px-4 py-2 text-gray-700"
|
||||
>{row[
|
||||
col.key as keyof PurchaseOrderDisplay
|
||||
]}</td
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination controls -->
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div>
|
||||
Showing {(currentPage - 1) * rowsPerPage + 1}–
|
||||
{Math.min(currentPage * rowsPerPage, allRows.length)} of {allRows.length}
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<button
|
||||
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
|
||||
on:click={() => goToPage(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
{#each Array(totalPages)
|
||||
.fill(0)
|
||||
.map((_, i) => i + 1) as page}
|
||||
<button
|
||||
class="px-3 py-1 rounded border text-sm
|
||||
{currentPage === page
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'bg-white border-gray-300 hover:bg-gray-100'}"
|
||||
on:click={() => goToPage(page)}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
{/each}
|
||||
<button
|
||||
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
|
||||
on:click={() => goToPage(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showModal}
|
||||
<div
|
||||
class="fixed inset-0 flex items-center justify-center bg-gray-800 bg-opacity-50 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 mb-4">
|
||||
{isEditing ? "Edit Project" : "Add Project"}
|
||||
</h3>
|
||||
<form on:submit|preventDefault={saveProject}>
|
||||
<!-- choose issuess -->
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for="issue_id"
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Choose Issue
|
||||
</label>
|
||||
<select
|
||||
id="issue_id"
|
||||
bind:value={newPurchaseOrders.issue_id}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
on:change={(e: Event) => {
|
||||
const selectedIssue =
|
||||
(e.target as HTMLSelectElement)?.value ?? "";
|
||||
newPurchaseOrders.issue_id = selectedIssue;
|
||||
}}
|
||||
>
|
||||
<option value="">Select Issue</option>
|
||||
{#each issues as issue}
|
||||
<option value={issue.id}>{issue.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{#each formColumns as col}
|
||||
{#if col.key === "po_status"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<select
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
on:change={(e: Event) => {
|
||||
const selectedOption =
|
||||
(e.target as HTMLSelectElement)
|
||||
?.value ?? "";
|
||||
const option =
|
||||
getStatusOption(selectedOption);
|
||||
if (
|
||||
option?.value === "APPROVED" &&
|
||||
!validateInput()
|
||||
) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#each statusOptions as option}
|
||||
<option
|
||||
value={option.value}
|
||||
style="background-color: {option.color};"
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{:else if col.key === "po_type"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<select
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Select PO Type</option>
|
||||
<option value="Regular">Regular</option>
|
||||
<option value="Urgent">Urgent</option>
|
||||
</select>
|
||||
</div>
|
||||
{:else if col.key === "prepared_date"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{:else if col.key === "po_quantity"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{:else if col.key === "approved_price"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{:else if col.key === "approved_vendor"}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<select
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
on:change={(e: Event) => {
|
||||
const selectedVendor =
|
||||
(e.target as HTMLSelectElement)
|
||||
?.value ?? "";
|
||||
newPurchaseOrders[col.key] = selectedVendor;
|
||||
}}
|
||||
>
|
||||
<option value="">Select Vendor</option>
|
||||
{#each vendors as vendor}
|
||||
<option value={vendor.id}>
|
||||
{vendor.name}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mb-4">
|
||||
<label
|
||||
for={col.key}
|
||||
class="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{col.title}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id={col.key}
|
||||
bind:value={newPurchaseOrders[col.key]}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
<div class="flex justify-end space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300"
|
||||
on:click={() => (showModal = false)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -122,17 +122,37 @@
|
||||
{ key: "actions", title: "Actions" },
|
||||
];
|
||||
|
||||
async function fetchTimeSheets() {
|
||||
const { data: timesheet, error: timesheetError } = await supabase
|
||||
async function fetchTimeSheets(
|
||||
filter: string | null = null,
|
||||
searchTerm: string | null = null,
|
||||
sortColumn: string | null = "created_at",
|
||||
sortOrder: "asc" | "desc" = "desc",
|
||||
offset: number = 0,
|
||||
limit: number = 10,
|
||||
) {
|
||||
let query = supabase
|
||||
.from("timesheets")
|
||||
.select("*")
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
if (timesheetError) {
|
||||
console.error("Error fetching issues:", timesheetError);
|
||||
.select("*", { count: "exact" })
|
||||
.order(sortColumn || "created_at", {
|
||||
ascending: sortOrder === "asc",
|
||||
});
|
||||
if (filter) {
|
||||
query = query.eq("category_of_work", filter);
|
||||
}
|
||||
if (searchTerm) {
|
||||
query = query.ilike("name", `%${searchTerm}%`);
|
||||
}
|
||||
if (offset) {
|
||||
query = query.range(offset, offset + limit - 1);
|
||||
}
|
||||
if (limit) {
|
||||
query = query.limit(limit);
|
||||
}
|
||||
const { data: timesheet, error } = await query;
|
||||
if (error) {
|
||||
console.error("Error fetching timesheets:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ambil semua villa_id unik dari issues
|
||||
const villaIds = [
|
||||
...new Set(timesheet.map((i: Timesheets) => i.villa_id)),
|
||||
@@ -328,13 +348,47 @@
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<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-lg font-semibold text-gray-800">Timesheet List</h2>
|
||||
<h2 class="text-lg font-semibold text-gray-800">
|
||||
🕒 Timesheet List
|
||||
</h2>
|
||||
<p class="text-sm text-gray-600">
|
||||
Manage and track timesheets for staff members.
|
||||
</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();
|
||||
fetchTimeSheets(null, searchTerm, "created_at", "desc");
|
||||
}}
|
||||
/>
|
||||
<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 filter = (e.target as HTMLSelectElement).value;
|
||||
fetchTimeSheets(filter, null, null, "desc");
|
||||
}}
|
||||
>
|
||||
<option value="">All Issues</option>
|
||||
<option value="PROJECT">Project Issues</option>
|
||||
<option value="PURCHASE_ORDER">Purchase Order Issues</option>
|
||||
</select>
|
||||
<button
|
||||
class="bg-gray-200 text-gray-700 px-4 py-2 rounded-xl hover:bg-gray-300 text-sm transition"
|
||||
on:click={() =>
|
||||
fetchTimeSheets(null, null, "created_at", "desc", 0, 10)}
|
||||
>
|
||||
🔄 Reset
|
||||
</button>
|
||||
<button
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
|
||||
on:click={() => openModal()}
|
||||
@@ -342,6 +396,7 @@
|
||||
➕ Add Timesheet
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
|
||||
Reference in New Issue
Block a user