penambahan menu inventory
This commit is contained in:
@@ -1,7 +1,778 @@
|
||||
<div
|
||||
style="display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh;"
|
||||
>
|
||||
<div style="font-size: 4rem; margin-bottom: 1rem;">🔒🔨</div>
|
||||
<h2>Halaman sedang dalam tahap development</h2>
|
||||
<p>Mohon maaf, halaman ini belum tersedia.</p>
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { supabase } from "$lib/supabaseClient";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
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>
|
||||
<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