868 lines
31 KiB
Svelte
868 lines
31 KiB
Svelte
<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 = null;
|
||
|
||
$: if (selectedYear) {
|
||
fetchInventory(
|
||
searchTerm,
|
||
selectedVillaId,
|
||
selectedYear,
|
||
sortBy,
|
||
sortOrder,
|
||
offset,
|
||
limit,
|
||
);
|
||
}
|
||
|
||
export let data;
|
||
|
||
async function fetchInventory(
|
||
searchTerm: string | null = "",
|
||
searchedVillaId: string | null = null,
|
||
year: number | 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);
|
||
}
|
||
|
||
if (year) {
|
||
query = query.or(`year.eq.${year},year.eq.0`);
|
||
}
|
||
|
||
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 () => {
|
||
selectedYear = new Date().getFullYear();
|
||
|
||
await fetchInventory(
|
||
searchTerm,
|
||
selectedVillaId,
|
||
selectedYear,
|
||
sortBy,
|
||
sortOrder,
|
||
offset,
|
||
limit,
|
||
);
|
||
|
||
// 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(
|
||
searchTerm,
|
||
selectedVillaId,
|
||
selectedYear,
|
||
sortBy,
|
||
sortOrder,
|
||
offset,
|
||
limit,
|
||
);
|
||
}
|
||
} 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(
|
||
searchTerm,
|
||
selectedVillaId,
|
||
selectedYear,
|
||
sortBy,
|
||
sortOrder,
|
||
offset,
|
||
limit,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
function goToPage(page: number) {
|
||
if (page < 1 || page > totalPages) return;
|
||
currentPage = page;
|
||
|
||
fetchInventory(
|
||
searchTerm,
|
||
selectedVillaId,
|
||
selectedYear,
|
||
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,
|
||
selectedVillaId,
|
||
selectedYear,
|
||
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 mb-2">
|
||
Manage your inventory items, track stock levels, and ensure
|
||
everything is in order.
|
||
</p>
|
||
<p class="text-xs text-gray-500 italic mb-2">
|
||
Note : Tekan tombol Enter untuk menyimpan. <span
|
||
class="text-red-500">⚠️</span
|
||
>
|
||
</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(
|
||
searchTerm,
|
||
selectedVillaId,
|
||
selectedYear,
|
||
sortBy,
|
||
sortOrder,
|
||
offset,
|
||
limit,
|
||
);
|
||
}}
|
||
/>
|
||
<!-- 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,
|
||
selectedYear,
|
||
sortBy,
|
||
sortOrder,
|
||
offset,
|
||
limit,
|
||
);
|
||
}}
|
||
>
|
||
<option value="" selected>All 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(
|
||
searchTerm,
|
||
selectedVillaId,
|
||
selectedYear,
|
||
sortBy,
|
||
sortOrder,
|
||
offset,
|
||
limit,
|
||
)}
|
||
>
|
||
🔄 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
|
||
</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 === "item_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 class="hover:bg-gray-100 transition duration-150">
|
||
{#each columns as col}
|
||
{#if +col.key >= 1 && +col.key <= 12}
|
||
<td class="px-4 py-3 whitespace-nowrap">
|
||
<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}.`,
|
||
);
|
||
}
|
||
});
|
||
}}
|
||
/>
|
||
</td>
|
||
{:else if col.key === "item_name"}
|
||
<td
|
||
class="px-4 py-2 sticky left-0 whitespace-nowrap bg-gray-100 z-10"
|
||
>
|
||
<span
|
||
class="font-medium text-gray-800 hover:text-blue-600"
|
||
>
|
||
{item.item_name}
|
||
</span>
|
||
</td>
|
||
{: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"}
|
||
<td class="px-4 py-2">
|
||
<div class="flex space-x-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={() => {
|
||
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="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={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(
|
||
searchTerm,
|
||
selectedVillaId,
|
||
selectedYear,
|
||
sortBy,
|
||
sortOrder,
|
||
offset,
|
||
limit,
|
||
);
|
||
}
|
||
}}
|
||
>
|
||
🗑️ Delete
|
||
</button>
|
||
</div>
|
||
</td>
|
||
{:else}
|
||
<td class="px-4 py-2">
|
||
{item[col.key as keyof Inventory]}
|
||
</td>
|
||
{/if}
|
||
{/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,
|
||
selectedYear,
|
||
sortBy,
|
||
sortOrder,
|
||
(currentPage - 1) * rowsPerPage,
|
||
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>
|
||
|
||
<!-- 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 Inventory" : "New Entry"}
|
||
</button>
|
||
</form>
|
||
</div>
|
||
{/if}
|