penambahan menu inventory
This commit is contained in:
@@ -1,7 +1,778 @@
|
|||||||
<div
|
<script lang="ts">
|
||||||
style="display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh;"
|
import { onMount } from "svelte";
|
||||||
>
|
import { supabase } from "$lib/supabaseClient";
|
||||||
<div style="font-size: 4rem; margin-bottom: 1rem;">🔒🔨</div>
|
import { goto } from "$app/navigation";
|
||||||
<h2>Halaman sedang dalam tahap development</h2>
|
|
||||||
<p>Mohon maaf, halaman ini belum tersedia.</p>
|
type ConditionType =
|
||||||
|
| "NEW"
|
||||||
|
| "GOOD"
|
||||||
|
| "AVERAGE"
|
||||||
|
| "POOR"
|
||||||
|
| "SUBSTANDARD"
|
||||||
|
| "BROKEN";
|
||||||
|
|
||||||
|
type Inventory = {
|
||||||
|
id: string;
|
||||||
|
item_name: string;
|
||||||
|
villa_id: string;
|
||||||
|
villa_name: string;
|
||||||
|
item_location: string;
|
||||||
|
brand_color_material: string;
|
||||||
|
condition: ConditionType;
|
||||||
|
remarks: string;
|
||||||
|
"1": number;
|
||||||
|
"2": number;
|
||||||
|
"3": number;
|
||||||
|
"4": number;
|
||||||
|
"5": number;
|
||||||
|
"6": number;
|
||||||
|
"7": number;
|
||||||
|
"8": number;
|
||||||
|
"9": number;
|
||||||
|
"10": number;
|
||||||
|
"11": number;
|
||||||
|
"12": number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type InventoryInsert = {
|
||||||
|
item_name: string;
|
||||||
|
villa_id: string;
|
||||||
|
item_location: string;
|
||||||
|
brand_color_material: string;
|
||||||
|
condition: ConditionType;
|
||||||
|
remarks: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ key: "item_name", title: "Item Name" },
|
||||||
|
{ key: "villa_name", title: "Villa Name" },
|
||||||
|
{ key: "item_location", title: "Location" },
|
||||||
|
{ key: "brand_color_material", title: "Brand/Color/Material" },
|
||||||
|
{ key: "condition", title: "Condition" },
|
||||||
|
{ key: "remarks", title: "Remarks" },
|
||||||
|
{ key: "1", title: "Jan" },
|
||||||
|
{ key: "2", title: "Feb" },
|
||||||
|
{ key: "3", title: "Mar" },
|
||||||
|
{ key: "4", title: "Apr" },
|
||||||
|
{ key: "5", title: "May" },
|
||||||
|
{ key: "6", title: "Jun" },
|
||||||
|
{ key: "7", title: "Jul" },
|
||||||
|
{ key: "8", title: "Aug" },
|
||||||
|
{ key: "9", title: "Sep" },
|
||||||
|
{ key: "10", title: "Oct" },
|
||||||
|
{ key: "11", title: "Nov" },
|
||||||
|
{ key: "12", title: "Dec" },
|
||||||
|
{ key: "created_at", title: "Created At" },
|
||||||
|
{ key: "updated_at", title: "Updated At" },
|
||||||
|
{ key: "actions", title: "Actions" },
|
||||||
|
];
|
||||||
|
|
||||||
|
type Villa = {
|
||||||
|
id: string;
|
||||||
|
villa_name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let inventoryItems: Inventory[] = [];
|
||||||
|
let villaItems: Villa[] = [];
|
||||||
|
let totalItems = 0;
|
||||||
|
let currentPage = 1;
|
||||||
|
let rowsPerPage = 10;
|
||||||
|
let offset = 0;
|
||||||
|
let limit = 10;
|
||||||
|
|
||||||
|
$: totalPages = Math.ceil(totalItems / rowsPerPage);
|
||||||
|
|
||||||
|
let sortBy = "created_at";
|
||||||
|
let sortOrder = "desc";
|
||||||
|
let searchQuery = "";
|
||||||
|
let selectedVillaId: string | null = null;
|
||||||
|
let showModal = false;
|
||||||
|
let isEditing = false;
|
||||||
|
let currentEditingId: string | null = null;
|
||||||
|
let form: InventoryInsert = {
|
||||||
|
item_name: "",
|
||||||
|
villa_id: "",
|
||||||
|
item_location: "",
|
||||||
|
brand_color_material: "",
|
||||||
|
condition: "NEW",
|
||||||
|
remarks: "",
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// validation
|
||||||
|
function validateForm() {
|
||||||
|
if (!form.item_name) {
|
||||||
|
alert("Item name is required");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!form.villa_id) {
|
||||||
|
alert("Villa name is required");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!form.item_location) {
|
||||||
|
alert("Item location is required");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!form.brand_color_material) {
|
||||||
|
alert("Brand/Color/Material is required");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!form.condition) {
|
||||||
|
alert("Condition is required");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.remarks && form.remarks.length > 500) {
|
||||||
|
alert("Remarks cannot exceed 500 characters");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let searchTerm: string | null = null;
|
||||||
|
let newItem = {
|
||||||
|
item_name: "",
|
||||||
|
quantity: 0,
|
||||||
|
unit_price: 0,
|
||||||
|
description: "",
|
||||||
|
};
|
||||||
|
let selectedYear: number | null = new Date().getFullYear(); // Current year
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
async function fetchInventory(
|
||||||
|
searchTerm: string | null = "",
|
||||||
|
searchedVillaId: string | null = null,
|
||||||
|
sortBy = "created_at",
|
||||||
|
sortOrder = "desc",
|
||||||
|
offset = 0,
|
||||||
|
limit = 10,
|
||||||
|
) {
|
||||||
|
let query = supabase
|
||||||
|
.from("vb_inventory_data")
|
||||||
|
.select("*", { count: "exact" })
|
||||||
|
.order(sortBy, { ascending: sortOrder === "asc" })
|
||||||
|
.range(offset, offset + limit - 1);
|
||||||
|
|
||||||
|
if (searchTerm && searchTerm.length > 4) {
|
||||||
|
query = query.ilike("item_name", `%${searchTerm}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchedVillaId) {
|
||||||
|
query = query.eq("villa_id", searchedVillaId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error, count } = await query;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Error fetching inventory:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
inventoryItems = data as Inventory[];
|
||||||
|
totalItems = count || 0;
|
||||||
|
|
||||||
|
return { items: inventoryItems, total: totalItems };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateQtyMonth(
|
||||||
|
itemId: string,
|
||||||
|
month: number,
|
||||||
|
year: number,
|
||||||
|
qty: number,
|
||||||
|
) {
|
||||||
|
// Update the quantity for a specific month di vb_inventory_qty_month jika tidak ada, insert baru
|
||||||
|
|
||||||
|
const { data, error: fetchError } = await supabase
|
||||||
|
.from("vb_inventory_qty_month")
|
||||||
|
.select("*")
|
||||||
|
.eq("id_inventory", itemId)
|
||||||
|
.eq("month", month)
|
||||||
|
.eq("year", year)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (fetchError && fetchError.code !== "PGRST116") {
|
||||||
|
console.error("Error fetching quantity for month:", fetchError);
|
||||||
|
alert("Failed to fetch quantity for month. Please try again.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Fetched data for item ${itemId} month ${month} year ${year}:`,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
// If data exists, update it
|
||||||
|
const { error: updateError } = await supabase
|
||||||
|
.from("vb_inventory_qty_month")
|
||||||
|
.update({ qty: qty })
|
||||||
|
.eq("id_inventory", itemId)
|
||||||
|
.eq("month", month)
|
||||||
|
.eq("year", year);
|
||||||
|
if (updateError) {
|
||||||
|
console.error(
|
||||||
|
"Error updating quantity for month:",
|
||||||
|
updateError,
|
||||||
|
);
|
||||||
|
alert("Failed to update quantity for month. Please try again.");
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`Quantity for item ${itemId} month ${month} year ${year} updated successfully.`,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If no data exists, insert a new record
|
||||||
|
const { error: insertError } = await supabase
|
||||||
|
.from("vb_inventory_qty_month")
|
||||||
|
.insert({
|
||||||
|
id_inventory: itemId,
|
||||||
|
month: month,
|
||||||
|
year: year,
|
||||||
|
qty: qty,
|
||||||
|
});
|
||||||
|
if (insertError) {
|
||||||
|
console.error(
|
||||||
|
"Error inserting quantity for month:",
|
||||||
|
insertError,
|
||||||
|
);
|
||||||
|
alert("Failed to insert quantity for month. Please try again.");
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`Quantity for item ${itemId} month ${month} year ${year} inserted successfully.`,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await fetchInventory();
|
||||||
|
|
||||||
|
// Fetch villa names for dropdown
|
||||||
|
const { data: villaData, error: villaError } = await supabase
|
||||||
|
.from("vb_villas")
|
||||||
|
.select("id, villa_name");
|
||||||
|
if (villaError) {
|
||||||
|
console.error("Error fetching villas:", villaError);
|
||||||
|
} else {
|
||||||
|
villaItems = villaData as Villa[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is logged in
|
||||||
|
const {
|
||||||
|
data: { session },
|
||||||
|
} = await supabase.auth.getSession();
|
||||||
|
if (!session) {
|
||||||
|
goto("/login");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function openModal() {
|
||||||
|
showModal = true;
|
||||||
|
newItem = {
|
||||||
|
item_name: "",
|
||||||
|
quantity: 0,
|
||||||
|
unit_price: 0,
|
||||||
|
description: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitForm() {
|
||||||
|
if (isEditing) {
|
||||||
|
// Update existing item
|
||||||
|
const { error } = await supabase
|
||||||
|
.from("vb_inventory")
|
||||||
|
.update({
|
||||||
|
item_name: form.item_name,
|
||||||
|
villa_id: form.villa_id,
|
||||||
|
item_location: form.item_location,
|
||||||
|
brand_color_material: form.brand_color_material,
|
||||||
|
condition: form.condition,
|
||||||
|
remarks: form.remarks,
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
.eq("id", currentEditingId);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Error updating item:", error);
|
||||||
|
} else {
|
||||||
|
alert("Item updated successfully!");
|
||||||
|
showModal = false;
|
||||||
|
await fetchInventory();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Insert new item
|
||||||
|
const { error } = await supabase.from("vb_inventory").insert({
|
||||||
|
...form,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Error inserting item:", error);
|
||||||
|
} else {
|
||||||
|
alert("New item added successfully!");
|
||||||
|
showModal = false;
|
||||||
|
await fetchInventory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToPage(page: number) {
|
||||||
|
if (page < 1 || page > totalPages) return;
|
||||||
|
currentPage = page;
|
||||||
|
|
||||||
|
fetchInventory(
|
||||||
|
searchTerm,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
(currentPage - 1) * rowsPerPage,
|
||||||
|
rowsPerPage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageRange(
|
||||||
|
totalPages: number,
|
||||||
|
currentPage: number,
|
||||||
|
): (number | string)[] {
|
||||||
|
const range: (number | string)[] = [];
|
||||||
|
const maxDisplay = 5;
|
||||||
|
|
||||||
|
if (totalPages <= maxDisplay + 2) {
|
||||||
|
for (let i = 1; i <= totalPages; i++) range.push(i);
|
||||||
|
} else {
|
||||||
|
const start = Math.max(2, currentPage - 2);
|
||||||
|
const end = Math.min(totalPages - 1, currentPage + 2);
|
||||||
|
|
||||||
|
range.push(1);
|
||||||
|
if (start > 2) range.push("...");
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
range.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end < totalPages - 1) range.push("...");
|
||||||
|
range.push(totalPages);
|
||||||
|
}
|
||||||
|
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePage(page: number) {
|
||||||
|
if (page < 1 || page > totalPages || page === currentPage) return;
|
||||||
|
currentPage = page;
|
||||||
|
|
||||||
|
fetchInventory(
|
||||||
|
searchTerm,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
(currentPage - 1) * rowsPerPage,
|
||||||
|
rowsPerPage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</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>
|
||||||
|
Inventory Management
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm text-gray-600">
|
||||||
|
Manage your inventory items, track stock levels, and ensure
|
||||||
|
everything is in order.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="🔍 Search by item 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();
|
||||||
|
fetchInventory(null, "created_at", "desc", 0, 10);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<!-- dropdown villa -->
|
||||||
|
<select
|
||||||
|
class="border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:outline-none px-2 py-2 rounded-xl text-sm w-48 transition"
|
||||||
|
bind:value={selectedVillaId}
|
||||||
|
on:change={() => {
|
||||||
|
fetchInventory(
|
||||||
|
searchTerm,
|
||||||
|
selectedVillaId,
|
||||||
|
"created_at",
|
||||||
|
"desc",
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="" disabled selected>Select Villa</option>
|
||||||
|
{#each villaItems as villa}
|
||||||
|
<option value={villa.id}>{villa.villa_name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
id="year"
|
||||||
|
class="border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:outline-none px-2 py-2 rounded-xl text-sm w-20 transition"
|
||||||
|
bind:value={selectedYear}
|
||||||
|
>
|
||||||
|
<option value="" disabled selected>Select</option>
|
||||||
|
{#each Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i) as y}
|
||||||
|
<option value={y}>{y}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
class="bg-gray-200 text-gray-700 px-4 py-2 rounded-xl hover:bg-gray-300 text-sm transition"
|
||||||
|
on:click={() =>
|
||||||
|
fetchInventory(null, "created_at", "desc", 0, 10)}
|
||||||
|
>
|
||||||
|
🔄 Reset
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="bg-blue-600 text-white px-4 py-2 rounded-xl hover:bg-blue-700 text-sm transition"
|
||||||
|
on:click={() => openModal()}
|
||||||
|
>
|
||||||
|
➕ New Item
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto rounded-lg shadow mb-4">
|
||||||
|
<table class="w-full divide-y divide-gray-200 text-sm">
|
||||||
|
<thead class="bg-gray-100">
|
||||||
|
<tr>
|
||||||
|
{#each columns as col}
|
||||||
|
{#if col.key === "guest_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="bg-white divide-y divide-gray-200">
|
||||||
|
{#each inventoryItems as item}
|
||||||
|
<tr>
|
||||||
|
{#each columns as col}
|
||||||
|
<td class="px-4 py-3 whitespace-nowrap">
|
||||||
|
{#if +col.key >= 1 && +col.key <= 12}
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
class="border border-gray-300 rounded px-2 py-1 w-16"
|
||||||
|
bind:value={
|
||||||
|
item[col.key as keyof Inventory]
|
||||||
|
}
|
||||||
|
on:change={(e) => {
|
||||||
|
const target =
|
||||||
|
e.target as HTMLInputElement | null;
|
||||||
|
if (!target) return;
|
||||||
|
const value = Number(target.value);
|
||||||
|
updateQtyMonth(
|
||||||
|
item.id,
|
||||||
|
+col.key,
|
||||||
|
selectedYear ||
|
||||||
|
new Date().getFullYear(),
|
||||||
|
value,
|
||||||
|
).then((success) => {
|
||||||
|
if (success) {
|
||||||
|
alert(
|
||||||
|
`Quantity for ${item.item_name} in month ${col.key} updated successfully.`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
alert(
|
||||||
|
`Failed to update quantity for ${item.item_name} in month ${col.key}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else if col.key === "created_at" || col.key === "updated_at"}
|
||||||
|
<td class="px-4 py-2">
|
||||||
|
{item[col.key] !== undefined
|
||||||
|
? new Date(
|
||||||
|
item[col.key] as
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| Date,
|
||||||
|
).toLocaleString()
|
||||||
|
: "N/A"}
|
||||||
|
</td>
|
||||||
|
{:else if col.key === "actions"}
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<button
|
||||||
|
class="text-blue-600 hover:underline"
|
||||||
|
on:click={() => {
|
||||||
|
isEditing = true;
|
||||||
|
currentEditingId = item.id;
|
||||||
|
form = {
|
||||||
|
item_name: item.item_name,
|
||||||
|
villa_id: item.villa_id,
|
||||||
|
item_location:
|
||||||
|
item.item_location,
|
||||||
|
brand_color_material:
|
||||||
|
item.brand_color_material,
|
||||||
|
condition: item.condition,
|
||||||
|
remarks: item.remarks,
|
||||||
|
created_at: item.created_at,
|
||||||
|
updated_at: item.updated_at,
|
||||||
|
};
|
||||||
|
showModal = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✏️ Edit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="text-red-600 hover:underline"
|
||||||
|
on:click={async () => {
|
||||||
|
const { error } = await supabase
|
||||||
|
.from("vb_inventory")
|
||||||
|
.delete()
|
||||||
|
.eq("id", item.id);
|
||||||
|
if (error) {
|
||||||
|
console.error(
|
||||||
|
"Error deleting item:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
alert(
|
||||||
|
"Item deleted successfully!",
|
||||||
|
);
|
||||||
|
await fetchInventory();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🗑️ Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
{item[col.key as keyof Inventory]}
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
{:else}
|
||||||
|
<tr>
|
||||||
|
<td colspan={columns.length} class="text-center py-4">
|
||||||
|
No items found.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center text-sm">
|
||||||
|
<div>
|
||||||
|
Showing {(currentPage - 1) * rowsPerPage + 1}–
|
||||||
|
{Math.min(currentPage * rowsPerPage, totalItems)} of {totalItems} items
|
||||||
|
</div>
|
||||||
|
<div class="space-x-2 flex">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<label for="rowsPerPage" class="text-gray-700"
|
||||||
|
>Rows per page:</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
id="rowsPerPage"
|
||||||
|
class="border border-gray-300 rounded px-2 py-1"
|
||||||
|
bind:value={rowsPerPage}
|
||||||
|
on:change={() => {
|
||||||
|
currentPage = 1; // Reset to first page
|
||||||
|
fetchInventory(
|
||||||
|
searchTerm,
|
||||||
|
selectedVillaId,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
0,
|
||||||
|
rowsPerPage,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="20">20</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<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 pageRange(totalPages, currentPage) as page}
|
||||||
|
{#if page === "..."}
|
||||||
|
<span class="px-2">...</span>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
on:click={() => changePage(page as number)}
|
||||||
|
class="px-2 py-1 border rounded {page === currentPage
|
||||||
|
? 'bg-blue-600 text-white border-blue-600'
|
||||||
|
: 'bg-white border-gray-300 hover:bg-gray-100'}"
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/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>
|
||||||
|
<p class="text-xs text-gray-500 italic mb-2">
|
||||||
|
Tekan tombol Enter untuk menyimpan.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
{#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"
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
on:submit|preventDefault={submitForm}
|
||||||
|
class="w-full max-w-lg bg-white p-6 rounded-2xl shadow-xl space-y-4"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h2 class="text-xl font-semibold">
|
||||||
|
{isEditing ? "Edit Item" : "New Item"}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-gray-500 hover:text-gray-700"
|
||||||
|
on:click={() => (showModal = false)}
|
||||||
|
>
|
||||||
|
✖️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="t_eb" class="block text-sm font-medium mb-1"
|
||||||
|
>Item Name</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="t_eb"
|
||||||
|
class="w-full border border-gray-300 p-2 rounded"
|
||||||
|
bind:value={form.item_name}
|
||||||
|
placeholder="Enter item name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="tvn" class="block text-sm font-medium mb-1"
|
||||||
|
>Villa Name</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
id="tvn"
|
||||||
|
class="w-full border border-gray-300 p-2 rounded"
|
||||||
|
bind:value={form.villa_id}
|
||||||
|
>
|
||||||
|
<option value="" disabled selected>Select Villa</option>
|
||||||
|
{#each villaItems as villa}
|
||||||
|
<option value={villa.id}>
|
||||||
|
{villa.villa_name}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="til" class="block text-sm font-medium mb-1"
|
||||||
|
>Item Location</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="til"
|
||||||
|
class="w-full border border-gray-300 p-2 rounded"
|
||||||
|
bind:value={form.item_location}
|
||||||
|
placeholder="Enter item location"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="tbcm" class="block text-sm font-medium mb-1"
|
||||||
|
>Brand/Color/Material</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="tbcm"
|
||||||
|
class="w-full border border-gray-300 p-2 rounded"
|
||||||
|
bind:value={form.brand_color_material}
|
||||||
|
placeholder="Enter brand, color, or material"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="tcond" class="block text-sm font-medium mb-1"
|
||||||
|
>Condition</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
id="tcond"
|
||||||
|
class="w-full border border-gray-300 p-2 rounded"
|
||||||
|
bind:value={form.condition}
|
||||||
|
>
|
||||||
|
<option value="NEW">New</option>
|
||||||
|
<option value="GOOD">Good</option>
|
||||||
|
<option value="AVERAGE">Average</option>
|
||||||
|
<option value="POOR">Poor</option>
|
||||||
|
<option value="SUBSTANDARD">Substandard</option>
|
||||||
|
<option value="BROKEN">Broken</option>
|
||||||
|
</select>
|
||||||
|
</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>
|
||||||
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user