perbaikan data

This commit is contained in:
aji@catalis.app
2025-06-22 12:26:43 +07:00
parent 2e4ff82e98
commit 2ad0f5093d
10 changed files with 752 additions and 353 deletions

10
src/lib/utils/authUtil.ts Normal file
View File

@@ -0,0 +1,10 @@
import { supabase } from "$lib/supabaseClient";
export async function getSessionAuthId() {
const { data: { session }, error } = await supabase.auth.getSession();
if (error) {
console.error("Error fetching session:", error);
return null;
}
return session?.user?.id || null;
}

View File

@@ -211,7 +211,6 @@
{ key: "issue_number", title: "Issue Number" }, { key: "issue_number", title: "Issue Number" },
{ key: "move_issue", title: "Move Issue" }, { key: "move_issue", title: "Move Issue" },
{ key: "description_of_the_issue", title: "Description of The Issue" }, { key: "description_of_the_issue", title: "Description of The Issue" },
{ key: "reported_date", title: "Reported Date" },
{ key: "issue_related_image", title: "Issue Related Image" }, { key: "issue_related_image", title: "Issue Related Image" },
{ key: "issue_source", title: "Issue Source" }, { key: "issue_source", title: "Issue Source" },
{ key: "reported_name", title: "Reported By" }, { key: "reported_name", title: "Reported By" },
@@ -732,7 +731,7 @@
row[col.key as keyof Issue] as row[col.key as keyof Issue] as
| string | string
| number, | number,
).toLocaleDateString("en-US") ).toLocaleString("en-US")
: ""} : ""}
</td> </td>
{:else if col.key === "need_approval"} {:else if col.key === "need_approval"}
@@ -751,6 +750,23 @@
{/if} {/if}
</td> </td>
{:else if col.key === "created_at"}
<!-- beri jam jg -->
<td class="px-4 py-2">
{new Date(
row[col.key as keyof Issue] as string,
).toLocaleString("en-US")}
</td>
{:else if col.key === "updated_at"}
<td class="px-4 py-2">
{row[col.key as keyof Issue]
? new Date(
row[
col.key as keyof Issue
] as string,
).toLocaleString("en-US")
: ""}
</td>
{:else} {:else}
<td class="px-4 py-2 text-gray-700" <td class="px-4 py-2 text-gray-700"
>{row[col.key as keyof Issue]}</td >{row[col.key as keyof Issue]}</td

View File

