penamabahan menu approval untill received
This commit is contained in:
@@ -1,5 +1,21 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
let menuItems = [
|
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: "Beranda", icon: "🏠", url: "/" },
|
||||||
{ name: "Profile", icon: "🧑", url: "/profile" },
|
{ name: "Profile", icon: "🧑", url: "/profile" },
|
||||||
{ name: "Issues", icon: "📂", url: "/backoffice/issue" },
|
{ name: "Issues", icon: "📂", url: "/backoffice/issue" },
|
||||||
@@ -7,8 +23,30 @@
|
|||||||
{ name: "Projects", icon: "📂", url: "/backoffice/project" },
|
{ name: "Projects", icon: "📂", url: "/backoffice/project" },
|
||||||
{
|
{
|
||||||
name: "Purchase Orders",
|
name: "Purchase Orders",
|
||||||
icon: "📂",
|
icon: "📦",
|
||||||
url: "/backoffice/purchaseorder",
|
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" },
|
{ name: "Timesheets", icon: "📂", url: "/backoffice/timesheets" },
|
||||||
{
|
{
|
||||||
@@ -20,15 +58,39 @@
|
|||||||
{ name: "Inventories", icon: "📂", url: "/backoffice/inventories" },
|
{ name: "Inventories", icon: "📂", url: "/backoffice/inventories" },
|
||||||
{ name: "Vendor", icon: "📂", url: "/backoffice/vendor" },
|
{ name: "Vendor", icon: "📂", url: "/backoffice/vendor" },
|
||||||
{ name: "Booking", icon: "📂", url: "/backoffice/booking" },
|
{ name: "Booking", icon: "📂", url: "/backoffice/booking" },
|
||||||
{ name: "Users", icon: "👤", url: "/backoffice/users" },
|
{ name: "Users", icon: "👤", url: "/backoffice/account" },
|
||||||
{ name: "Logout", icon: "🚪", url: "/logout" },
|
{ 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>
|
</script>
|
||||||
|
|
||||||
<div class="w-64 h-screen bg-white border-r shadow-sm">
|
<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>
|
<h1 class="text-xl font-semibold text-gray-800">Backoffice</h1>
|
||||||
<p class="text-sm text-gray-500">Manage your application</p>
|
<p class="text-sm text-gray-500">Manage your application</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,16 +100,38 @@
|
|||||||
<a
|
<a
|
||||||
href={item.url}
|
href={item.url}
|
||||||
class={`flex items-center gap-2 px-4 py-2 rounded transition-colors duration-150
|
class={`flex items-center gap-2 px-4 py-2 rounded transition-colors duration-150
|
||||||
${
|
${activeUrl === item.url ? "bg-blue-100 text-blue-600 font-semibold" : "text-gray-700 hover:bg-blue-50"}
|
||||||
active === item.name
|
${item.sub ? "justify-between" : ""}
|
||||||
? "bg-blue-100 text-blue-600 font-semibold"
|
`}
|
||||||
: "text-gray-700 hover:bg-blue-50"
|
on:click|preventDefault={() => handleMenuClick(item)}
|
||||||
}`}
|
|
||||||
on:click={() => (active = item.name)}
|
|
||||||
>
|
>
|
||||||
<span class="text-xl">{item.icon}</span>
|
<span class="flex items-center gap-2">
|
||||||
<span class="truncate">{item.name}</span>
|
<span class="text-xl">{item.icon}</span>
|
||||||
|
<span class="truncate">{item.name}</span>
|
||||||
|
</span>
|
||||||
|
{#if item.sub}
|
||||||
|
<span>{openMenus[item.name] ? "▲" : "▼"}</span>
|
||||||
|
{/if}
|
||||||
</a>
|
</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>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
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 = {
|
type Issue = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
villa_id: string;
|
||||||
villa_name: string;
|
villa_name: string;
|
||||||
area_of_villa: string;
|
area_of_villa: string;
|
||||||
priority: string;
|
priority: string;
|
||||||
@@ -148,11 +149,12 @@
|
|||||||
follow_up: boolean;
|
follow_up: boolean;
|
||||||
need_approval: boolean;
|
need_approval: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
// Optional field to hold the name of the villa
|
||||||
};
|
};
|
||||||
|
|
||||||
type issueInsert = {
|
type issueInsert = {
|
||||||
name: string;
|
name: string;
|
||||||
villa_name: string;
|
villa_id: string;
|
||||||
area_of_villa: string;
|
area_of_villa: string;
|
||||||
priority: string;
|
priority: string;
|
||||||
issue_type: string;
|
issue_type: string;
|
||||||
@@ -244,7 +246,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ambil semua villa_id unik dari issues
|
// 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
|
const { data: villas, error: villaError } = await supabase
|
||||||
.from("villas")
|
.from("villas")
|
||||||
@@ -260,7 +262,7 @@
|
|||||||
allRows = issues.map((issue: Issue) => ({
|
allRows = issues.map((issue: Issue) => ({
|
||||||
...issue,
|
...issue,
|
||||||
villa_name:
|
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;
|
let rowsPerPage = limit;
|
||||||
$: totalPages = Math.ceil(totalItems / rowsPerPage);
|
$: totalPages = Math.ceil(totalItems / rowsPerPage);
|
||||||
|
|
||||||
function editIssue(id: number) {
|
|
||||||
alert(`Edit issue with ID ${id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function goToPage(page: number) {
|
function goToPage(page: number) {
|
||||||
if (page >= 1 && page <= totalPages) currentPage = page;
|
if (page >= 1 && page <= totalPages) currentPage = page;
|
||||||
}
|
}
|
||||||
@@ -352,7 +350,7 @@
|
|||||||
} else {
|
} else {
|
||||||
const issueInsert: issueInsert = {
|
const issueInsert: issueInsert = {
|
||||||
name: formData.get("name") as string,
|
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,
|
area_of_villa: formData.get("area_of_villa") as string,
|
||||||
priority: formData.get("priority") as string,
|
priority: formData.get("priority") as string,
|
||||||
issue_type: formData.get("issue_type") as string,
|
issue_type: formData.get("issue_type") as string,
|
||||||
@@ -429,7 +427,7 @@
|
|||||||
"name",
|
"name",
|
||||||
"description_of_the_issue",
|
"description_of_the_issue",
|
||||||
"issue_source",
|
"issue_source",
|
||||||
"villa_name",
|
"villa_id",
|
||||||
"reported_date",
|
"reported_date",
|
||||||
"reported_by",
|
"reported_by",
|
||||||
"priority",
|
"priority",
|
||||||
@@ -1029,15 +1027,15 @@
|
|||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if col.key === "villa_name"}
|
{:else if col.key === "villa_id"}
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<label class="block text-sm font-medium text-gray-700"
|
<label class="block text-sm font-medium text-gray-700"
|
||||||
>Villa Name</label
|
>Villa Name</label
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
name="villa_name"
|
name="villa_id"
|
||||||
class="w-full border px-3 py-2 rounded {errorClass(
|
class="w-full border px-3 py-2 rounded {errorClass(
|
||||||
'villa_name',
|
'villa_id',
|
||||||
)}"
|
)}"
|
||||||
bind:value={newIssue[col.key as keyof Issue]}
|
bind:value={newIssue[col.key as keyof Issue]}
|
||||||
>
|
>
|
||||||
@@ -1048,9 +1046,9 @@
|
|||||||
<option value={villa.id}>{villa.name}</option>
|
<option value={villa.id}>{villa.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
{#if $formErrors.villa_name}
|
{#if $formErrors.villa_id}
|
||||||
<p class="text-red-500 text-xs">
|
<p class="text-red-500 text-xs">
|
||||||
{$formErrors.villa_name}
|
{$formErrors.villa_id}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
input_by: string;
|
input_by: string;
|
||||||
project_due_date: string;
|
project_due_date: string;
|
||||||
picture_link: string;
|
picture_link: string;
|
||||||
|
villa_data?: string; // Optional, if not always present
|
||||||
};
|
};
|
||||||
|
|
||||||
type insetProject = {
|
type insetProject = {
|
||||||
@@ -31,7 +32,6 @@
|
|||||||
input_by: string;
|
input_by: string;
|
||||||
issue_number: string;
|
issue_number: string;
|
||||||
issue_id: string;
|
issue_id: string;
|
||||||
villa_name: string;
|
|
||||||
report_date: string;
|
report_date: string;
|
||||||
project_due_date: string;
|
project_due_date: string;
|
||||||
};
|
};
|
||||||
@@ -82,12 +82,34 @@
|
|||||||
if (page >= 1 && page <= totalPages) currentPage = page;
|
if (page >= 1 && page <= totalPages) currentPage = page;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchProjects() {
|
async function fetchProjects(
|
||||||
// Fetch all projects
|
filter: string | null = null,
|
||||||
const { data, error } = await supabase
|
searchTerm: string | null = null,
|
||||||
.from("projects")
|
sortBy: string | null = null,
|
||||||
.select("*")
|
sortOrder: "asc" | "desc" = "asc",
|
||||||
.order("id", { ascending: false });
|
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
|
// ambil issue_id dari projects kemudian ambil data issue yang sesuai
|
||||||
const issueIds = data?.map((project: Project) => project.issue_id);
|
const issueIds = data?.map((project: Project) => project.issue_id);
|
||||||
@@ -103,6 +125,8 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allRows = []; // Reset allRows before populating
|
||||||
|
|
||||||
// Set allRows to the combined data
|
// Set allRows to the combined data
|
||||||
allRows = data.map((project: Project) => {
|
allRows = data.map((project: Project) => {
|
||||||
const issue = issueData.find(
|
const issue = issueData.find(
|
||||||
@@ -122,7 +146,7 @@
|
|||||||
area_of_villa: issue ? issue.area_of_villa : "Unknown",
|
area_of_villa: issue ? issue.area_of_villa : "Unknown",
|
||||||
input_by: project.input_by,
|
input_by: project.input_by,
|
||||||
issue_number: issue ? issue.issue_number : "Unknown",
|
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",
|
report_date: issue ? issue.reported_date : "Unknown",
|
||||||
project_due_date: project.project_due_date,
|
project_due_date: project.project_due_date,
|
||||||
};
|
};
|
||||||
@@ -357,20 +381,53 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<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>
|
<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">
|
<p class="text-sm text-gray-600">
|
||||||
Manage your projects here. You can add, edit, or delete
|
Manage your projects and tasks efficiently.
|
||||||
projects.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
|
||||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
|
<input
|
||||||
on:click={() => openModal()}
|
type="text"
|
||||||
>
|
placeholder="🔍 Search by name..."
|
||||||
➕ Add Projects
|
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"
|
||||||
</button>
|
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 Project
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-x-auto rounded-lg shadow mb-4">
|
<div class="overflow-x-auto rounded-lg shadow mb-4">
|
||||||
<table class="min-w-[1000px] divide-y divide-gray-200 text-sm w-max">
|
<table class="min-w-[1000px] divide-y divide-gray-200 text-sm w-max">
|
||||||
|
|||||||
@@ -122,12 +122,28 @@
|
|||||||
if (page >= 1 && page <= totalPages) currentPage = page;
|
if (page >= 1 && page <= totalPages) currentPage = page;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchPurchaseOrder() {
|
async function fetchPurchaseOrder(
|
||||||
const { data, error } = await supabase
|
filter: string | null = null,
|
||||||
.from("purchase_orders")
|
search: string | null = null,
|
||||||
.select("*")
|
sort: string | null = null,
|
||||||
.order("created_at", { ascending: false });
|
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) {
|
if (error) {
|
||||||
console.error("Error fetching purchase orders:", error);
|
console.error("Error fetching purchase orders:", error);
|
||||||
return;
|
return;
|
||||||
@@ -158,7 +174,7 @@
|
|||||||
// masukkan villa name dan issue name ke dalam data
|
// masukkan villa name dan issue name ke dalam data
|
||||||
allRows = data.map((row) => {
|
allRows = data.map((row) => {
|
||||||
const issue = issues.find((issue) => issue.id === row.issue_id);
|
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(
|
const vendor = vendors.find(
|
||||||
(vendor) => vendor.id === row.approved_vendor,
|
(vendor) => vendor.id === row.approved_vendor,
|
||||||
);
|
);
|
||||||
@@ -166,7 +182,7 @@
|
|||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
name: issue ? issue.name : "Unknown Issue",
|
name: issue ? issue.name : "Unknown Issue",
|
||||||
villa_name: villa ? villa.name : "Unknown Villa",
|
villa_name: row.villa_data,
|
||||||
priority: issue ? issue.priority : "Unknown Priority",
|
priority: issue ? issue.priority : "Unknown Priority",
|
||||||
approved_vendor: vendor
|
approved_vendor: vendor
|
||||||
? vendor.name
|
? vendor.name
|
||||||
@@ -476,9 +492,14 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<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>
|
<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
|
Purchase Order List
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-sm text-gray-600">
|
<p class="text-sm text-gray-600">
|
||||||
@@ -486,12 +507,36 @@
|
|||||||
delete purchase orders as needed.
|
delete purchase orders as needed.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
|
||||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
|
<input
|
||||||
on:click={() => openModal()}
|
type="text"
|
||||||
>
|
placeholder="🔍 Search by name..."
|
||||||
➕ Add Purchase Order
|
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"
|
||||||
</button>
|
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()}
|
||||||
|
>
|
||||||
|
➕ Add Purchase Order
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-x-auto rounded-lg shadow mb-4">
|
<div class="overflow-x-auto rounded-lg shadow mb-4">
|
||||||
<table class="min-w-[1000px] divide-y divide-gray-200 text-sm w-max">
|
<table class="min-w-[1000px] divide-y divide-gray-200 text-sm w-max">
|
||||||
@@ -526,33 +571,6 @@
|
|||||||
>
|
>
|
||||||
{row[col.key as keyof PurchaseOrderDisplay]}
|
{row[col.key as keyof PurchaseOrderDisplay]}
|
||||||
</td>
|
</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"}
|
{:else if col.key === "approval"}
|
||||||
<td class="px-4 py-2">
|
<td class="px-4 py-2">
|
||||||
<select
|
<select
|
||||||
@@ -569,9 +587,10 @@
|
|||||||
row,
|
row,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
disabled
|
||||||
>
|
>
|
||||||
<option value="" disabled selected
|
<option value="" disabled selected
|
||||||
>SELECT APPROVAL</option
|
>ON PROSES</option
|
||||||
>
|
>
|
||||||
<option value="APPROVED"
|
<option value="APPROVED"
|
||||||
>APPROVED</option
|
>APPROVED</option
|
||||||
@@ -600,6 +619,7 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
disabled
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
{:else if col.key === "received"}
|
{:else if col.key === "received"}
|
||||||
@@ -621,6 +641,7 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
disabled
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
{:else if col.key === "completed_status"}
|
{:else if col.key === "completed_status"}
|
||||||
@@ -645,9 +666,10 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
disabled
|
||||||
>
|
>
|
||||||
<option value="" disabled selected
|
<option value="" disabled selected
|
||||||
>SELECT COMPLETE</option
|
>ON PROSES</option
|
||||||
>
|
>
|
||||||
<option value="APPROVED"
|
<option value="APPROVED"
|
||||||
>RECEIVED COMPLETE</option
|
>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" },
|
{ key: "actions", title: "Actions" },
|
||||||
];
|
];
|
||||||
|
|
||||||
async function fetchTimeSheets() {
|
async function fetchTimeSheets(
|
||||||
const { data: timesheet, error: timesheetError } = await supabase
|
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")
|
.from("timesheets")
|
||||||
.select("*")
|
.select("*", { count: "exact" })
|
||||||
.order("created_at", { ascending: false });
|
.order(sortColumn || "created_at", {
|
||||||
|
ascending: sortOrder === "asc",
|
||||||
if (timesheetError) {
|
});
|
||||||
console.error("Error fetching issues:", timesheetError);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ambil semua villa_id unik dari issues
|
// Ambil semua villa_id unik dari issues
|
||||||
const villaIds = [
|
const villaIds = [
|
||||||
...new Set(timesheet.map((i: Timesheets) => i.villa_id)),
|
...new Set(timesheet.map((i: Timesheets) => i.villa_id)),
|
||||||
@@ -328,19 +348,54 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<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>
|
<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">
|
<p class="text-sm text-gray-600">
|
||||||
Manage and track timesheets for staff members.
|
Manage and track timesheets for staff members.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
|
||||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
|
<input
|
||||||
on:click={() => openModal()}
|
type="text"
|
||||||
>
|
placeholder="🔍 Search by name..."
|
||||||
➕ Add Timesheet
|
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"
|
||||||
</button>
|
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()}
|
||||||
|
>
|
||||||
|
➕ Add Timesheet
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-x-auto rounded-lg shadow mb-4">
|
<div class="overflow-x-auto rounded-lg shadow mb-4">
|
||||||
<table class="min-w-[1000px] divide-y divide-gray-200 text-sm w-max">
|
<table class="min-w-[1000px] divide-y divide-gray-200 text-sm w-max">
|
||||||
|
|||||||
Reference in New Issue
Block a user