initial commit

This commit is contained in:
Karlsson
2025-11-21 15:11:33 +01:00
commit c16fd804c0
62 changed files with 9335 additions and 0 deletions

View File

@ -0,0 +1,451 @@
<script lang="ts">
import { page } from '$app/stores';
import { enhance } from '$app/forms';
import ItemModal from '$lib/components/ItemModal.svelte';
import MoveItemModal from '$lib/components/MoveItemModal.svelte';
import DeleteConfirmModal from '$lib/components/DeleteConfirmModal.svelte';
import { propertyConfig } from '$lib/config/properties';
import { formatValue } from '$lib/utils/units';
export let data;
$: ({ category, items, folders, categories } = data);
// Item Modal State
let showItemModal = false;
let showMoveItemModal = false;
let showDeleteModal = false;
let editingItem: any = null;
// Bulk/Single Action State
let selectedItems = new Set<string>();
let actionItemIds: string[] = [];
let actionItemName: string | null = null; // For display in single item actions
// Reactive statement to handle selection updates
$: allSelected = items.length > 0 && selectedItems.size === items.length;
$: isIndeterminate = selectedItems.size > 0 && selectedItems.size < items.length;
function openCreateItemModal() {
editingItem = null;
showItemModal = true;
}
function openEditItemModal(item: any) {
editingItem = item;
showItemModal = true;
}
function openMoveItemModal(item: any) {
actionItemIds = [item.id];
actionItemName = item.name;
showMoveItemModal = true;
}
function openBulkMoveModal() {
actionItemIds = Array.from(selectedItems);
actionItemName = null;
showMoveItemModal = true;
}
function openDeleteModal(item: any) {
actionItemIds = [item.id];
actionItemName = item.name;
showDeleteModal = true;
}
function openBulkDeleteModal() {
actionItemIds = Array.from(selectedItems);
actionItemName = null;
showDeleteModal = true;
}
function toggleSelection(id: string) {
if (selectedItems.has(id)) {
selectedItems.delete(id);
} else {
selectedItems.add(id);
}
selectedItems = selectedItems; // trigger update
}
function toggleAll(e: Event) {
const checked = (e.target as HTMLInputElement).checked;
if (checked) {
selectedItems = new Set(items.map((i) => i.id));
} else {
selectedItems = new Set();
}
selectedItems = selectedItems;
}
function getFormattedValue(key: string, value: any) {
if (propertyConfig[key]) {
return formatValue(value, propertyConfig[key]);
}
return value;
}
function getOtherProperties(item: any) {
const defaultProps = new Set(category.defaultProperties);
return Object.entries(item.properties || {}).filter(([key]) => !defaultProps.has(key));
}
</script>
<div class="space-y-4">
<!-- Header & Breadcrumbs -->
<div class="flex items-center justify-between">
<div class="flex items-center gap-2 text-sm text-zinc-500">
<a href="/categories" class="transition-colors hover:text-white">Categories</a>
<span>/</span>
<span class="font-medium text-white">{category.name}</span>
</div>
<div class="flex gap-2">
{#if selectedItems.size > 0}
<div
class="animate-in fade-in slide-in-from-top-2 mr-4 flex items-center gap-2 duration-200"
>
<span class="text-sm font-medium text-zinc-400">{selectedItems.size} selected</span>
<button
on:click={openBulkMoveModal}
class="flex items-center gap-1 rounded-md border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-sm font-medium text-zinc-300 transition-colors hover:bg-zinc-800 hover:text-white"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"
/>
</svg>
Move
</button>
<button
on:click={openBulkDeleteModal}
class="flex items-center gap-1 rounded-md border border-zinc-700 bg-zinc-900 px-3 py-1.5 text-sm font-medium text-zinc-300 transition-colors hover:border-red-900/30 hover:bg-red-900/20 hover:text-red-500"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
Delete
</button>
</div>
{/if}
<button
on:click={openCreateItemModal}
class="rounded-md bg-red-600 px-3 py-1.5 text-sm font-medium text-white shadow-lg shadow-red-900/20 transition-colors hover:bg-red-700"
>
+ New Item
</button>
</div>
</div>
<!-- Item Modal -->
<ItemModal
bind:show={showItemModal}
item={editingItem}
{categories}
{folders}
currentFolderId={null}
currentCategoryId={category.id}
user={data.user}
onClose={() => (showItemModal = false)}
/>
<!-- Move Item Modal -->
<MoveItemModal
bind:show={showMoveItemModal}
itemIds={actionItemIds}
itemName={actionItemName}
{folders}
onClose={() => (showMoveItemModal = false)}
/>
<!-- Delete Confirm Modal -->
<DeleteConfirmModal
bind:show={showDeleteModal}
itemIds={actionItemIds}
itemName={actionItemName}
onClose={() => (showDeleteModal = false)}
/>
<!-- Items List -->
{#if items.length > 0}
<section>
<h2 class="mb-3 text-xs font-semibold tracking-wider text-zinc-500 uppercase">
Items in {category.name}
</h2>
<div class="overflow-hidden rounded-md border border-zinc-700 bg-zinc-900/30">
<table class="w-full text-left text-sm">
<thead class="border-b border-zinc-700 bg-zinc-900/50">
<tr>
<th class="w-12 px-4 py-2">
<input
type="checkbox"
checked={allSelected}
indeterminate={isIndeterminate}
on:change={toggleAll}
class="checked:bg-[url('data:image/svg+xml,%3csvg viewBox=\'0 0 16 16\' fill=\'white\' xmlns=\'http://www.w3.org/2000/svg\'%3e%3cpath d=\'M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z\'/%3e%3c/svg%3e')] h-4 w-4 shrink-0 cursor-pointer appearance-none rounded border border-zinc-700 bg-zinc-900 bg-center bg-no-repeat transition-colors checked:border-zinc-700 checked:bg-zinc-900 focus:ring-1 focus:ring-red-500 focus:ring-offset-0"
/>
</th>
<th class="px-4 py-2 font-medium text-zinc-500">Name</th>
<th class="px-4 py-2 font-medium text-zinc-500">Location</th>
<th class="px-4 py-2 font-medium text-zinc-500">Count</th>
<!-- Dynamic Headers for Default Properties -->
{#each category.defaultProperties as prop}
<th class="px-4 py-2 font-medium text-zinc-500 capitalize">{prop}</th>
{/each}
<th class="px-4 py-2 font-medium text-zinc-500">Other Properties</th>
<th class="px-4 py-2 text-right font-medium text-zinc-500">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-zinc-800/50">
{#each items as item}
<tr
class="group transition-colors hover:bg-zinc-800/30 {selectedItems.has(item.id)
? 'bg-red-900/5'
: ''}"
>
<td class="px-4 py-2">
<input
type="checkbox"
checked={selectedItems.has(item.id)}
on:change={() => toggleSelection(item.id)}
class="checked:bg-[url('data:image/svg+xml,%3csvg viewBox=\'0 0 16 16\' fill=\'white\' xmlns=\'http://www.w3.org/2000/svg\'%3e%3cpath d=\'M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z\'/%3e%3c/svg%3e')] h-4 w-4 shrink-0 cursor-pointer appearance-none rounded border border-zinc-700 bg-zinc-900 bg-center bg-no-repeat transition-colors checked:border-zinc-700 checked:bg-zinc-900 focus:ring-1 focus:ring-red-500 focus:ring-offset-0"
/>
</td>
<td class="px-4 py-2 font-medium text-zinc-200">
<button
class="text-left hover:text-red-400 hover:underline"
on:click={() => openEditItemModal(item)}
>
{item.name}
</button>
</td>
<td class="px-4 py-2 text-zinc-500">
{#if item.folderPath}
<a
href="/folders/{item.folderPath}"
class="transition-colors hover:text-white hover:underline"
>
{item.folderName}
</a>
{:else}
Unassigned
{/if}
</td>
<td class="px-4 py-2 font-mono text-zinc-400">
<div class="group/count flex items-center gap-3">
<span>
{item.count}
{#if item.reservedCount > 0}
<span class="text-red-500"> - {item.reservedCount}</span>
{/if}
</span>
<div
class="flex items-center rounded-md border border-zinc-700 bg-zinc-800 opacity-0 shadow-sm transition-opacity group-hover/count:opacity-100"
>
<form method="POST" action="?/updateCount" use:enhance class="flex">
<input type="hidden" name="id" value={item.id} />
<input type="hidden" name="delta" value="-1" />
<button
type="submit"
class="rounded-l-md border-r border-zinc-700 p-1 text-zinc-400 transition-colors hover:bg-zinc-700 hover:text-white"
title="Decrement"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-3 w-3"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
clip-rule="evenodd"
/>
</svg>
</button>
</form>
<form method="POST" action="?/updateCount" use:enhance class="flex">
<input type="hidden" name="id" value={item.id} />
<input type="hidden" name="delta" value="1" />
<button
type="submit"
class="rounded-r-md p-1 text-zinc-400 transition-colors hover:bg-zinc-700 hover:text-white"
title="Increment"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-3 w-3"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"
clip-rule="evenodd"
/>
</svg>
</button>
</form>
</div>
</div>
</td>
<!-- Dynamic Values for Default Properties -->
{#each category.defaultProperties as prop}
<td class="px-4 py-2 text-zinc-400">
{#if prop === 'Datasheet' || prop === 'Link'}
{#if item.properties[prop]}
<a
href={item.properties[prop]}
target="_blank"
rel="noopener noreferrer"
class="text-red-400 hover:underline">Link</a
>
{:else}
-
{/if}
{:else}
{item.properties[prop] ? getFormattedValue(prop, item.properties[prop]) : '-'}
{/if}
</td>
{/each}
<td class="px-4 py-2 text-xs text-zinc-500">
<div class="flex flex-col gap-0.5">
{#each Object.entries(item.properties) as [key, value]}
{#if !category.defaultProperties.includes(key)}
<span
><span class="font-medium text-zinc-400">{key}:</span>
{#if key === 'Datasheet' || key === 'Link'}
<a
href={value}
target="_blank"
rel="noopener noreferrer"
class="text-red-400 hover:underline">Link</a
>
{:else}
{getFormattedValue(key, value)}
{/if}</span
>
{/if}
{/each}
</div>
</td>
<td
class="px-4 py-2 text-right opacity-0 transition-opacity group-hover:opacity-100"
>
<div class="flex justify-end gap-2">
<button
on:click={() => openMoveItemModal(item)}
class="text-zinc-600 transition-colors hover:text-white"
title="Move"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"
/>
</svg>
</button>
<button
on:click={() => openEditItemModal(item)}
class="text-zinc-600 transition-colors hover:text-white"
title="Edit"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
/>
</svg>
</button>
<button
on:click={() => openDeleteModal(item)}
class="text-zinc-600 transition-colors hover:text-red-500"
title="Delete"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</section>
{:else}
<div class="rounded-md border border-dashed border-zinc-800 bg-zinc-900/20 py-12 text-center">
<div class="mb-2 text-zinc-600">
<svg
xmlns="http://www.w3.org/2000/svg"
class="mx-auto h-12 w-12"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
/>
</svg>
</div>
<h3 class="text-lg font-medium text-zinc-300">No Items</h3>
<p class="mt-1 text-sm text-zinc-500">This category has no items yet.</p>
</div>
{/if}
</div>