@@ -13,11 +13,13 @@
villa_data?: string; // Optional, if not always present villa_data?: string; // Optional, if not always present
}; };
type insetProject = { type insertProject = {
issue_id: string; issue_id: string;
input_by: string; input_by: string;
project_due_date: string; project_due_date: string;
picture_link: string; picture_link: string;
updated_at?: string; // Optional, if not always present
updated_by?: string; // Optional, if not always present
}; };
type Projects = { type Projects = {
@@ -34,6 +36,9 @@
issue_id: string; issue_id: string;
report_date: string; report_date: string;
project_due_date: string; project_due_date: string;
project_number?: string; // Optional, if not always present
updated_at?: string; // Optional, if not always present
updated_name?: string; // Optional, if not always present
}; };
let allRows: Projects[] = []; let allRows: Projects[] = [];
@@ -51,11 +56,14 @@
{ key: "picture_link", title: "Picture Link" }, { key: "picture_link", title: "Picture Link" },
{ key: "need_approval", title: "Need Approval" }, { key: "need_approval", title: "Need Approval" },
{ key: "area_of_villa", title: "Area of Villa" }, { key: "area_of_villa", title: "Area of Villa" },
{ key: "input_by", title: "Input By" }, { key: "input_name", title: "Input By" },
{ key: "issue_number", title: "Issue Number" }, { key: "issue_number", title: "Issue Number" },
{ key: "villa_name", title: "Villa Name" }, { key: "villa_name", title: "Villa Name" },
{ key: "report_date", title: "Report Date" }, { key: "report_date", title: "Report Date" },
{ key: "project_due_date", title: "Project Due Date" }, { key: "project_due_date", title: "Project Due Date" },
{ key: "project_number", title: "Project Number" },
{ key: "updated_at", title: "Updated At" },
{ key: "updated_name", title: "Updated By" },
{ key: "actions", title: "Actions" }, { key: "actions", title: "Actions" },
]; ];
@@ -187,6 +195,10 @@
"actions", "actions",
"add_to_po", "add_to_po",
"issue_number", "issue_number",
"updated_at",
"updated_name",
"input_name",
"input_by",
]; ];
const formColumns = columns.filter( const formColumns = columns.filter(
(col) => !excludedKeys.includes(col.key), (col) => !excludedKeys.includes(col.key),
@@ -207,7 +219,7 @@
} }
//validation project make //validation project make
function validateProjectCheckBox(project: insetProject): boolean { function validateProjectCheckBox(project: insertProject): boolean {
if (!project.project_due_date) { if (!project.project_due_date) {
console.error("Project due date is required"); console.error("Project due date is required");
alert("Project due date is required"); alert("Project due date is required");
@@ -259,6 +271,17 @@
} }
async function addToPo(project: Project) { async function addToPo(project: Project) {
// session user
const session = await supabase.auth.getSession();
const user = session?.data?.session?.user;
if (!user) {
console.error("User not authenticated");
alert("User not authenticated");
return;
}
const inputBy = user.id; // Use user id as input_by
if (!project.add_to_po) { if (!project.add_to_po) {
console.error("Project must be added to PO"); console.error("Project must be added to PO");
alert("Project must be added to PO"); alert("Project must be added to PO");
@@ -274,6 +297,8 @@
input_by: project?.input_by, input_by: project?.input_by,
project_due_date: project?.project_due_date, project_due_date: project?.project_due_date,
picture_link: project?.picture_link, picture_link: project?.picture_link,
updated_at: new Date().toISOString(),
updated_by: inputBy,
}; };
const { data, error } = await supabase const { data, error } = await supabase
@@ -310,6 +335,16 @@
} }
async function saveProject(event: Event) { async function saveProject(event: Event) {
//get session user
const session = await supabase.auth.getSession();
const user = session?.data?.session?.user;
if (!user) {
console.error("User not authenticated");
return;
}
const inputBy = user.id; // Use email or id as input_by
const formData = new FormData(event.target as HTMLFormElement); const formData = new FormData(event.target as HTMLFormElement);
// Upload image if selected // Upload image if selected
@@ -326,12 +361,14 @@
imagePreviewUrl = data?.path; imagePreviewUrl = data?.path;
} }
const projectUpdate: insetProject = { const projectUpdate: insertProject = {
issue_id: formData.get("issue_id") as string, issue_id: formData.get("issue_id") as string,
input_by: formData.get("input_by") as string, input_by: inputBy,
project_due_date: formData.get("project_due_date") as string, project_due_date: formData.get("project_due_date") as string,
picture_link: picture_link:
imagePreviewUrl || (formData.get("picture_link") as string), imagePreviewUrl || (formData.get("picture_link") as string),
updated_at: new Date().toISOString(),
updated_by: user.id,
}; };
// Validate project before saving // Validate project before saving
@@ -553,6 +590,42 @@
No Picture No Picture
{/if} {/if}
</td> </td>
{:else if col.key === "project_due_date"}
<td class="px-4 py-2">
{#if row.project_due_date}
{new Date(
row.project_due_date,
).toLocaleString("en-US")}
{:else}
No Due Date
{/if}
</td>
{:else if col.key === "updated_at"}
<td class="px-4 py-2">
{#if row.updated_at}
{new Date(
row.updated_at,
).toLocaleString("en-US")}
{:else}
No Update
{/if}
</td>
{:else if col.key === "input_by"}
<td class="px-4 py-2">
{#if row.input_by}
{row.input_by}
{:else}
Not Yet Input
{/if}
</td>
{:else if col.key === "updated_name"}
<td class="px-4 py-2">
{#if row.updated_name}
{row.updated_name}
{:else}
No Updater
{/if}
</td>
{:else} {:else}
<td class="px-4 py-2 text-gray-700" <td class="px-4 py-2 text-gray-700"
>{row[col.key as keyof Projects]}</td >{row[col.key as keyof Projects]}</td

View File

@@ -3,6 +3,7 @@
import Select from "svelte-select"; import Select from "svelte-select";
import { supabase } from "$lib/supabaseClient"; import { supabase } from "$lib/supabaseClient";
import { timestampToDateTime } from "$lib/utils/conversion"; import { timestampToDateTime } from "$lib/utils/conversion";
import { getSessionAuthId } from "$lib/utils/authUtil";
type PurchaseOrderInsert = { type PurchaseOrderInsert = {
issue_id: string; issue_id: string;
@@ -11,6 +12,9 @@
po_quantity: number; po_quantity: number;
po_price: number; po_price: number;
po_total_price: number; po_total_price: number;
input_by: string;
updated_by: string;
updated_at: string;
}; };
let purchaseOrderInsert: PurchaseOrderInsert = { let purchaseOrderInsert: PurchaseOrderInsert = {
@@ -20,6 +24,9 @@
po_quantity: 0, po_quantity: 0,
po_price: 0, po_price: 0,
po_total_price: 0, po_total_price: 0,
input_by: "",
updated_by: "",
updated_at: new Date().toISOString(),
}; };
type PurchaseOrders = { type PurchaseOrders = {
@@ -72,6 +79,11 @@
received: boolean; received: boolean;
received_by: string; received_by: string;
created_at: string; created_at: string;
updated_at?: string;
updated_by?: string;
updated_name?: string;
input_by?: string;
input_name?: string;
}; };
let allRows: PurchaseOrderDisplay[] = []; let allRows: PurchaseOrderDisplay[] = [];
@@ -95,8 +107,8 @@
{ key: "proses_to_approval", title: "PROSES TO APPROVAL" }, { key: "proses_to_approval", title: "PROSES TO APPROVAL" },
{ key: "approved_vendor", title: "Approved Vendor" }, { key: "approved_vendor", title: "Approved Vendor" },
{ key: "acknowledged", title: "Acknowledged" }, { key: "acknowledged", title: "Acknowledged" },
{ key: "acknowledge_by", title: "Acknowledged By" }, { key: "acknowledged_name", title: "Acknowledged By" },
{ key: "approved_by", title: "Approved By" }, { key: "approved_name", title: "Approved By" },
{ key: "approved_price", title: "Approved Price" }, { key: "approved_price", title: "Approved Price" },
{ key: "approved_quantity", title: "Approved Quantity" }, { key: "approved_quantity", title: "Approved Quantity" },
{ {
@@ -106,7 +118,10 @@
{ key: "approval", title: "Approval" }, { key: "approval", title: "Approval" },
{ key: "completed_status", title: "Completed Status" }, { key: "completed_status", title: "Completed Status" },
{ key: "received", title: "Received" }, { key: "received", title: "Received" },
{ key: "received_by", title: "Received By" }, { key: "received_name", title: "Received By" },
{ key: "inputed_name", title: "Input By" },
{ key: "updated_name", title: "Updated By" },
{ key: "updated_at", title: "Updated At" },
{ key: "created_at", title: "Created At" }, { key: "created_at", title: "Created At" },
{ key: "actions", title: "Actions" }, // For edit/delete buttons { key: "actions", title: "Actions" }, // For edit/delete buttons
]; ];
@@ -265,6 +280,10 @@
"total_approved_order_amount", "total_approved_order_amount",
"approved_vendor", "approved_vendor",
"approved_price", "approved_price",
"updated_at",
"updated_by",
"updated_name",
"inputed_name",
]; ];
const formColumns = columns.filter( const formColumns = columns.filter(
(col) => !excludedKeys.includes(col.key), (col) => !excludedKeys.includes(col.key),
@@ -285,6 +304,18 @@
} }
async function saveProject() { async function saveProject() {
// session check
const session = await supabase.auth.getSession();
if (!session.data.session) {
alert("You must be logged in to perform this action.");
return;
}
const user = session.data.session.user;
const inputBy = user.id;
if (!validateInput()) { if (!validateInput()) {
return; return;
} }
@@ -297,6 +328,9 @@
po_price: newPurchaseOrders.po_price || 0, po_price: newPurchaseOrders.po_price || 0,
po_total_price: po_total_price:
newPurchaseOrders.po_quantity * newPurchaseOrders.po_price || 0, newPurchaseOrders.po_quantity * newPurchaseOrders.po_price || 0,
input_by: inputBy,
updated_by: inputBy,
updated_at: new Date().toISOString(),
}; };
if (isEditing && currentEditingId) { if (isEditing && currentEditingId) {
@@ -400,13 +434,24 @@
} }
async function updateProsesToApproval(id: string, status: boolean) { async function updateProsesToApproval(id: string, status: boolean) {
const sessionId = await getSessionAuthId();
if (!sessionId) {
alert("You must be logged in to perform this action.");
return;
}
if (!validateInputProsesToAprroval()) { if (!validateInputProsesToAprroval()) {
return; return;
} }
const { data, error } = await supabase const { data, error } = await supabase
.from("vb_purchase_orders") .from("vb_purchase_orders")
.update({ proses_to_approval: status, po_status: "REQUESTED" }) .update({
proses_to_approval: status,
po_status: "REQUESTED",
updated_by: sessionId,
updated_at: new Date().toISOString(),
})
.eq("id", id); .eq("id", id);
if (error) { if (error) {
@@ -425,6 +470,11 @@
id: string, id: string,
row: PurchaseOrderDisplay, row: PurchaseOrderDisplay,
) { ) {
const sessionId = await getSessionAuthId();
if (!sessionId) {
alert("You must be logged in to perform this action.");
return;
}
const selectedOption = (e.target as HTMLSelectElement)?.value ?? ""; const selectedOption = (e.target as HTMLSelectElement)?.value ?? "";
const option = getStatusOption(selectedOption); const option = getStatusOption(selectedOption);
@@ -454,9 +504,19 @@
} }
async function acknowledgedOk(id: string, status: boolean) { async function acknowledgedOk(id: string, status: boolean) {
const sessionId = await getSessionAuthId();
if (!sessionId) {
alert("You must be logged in to perform this action.");
return;
}
const { data, error } = await supabase const { data, error } = await supabase
.from("vb_purchase_orders") .from("vb_purchase_orders")
.update({ acknowledged: status }) .update({
acknowledged: status,
updated_by: sessionId,
updated_at: new Date().toISOString(),
})
.eq("id", id); .eq("id", id);
if (error) { if (error) {
@@ -468,9 +528,19 @@
} }
async function receivedOk(id: string, status: boolean) { async function receivedOk(id: string, status: boolean) {
const sessionId = await getSessionAuthId();
if (!sessionId) {
alert("You must be logged in to perform this action.");
return;
}
const { data, error } = await supabase const { data, error } = await supabase
.from("vb_purchase_orders") .from("vb_purchase_orders")
.update({ receivedOk: status }) .update({
receivedOk: status,
updated_by: sessionId,
updated_at: new Date().toISOString(),
})
.eq("id", id); .eq("id", id);
if (error) { if (error) {
@@ -486,6 +556,11 @@
id: string, id: string,
row: PurchaseOrderDisplay, row: PurchaseOrderDisplay,
) { ) {
const sessionId = await getSessionAuthId();
if (!sessionId) {
alert("You must be logged in to perform this action.");
return;
}
const selectedOption = (e.target as HTMLSelectElement)?.value ?? ""; const selectedOption = (e.target as HTMLSelectElement)?.value ?? "";
const option = getStatusOption(selectedOption); const option = getStatusOption(selectedOption);
@@ -503,7 +578,11 @@
const { data, error } = await supabase const { data, error } = await supabase
.from("vb_purchase_orders") .from("vb_purchase_orders")
.update({ approval: newPurchaseOrders.approval }) .update({
approval: newPurchaseOrders.approval,
updated_by: sessionId,
updated_at: new Date().toISOString(),
})
.eq("id", id); .eq("id", id);
if (error) { if (error) {
@@ -515,9 +594,18 @@
} }
async function completedStatusOk(id: string, status: string) { async function completedStatusOk(id: string, status: string) {
const sessionId = await getSessionAuthId();
if (!sessionId) {
alert("You must be logged in to perform this action.");
return;
}
const { data, error } = await supabase const { data, error } = await supabase
.from("vb_purchase_orders") .from("vb_purchase_orders")
.update({ completed_status: status }) .update({
completed_status: status,
updated_by: sessionId,
updated_at: new Date().toISOString(),
})
.eq("id", id); .eq("id", id);
if (error) { if (error) {
@@ -753,6 +841,14 @@
> >
</select> </select>
</td> </td>
{:else if col.key === "updated_at"}
<td class="px-4 py-2 text-gray-500"
>{new Date(
row[
col.key as keyof PurchaseOrderDisplay
] as string,
).toLocaleString()}</td
>
{:else if col.key === "actions"} {:else if col.key === "actions"}
<td class="px-4 py-2"> <td class="px-4 py-2">
<button <button
@@ -770,11 +866,11 @@
</td> </td>
{:else if col.key === "created_at"} {:else if col.key === "created_at"}
<td class="px-4 py-2 text-gray-500" <td class="px-4 py-2 text-gray-500"
>{timestampToDateTime( >{new Date(
row[ row[
col.key as keyof PurchaseOrderDisplay col.key as keyof PurchaseOrderDisplay
] as string, ] as string,
)}</td ).toLocaleString()}</td
> >
{:else} {:else}
<td class="px-4 py-2 text-gray-700" <td class="px-4 py-2 text-gray-700"

View File

@@ -14,6 +14,8 @@
approved_by: string; approved_by: string;
approved_price: number; approved_price: number;
completed_status: string; completed_status: string;
updated_by?: string;
updated_at?: string;
}; };
let purchaseOrderInsert: PurchaseOrderInsert = { let purchaseOrderInsert: PurchaseOrderInsert = {
@@ -27,6 +29,8 @@
approved_by: "", approved_by: "",
approved_price: 0, approved_price: 0,
completed_status: "", completed_status: "",
updated_by: "",
updated_at: new Date().toISOString(),
}; };
type PurchaseOrders = { type PurchaseOrders = {
@@ -50,6 +54,8 @@
input_by: string; input_by: string;
issue_id: string; issue_id: string;
approved_by: string; approved_by: string;
updated_at: string;
updated_by: string;
created_at: string; created_at: string;
}; };
@@ -74,6 +80,9 @@
completed_status: string; completed_status: string;
received: boolean; received: boolean;
received_by: string; received_by: string;
updated_name: string;
updated_at: string;
created_at: string;
}; };
let allRows: PurchaseOrderDisplay[] = []; let allRows: PurchaseOrderDisplay[] = [];
@@ -96,7 +105,8 @@
title: "Total Approved Order Amount", title: "Total Approved Order Amount",
}, },
{ key: "acknowledged", title: "Acknowledged" }, { key: "acknowledged", title: "Acknowledged" },
{ key: "acknowledge_by", title: "Acknowledge By" }, { key: "updated_name", title: "Updated By" },
{ key: "updated_at", title: "Updated At" },
{ key: "created_at", title: "Created At" }, { key: "created_at", title: "Created At" },
]; ];
@@ -249,6 +259,8 @@
"approved_by", "approved_by",
"name", "name",
"po_status", "po_status",
"updated_name",
"updated_at",
]; ];
const formColumns = columns.filter( const formColumns = columns.filter(
(col) => !excludedKeys.includes(col.key), (col) => !excludedKeys.includes(col.key),
@@ -269,6 +281,12 @@
} }
async function saveProject() { async function saveProject() {
const sessionId = await supabase.auth.getSession();
if (!sessionId) {
console.error("User not authenticated");
return;
}
purchaseOrderInsert = { purchaseOrderInsert = {
issue_id: newPurchaseOrders.issue_id || "", issue_id: newPurchaseOrders.issue_id || "",
prepared_date: newPurchaseOrders.prepared_date || "", prepared_date: newPurchaseOrders.prepared_date || "",
@@ -280,6 +298,8 @@
approved_price: newPurchaseOrders.approved_price || "", approved_price: newPurchaseOrders.approved_price || "",
approved_by: newPurchaseOrders.approved_by || "", approved_by: newPurchaseOrders.approved_by || "",
completed_status: newPurchaseOrders.completed_status || "", completed_status: newPurchaseOrders.completed_status || "",
updated_by: sessionId.data.session?.user.id || "",
updated_at: new Date().toISOString(),
}; };
if (isEditing && currentEditingId) { if (isEditing && currentEditingId) {
@@ -405,9 +425,21 @@
} }
async function acknowledgedOk(id: string, status: boolean) { async function acknowledgedOk(id: string, status: boolean) {
const sessionId = await supabase.auth.getSession();
if (!sessionId) {
console.error("User not authenticated");
return;
}
const { data, error } = await supabase const { data, error } = await supabase
.from("vb_purchase_orders") .from("vb_purchase_orders")
.update({ acknowledged: status, po_status: "ACKNOWLEDGED" }) .update({
acknowledged: status,
po_status: "ACKNOWLEDGED",
acknowledge_by: sessionId.data.session?.user.id,
updated_by: sessionId.data.session?.user.id,
updated_at: new Date().toISOString(),
})
.eq("id", id); .eq("id", id);
if (error) { if (error) {
@@ -679,6 +711,14 @@
➡️ PURCHASE ORDER ➡️ PURCHASE ORDER
</button> </button>
</td> </td>
{:else if col.key === "updated_at"}
<td class="px-4 py-2">
{new Date(row.updated_at).toLocaleString()}
</td>
{:else if col.key === "created_at"}
<td class="px-4 py-2">
{new Date(row.created_at).toLocaleString()}
</td>
{:else} {:else}
<td class="px-4 py-2 text-gray-700" <td class="px-4 py-2 text-gray-700"
>{row[ >{row[

View File

@@ -2,6 +2,8 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import Select from "svelte-select"; import Select from "svelte-select";
import { supabase } from "$lib/supabaseClient"; import { supabase } from "$lib/supabaseClient";
import { getSessionAuthId } from "$lib/utils/authUtil";
import { timestampToDateTime } from "$lib/utils/conversion";
type PurchaseOrderInsert = { type PurchaseOrderInsert = {
issue_id: string; issue_id: string;
@@ -10,6 +12,9 @@
approved_price: number; approved_price: number;
approved_quantity: number; approved_quantity: number;
total_approved_order_amount?: number; total_approved_order_amount?: number;
input_by?: string;
updated_by?: string;
updated_at?: string;
}; };
let purchaseOrderInsert: PurchaseOrderInsert = { let purchaseOrderInsert: PurchaseOrderInsert = {
@@ -19,6 +24,9 @@
approved_price: 0, approved_price: 0,
approved_quantity: 0, approved_quantity: 0,
total_approved_order_amount: 0, total_approved_order_amount: 0,
input_by: "",
updated_by: "",
updated_at: new Date().toISOString(),
}; };
type PurchaseOrders = { type PurchaseOrders = {
@@ -42,6 +50,10 @@
input_by: string; input_by: string;
issue_id: string; issue_id: string;
approved_by: string; approved_by: string;
approved_name: string;
updated_by: string;
updated_at: string;
updated_name: string;
created_at: string; created_at: string;
}; };
@@ -91,7 +103,10 @@
key: "total_approved_order_amount", key: "total_approved_order_amount",
title: "Total Approved Order Amount", title: "Total Approved Order Amount",
}, },
{ key: "approved_by", title: "Approved By" }, { key: "approved_name", title: "Approved By" },
{ key: "inputed_name", title: "Input By" },
{ key: "updated_name", title: "Updated By" },
{ key: "updated_at", title: "Updated At" },
{ key: "created_at", title: "Created At" }, { key: "created_at", title: "Created At" },
{ key: "actions", title: "Actions" }, // For edit/delete buttons { key: "actions", title: "Actions" }, // For edit/delete buttons
]; ];
@@ -243,6 +258,10 @@
"approved_by", "approved_by",
"name", "name",
"po_status", "po_status",
"input_name",
"updated_name",
"updated_at",
"approved_name",
]; ];
const formColumns = columns.filter( const formColumns = columns.filter(
(col) => !excludedKeys.includes(col.key), (col) => !excludedKeys.includes(col.key),
@@ -263,6 +282,11 @@
} }
async function saveProject() { async function saveProject() {
const sessionId = await getSessionAuthId();
if (!sessionId) {
alert("You must be logged in to perform this action.");
return;
}
purchaseOrderInsert = { purchaseOrderInsert = {
issue_id: newPurchaseOrders.issue_id || "", issue_id: newPurchaseOrders.issue_id || "",
approved_vendor: newPurchaseOrders.approved_vendor || "", approved_vendor: newPurchaseOrders.approved_vendor || "",
@@ -272,6 +296,9 @@
total_approved_order_amount: total_approved_order_amount:
newPurchaseOrders.approved_quantity * newPurchaseOrders.approved_quantity *
newPurchaseOrders.approved_price, newPurchaseOrders.approved_price,
input_by: sessionId,
updated_by: sessionId,
updated_at: new Date().toISOString(),
}; };
if (!validateInput()) { if (!validateInput()) {
@@ -439,6 +466,11 @@
) { ) {
const selectedOption = (e.target as HTMLSelectElement)?.value ?? ""; const selectedOption = (e.target as HTMLSelectElement)?.value ?? "";
const option = getStatusOption(selectedOption); const option = getStatusOption(selectedOption);
const sessionId = await getSessionAuthId();
if (!sessionId) {
alert("You must be logged in to perform this action.");
return;
}
newPurchaseOrders = { newPurchaseOrders = {
...row, ...row,
@@ -457,6 +489,9 @@
.update({ .update({
approval: newPurchaseOrders.approval, approval: newPurchaseOrders.approval,
po_status: "APPROVED", po_status: "APPROVED",
approved_by: sessionId,
updated_by: sessionId,
updated_at: new Date().toISOString(),
}) })
.eq("id", id); .eq("id", id);
@@ -606,61 +641,6 @@
}} }}
/> />
</td> </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"} {:else if col.key === "actions"}
<td class="px-4 py-2"> <td class="px-4 py-2">
<button <button
@@ -697,6 +677,42 @@
➡️ PURCHASE ORDER ➡️ PURCHASE ORDER
</button> </button>
</td> </td>
{:else if col.key === "updated_at"}
<td class="px-4 py-2 text-gray-500"
>{timestampToDateTime(
row[
col.key as keyof PurchaseOrderDisplay
] as string,
)}</td
>
{:else if col.key === "created_at"}
<td class="px-4 py-2 text-gray-500"
>{timestampToDateTime(
row[
col.key as keyof PurchaseOrderDisplay
] as string,
)}</td
>
{:else if col.key === "po_price"}
<td class="px-4 py-2 text-gray-700"
>{row.approved_price.toLocaleString(
"en-US",
{
style: "currency",
currency: "USD",
},
)}</td
>
{:else if col.key === "po_total_price"}
<td class="px-4 py-2 text-gray-700"
>{(
row.approved_quantity *
row.approved_price
).toLocaleString("en-US", {
style: "currency",
currency: "USD",
})}</td
>
{:else} {:else}
<td class="px-4 py-2 text-gray-700" <td class="px-4 py-2 text-gray-700"
>{row[ >{row[
@@ -936,6 +952,22 @@
{/each} {/each}
</select> </select>
</div> </div>
{:else if col.key === "approved_by"}
<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"
disabled
/>
</div>
{:else} {:else}
<div class="mb-4"> <div class="mb-4">
<label <label

View File

@@ -14,6 +14,8 @@
approved_by: string; approved_by: string;
approved_price: number; approved_price: number;
completed_status: string; completed_status: string;
updated_at?: string;
updated_by?: string;
}; };
let purchaseOrderInsert: PurchaseOrderInsert = { let purchaseOrderInsert: PurchaseOrderInsert = {
@@ -27,6 +29,8 @@
approved_by: "", approved_by: "",
approved_price: 0, approved_price: 0,
completed_status: "", completed_status: "",
updated_at: new Date().toISOString(),
updated_by: "",
}; };
type PurchaseOrders = { type PurchaseOrders = {
@@ -50,7 +54,9 @@
input_by: string; input_by: string;
issue_id: string; issue_id: string;
approved_by: string; approved_by: string;
created_at: string; updated_at: Date;
updated_by: string;
created_at: Date;
}; };
type PurchaseOrderDisplay = { type PurchaseOrderDisplay = {
@@ -74,6 +80,7 @@
completed_status: string; completed_status: string;
received: boolean; received: boolean;
received_by: string; received_by: string;
created_at: Date;
}; };
let allRows: PurchaseOrderDisplay[] = []; let allRows: PurchaseOrderDisplay[] = [];
@@ -96,7 +103,9 @@
title: "Total Approved Order Amount", title: "Total Approved Order Amount",
}, },
{ key: "completed_status", title: "Completed Status" }, { key: "completed_status", title: "Completed Status" },
{ key: "acknowledge_by", title: "Acknowledged By" }, { key: "acknowledged_name", title: "Acknowledged By" },
{ key: "updated_name", title: "Updated By" },
{ key: "updated_at", title: "Updated At" },
{ key: "created_at", title: "Created At" }, { key: "created_at", title: "Created At" },
// { key: "actions", title: "Actions" }, // For edit/delete buttons // { key: "actions", title: "Actions" }, // For edit/delete buttons
]; ];
@@ -420,9 +429,20 @@
} }
async function completedStatusOk(id: string, status: string) { async function completedStatusOk(id: string, status: string) {
const sesssionId = await supabase.auth.getSession();
if (!sesssionId.data.session) {
console.error("User not authenticated");
return;
}
const userId = sesssionId.data.session?.user.id || "";
const { data, error } = await supabase const { data, error } = await supabase
.from("vb_purchase_orders") .from("vb_purchase_orders")
.update({ completed_status: status, po_status: status }) .update({
completed_status: status,
po_status: status,
updated_by: userId,
updated_at: new Date().toISOString(),
})
.eq("id", id); .eq("id", id);
if (error) { if (error) {
@@ -627,6 +647,22 @@
➡️ PURCHASE ORDER ➡️ PURCHASE ORDER
</button> </button>
</td> </td>
{:else if col.key === "updated_at"}
<td class="px-4 py-2">
{new Date(
row[
col.key as keyof PurchaseOrderDisplay
] as string,
).toLocaleString("en-US")}
</td>
{:else if col.key === "created_at"}
<td class="px-4 py-2">
{new Date(
row[
col.key as keyof PurchaseOrderDisplay
] as string,
).toLocaleString("en-US")}
</td>
{:else} {:else}
<td class="px-4 py-2 text-gray-700" <td class="px-4 py-2 text-gray-700"
>{row[ >{row[

View File

@@ -14,6 +14,8 @@
approved_by: string; approved_by: string;
approved_price: number; approved_price: number;
completed_status: string; completed_status: string;
updated_at: Date;
updated_by: string;
}; };
let purchaseOrderInsert: PurchaseOrderInsert = { let purchaseOrderInsert: PurchaseOrderInsert = {
@@ -27,6 +29,8 @@
approved_by: "", approved_by: "",
approved_price: 0, approved_price: 0,
completed_status: "", completed_status: "",
updated_at: new Date(),
updated_by: "",
}; };
type PurchaseOrders = { type PurchaseOrders = {
@@ -50,7 +54,9 @@
input_by: string; input_by: string;
issue_id: string; issue_id: string;
approved_by: string; approved_by: string;
created_at: string; created_at: Date;
updated_at: Date;
updated_by: string;
}; };
type PurchaseOrderDisplay = { type PurchaseOrderDisplay = {
@@ -74,6 +80,8 @@
completed_status: string; completed_status: string;
received: boolean; received: boolean;
received_by: string; received_by: string;
updated_at: Date;
updated_by: string;
}; };
let allRows: PurchaseOrderDisplay[] = []; let allRows: PurchaseOrderDisplay[] = [];
@@ -96,7 +104,9 @@
title: "Total Approved Order Amount", title: "Total Approved Order Amount",
}, },
{ key: "received", title: "Received" }, { key: "received", title: "Received" },
{ key: "received_by", title: "Received By" }, { key: "received_name", title: "Received By" },
{ key: "updated_at", title: "Updated At" },
{ key: "updated_name", title: "Updated By" },
{ key: "created_at", title: "Created At" }, { key: "created_at", title: "Created At" },
// { key: "actions", title: "Actions" }, // For edit/delete buttons // { key: "actions", title: "Actions" }, // For edit/delete buttons
]; ];
@@ -282,6 +292,8 @@
approved_price: newPurchaseOrders.approved_price || "", approved_price: newPurchaseOrders.approved_price || "",
approved_by: newPurchaseOrders.approved_by || "", approved_by: newPurchaseOrders.approved_by || "",
completed_status: newPurchaseOrders.completed_status || "", completed_status: newPurchaseOrders.completed_status || "",
updated_at: new Date(),
updated_by: (await supabase.auth.getUser()).data.user?.id || "",
}; };
if (isEditing && currentEditingId) { if (isEditing && currentEditingId) {
@@ -421,9 +433,21 @@
} }
async function receivedOk(id: string, status: boolean) { async function receivedOk(id: string, status: boolean) {
const sessionId = await supabase.auth.getSession();
if (!sessionId.data.session) {
console.error("User not authenticated");
return;
}
const userId = sessionId.data.session?.user.id || "";
const { data, error } = await supabase const { data, error } = await supabase
.from("vb_purchase_orders") .from("vb_purchase_orders")
.update({ received: status }) .update({
received: status,
received_by: userId,
updated_by: userId,
updated_at: new Date(),
})
.eq("id", id); .eq("id", id);
if (error) { if (error) {

View File

@@ -22,6 +22,8 @@
total_work_hour: number; total_work_hour: number;
remarks: string; remarks: string;
approval: boolean; approval: boolean;
approved_by?: string;
approved_date?: Date;
created_at?: Date; created_at?: Date;
}; };
@@ -148,7 +150,7 @@
} else { } else {
form.total_work_hour = 0; form.total_work_hour = 0;
} }
} }
async function fetchTimeSheets( async function fetchTimeSheets(
filter: string | null = null, filter: string | null = null,
@@ -224,8 +226,8 @@
const villa = villas.find((v) => v.id === issue.villa_id); const villa = villas.find((v) => v.id === issue.villa_id);
// Map entered_by to staff_id // Map entered_by to staff_id
const staff = reportedBy.find((s) => s.value === issue.entered_by); const staff = reportedBy.find((s) => s.value === issue.entered_by);
const approver = approvers?.find(u => u.id === issue.approved_by); const approver = approvers?.find((u) => u.id === issue.approved_by);
return { return {
id: issue.id, id: issue.id,
name: issue.work_description, // Map work_description to name name: issue.work_description, // Map work_description to name
@@ -284,8 +286,8 @@
if (!empErr && empData) { if (!empErr && empData) {
employees = empData.map((e) => ({ employees = empData.map((e) => ({
id: e.id, id: e.id,
name: e.employee_name, name: e.employee_name,
})); }));
} else { } else {
console.error("Failed to load employees", empErr); console.error("Failed to load employees", empErr);
@@ -329,16 +331,19 @@
currentEditingId = issue.id; currentEditingId = issue.id;
form = { form = {
entered_by: employees.find(e => e.name === issue.staff_id)?.id || "", entered_by:
work_description: issue.name, employees.find((e) => e.name === issue.staff_id)?.id || "",
type_of_work: issue.type_of_work, work_description: issue.name,
category_of_work: issue.category_of_work, type_of_work: issue.type_of_work,
villa_id: villas.find(v => v.villa_name === issue.villa_name)?.id || "", category_of_work: issue.category_of_work,
datetime_in: issue.date_in?.toISOString().slice(0, 16), villa_id:
datetime_out: issue.date_out?.toISOString().slice(0, 16), villas.find((v) => v.villa_name === issue.villa_name)?.id ||
total_work_hour: 0, "",
remarks: issue.remarks, datetime_in: issue.date_in?.toISOString().slice(0, 16),
approval: null // leave null or bring in if editing allowed datetime_out: issue.date_out?.toISOString().slice(0, 16),
total_work_hour: 0,
remarks: issue.remarks,
approval: null, // leave null or bring in if editing allowed
}; };
calculateTotalHours(); calculateTotalHours();
} else { } else {
@@ -346,36 +351,31 @@
isEditing = false; isEditing = false;
currentEditingId = null; currentEditingId = null;
form = { form = {
entered_by: "", entered_by: "",
work_description: "", work_description: "",
type_of_work: "Running", type_of_work: "Running",
category_of_work: "Cleaning", category_of_work: "Cleaning",
villa_id: "", villa_id: "",
datetime_in: "", datetime_in: "",
datetime_out: "", datetime_out: "",
total_work_hour: 0, total_work_hour: 0,
remarks: "", remarks: "",
approval: null, approval: null,
}; };
} }
showModal = true; showModal = true;
} }
type Employee = { type Employee = {
id: string; id: string;
name: string; name: string;
}; };
let employees: Employee[] = []; let employees: Employee[] = [];
let villas: Villa[] = []; let villas: Villa[] = [];
const typeOfWorkOptions: TimesheetForm["type_of_work"][] = [ const typeOfWorkOptions = ["Running", "Periodic", "Irregular"];
"Running", const categoryOptions = [
"Periodic",
"Irregular",
];
const categoryOptions: TimesheetForm["category_of_work"][] = [
"Cleaning", "Cleaning",
"Gardening/Pool", "Gardening/Pool",
"Maintenance", "Maintenance",
@@ -384,7 +384,7 @@
"Administration", "Administration",
"Non Billable", "Non Billable",
]; ];
let form: TimesheetForm = { let form = {
entered_by: "", entered_by: "",
work_description: "", work_description: "",
type_of_work: "Running", type_of_work: "Running",
@@ -440,10 +440,9 @@
datetime_out: formData.get("date_out") as string, datetime_out: formData.get("date_out") as string,
total_work_hour: newIssue.total_work_hour ?? 0, total_work_hour: newIssue.total_work_hour ?? 0,
remarks: formData.get("remarks") as string, remarks: formData.get("remarks") as string,
approval: null approval: null,
}; };
const { error } = await supabase const { error } = await supabase
.from("vb_timesheet") .from("vb_timesheet")
.insert([TimesheetsInsert]); .insert([TimesheetsInsert]);
@@ -532,26 +531,26 @@
if (isEditing && currentEditingId) { if (isEditing && currentEditingId) {
const { error: updateError } = await supabase const { error: updateError } = await supabase
.from("vb_timesheet") .from("vb_timesheet")
.update({ .update({
entered_by: form.entered_by, entered_by: form.entered_by,
work_description: form.work_description, work_description: form.work_description,
type_of_work: form.type_of_work, type_of_work: form.type_of_work,
category_of_work: form.category_of_work, category_of_work: form.category_of_work,
villa_id: form.villa_id, villa_id: form.villa_id,
datetime_in: form.datetime_in, datetime_in: form.datetime_in,
datetime_out: form.datetime_out, datetime_out: form.datetime_out,
total_work_hour: form.total_work_hour, total_work_hour: form.total_work_hour,
remarks: form.remarks, remarks: form.remarks,
approval: form.approval, approval: form.approval,
}) })
.eq("id", currentEditingId); .eq("id", currentEditingId);
error = updateError; error = updateError;
} else { } else {
const { error: insertError } = await supabase const { error: insertError } = await supabase
.from("vb_timesheet") .from("vb_timesheet")
.insert([form]); .insert([form]);
error = insertError; error = insertError;
} }
@@ -561,22 +560,21 @@
} else { } else {
alert("Timesheet saved successfully!"); alert("Timesheet saved successfully!");
form = { form = {
entered_by: "", entered_by: "",
work_description: "", work_description: "",
type_of_work: "Running", type_of_work: "Running",
category_of_work: "Cleaning", category_of_work: "Cleaning",
villa_id: "", villa_id: "",
datetime_in: "", datetime_in: "",
datetime_out: "", datetime_out: "",
total_work_hour: 0, total_work_hour: 0,
remarks: "", remarks: "",
approval: null, approval: null,
}; };
await fetchTimeSheets(); await fetchTimeSheets();
showModal = false; showModal = false;
} }
} }
</script> </script>
<div> <div>
@@ -699,12 +697,17 @@
<td class="px-4 py-2"> <td class="px-4 py-2">
{row[col.key] || "Not Approved"} {row[col.key] || "Not Approved"}
</td> </td>
{:else if col.key === "approved_date"} {:else if col.key === "approved_date"}
<td class="px-4 py-2"> <td class="px-4 py-2">
{(row[col.key] && !isNaN(Date.parse(row[col.key]))) {row[col.key] &&
? new Date(row[col.key]).toLocaleDateString() !isNaN(Date.parse(String(row[col.key])))
: "N/A"} ? new Date(
</td> row[
col.key as keyof TimesheetDisplay
] as string | number | Date,
).toLocaleDateString()
: "N/A"}
</td>
{:else if col.key === "total_hours_work"} {:else if col.key === "total_hours_work"}
<td class="px-4 py-2"> <td class="px-4 py-2">
{row[col.key].toFixed(2)} hours {row[col.key].toFixed(2)} hours
@@ -745,20 +748,22 @@
🗑️ Delete 🗑️ Delete
</button> </button>
</td> </td>
{:else if col.key === "date_in" || col.key === "date_out"} {:else if col.key === "date_in" || col.key === "date_out"}
<td class="px-4 py-2"> <td class="px-4 py-2">
{row[col.key] {row[col.key]
? new Date(row[col.key]).toLocaleString("en-GB", { ? new Date(row[col.key]).toLocaleString(
day: "2-digit", "en-GB",
month: "2-digit", {
year: "numeric", day: "2-digit",
hour: "2-digit", month: "2-digit",
minute: "2-digit", year: "numeric",
hour12: false, hour: "2-digit",
}) minute: "2-digit",
hour12: false,
},
)
: "N/A"} : "N/A"}
</td> </td>
{:else} {:else}
<td class="px-4 py-2"> <td class="px-4 py-2">
{row[col.key as keyof TimesheetDisplay]} {row[col.key as keyof TimesheetDisplay]}
@@ -811,138 +816,145 @@
<!-- Modal --> <!-- Modal -->
{#if showModal} {#if showModal}
<div class="fixed inset-0 bg-black bg-opacity-50 z-50 overflow-y-auto py-10 px-4 flex justify-center items-start"> <div
class="fixed inset-0 bg-black bg-opacity-50 z-50 overflow-y-auto py-10 px-4 flex justify-center items-start"
<form
on:submit|preventDefault={submitForm}
class="w-full max-w-lg bg-white p-6 rounded-2xl shadow-xl space-y-4"
>
<h2 class="text-2xl font-bold text-center mb-6">Timesheet Entry</h2>
<div>
<label for="t_eb" class="block text-sm font-medium mb-1">Entered By</label
>
<select
id="t_eb"
class="w-full border p-2 rounded"
bind:value={form.entered_by}
required
>
<option value="" disabled selected>Select Employee</option>
{#each employees as employee}
<option value={employee.id}>{employee.name}</option>
{/each}
</select>
</div>
<div>
<label for="t_wd" class="block text-sm font-medium mb-1"
>Work Description</label
>
<textarea
id="t_wd"
class="w-full border border-gray-300 p-2 rounded"
bind:value={form.work_description}
placeholder="Describe the work"
required
></textarea>
</div>
<div>
<label for="t_ow" class="block text-sm font-medium mb-1"
>Type of Work</label
>
<select
id="t_ow"
class="w-full border p-2 rounded"
bind:value={form.type_of_work}
>
{#each typeOfWorkOptions as option}
<option value={option}>{option}</option>
{/each}
</select>
</div>
<div>
<label for="t_cow" class="block text-sm font-medium mb-1"
>Category of Work</label
>
<select
id="t_cow"
class="w-full border p-2 rounded"
bind:value={form.category_of_work}
>
{#each categoryOptions as option}
<option value={option}>{option}</option>
{/each}
</select>
</div>
<div>
<label for="t_vn" class="block text-sm font-medium mb-1">Villa</label>
<select
id="t_vn"
class="w-full border p-2 rounded"
bind:value={form.villa_id}
required
>
<option value="" disabled selected>Select Villa</option>
{#each villas as villa}
<option value={villa.id}>{villa.villa_name}</option>
{/each}
</select>
</div>
<div>
<label for="tdto" class="block text-sm font-medium mb-1"
>Date/Time In</label
>
<input
id="tdto"
type="datetime-local"
class="w-full border p-2 rounded"
bind:value={form.datetime_in}
on:change={calculateTotalHours}
required
/>
</div>
<div>
<label for="dto" class="block text-sm font-medium mb-1"
>Date/Time Out</label
>
<input
id="dto"
type="datetime-local"
class="w-full border p-2 rounded"
bind:value={form.datetime_out}
on:change={calculateTotalHours}
required
/>
</div>
<div class="text-sm">
<label for="ttwo" class="block font-medium mb-1">Total Work Hours</label>
<div id="ttwo" class="px-3 py-2">{form.total_work_hour}</div>
</div>
<div>
<label for="trmk" class="block text-sm font-medium mb-1">Remarks</label>
<textarea
id="trmk"
class="w-full border border-gray-300 p-2 rounded"
bind:value={form.remarks}
placeholder="Optional remarks"
></textarea>
</div>
<button
type="submit"
class="w-full bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition"
> >
{isEditing ? 'Update Timesheet' : 'New Entry'} <form
</button> on:submit|preventDefault={submitForm}
</form> class="w-full max-w-lg bg-white p-6 rounded-2xl shadow-xl space-y-4"
>
<h2 class="text-2xl font-bold text-center mb-6">Timesheet Entry</h2>
<div>
<label for="t_eb" class="block text-sm font-medium mb-1"
>Entered By</label
>
<select
id="t_eb"
class="w-full border p-2 rounded"
bind:value={form.entered_by}
required
>
<option value="" disabled selected>Select Employee</option>
{#each employees as employee}
<option value={employee.id}>{employee.name}</option>
{/each}
</select>
</div>
<div>
<label for="t_wd" class="block text-sm font-medium mb-1"
>Work Description</label
>
<textarea
id="t_wd"
class="w-full border border-gray-300 p-2 rounded"
bind:value={form.work_description}
placeholder="Describe the work"
required
></textarea>
</div>
<div>
<label for="t_ow" class="block text-sm font-medium mb-1"
>Type of Work</label
>
<select
id="t_ow"
class="w-full border p-2 rounded"
bind:value={form.type_of_work}
>
{#each typeOfWorkOptions as option}
<option value={option}>{option}</option>
{/each}
</select>
</div>
<div>
<label for="t_cow" class="block text-sm font-medium mb-1"
>Category of Work</label
>
<select
id="t_cow"
class="w-full border p-2 rounded"
bind:value={form.category_of_work}
>
{#each categoryOptions as option}
<option value={option}>{option}</option>
{/each}
</select>
</div>
<div>
<label for="t_vn" class="block text-sm font-medium mb-1"
>Villa</label
>
<select
id="t_vn"
class="w-full border p-2 rounded"
bind:value={form.villa_id}
required
>
<option value="" disabled selected>Select Villa</option>
{#each villas as villa}
<option value={villa.id}>{villa.villa_name}</option>
{/each}
</select>
</div>
<div>
<label for="tdto" class="block text-sm font-medium mb-1"
>Date/Time In</label
>
<input
id="tdto"
type="datetime-local"
class="w-full border p-2 rounded"
bind:value={form.datetime_in}
on:change={calculateTotalHours}
required
/>
</div>
<div>
<label for="dto" class="block text-sm font-medium mb-1"
>Date/Time Out</label
>
<input
id="dto"
type="datetime-local"
class="w-full border p-2 rounded"
bind:value={form.datetime_out}
on:change={calculateTotalHours}
required
/>
</div>
<div class="text-sm">
<label for="ttwo" class="block font-medium mb-1"
>Total Work Hours</label
>
<div id="ttwo" class="px-3 py-2">{form.total_work_hour}</div>
</div>
<div>
<label for="trmk" class="block text-sm font-medium mb-1"
>Remarks</label
>
<textarea
id="trmk"
class="w-full border border-gray-300 p-2 rounded"
bind:value={form.remarks}
placeholder="Optional remarks"
></textarea>
</div>
<button
type="submit"
class="w-full bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition"
>
{isEditing ? "Update Timesheet" : "New Entry"}
</button>
</form>
</div> </div>
{/if} {/if}

View File

@@ -28,18 +28,6 @@
website: string; website: string;
}; };
let allRowsVendor: Vendor[] = [];
let currentPage = 1;
let itemsPerPage = 10;
$: offset = (currentPage - 1) * itemsPerPage;
$: totalPages = Math.ceil(totalItems / itemsPerPage);
let totalItems = 0;
let newVendor: Record<string, any> = {};
let showModal = false;
let isEditing = false;
let currentEditingId: string | null = null;
let searchTerm = "";
const columns = [ const columns = [
{ key: "name", title: "Name" }, { key: "name", title: "Name" },
{ key: "vendor_type", title: "Vendor Type" }, { key: "vendor_type", title: "Vendor Type" },
@@ -63,6 +51,19 @@
const excludedKeys = ["id", "created_by", "created_at", "updated_at"]; const excludedKeys = ["id", "created_by", "created_at", "updated_at"];
$: formColumns = columns.filter((col) => !excludedKeys.includes(col.key)); $: formColumns = columns.filter((col) => !excludedKeys.includes(col.key));
let allRowsVendor: Vendor[] = [];
let currentPage = 1;
let offset = 0;
let itemsPerPage = 10;
let totalItems = 0;
let newVendor: Record<string, any> = {};
let showModal = false;
let isEditing = false;
let currentEditingId: string | null = null;
let searchTerm = "";
$: offset = (currentPage - 1) * itemsPerPage;
$: totalPages = Math.ceil(totalItems / itemsPerPage);
async function fetchVendor(search = "", resetPage = false) { async function fetchVendor(search = "", resetPage = false) {
if (resetPage) currentPage = 1; if (resetPage) currentPage = 1;
@@ -84,7 +85,13 @@
function resetPagination() { function resetPagination() {
currentPage = 1; currentPage = 1;
offset = 0;
totalItems = 0;
fetchVendor(searchTerm); fetchVendor(searchTerm);
showModal = false;
isEditing = false;
currentEditingId = null;
newVendor = {};
} }
function nextPage() { function nextPage() {
@@ -102,6 +109,7 @@
} }
function changePage(page: number) { function changePage(page: number) {
if (page < 1 || page > totalPages || page === currentPage) return;
currentPage = page; currentPage = page;
fetchVendor(searchTerm); fetchVendor(searchTerm);
} }
@@ -159,7 +167,10 @@
} }
async function deleteVendor(id: string) { async function deleteVendor(id: string) {
const { error } = await supabase.from("vb_vendor").delete().eq("id", id); const { error } = await supabase
.from("vb_vendor")
.delete()
.eq("id", id);
if (error) { if (error) {
console.error("Delete error:", error); console.error("Delete error:", error);
} else { } else {
@@ -167,7 +178,10 @@
} }
} }
function pageRange(totalPages: number, currentPage: number): (number | string)[] { function pageRange(
totalPages: number,
currentPage: number,
): (number | string)[] {
const range: (number | string)[] = []; const range: (number | string)[] = [];
const maxDisplay = 5; const maxDisplay = 5;
@@ -207,79 +221,125 @@
fetchVendor(searchTerm, true); fetchVendor(searchTerm, true);
}} }}
/> />
<button class="bg-blue-600 text-white px-4 py-2 rounded" on:click={() => openModal()}> <button
class="bg-blue-600 text-white px-4 py-2 rounded"
on:click={() => openModal()}
>
Add Vendor Add Vendor
</button> </button>
</div> </div>
<!-- Table --> <!-- Table -->
<table class="min-w-[1000px] divide-y divide-gray-200 text-sm w-max"> <div class="overflow-x-auto rounded-lg shadow mb-4">
<thead class="bg-gray-100"> <table class="min-w-[1000px] divide-y divide-gray-200 text-sm w-max">
<tr> <thead class="bg-gray-100">
{#each columns as col} <tr>
<th class="p-2 text-left border">{col.title}</th>
{/each}
<th class="p-2 border">Actions</th>
</tr>
</thead>
<tbody>
{#each allRowsVendor as row}
<tr class="hover:bg-gray-50">
{#each columns as col} {#each columns as col}
<td class="p-2 border">{row[col.key]}</td> <th class="p-2 text-left border">{col.title}</th>
{/each} {/each}
<td class="p-2 border"> <th class="p-2 border">Actions</th>
<button class="text-blue-600" on:click={() => openModal(row)}>✏️</button>
<button class="text-red-600 ml-2" on:click={() => deleteVendor(row.id)}>🗑️</button>
</td>
</tr> </tr>
{/each} </thead>
</tbody> <tbody>
</table> {#each allRowsVendor as row}
<tr class="hover:bg-gray-50">
{#each columns as col}
<td class="p-2 border"
>{row[col.key as keyof typeof row]}</td
>
{/each}
<td class="p-2 border">
<button
class="text-blue-600"
on:click={() => openModal(row)}>✏️</button
>
<button
class="text-red-600 ml-2"
on:click={() => deleteVendor(row.id)}>🗑️</button
>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<!-- Pagination --> <!-- Pagination -->
<div class="flex justify-between items-center mt-4 text-sm"> <div class="flex justify-between items-center mt-4 text-sm">
<div> <div>
Showing {(currentPage - 1) * itemsPerPage + 1} Page {currentPage} of {totalPages} | Showing
{Math.min(currentPage * itemsPerPage, totalItems)} of {totalItems} {currentPage === totalPages && totalItems > 0
? totalItems - offset
: itemsPerPage} items | Showing
{offset + 1} to {Math.min(currentPage * itemsPerPage, totalItems)} of {totalItems}
</div> </div>
<div class="flex space-x-1"> <div class="flex space-x-1">
<button on:click={previousPage} disabled={currentPage === 1} class="px-2 py-1 border rounded disabled:opacity-50">Prev</button> <button
on:click={previousPage}
disabled={currentPage <= 1}
class="px-2 py-1 border rounded disabled:opacity-50">Prev</button
>
{#each pageRange(totalPages, currentPage) as page} {#each pageRange(totalPages, currentPage) as page}
{#if page === '...'} {#if page === "..."}
<span class="px-2">...</span> <span class="px-2">...</span>
{:else} {:else}
<button <button
on:click={() => changePage(page)} on:click={() => changePage(page as number)}
class="px-2 py-1 border rounded {page === currentPage ? 'bg-blue-600 text-white' : ''}"> class="px-2 py-1 border rounded {page === currentPage
? 'bg-blue-600 text-white'
: ''}"
>
{page} {page}
</button> </button>
{/if} {/if}
{/each} {/each}
<button on:click={nextPage} disabled={currentPage === totalPages} class="px-2 py-1 border rounded disabled:opacity-50">Next</button> <button
on:click={nextPage}
disabled={currentPage >= totalPages}
class="px-2 py-1 border rounded disabled:opacity-50">Next</button
>
</div> </div>
</div> </div>
<!-- Modal --> <!-- Modal -->
{#if showModal} {#if showModal}
<div class="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50"> <div
<div class="bg-white p-6 rounded w-[500px] max-h-[90vh] overflow-y-auto"> class="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50"
<h2 class="text-lg font-semibold mb-4">{isEditing ? "Edit Vendor" : "Add Vendor"}</h2> >
{#each formColumns as col} <div
<div class="mb-3"> class="bg-white p-6 rounded w-[500px] max-h-[90vh] overflow-y-auto"
<label class="text-sm">{col.title}</label> >
{#if col.key === 'vendor_unique'} <h2 class="text-lg font-semibold mb-4">
<input class="w-full p-2 border rounded bg-gray-100" bind:value={newVendor[col.key]} readonly /> {isEditing ? "Edit Vendor" : "Add Vendor"}
{:else} </h2>
<input class="w-full p-2 border rounded" bind:value={newVendor[col.key]} /> {#each formColumns as col}
{/if} <div class="mb-3">
<label class="text-sm">{col.title}</label>
{#if col.key === "vendor_unique"}
<input
class="w-full p-2 border rounded bg-gray-100"
bind:value={newVendor[col.key]}
readonly
/>
{:else}
<input
class="w-full p-2 border rounded"
bind:value={newVendor[col.key]}
/>
{/if}
</div>
{/each}
<div class="flex justify-end mt-4 space-x-2">
<button
class="px-4 py-2 bg-gray-200 rounded"
on:click={() => (showModal = false)}>Cancel</button
>
<button
class="px-4 py-2 bg-blue-600 text-white rounded"
on:click={isEditing ? updateVendor : addVendor}>Save</button
>
</div> </div>
{/each}
<div class="flex justify-end mt-4 space-x-2">
<button class="px-4 py-2 bg-gray-200 rounded" on:click={() => showModal = false}>Cancel</button>
<button class="px-4 py-2 bg-blue-600 text-white rounded" on:click={isEditing ? updateVendor : addVendor}>Save</button>
</div> </div>
</div> </div>
</div>
{/if} {/if}