reservations applyable, fixed folder views, checkmarks
This commit is contained in:
@ -1,2 +1,8 @@
|
||||
# Replace with your DB credentials!
|
||||
DATABASE_URL="postgres://user:password@host:port/db-name"
|
||||
|
||||
# Replace with your Gitea oauth credentials!
|
||||
GITEA_BASE_URL="https://gitea.example.com"
|
||||
GITEA_CLIENT_ID="your-client-id"
|
||||
GITEA_CLIENT_SECRET="your-client-secret"
|
||||
CALLBACK_BASE_URL="http://localhost:5173"
|
||||
|
||||
@ -168,7 +168,6 @@
|
||||
name="name"
|
||||
id="itemName"
|
||||
value={item?.name || ''}
|
||||
required
|
||||
class="w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-white placeholder-zinc-600 transition-all outline-none focus:border-red-500 focus:ring-1 focus:ring-red-500"
|
||||
placeholder="e.g. 10k Resistor"
|
||||
/>
|
||||
@ -470,6 +469,34 @@
|
||||
>{reservation.count} reserved</span
|
||||
>
|
||||
{/if}
|
||||
|
||||
{#if user}
|
||||
<div class="ml-2">
|
||||
<form
|
||||
action="/items?/applyReservation"
|
||||
method="POST"
|
||||
use:enhance={() => {
|
||||
return async ({ result }) => {
|
||||
if (result.type === 'success') {
|
||||
// Update local item count if returned
|
||||
if (result.data && result.data.newCount !== undefined) {
|
||||
item.count = result.data.newCount;
|
||||
}
|
||||
fetchDetails(false);
|
||||
}
|
||||
};
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="id" value={reservation.id} />
|
||||
<button
|
||||
class="rounded border border-zinc-700 bg-zinc-800 px-2 py-1 text-xs font-medium text-zinc-300 transition-colors hover:border-green-800 hover:bg-green-900/30 hover:text-green-400"
|
||||
title="Apply reservation (subtract from stock and remove)"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ export const items = pgTable('items', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
folderId: uuid('folder_id').references(() => folders.id).notNull(),
|
||||
categoryId: uuid('category_id').references(() => categories.id).notNull(),
|
||||
name: text('name').notNull(),
|
||||
name: text('name'),
|
||||
count: integer('count').default(0).notNull(),
|
||||
tags: jsonb('tags').$type<string[]>().default([]).notNull(),
|
||||
properties: jsonb('properties').$type<Record<string, any>>().default({}).notNull(),
|
||||
|
||||
25
src/lib/server/db/utils.ts
Normal file
25
src/lib/server/db/utils.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { db } from '$lib/server/db';
|
||||
import { folders } from '$lib/server/db/schema';
|
||||
import { eq, isNull, and } from 'drizzle-orm';
|
||||
|
||||
export async function getOrSeedHomeFolder() {
|
||||
// Try to find the "Home" folder at the root level (parentId is null)
|
||||
const existingHome = await db.select().from(folders)
|
||||
.where(and(
|
||||
eq(folders.name, 'Home'),
|
||||
isNull(folders.parentId)
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
if (existingHome.length > 0) {
|
||||
return existingHome[0];
|
||||
}
|
||||
|
||||
// If not found, create it
|
||||
const [newHome] = await db.insert(folders).values({
|
||||
name: 'Home',
|
||||
parentId: null
|
||||
}).returning();
|
||||
|
||||
return newHome;
|
||||
}
|
||||
@ -7,12 +7,25 @@ export async function GET({ url }) {
|
||||
const folderId = url.searchParams.get('folderId');
|
||||
const categoryId = url.searchParams.get('categoryId');
|
||||
|
||||
let query = db.select().from(items);
|
||||
|
||||
let conditions = [];
|
||||
if (folderId) {
|
||||
query = query.where(eq(items.folderId, folderId));
|
||||
conditions.push(eq(items.folderId, folderId));
|
||||
} else if (categoryId) {
|
||||
query = query.where(eq(items.categoryId, categoryId));
|
||||
conditions.push(eq(items.categoryId, categoryId));
|
||||
}
|
||||
|
||||
let query = db.select().from(items);
|
||||
if (conditions.length > 0) {
|
||||
// @ts-ignore - spread operator with conditions works but TS might complain about empty array if not handled, but here we check length.
|
||||
// Actually, cleaner to just chain if we can, or just use `where` once.
|
||||
// Let's just use the if/else to construct the query directly without reassignment if possible, or use `let query` with explicit type.
|
||||
// Easiest is to just do:
|
||||
if (folderId) {
|
||||
return json(await db.select().from(items).where(eq(items.folderId, folderId)));
|
||||
} else if (categoryId) {
|
||||
return json(await db.select().from(items).where(eq(items.categoryId, categoryId)));
|
||||
}
|
||||
return json(await db.select().from(items));
|
||||
}
|
||||
|
||||
const result = await query;
|
||||
|
||||
@ -67,7 +67,7 @@ export const actions = {
|
||||
|
||||
await db.insert(items).values({
|
||||
name,
|
||||
folderId: folderId || null,
|
||||
folderId: folderId,
|
||||
categoryId,
|
||||
count,
|
||||
properties
|
||||
@ -113,7 +113,7 @@ export const actions = {
|
||||
const data = await request.formData();
|
||||
const id = data.get('id') as string;
|
||||
const folderId = data.get('folderId') as string;
|
||||
await db.update(items).set({ folderId: folderId || null }).where(eq(items.id, id));
|
||||
await db.update(items).set({ folderId: folderId }).where(eq(items.id, id));
|
||||
return { success: true };
|
||||
},
|
||||
bulkDelete: async ({ request }: { request: Request }) => {
|
||||
@ -129,7 +129,7 @@ export const actions = {
|
||||
const ids = JSON.parse(data.get('ids') as string);
|
||||
const folderId = data.get('folderId') as string;
|
||||
if (ids.length > 0) {
|
||||
await db.update(items).set({ folderId: folderId || null }).where(inArray(items.id, ids));
|
||||
await db.update(items).set({ folderId: folderId }).where(inArray(items.id, ids));
|
||||
}
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { enhance } from '$app/forms';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import ItemModal from '$lib/components/ItemModal.svelte';
|
||||
import MoveItemModal from '$lib/components/MoveItemModal.svelte';
|
||||
import DeleteConfirmModal from '$lib/components/DeleteConfirmModal.svelte';
|
||||
@ -102,10 +103,10 @@
|
||||
<span>/</span>
|
||||
<span class="font-medium text-white">{category.name}</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="relative 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"
|
||||
class="animate-in fade-in slide-in-from-top-2 absolute top-1/2 right-full mr-4 flex -translate-y-1/2 items-center gap-2 whitespace-nowrap duration-200"
|
||||
>
|
||||
<span class="text-sm font-medium text-zinc-400">{selectedItems.size} selected</span>
|
||||
<button
|
||||
@ -169,7 +170,10 @@
|
||||
currentFolderId={null}
|
||||
currentCategoryId={category.id}
|
||||
user={data.user}
|
||||
onClose={() => (showItemModal = false)}
|
||||
onClose={() => {
|
||||
showItemModal = false;
|
||||
invalidateAll();
|
||||
}}
|
||||
/>
|
||||
|
||||
<!-- Move Item Modal -->
|
||||
@ -205,7 +209,7 @@
|
||||
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"
|
||||
class="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 checked:bg-[url('data:image/svg+xml,%3csvg%20viewBox=%270%200%2016%2016%27%20fill=%27white%27%20xmlns=%27http://www.w3.org/2000/svg%27%3e%3cpath%20d=%27M12.207%204.793a1%201%200%20010%201.414l-5%205a1%201%200%2001-1.414%200l-2-2a1%201%200%20011.414-1.414L6.5%209.086l4.293-4.293a1%201%200%20011.414%200z%27/%3e%3c/svg%3e')] checked:bg-[length:75%] 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>
|
||||
@ -232,7 +236,7 @@
|
||||
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"
|
||||
class="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 checked:bg-[url('data:image/svg+xml,%3csvg%20viewBox=%270%200%2016%2016%27%20fill=%27white%27%20xmlns=%27http://www.w3.org/2000/svg%27%3e%3cpath%20d=%27M12.207%204.793a1%201%200%20010%201.414l-5%205a1%201%200%2001-1.414%200l-2-2a1%201%200%20011.414-1.414L6.5%209.086l4.293-4.293a1%201%200%20011.414%200z%27/%3e%3c/svg%3e')] checked:bg-[length:75%] focus:ring-1 focus:ring-red-500 focus:ring-offset-0"
|
||||
/>
|
||||
</td>
|
||||
<td class="px-4 py-2 font-medium text-zinc-200">
|
||||
|
||||
@ -3,17 +3,25 @@ import { items, folders, categories, reservations } from '$lib/server/db/schema'
|
||||
import { eq, and, isNull, inArray, ilike, sql } from 'drizzle-orm';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { processProperties } from '$lib/utils/propertyUtils';
|
||||
import { getOrSeedHomeFolder } from '$lib/server/db/utils';
|
||||
|
||||
export const load = async ({ url }) => {
|
||||
const currentFolder = null;
|
||||
const currentFolder = await getOrSeedHomeFolder();
|
||||
const breadcrumbs: any[] = [];
|
||||
const searchQuery = url.searchParams.get('search');
|
||||
|
||||
// Fetch subfolders (root)
|
||||
// Fetch subfolders (children of Home)
|
||||
const subfolders = await db.select().from(folders)
|
||||
.where(isNull(folders.parentId));
|
||||
.where(eq(folders.parentId, currentFolder.id));
|
||||
|
||||
let folderItemsQuery = db.select({
|
||||
const conditions = [];
|
||||
if (searchQuery) {
|
||||
conditions.push(ilike(items.name, `%${searchQuery}%`));
|
||||
} else {
|
||||
conditions.push(eq(items.folderId, currentFolder.id));
|
||||
}
|
||||
|
||||
const folderItemsQuery = db.select({
|
||||
id: items.id,
|
||||
name: items.name,
|
||||
count: items.count,
|
||||
@ -25,13 +33,8 @@ export const load = async ({ url }) => {
|
||||
reservedCount: sql<number>`(SELECT COALESCE(SUM(count), 0) FROM reservations WHERE item_id = ${items.id})`.mapWith(Number)
|
||||
})
|
||||
.from(items)
|
||||
.leftJoin(categories, eq(items.categoryId, categories.id));
|
||||
|
||||
if (searchQuery) {
|
||||
folderItemsQuery = folderItemsQuery.where(ilike(items.name, `%${searchQuery}%`));
|
||||
} else {
|
||||
folderItemsQuery = folderItemsQuery.where(isNull(items.folderId));
|
||||
}
|
||||
.leftJoin(categories, eq(items.categoryId, categories.id))
|
||||
.where(and(...conditions));
|
||||
|
||||
const folderItems = await folderItemsQuery;
|
||||
|
||||
@ -72,7 +75,7 @@ export const actions = {
|
||||
|
||||
await db.insert(items).values({
|
||||
name,
|
||||
folderId: folderId || null,
|
||||
folderId: folderId,
|
||||
categoryId,
|
||||
count,
|
||||
properties
|
||||
@ -99,7 +102,7 @@ export const actions = {
|
||||
};
|
||||
|
||||
if (folderId !== undefined) {
|
||||
updateData.folderId = folderId || null;
|
||||
updateData.folderId = folderId;
|
||||
}
|
||||
|
||||
await db.update(items)
|
||||
@ -118,7 +121,7 @@ export const actions = {
|
||||
const data = await request.formData();
|
||||
const id = data.get('id') as string;
|
||||
const folderId = data.get('folderId') as string;
|
||||
await db.update(items).set({ folderId: folderId || null }).where(eq(items.id, id));
|
||||
await db.update(items).set({ folderId: folderId }).where(eq(items.id, id));
|
||||
return { success: true };
|
||||
},
|
||||
bulkDelete: async ({ request }) => {
|
||||
@ -134,8 +137,60 @@ export const actions = {
|
||||
const ids = JSON.parse(data.get('ids') as string);
|
||||
const folderId = data.get('folderId') as string;
|
||||
if (ids.length > 0) {
|
||||
await db.update(items).set({ folderId: folderId || null }).where(inArray(items.id, ids));
|
||||
await db.update(items).set({ folderId: folderId }).where(inArray(items.id, ids));
|
||||
}
|
||||
return { success: true };
|
||||
},
|
||||
updateCount: async ({ request }) => {
|
||||
const data = await request.formData();
|
||||
const id = data.get('id') as string;
|
||||
const delta = parseInt(data.get('delta') as string) || 0;
|
||||
|
||||
const item = await db.select({ count: items.count }).from(items).where(eq(items.id, id)).limit(1);
|
||||
|
||||
if (item.length > 0) {
|
||||
const newCount = Math.max(0, (item[0].count || 0) + delta);
|
||||
await db.update(items).set({ count: newCount }).where(eq(items.id, id));
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
renameFolder: async ({ request }) => {
|
||||
const data = await request.formData();
|
||||
const id = data.get('id') as string;
|
||||
const name = data.get('name') as string;
|
||||
|
||||
await db.update(folders)
|
||||
.set({ name })
|
||||
.where(eq(folders.id, id));
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
moveFolder: async ({ request }) => {
|
||||
const data = await request.formData();
|
||||
const id = data.get('id') as string;
|
||||
const parentId = data.get('folderId') as string; // Reusing folderId param name from item move
|
||||
|
||||
// Prevent moving folder into itself or its children (basic check: not into itself)
|
||||
if (id === parentId) {
|
||||
// In a real app we'd check for cycles, but for now just prevent direct self-parenting
|
||||
return { success: false, error: "Cannot move folder into itself" };
|
||||
}
|
||||
|
||||
await db.update(folders)
|
||||
.set({ parentId: parentId || null })
|
||||
.where(eq(folders.id, id));
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
deleteFolder: async ({ request }) => {
|
||||
const data = await request.formData();
|
||||
const id = data.get('id') as string;
|
||||
|
||||
// Note: This assumes cascading delete or that the user has emptied the folder.
|
||||
// If not cascading, this might fail. For now, we assume simple delete.
|
||||
await db.delete(folders).where(eq(folders.id, id));
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { enhance } from '$app/forms';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import ItemModal from '$lib/components/ItemModal.svelte';
|
||||
import MoveItemModal from '$lib/components/MoveItemModal.svelte';
|
||||
import DeleteConfirmModal from '$lib/components/DeleteConfirmModal.svelte';
|
||||
import FolderModal from '$lib/components/FolderModal.svelte';
|
||||
|
||||
import { propertyConfig } from '$lib/config/properties';
|
||||
import { formatValue } from '$lib/utils/units';
|
||||
@ -12,30 +14,33 @@
|
||||
|
||||
$: ({ currentFolder, breadcrumbs, subfolders, items, categories } = data);
|
||||
|
||||
let folderDialog: HTMLDialogElement;
|
||||
|
||||
// Item Modal State
|
||||
let showItemModal = false;
|
||||
let showFolderModal = false;
|
||||
let showMoveItemModal = false;
|
||||
let showDeleteModal = false;
|
||||
|
||||
let editingItem: any = null;
|
||||
let editingFolder: 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
|
||||
let actionType: 'item' | 'folder' = 'item';
|
||||
|
||||
// Reactive statement to handle selection updates
|
||||
$: allSelected = items.length > 0 && selectedItems.size === items.length;
|
||||
$: isIndeterminate = selectedItems.size > 0 && selectedItems.size < items.length;
|
||||
|
||||
function openFolderModal() {
|
||||
folderDialog.showModal();
|
||||
function openCreateFolderModal() {
|
||||
editingFolder = null;
|
||||
showFolderModal = true;
|
||||
}
|
||||
|
||||
function closeFolderModal() {
|
||||
folderDialog.close();
|
||||
function openRenameFolderModal(folder: any) {
|
||||
editingFolder = folder;
|
||||
showFolderModal = true;
|
||||
}
|
||||
|
||||
function openCreateItemModal() {
|
||||
@ -51,24 +56,42 @@
|
||||
function openMoveItemModal(item: any) {
|
||||
actionItemIds = [item.id];
|
||||
actionItemName = item.name;
|
||||
actionType = 'item';
|
||||
showMoveItemModal = true;
|
||||
}
|
||||
|
||||
function openMoveFolderModal(folder: any) {
|
||||
actionItemIds = [folder.id];
|
||||
actionItemName = folder.name;
|
||||
actionType = 'folder';
|
||||
showMoveItemModal = true;
|
||||
}
|
||||
|
||||
function openBulkMoveModal() {
|
||||
actionItemIds = Array.from(selectedItems);
|
||||
actionItemName = null;
|
||||
actionType = 'item';
|
||||
showMoveItemModal = true;
|
||||
}
|
||||
|
||||
function openDeleteModal(item: any) {
|
||||
actionItemIds = [item.id];
|
||||
actionItemName = item.name;
|
||||
actionType = 'item';
|
||||
showDeleteModal = true;
|
||||
}
|
||||
|
||||
function openDeleteFolderModal(folder: any) {
|
||||
actionItemIds = [folder.id];
|
||||
actionItemName = folder.name;
|
||||
actionType = 'folder';
|
||||
showDeleteModal = true;
|
||||
}
|
||||
|
||||
function openBulkDeleteModal() {
|
||||
actionItemIds = Array.from(selectedItems);
|
||||
actionItemName = null;
|
||||
actionType = 'item';
|
||||
showDeleteModal = true;
|
||||
}
|
||||
|
||||
@ -106,18 +129,15 @@
|
||||
<a href="/folders" class="transition-colors hover:text-white">Home</a>
|
||||
{#each breadcrumbs as crumb}
|
||||
<span>/</span>
|
||||
<a
|
||||
href="/folders{crumb.url.startsWith('/') ? '' : '/'}{crumb.url}"
|
||||
class="font-medium text-zinc-300 transition-colors hover:text-white"
|
||||
>
|
||||
<a href={crumb.url} class="font-medium text-zinc-300 transition-colors hover:text-white">
|
||||
{crumb.name}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="relative 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"
|
||||
class="animate-in fade-in slide-in-from-top-2 absolute top-1/2 right-full mr-4 flex -translate-y-1/2 items-center gap-2 whitespace-nowrap duration-200"
|
||||
>
|
||||
<span class="text-sm font-medium text-zinc-400">{selectedItems.size} selected</span>
|
||||
<button
|
||||
@ -164,10 +184,10 @@
|
||||
{/if}
|
||||
|
||||
<button
|
||||
on:click={openFolderModal}
|
||||
on:click={openCreateFolderModal}
|
||||
class="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"
|
||||
>
|
||||
+ New Location
|
||||
+ New Folder
|
||||
</button>
|
||||
{#if currentFolder}
|
||||
<button
|
||||
@ -180,44 +200,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Folder Modal -->
|
||||
<dialog
|
||||
bind:this={folderDialog}
|
||||
class="m-auto w-full max-w-md rounded-lg border border-zinc-700 bg-zinc-950 p-0 text-white shadow-2xl backdrop:bg-black/80"
|
||||
>
|
||||
<div class="p-6">
|
||||
<h2 class="mb-4 text-lg font-bold text-white">Create New Location</h2>
|
||||
<form method="POST" action="?/createFolder" class="space-y-4" on:submit={closeFolderModal}>
|
||||
<input type="hidden" name="parentId" value={currentFolder?.id || ''} />
|
||||
<div>
|
||||
<label for="folderName" class="mb-1 block text-sm font-medium text-zinc-400"
|
||||
>Location Name</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
id="folderName"
|
||||
required
|
||||
class="w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-white placeholder-zinc-600 transition-all outline-none focus:border-red-500 focus:ring-1 focus:ring-red-500"
|
||||
placeholder="e.g. Components"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
on:click={closeFolderModal}
|
||||
class="rounded-md px-4 py-2 font-medium text-zinc-400 transition-colors hover:bg-zinc-800"
|
||||
>Cancel</button
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-red-600 px-4 py-2 font-medium text-white shadow-lg shadow-red-900/20 transition-colors hover:bg-red-700"
|
||||
>Create Location</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
<!-- Create/Edit Folder Modal -->
|
||||
<FolderModal
|
||||
bind:show={showFolderModal}
|
||||
folder={editingFolder}
|
||||
parentId={currentFolder?.id}
|
||||
onClose={() => (showFolderModal = false)}
|
||||
/>
|
||||
|
||||
<!-- Item Modal -->
|
||||
<ItemModal
|
||||
@ -226,15 +215,20 @@
|
||||
{categories}
|
||||
folders={data.allFolders || []}
|
||||
currentFolderId={currentFolder?.id}
|
||||
onClose={() => (showItemModal = false)}
|
||||
user={data.user}
|
||||
onClose={() => {
|
||||
showItemModal = false;
|
||||
invalidateAll();
|
||||
}}
|
||||
/>
|
||||
|
||||
<!-- Move Item Modal -->
|
||||
<!-- Move Item/Folder Modal -->
|
||||
<MoveItemModal
|
||||
bind:show={showMoveItemModal}
|
||||
itemIds={actionItemIds}
|
||||
itemName={actionItemName}
|
||||
folders={data.allFolders || []}
|
||||
type={actionType}
|
||||
onClose={() => (showMoveItemModal = false)}
|
||||
/>
|
||||
|
||||
@ -243,23 +237,29 @@
|
||||
bind:show={showDeleteModal}
|
||||
itemIds={actionItemIds}
|
||||
itemName={actionItemName}
|
||||
type={actionType}
|
||||
onClose={() => (showDeleteModal = false)}
|
||||
/>
|
||||
|
||||
<!-- Folders Grid -->
|
||||
{#if subfolders.length > 0}
|
||||
<section>
|
||||
<h2 class="mb-3 text-xs font-semibold tracking-wider text-zinc-500 uppercase">Locations</h2>
|
||||
<h2 class="mb-3 text-xs font-semibold tracking-wider text-zinc-500 uppercase">Folders</h2>
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||
{#each subfolders as folder}
|
||||
<a
|
||||
href="{$page.url.pathname === '/folders'
|
||||
? '/folders'
|
||||
: $page.url.pathname}/{folder.name}"
|
||||
class="group flex items-center gap-3 rounded-md border border-zinc-700 bg-zinc-900/50 p-3 transition-all duration-200 hover:border-red-500/50"
|
||||
<div
|
||||
class="group relative flex items-center gap-3 rounded-md border border-zinc-700 bg-zinc-900/50 p-3 transition-all duration-200 hover:border-red-500/50"
|
||||
>
|
||||
<a
|
||||
href="{$page.url.pathname === '/folders'
|
||||
? '/folders'
|
||||
: $page.url.pathname}/{folder.name}"
|
||||
class="absolute inset-0 z-0"
|
||||
>
|
||||
<span class="sr-only">Open {folder.name}</span>
|
||||
</a>
|
||||
<div
|
||||
class="rounded bg-zinc-800 p-2 text-zinc-400 transition-colors group-hover:text-white"
|
||||
class="pointer-events-none z-10 rounded bg-zinc-800 p-2 text-zinc-400 transition-colors group-hover:text-white"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -276,10 +276,77 @@
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="truncate font-medium text-zinc-300 group-hover:text-white"
|
||||
<span
|
||||
class="pointer-events-none z-10 truncate font-medium text-zinc-300 group-hover:text-white"
|
||||
>{folder.name}</span
|
||||
>
|
||||
</a>
|
||||
|
||||
<!-- Folder Actions -->
|
||||
<div
|
||||
class="absolute top-1/2 right-2 z-20 hidden -translate-y-1/2 gap-1 group-hover:flex"
|
||||
>
|
||||
<button
|
||||
on:click|preventDefault={() => openRenameFolderModal(folder)}
|
||||
class="rounded p-1.5 text-zinc-400 hover:bg-zinc-800 hover:text-white"
|
||||
title="Rename"
|
||||
>
|
||||
<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|preventDefault={() => openMoveFolderModal(folder)}
|
||||
class="rounded p-1.5 text-zinc-400 hover:bg-zinc-800 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|preventDefault={() => openDeleteFolderModal(folder)}
|
||||
class="rounded p-1.5 text-zinc-400 hover:bg-red-900/30 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>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
@ -299,13 +366,13 @@
|
||||
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"
|
||||
class="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 checked:bg-[url('data:image/svg+xml,%3csvg%20viewBox=%270%200%2016%2016%27%20fill=%27white%27%20xmlns=%27http://www.w3.org/2000/svg%27%3e%3cpath%20d=%27M12.207%204.793a1%201%200%20010%201.414l-5%205a1%201%200%2001-1.414%200l-2-2a1%201%200%20011.414-1.414L6.5%209.086l4.293-4.293a1%201%200%20011.414%200z%27/%3e%3c/svg%3e')] checked:bg-[length:75%] 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">Category</th>
|
||||
<th class="px-4 py-2 font-medium text-zinc-500">Count</th>
|
||||
<th class="px-4 py-2 font-medium text-zinc-500">Tags</th>
|
||||
|
||||
<th class="px-4 py-2 font-medium text-zinc-500">Properties</th>
|
||||
<th class="px-4 py-2 text-right font-medium text-zinc-500">Actions</th>
|
||||
</tr>
|
||||
@ -322,10 +389,17 @@
|
||||
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"
|
||||
class="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 checked:bg-[url('data:image/svg+xml,%3csvg%20viewBox=%270%200%2016%2016%27%20fill=%27white%27%20xmlns=%27http://www.w3.org/2000/svg%27%3e%3cpath%20d=%27M12.207%204.793a1%201%200%20010%201.414l-5%205a1%201%200%2001-1.414%200l-2-2a1%201%200%20011.414-1.414L6.5%209.086l4.293-4.293a1%201%200%20011.414%200z%27/%3e%3c/svg%3e')] checked:bg-[length:75%] focus:ring-1 focus:ring-red-500 focus:ring-offset-0"
|
||||
/>
|
||||
</td>
|
||||
<td class="px-4 py-2 font-medium text-zinc-200">{item.name}</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">
|
||||
<span
|
||||
class="rounded border border-zinc-700 bg-zinc-800 px-2 py-0.5 text-xs font-medium text-zinc-300"
|
||||
@ -334,28 +408,79 @@
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-2 font-mono text-zinc-400">
|
||||
{item.count}
|
||||
{#if item.reservedCount > 0}
|
||||
<span class="text-red-500"> - {item.reservedCount}</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#each item.tags as tag}
|
||||
<span
|
||||
class="rounded border border-zinc-700 bg-zinc-800 px-2 py-0.5 text-xs text-zinc-400"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
{/each}
|
||||
<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>
|
||||
|
||||
<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]}
|
||||
<span
|
||||
><span class="font-medium text-zinc-400">{key}:</span>
|
||||
{getFormattedValue(key, value)}</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
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
@ -450,9 +575,9 @@
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-zinc-300">Empty Location</h3>
|
||||
<h3 class="text-lg font-medium text-zinc-300">Empty Folder</h3>
|
||||
<p class="mt-1 text-sm text-zinc-500">
|
||||
This location is empty. Add items or sublocations to get started.
|
||||
This folder is empty. Add items or subfolders to get started.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@ -4,17 +4,22 @@ import { eq, and, isNull, inArray, sql } from 'drizzle-orm';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { processProperties } from '$lib/utils/propertyUtils';
|
||||
|
||||
import { getOrSeedHomeFolder } from '$lib/server/db/utils';
|
||||
|
||||
export const load = async ({ params, url }: { params: { path: string }, url: URL }) => {
|
||||
const pathSegments = params.path.split('/').filter(Boolean);
|
||||
let currentFolder = null;
|
||||
const breadcrumbs: any[] = [];
|
||||
let parentId = null;
|
||||
|
||||
// Start with Home folder as the root parent
|
||||
const homeFolder = await getOrSeedHomeFolder();
|
||||
let parentId: string | null = homeFolder.id;
|
||||
|
||||
for (const segment of pathSegments) {
|
||||
const folder = await db.select().from(folders)
|
||||
.where(and(
|
||||
eq(folders.name, decodeURIComponent(segment)),
|
||||
parentId ? eq(folders.parentId, parentId) : isNull(folders.parentId)
|
||||
eq(folders.parentId, parentId)
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
@ -31,9 +36,13 @@ export const load = async ({ params, url }: { params: { path: string }, url: URL
|
||||
});
|
||||
}
|
||||
|
||||
if (!currentFolder) {
|
||||
throw error(404, 'Folder not found');
|
||||
}
|
||||
|
||||
// Fetch subfolders
|
||||
const subfolders = await db.select().from(folders)
|
||||
.where(currentFolder ? eq(folders.parentId, currentFolder.id) : isNull(folders.parentId));
|
||||
.where(eq(folders.parentId, currentFolder.id));
|
||||
|
||||
// Fetch items
|
||||
const folderItems = await db.select({
|
||||
@ -49,7 +58,7 @@ export const load = async ({ params, url }: { params: { path: string }, url: URL
|
||||
})
|
||||
.from(items)
|
||||
.leftJoin(categories, eq(items.categoryId, categories.id))
|
||||
.where(currentFolder ? eq(items.folderId, currentFolder.id) : isNull(items.folderId));
|
||||
.where(eq(items.folderId, currentFolder.id));
|
||||
|
||||
const allCategories = await db.select().from(categories);
|
||||
const allFolders = await db.select().from(folders);
|
||||
@ -79,7 +88,7 @@ export const actions = {
|
||||
},
|
||||
createItem: async ({ request }: { request: Request }) => {
|
||||
const data = await request.formData();
|
||||
const name = data.get('name') as string;
|
||||
const name = (data.get('name') as string) || null;
|
||||
const folderId = data.get('folderId') as string;
|
||||
const categoryId = data.get('categoryId') as string;
|
||||
const count = parseInt(data.get('count') as string) || 0;
|
||||
@ -88,7 +97,7 @@ export const actions = {
|
||||
|
||||
await db.insert(items).values({
|
||||
name,
|
||||
folderId: folderId || null,
|
||||
folderId: folderId,
|
||||
categoryId,
|
||||
count,
|
||||
properties
|
||||
@ -99,7 +108,7 @@ export const actions = {
|
||||
updateItem: async ({ request }: { request: Request }) => {
|
||||
const data = await request.formData();
|
||||
const id = data.get('id') as string;
|
||||
const name = data.get('name') as string;
|
||||
const name = (data.get('name') as string) || null;
|
||||
const categoryId = data.get('categoryId') as string;
|
||||
const count = parseInt(data.get('count') as string) || 0;
|
||||
const folderId = data.get('folderId') as string;
|
||||
@ -115,7 +124,7 @@ export const actions = {
|
||||
};
|
||||
|
||||
if (folderId !== undefined) {
|
||||
updateData.folderId = folderId || null;
|
||||
updateData.folderId = folderId;
|
||||
}
|
||||
|
||||
await db.update(items)
|
||||
@ -134,7 +143,7 @@ export const actions = {
|
||||
const data = await request.formData();
|
||||
const id = data.get('id') as string;
|
||||
const folderId = data.get('folderId') as string;
|
||||
await db.update(items).set({ folderId: folderId || null }).where(eq(items.id, id));
|
||||
await db.update(items).set({ folderId: folderId }).where(eq(items.id, id));
|
||||
return { success: true };
|
||||
},
|
||||
bulkDelete: async ({ request }: { request: Request }) => {
|
||||
@ -150,7 +159,7 @@ export const actions = {
|
||||
const ids = JSON.parse(data.get('ids') as string);
|
||||
const folderId = data.get('folderId') as string;
|
||||
if (ids.length > 0) {
|
||||
await db.update(items).set({ folderId: folderId || null }).where(inArray(items.id, ids));
|
||||
await db.update(items).set({ folderId: folderId }).where(inArray(items.id, ids));
|
||||
}
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { enhance } from '$app/forms';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import ItemModal from '$lib/components/ItemModal.svelte';
|
||||
import MoveItemModal from '$lib/components/MoveItemModal.svelte';
|
||||
import DeleteConfirmModal from '$lib/components/DeleteConfirmModal.svelte';
|
||||
@ -133,10 +134,10 @@
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="relative 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"
|
||||
class="animate-in fade-in slide-in-from-top-2 absolute top-1/2 right-full mr-4 flex -translate-y-1/2 items-center gap-2 whitespace-nowrap duration-200"
|
||||
>
|
||||
<span class="text-sm font-medium text-zinc-400">{selectedItems.size} selected</span>
|
||||
<button
|
||||
@ -215,7 +216,10 @@
|
||||
folders={data.allFolders || []}
|
||||
currentFolderId={currentFolder?.id}
|
||||
user={data.user}
|
||||
onClose={() => (showItemModal = false)}
|
||||
onClose={() => {
|
||||
showItemModal = false;
|
||||
invalidateAll();
|
||||
}}
|
||||
/>
|
||||
|
||||
<!-- Move Item/Folder Modal -->
|
||||
@ -362,7 +366,7 @@
|
||||
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"
|
||||
class="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 checked:bg-[url('data:image/svg+xml,%3csvg%20viewBox=%270%200%2016%2016%27%20fill=%27white%27%20xmlns=%27http://www.w3.org/2000/svg%27%3e%3cpath%20d=%27M12.207%204.793a1%201%200%20010%201.414l-5%205a1%201%200%2001-1.414%200l-2-2a1%201%200%20011.414-1.414L6.5%209.086l4.293-4.293a1%201%200%20011.414%200z%27/%3e%3c/svg%3e')] checked:bg-[length:75%] 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>
|
||||
@ -385,7 +389,7 @@
|
||||
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"
|
||||
class="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 checked:bg-[url('data:image/svg+xml,%3csvg%20viewBox=%270%200%2016%2016%27%20fill=%27white%27%20xmlns=%27http://www.w3.org/2000/svg%27%3e%3cpath%20d=%27M12.207%204.793a1%201%200%20010%201.414l-5%205a1%201%200%2001-1.414%200l-2-2a1%201%200%20011.414-1.414L6.5%209.086l4.293-4.293a1%201%200%20011.414%200z%27/%3e%3c/svg%3e')] checked:bg-[length:75%] focus:ring-1 focus:ring-red-500 focus:ring-offset-0"
|
||||
/>
|
||||
</td>
|
||||
<td class="px-4 py-2 font-medium text-zinc-200">
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import { createReservation, updateReservation, deleteReservation } from '$lib/server/reservations';
|
||||
import { createComment, deleteComment } from '$lib/server/comments';
|
||||
import { db } from '$lib/server/db';
|
||||
import { items, reservations } from '$lib/server/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
export const actions = {
|
||||
reserve: async ({ request, locals }) => {
|
||||
@ -86,5 +89,38 @@ export const actions = {
|
||||
} catch (e) {
|
||||
return fail(500, { error: 'Failed to delete comment' });
|
||||
}
|
||||
},
|
||||
applyReservation: async ({ request, locals }) => {
|
||||
const data = await request.formData();
|
||||
const id = data.get('id')?.toString();
|
||||
const userId = locals.user?.id;
|
||||
|
||||
if (!id || !userId) return fail(400, { error: 'Missing ID' });
|
||||
|
||||
try {
|
||||
// Fetch reservation to get count and itemId
|
||||
const reservation = await db.select().from(reservations).where(eq(reservations.id, id)).limit(1);
|
||||
if (reservation.length === 0) return fail(404, { error: 'Reservation not found' });
|
||||
|
||||
const { itemId, count: reservedAmount } = reservation[0];
|
||||
|
||||
// Fetch item to get current count
|
||||
const item = await db.select().from(items).where(eq(items.id, itemId)).limit(1);
|
||||
if (item.length === 0) return fail(404, { error: 'Item not found' });
|
||||
|
||||
const currentCount = item[0].count;
|
||||
const newCount = Math.max(0, currentCount - reservedAmount);
|
||||
|
||||
// Update item count
|
||||
await db.update(items).set({ count: newCount }).where(eq(items.id, itemId));
|
||||
|
||||
// Delete reservation
|
||||
await db.delete(reservations).where(eq(reservations.id, id));
|
||||
|
||||
return { success: true, newCount };
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return fail(500, { error: 'Failed to apply reservation' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
import adapter from '@sveltejs/adapter-auto';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
|
||||
5
todos
5
todos
@ -1,5 +0,0 @@
|
||||
- Folder route
|
||||
- Fix Submitting items from category view
|
||||
- Editing Categories
|
||||
- Add global search
|
||||
- Add local search in folder & category views
|
||||
@ -1,6 +1,6 @@
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig, UserConfig } from 'vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss(), sveltekit()],
|
||||
@ -9,7 +9,5 @@ export default defineConfig({
|
||||
},
|
||||
server: {
|
||||
origin: 'https://el.inv.fasttube.de',
|
||||
host: '0.0.0.0',
|
||||
port: 443
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user