rev timesheet & project

This commit is contained in:
2025-07-07 03:57:20 +08:00
parent aa9f3de909
commit ad9f363166
3 changed files with 147 additions and 130 deletions

View File

@@ -0,0 +1,32 @@
<script lang="ts">
export let value: number = 0; // The raw number
export let label: string = ""; // Field label
export let onInput: (() => void) | null = null; // Optional extra handler
let formatted = "";
// Format whenever value changes
$: formatted = `Rp ${value.toLocaleString("id-ID", {
minimumFractionDigits: 0
})}`;
function handleInput(e: Event) {
let raw = (e.target as HTMLInputElement).value;
raw = raw.replace(/^Rp\s?/, "").replace(/[^\d]/g, "");
value = parseInt(raw) || 0;
formatted = `Rp ${value.toLocaleString("id-ID")}`;
// ✅ If extra handler provided, run it
if (onInput) onInput();
}
</script>
<label class="block text-sm font-medium text-gray-700">{label}</label>
<input
type="text"
bind:value={formatted}
placeholder="Rp 0"
class="w-full border p-2 rounded"
on:input={handleInput}
/>

View File

@@ -2,6 +2,7 @@
import { supabase } from "$lib/supabaseClient"; import { supabase } from "$lib/supabaseClient";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import Pagination from "$lib/Pagination.svelte";
const priority = [ const priority = [
{ label: "Low", value: "Low" }, { label: "Low", value: "Low" },
@@ -137,8 +138,9 @@
{ label: "Water Feature", value: "Water Feature" } { label: "Water Feature", value: "Water Feature" }
]; ];
const columns: columns[] = [ const columns: columns[] = [
{ key: "description_of_the_issue", title: "Description of The Issue" },
{ key: "issue_number", title: "Issue Number" }, { key: "issue_number", title: "Issue Number" },
{ key: "description_of_the_issue", title: "Description of The Issue" },
{ key: "villa_name", title: "Villa Name" }, { key: "villa_name", title: "Villa Name" },
{ key: "villa_id", title: "Villa ID" }, { key: "villa_id", title: "Villa ID" },
{ key: "input_by", title: "Input By" }, { key: "input_by", title: "Input By" },
@@ -253,7 +255,7 @@
}; };
// Reactive variables // Reactive variables
let currentVillaFilter: string | null = null; let currentVillaFilter: string | null = "";
let currentSearchTerm: string | null = null; let currentSearchTerm: string | null = null;
let showProjectModal = false; let showProjectModal = false;
let selectedIssueId: string | null = null; let selectedIssueId: string | null = null;
@@ -295,8 +297,31 @@
let totalItems = 0; let totalItems = 0;
let currentPage = offset + 1; let currentPage = offset + 1;
let rowsPerPage = limit; let rowsPerPage = limit;
$: totalPages = Math.ceil(totalItems / rowsPerPage); let filteredRows: Issue[] = [];
$: paginatedRows = filteredRows.slice(
(currentPage - 1) * rowsPerPage,
currentPage * rowsPerPage
);
$: totalItems = filteredRows.length;
$: totalPages = Math.ceil(totalItems / rowsPerPage);
$: {
let rows = allRows;
if (currentVillaFilter) {
rows = rows.filter(r => r.villa_id === currentVillaFilter);
}
if (currentSearchTerm && currentSearchTerm.trim() !== "") {
const term = currentSearchTerm.toLowerCase();
rows = rows.filter(r =>
(r.description_of_the_issue || "").toLowerCase().includes(term)
);
}
filteredRows = rows;
currentPage = 1; // Optional: reset page when filter/search changes
}
// Fetch existing project links and purchase orders // Fetch existing project links and purchase orders
async function fetchExistingProjectLinks() { async function fetchExistingProjectLinks() {
const { data, error } = await supabase const { data, error } = await supabase
@@ -319,49 +344,21 @@
if (data) poItems = data; if (data) poItems = data;
} }
// Fetch issues with optional filters // Fetch issues with optional filters
async function fetchIssues( async function fetchIssues() {
search: string | null = null,
villaNameFilter: string | null = null,
sort: string | null = "created_at",
order: "asc" | "desc" = "desc",
offset: number = 0,
limit: number = 10,
) {
let query = supabase let query = supabase
.from("vb_issues_data") .from("vb_issues_data")
.select("*", { count: "exact" }) .select("*").order("issue_number", { ascending: true });
.order(sort || "created_at", { ascending: order === "asc" })
.range(offset, offset + limit - 1);
if (villaNameFilter) { const { data: issues, error } = await query;
const villa = dataVilla.find(v => v.villa_name === villaNameFilter);
if (villa) {
query = query.eq("villa_id", villa.id);
}
}
if (search) {
query = query.ilike("description_of_the_issue", `%${search}%`);
}
const { data: issues, error, count } = await query;
if (error) { if (error) {
console.error("Error fetching issues:", error); console.error(error);
return; return;
} }
if (count !== undefined) { allRows = issues || [];
totalItems = count ?? 0;
}
if (!issues || issues.length === 0) {
allRows = [];
return;
}
// Gabungkan data villa ke dalam setiap issue
allRows = issues;
} }
// Function to handle form submission and save issue // Function to handle form submission and save issue
async function saveIssue(event: Event) { async function saveIssue(event: Event) {
const session = await supabase.auth.getSession(); const session = await supabase.auth.getSession();
@@ -610,8 +607,6 @@
requested_by: "", requested_by: "",
requested_date: formatDate(today), requested_date: formatDate(today),
po_due: formatDate(dueDate), po_due: formatDate(dueDate),
po_item: "",
po_quantity: "",
po_type: "" po_type: ""
}; };
@@ -748,39 +743,24 @@
id="issue-search" id="issue-search"
placeholder="🔍 Search by Issue..." placeholder="🔍 Search by Issue..."
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" 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) => { bind:value={currentSearchTerm}
currentSearchTerm = (e.target as HTMLInputElement).value.toLowerCase();
fetchIssues( currentSearchTerm, currentVillaFilter);
}}
/> />
<select <select
id="villa-filter" id="villa-filter"
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" 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) => { bind:value={currentVillaFilter}
currentVillaFilter = (e.target as HTMLSelectElement).value || null;
fetchIssues(currentSearchTerm, currentVillaFilter);
}}
> >
<option value="">All Villas</option> <option value="">All Villas</option>
{#each dataVilla as villa} {#each dataVilla as villa}
<option value={villa.villa_name}>{villa.villa_name}</option> <option value={villa.id}>{villa.villa_name}</option>
{/each} {/each}
</select> </select>
<button <button
class="bg-gray-200 text-gray-700 px-4 py-2 rounded-xl hover:bg-gray-300 text-sm transition" class="bg-gray-200 text-gray-700 px-4 py-2 rounded-xl hover:bg-gray-300 text-sm transition"
on:click={() =>{ on:click={() =>{
currentVillaFilter = null; currentVillaFilter = "";
currentSearchTerm = null; currentSearchTerm = "";
const searchInput = document.getElementById("issue-search") as HTMLInputElement;
if (searchInput) searchInput.value = "";
const moveSelect = document.getElementById("move-filter") as HTMLSelectElement;
if (moveSelect) moveSelect.value = "";
const villaSelect = document.getElementById("villa-filter") as HTMLSelectElement;
if (villaSelect) villaSelect.value = "";
fetchIssues(null, null, null);
}} }}
> >
🔄 Reset 🔄 Reset
@@ -798,7 +778,7 @@
<thead class="bg-gray-100"> <thead class="bg-gray-100">
<tr> <tr>
{#each formColumnsDisplay as col} {#each formColumnsDisplay as col}
{#if col.key === "description_of_the_issue"} {#if col.key === "issue_number"}
<th <th
class="sticky left-0 px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap" 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;" style="background-color: #f0f8ff; z-index: 10;"
@@ -816,10 +796,10 @@
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200 bg-white"> <tbody class="divide-y divide-gray-200 bg-white">
{#each allRows as row} {#each paginatedRows as row}
<tr class="hover:bg-gray-50 transition"> <tr class="hover:bg-gray-50 transition">
{#each formColumnsDisplay as col} {#each formColumnsDisplay as col}
{#if col.key === "description_of_the_issue"} {#if col.key === "issue_number"}
<td <td
class="sticky left-0 px-4 py-2 font-medium text-blue-600 max-w-xs whitespace-normal align-top break-words" class="sticky left-0 px-4 py-2 font-medium text-blue-600 max-w-xs whitespace-normal align-top break-words"
style="background-color: #f0f8ff; cursor: pointer;" style="background-color: #f0f8ff; cursor: pointer;"
@@ -1007,39 +987,31 @@
</div> </div>
<!-- Pagination controls --> <!-- Pagination controls -->
<div class="flex justify-between items-center text-sm"> <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2 text-sm">
<!-- Left: Showing XY of Z -->
<div> <div>
Showing {(currentPage - 1) * rowsPerPage + 1} Showing {(currentPage - 1) * rowsPerPage + 1}
{Math.min(currentPage * rowsPerPage, allRows.length)} of {allRows.length} {Math.min(currentPage * rowsPerPage, filteredRows.length)}
of {filteredRows.length}
</div> </div>
<div class="space-x-2">
<button <!-- Right: Rows per page & Pagination -->
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50" <div class="flex items-center space-x-2">
on:click={() => goToPage(currentPage - 1)} <label class="text-gray-600">Rows per page:</label>
disabled={currentPage === 1} <select
> bind:value={rowsPerPage}
Previous class="border px-2 py-1 rounded"
</button> on:change={() => {
{#each Array(totalPages) currentPage = 1; // Reset to first page when changing page size
.fill(0) }}
.map((_, i) => i + 1) as page} >
<button <option value="10">10</option>
class="px-3 py-1 rounded border text-sm <option value="20">20</option>
{currentPage === page <option value="50">50</option>
? 'bg-blue-600 text-white border-blue-600' <option value="100">100</option>
: 'bg-white border-gray-300 hover:bg-gray-100'}" </select>
on:click={() => goToPage(page)}
> <Pagination {totalPages} {currentPage} {goToPage} />
{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> </div>
</div> </div>
@@ -1486,21 +1458,6 @@
</select> </select>
</label> </label>
<label class="block">
Product *
<select bind:value={newPO.po_item} class="input w-full">
<option value="">-- Select Product --</option>
{#each poItems as item}
<option value={item.item_name}>{item.item_name}</option>
{/each}
</select>
</label>
<label class="block">
Quantity *
<input type="number" bind:value={newPO.po_quantity} class="input w-full" />
</label>
<label class="block"> <label class="block">
Type * Type *
<select bind:value={newPO.po_type} class="input w-full"> <select bind:value={newPO.po_type} class="input w-full">

View File

@@ -2,6 +2,7 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import { supabase } from "$lib/supabaseClient"; import { supabase } from "$lib/supabaseClient";
import { getSessionAuthId } from "$lib/utils/authUtil"; import { getSessionAuthId } from "$lib/utils/authUtil";
import CurrencyInput from "$lib/CurrencyInput.svelte";
type PurchaseOrderInsert = { type PurchaseOrderInsert = {
@@ -77,9 +78,10 @@
}; };
const columns: columns[] = [ const columns: columns[] = [
{ key: "purchase_order_number", title: "PO Number" },
{ key: "issue_name", title: "Issue Name" }, { key: "issue_name", title: "Issue Name" },
{ key: "requested_date", title: "Requested Date" }, { key: "requested_date", title: "Requested Date" },
{ key: "purchase_order_number", title: "PO Number" },
{ key: "po_status", title: "PO Status" }, { key: "po_status", title: "PO Status" },
{ key: "villa_data", title: "Villa Name" }, { key: "villa_data", title: "Villa Name" },
{ key: "po_item", title: "PO Product" }, { key: "po_item", title: "PO Product" },
@@ -224,6 +226,8 @@
acknowledged_by: "", acknowledged_by: "",
acknowledged_date: "" acknowledged_date: ""
}; };
let selectedVillaId: string | null = null;
let searchTerm: string = "";
let showPreparedModal = false; let showPreparedModal = false;
let selectedPO = null; let selectedPO = null;
let preparedByOptions: any[] = []; let preparedByOptions: any[] = [];
@@ -244,7 +248,8 @@
approved_vendor: "", approved_vendor: "",
approved_price: 0, approved_price: 0,
total_approved_order_amount: 0 total_approved_order_amount: 0
}; };
let formattedPrice = "";
let showReceivedModal = false; let showReceivedModal = false;
let receivedForm = { let receivedForm = {
po_number: "", po_number: "",
@@ -273,6 +278,11 @@
currentPage * rowsPerPage, currentPage * rowsPerPage,
); );
$: currentPage = 1; // Reset to first page when allRows changes $: currentPage = 1; // Reset to first page when allRows changes
$: formattedPrice = preparedForm.q1_vendor_price.toLocaleString("id-ID", {
style: "currency",
currency: "IDR",
minimumFractionDigits: 0,
});
// Function to format numbers as ordinal (1st, 2nd, 3rd, etc.) // Function to format numbers as ordinal (1st, 2nd, 3rd, etc.)
function ordinal(num: number) { function ordinal(num: number) {
@@ -682,7 +692,7 @@
let query = supabase let query = supabase
.from("vb_purchaseorder_data") .from("vb_purchaseorder_data")
.select("*") .select("*")
.order(sort || "created_at", { ascending: order === "asc" }) .order(sort || "purchase_order_number", { ascending: order === "desc" })
.range(offset, offset + limit - 1); .range(offset, offset + limit - 1);
if (filter) { if (filter) {
@@ -1065,7 +1075,7 @@
<thead class="bg-gray-100"> <thead class="bg-gray-100">
<tr> <tr>
{#each columns as col} {#each columns as col}
{#if col.key === "issue_name"} {#if col.key === "purchase_order_number"}
<th <th
class="sticky left-0 px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap" 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;" style="background-color: #f0f8ff; z-index: 10;"
@@ -1098,10 +1108,10 @@
<td class="px-4 py-2 text-gray-700 max-w-xs whitespace-normal align-top break-words"> <td class="px-4 py-2 text-gray-700 max-w-xs whitespace-normal align-top break-words">
{row[col.key] || "—"} {row[col.key] || "—"}
</td> </td>
{:else if col.key === "issue_name"} {:else if col.key === "purchase_order_number"}
<td class="sticky left-0 px-4 py-2 font-medium text-blue-600 max-w-xs whitespace-normal align-top break-words" <td class="sticky left-0 px-4 py-2 font-medium text-blue-600 max-w-xs whitespace-normal align-top break-words"
style="background-color: #f0f8ff; cursor: pointer;"> style="background-color: #f0f8ff; cursor: pointer;">
{row.issue_name || "—"} {row.purchase_order_number || "—"}
</td> </td>
{:else if col.key === "prepared"} {:else if col.key === "prepared"}
<td class="px-4 py-2 text-center"> <td class="px-4 py-2 text-center">
@@ -1209,16 +1219,7 @@
> >
Payment Payment
</button> </button>
</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={() => openEditModal(row)}
>
✏️ Edit
</button>
<button <button
class="inline-flex items-center gap-1 rounded bg-teal-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-teal-700 disabled:bg-gray-400 disabled:cursor-not-allowed" class="inline-flex items-center gap-1 rounded bg-teal-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-teal-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
on:click={() => printPO(row)} on:click={() => printPO(row)}
@@ -1232,6 +1233,16 @@
🖨️ Print 🖨️ Print
{/if} {/if}
</button> </button>
</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={() => openEditModal(row)}
>
✏️ Edit
</button>
<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" 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)} on:click={() => deleteProject(row.id)}
@@ -1567,8 +1578,23 @@
<option value={v.name}>{v.name}</option> <option value={v.name}>{v.name}</option>
{/each} {/each}
</select> </select>
<input type="number" bind:value={preparedForm.q1_vendor_price} placeholder="Q1 Vendor Price" class="w-full border p-2" /> <CurrencyInput bind:value={preparedForm.q1_vendor_price} label="Q1 Vendor Price" />
<label>Q2 Vendor</label>
<select bind:value={preparedForm.q2_vendor} class="w-full border p-2">
<option value="" disabled>Select Vendor</option>
{#each vendorOptions as v}
<option value={v.name}>{v.name}</option>
{/each}
</select>
<CurrencyInput bind:value={preparedForm.q2_vendor_price} label="Q1 Vendor Price" />
<label>Q3 Vendor</label>
<select bind:value={preparedForm.q3_vendor} class="w-full border p-2">
<option value="" disabled>Select Vendor</option>
{#each vendorOptions as v}
<option value={v.name}>{v.name}</option>
{/each}
</select>
<CurrencyInput bind:value={preparedForm.q3_vendor_price} label="Q1 Vendor Price" />
<!-- Repeat for Q2, Q3 --> <!-- Repeat for Q2, Q3 -->
<!-- Approved --> <!-- Approved -->
<label>Approved Quantity</label> <label>Approved Quantity</label>
@@ -1581,9 +1607,11 @@
<option value={v.name}>{v.name}</option> <option value={v.name}>{v.name}</option>
{/each} {/each}
</select> </select>
<CurrencyInput
<label>Approved Price</label> bind:value={preparedForm.approved_price}
<input type="number" bind:value={preparedForm.approved_price} on:input={() => updateTotalAmount()} class="w-full border p-2" /> label="Approved Price"
onInput={updateTotalAmount}
/>
<label>Total Approved Order Amount</label> <label>Total Approved Order Amount</label>
<input type="number" value={preparedForm.total_approved_order_amount} disabled class="w-full border p-2 bg-gray-100" /> <input type="number" value={preparedForm.total_approved_order_amount} disabled class="w-full border p-2 bg-gray-100" />