upd inventory

This commit is contained in:
beo3000 2026-03-06 22:08:03 +01:00
parent b068c75616
commit b4ed2055a2
14 changed files with 2139 additions and 28 deletions

View File

@ -1 +1 @@
1.2.31
1.2.33

View File

@ -0,0 +1,43 @@
<script lang="ts">
import {
Sofa, BedDouble, Baby, Utensils, Shirt, DoorOpen, ChefHat, Droplets,
Monitor, Archive, Home, Car, TreePine, Hammer, Package, Dumbbell,
Music, Flame, Box, Armchair, Bike,
} from 'lucide-svelte';
const ICON_MAP: Record<string, unknown> = {
'sofa': Sofa,
'bed': BedDouble,
'baby': Baby,
'utensils': Utensils,
'shirt': Shirt,
'door-open': DoorOpen,
'chef-hat': ChefHat,
'droplets': Droplets,
'monitor': Monitor,
'archive': Archive,
'home': Home,
'car': Car,
'tree-pine': TreePine,
'hammer': Hammer,
'dumbbell': Dumbbell,
'music': Music,
'flame': Flame,
'box': Box,
'armchair': Armchair,
'bike': Bike,
'package': Package,
};
interface Props {
icon: string;
color?: string | null;
size?: number;
}
let { icon, color = null, size = 28 }: Props = $props();
const component = $derived(ICON_MAP[icon] ?? Package);
const style = $derived(color ? `color: ${color}` : '');
</script>
<svelte:component this={component} {size} style={style} class={color ? '' : 'text-accent'} />

View File

@ -944,13 +944,14 @@ export async function getTasksByContext(contextId: string): Promise<Task[]> {
// --- Rooms ---
export async function createRoom(fields: { name: string; groupType: RoomGroupType; icon: string; sortOrder?: number; userId?: string }): Promise<Room> {
export async function createRoom(fields: { name: string; groupType: RoomGroupType; icon: string; color?: string | null; sortOrder?: number; userId?: string }): Promise<Room> {
const room: Room = {
id: newId(),
userId: fields.userId ?? '',
name: fields.name,
groupType: fields.groupType,
icon: fields.icon,
color: fields.color ?? null,
sortOrder: fields.sortOrder ?? 0,
updatedAt: now(),
deletedAt: null,
@ -965,7 +966,7 @@ export async function getRooms(): Promise<Room[]> {
return db.rooms.filter(r => !r.deletedAt).sortBy('sortOrder');
}
export async function updateRoom(id: string, changes: Partial<Pick<Room, 'name' | 'groupType' | 'icon' | 'sortOrder'>>): Promise<void> {
export async function updateRoom(id: string, changes: Partial<Pick<Room, 'name' | 'groupType' | 'icon' | 'color' | 'sortOrder'>>): Promise<void> {
const room = await db.rooms.get(id);
if (room) {
await db.rooms.update(id, { ...changes, updatedAt: now(), version: room.version + 1 });
@ -1123,22 +1124,22 @@ export async function getFamilyPersons(): Promise<AgendaContext[]> {
// --- Seed default rooms ---
const DEFAULT_ROOMS: Array<{ name: string; groupType: RoomGroupType; icon: string }> = [
{ name: 'Wohnzimmer', groupType: 'living', icon: 'sofa' },
{ name: 'Schlafzimmer', groupType: 'living', icon: 'bed' },
{ name: 'Kinderzimmer', groupType: 'living', icon: 'baby' },
{ name: 'Esszimmer', groupType: 'living', icon: 'utensils' },
{ name: 'Ankleidezimmer', groupType: 'living', icon: 'shirt' },
{ name: 'Flur/Diele', groupType: 'living', icon: 'door-open' },
{ name: 'Küche', groupType: 'functional', icon: 'chef-hat' },
{ name: 'Badezimmer', groupType: 'functional', icon: 'bath' },
{ name: 'WC', groupType: 'functional', icon: 'toilet' },
{ name: 'Arbeitszimmer', groupType: 'functional', icon: 'monitor' },
{ name: 'Hauswirtschaft', groupType: 'functional', icon: 'washing-machine' },
{ name: 'Keller', groupType: 'outdoor', icon: 'archive' },
{ name: 'Dachboden', groupType: 'outdoor', icon: 'home' },
{ name: 'Garage', groupType: 'outdoor', icon: 'car' },
{ name: 'Garten', groupType: 'outdoor', icon: 'tree' },
const DEFAULT_ROOMS: Array<{ name: string; groupType: RoomGroupType; icon: string; color: string }> = [
{ name: 'Wohnzimmer', groupType: 'living', icon: 'sofa', color: '#818cf8' },
{ name: 'Schlafzimmer', groupType: 'living', icon: 'bed', color: '#a78bfa' },
{ name: 'Kinderzimmer', groupType: 'living', icon: 'baby', color: '#f472b6' },
{ name: 'Esszimmer', groupType: 'living', icon: 'utensils', color: '#fb923c' },
{ name: 'Ankleidezimmer', groupType: 'living', icon: 'shirt', color: '#c084fc' },
{ name: 'Flur/Diele', groupType: 'living', icon: 'door-open', color: '#94a3b8' },
{ name: 'Küche', groupType: 'functional', icon: 'chef-hat', color: '#fbbf24' },
{ name: 'Badezimmer', groupType: 'functional', icon: 'droplets', color: '#22d3ee' },
{ name: 'WC', groupType: 'functional', icon: 'droplets', color: '#67e8f9' },
{ name: 'Arbeitszimmer', groupType: 'functional', icon: 'monitor', color: '#60a5fa' },
{ name: 'Hauswirtschaft', groupType: 'functional', icon: 'archive', color: '#94a3b8' },
{ name: 'Keller', groupType: 'outdoor', icon: 'archive', color: '#78716c' },
{ name: 'Dachboden', groupType: 'outdoor', icon: 'home', color: '#a8a29e' },
{ name: 'Garage', groupType: 'outdoor', icon: 'car', color: '#6b7280' },
{ name: 'Garten', groupType: 'outdoor', icon: 'tree-pine', color: '#4ade80' },
];
const SEED_SETTING_KEY = 'inventory.seeded';
@ -1156,6 +1157,7 @@ export async function seedDefaultRoomsIfNeeded(userId: string): Promise<void> {
name: r.name,
groupType: r.groupType,
icon: r.icon,
color: r.color,
sortOrder: i,
updatedAt: ts,
deletedAt: null,

View File

@ -0,0 +1,49 @@
export interface RoomIconDef {
key: string;
label: string;
}
export const ROOM_ICONS: RoomIconDef[] = [
{ key: 'sofa', label: 'Sofa' },
{ key: 'bed', label: 'Bett' },
{ key: 'armchair', label: 'Sessel' },
{ key: 'utensils', label: 'Besteck' },
{ key: 'chef-hat', label: 'Küche' },
{ key: 'droplets', label: 'Wasser' },
{ key: 'monitor', label: 'Bildschirm' },
{ key: 'shirt', label: 'Kleidung' },
{ key: 'baby', label: 'Kind' },
{ key: 'door-open', label: 'Tür' },
{ key: 'archive', label: 'Regal' },
{ key: 'home', label: 'Haus' },
{ key: 'car', label: 'Auto' },
{ key: 'tree-pine', label: 'Garten' },
{ key: 'hammer', label: 'Werkzeug' },
{ key: 'dumbbell', label: 'Fitness' },
{ key: 'music', label: 'Musik' },
{ key: 'flame', label: 'Feuer' },
{ key: 'bike', label: 'Fahrrad' },
{ key: 'box', label: 'Kiste' },
];
export interface RoomColorDef {
key: string;
label: string;
hex: string;
}
export const ROOM_COLORS: RoomColorDef[] = [
{ key: 'indigo', label: 'Indigo', hex: '#818cf8' },
{ key: 'violet', label: 'Violett', hex: '#a78bfa' },
{ key: 'purple', label: 'Lila', hex: '#c084fc' },
{ key: 'pink', label: 'Pink', hex: '#f472b6' },
{ key: 'rose', label: 'Rot', hex: '#f87171' },
{ key: 'orange', label: 'Orange', hex: '#fb923c' },
{ key: 'amber', label: 'Gelb', hex: '#fbbf24' },
{ key: 'green', label: 'Grün', hex: '#4ade80' },
{ key: 'teal', label: 'Türkis', hex: '#2dd4bf' },
{ key: 'cyan', label: 'Cyan', hex: '#22d3ee' },
{ key: 'blue', label: 'Blau', hex: '#60a5fa' },
{ key: 'slate', label: 'Grau', hex: '#94a3b8' },
{ key: 'stone', label: 'Stein', hex: '#a8a29e' },
];

View File

@ -4,9 +4,11 @@
import { db } from '$lib/db/schema';
import { createRoom, updateRoom, softDeleteRoom } from '$lib/db/repositories';
import { account } from '$lib/auth/authStore';
import { Plus, Package, MoreHorizontal } from 'lucide-svelte';
import { Plus, MoreHorizontal } from 'lucide-svelte';
import DarkSelect from '$lib/components/DarkSelect.svelte';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import RoomIcon from '$lib/components/RoomIcon.svelte';
import { ROOM_ICONS, ROOM_COLORS } from '$lib/utils/roomAppearance';
import type { Room, RoomGroupType } from '@ka-note/shared';
const rooms$ = liveQuery(() => db.rooms.filter(r => !r.deletedAt).sortBy('sortOrder'));
@ -26,19 +28,27 @@
return ($assets$ ?? []).filter(a => a.roomId === roomId).length;
}
// Add room
// --- Add room ---
let showAddRoom = $state(false);
let newRoomName = $state('');
let newRoomGroup = $state<RoomGroupType>('living');
let newRoomIcon = $state('sofa');
let newRoomColor = $state('#818cf8');
async function addRoom() {
if (!newRoomName.trim()) return;
await createRoom({ name: newRoomName.trim(), groupType: newRoomGroup, icon: 'package', userId: $account?.homeAccountId ?? '' });
await createRoom({
name: newRoomName.trim(),
groupType: newRoomGroup,
icon: newRoomIcon,
color: newRoomColor,
userId: $account?.homeAccountId ?? '',
});
newRoomName = '';
showAddRoom = false;
}
// Rename room
// --- Rename ---
let renamingId = $state<string | null>(null);
let renameValue = $state('');
@ -54,14 +64,32 @@
renamingId = null;
}
// Delete room
// --- Appearance picker ---
let appearanceRoom = $state<Room | null>(null);
let pickIcon = $state('sofa');
let pickColor = $state('#818cf8');
function openAppearance(room: Room) {
appearanceRoom = room;
pickIcon = room.icon;
pickColor = room.color ?? '#818cf8';
menuRoomId = null;
}
async function saveAppearance() {
if (!appearanceRoom) return;
await updateRoom(appearanceRoom.id, { icon: pickIcon, color: pickColor });
appearanceRoom = null;
}
// --- Delete ---
let deleteTarget = $state<Room | null>(null);
async function confirmDelete() {
if (deleteTarget) await softDeleteRoom(deleteTarget.id);
deleteTarget = null;
}
// Context menu
// --- Context menu ---
let menuRoomId = $state<string | null>(null);
function toggleMenu(id: string) { menuRoomId = menuRoomId === id ? null : id; }
</script>
@ -92,7 +120,7 @@
onclick={() => renamingId !== room.id && goto('/inventory/rooms/' + room.id)}
onkeydown={(e) => renamingId !== room.id && e.key === 'Enter' && goto('/inventory/rooms/' + room.id)}
>
<Package size={28} class="text-accent" />
<RoomIcon icon={room.icon} color={room.color} size={28} />
{#if renamingId === room.id}
<input
class="input w-full text-center text-sm"
@ -114,8 +142,9 @@
<MoreHorizontal size={14} />
</button>
{#if menuRoomId === room.id}
<div class="absolute right-0 top-8 z-20 min-w-[140px] rounded-lg border border-border bg-surface shadow-lg py-1" onclick={(e) => e.stopPropagation()}>
<div class="absolute right-0 top-8 z-20 min-w-[160px] rounded-lg border border-border bg-surface shadow-lg py-1" onclick={(e) => e.stopPropagation()}>
<button class="w-full px-4 py-2 text-left text-sm text-white hover:bg-white/10" onclick={() => { startRename(room); menuRoomId = null; }}>Umbenennen</button>
<button class="w-full px-4 py-2 text-left text-sm text-white hover:bg-white/10" onclick={() => openAppearance(room)}>Icon & Farbe</button>
<button class="w-full px-4 py-2 text-left text-sm text-danger hover:bg-white/10" onclick={() => { deleteTarget = room; menuRoomId = null; }}>Löschen</button>
</div>
{/if}
@ -126,6 +155,7 @@
{/each}
</div>
<!-- Add room modal -->
{#if showAddRoom}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60" role="dialog">
<div class="mx-4 w-full max-w-sm rounded-xl border border-border bg-surface p-5">
@ -136,6 +166,40 @@
bind:value={newRoomGroup}
options={[{ value: 'living', label: 'Wohnbereiche' }, { value: 'functional', label: 'Funktionsräume' }, { value: 'outdoor', label: 'Außen & Nebenbereiche' }]}
/>
<!-- Icon picker -->
<div>
<p class="text-xs text-muted mb-2">Icon</p>
<div class="flex flex-wrap gap-2">
{#each ROOM_ICONS as ic}
<button
class="p-2 rounded-lg border-2 transition-colors {newRoomIcon === ic.key ? 'border-accent bg-accent/10' : 'border-transparent bg-white/5 hover:bg-white/10'}"
onclick={() => newRoomIcon = ic.key}
title={ic.label}
>
<RoomIcon icon={ic.key} color={newRoomIcon === ic.key ? newRoomColor : null} size={18} />
</button>
{/each}
</div>
</div>
<!-- Color picker -->
<div>
<p class="text-xs text-muted mb-2">Farbe</p>
<div class="flex flex-wrap gap-2">
{#each ROOM_COLORS as col}
<button
class="h-7 w-7 rounded-full border-2 transition-all {newRoomColor === col.hex ? 'border-white scale-110' : 'border-transparent'}"
style="background-color: {col.hex}"
onclick={() => newRoomColor = col.hex}
title={col.label}
></button>
{/each}
</div>
</div>
<!-- Preview -->
<div class="flex items-center gap-3 rounded-lg bg-white/5 px-3 py-2">
<RoomIcon icon={newRoomIcon} color={newRoomColor} size={22} />
<span class="text-sm text-white">{newRoomName || 'Vorschau'}</span>
</div>
<div class="flex gap-2">
<button class="btn-primary flex-1" onclick={addRoom}>Anlegen</button>
<button class="btn-ghost flex-1" onclick={() => showAddRoom = false}>Abbrechen</button>
@ -145,6 +209,55 @@
</div>
{/if}
<!-- Appearance picker modal -->
{#if appearanceRoom}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60" role="dialog">
<div class="mx-4 w-full max-w-sm rounded-xl border border-border bg-surface p-5">
<h2 class="text-lg font-semibold text-white mb-4">Icon & Farbe — {appearanceRoom.name}</h2>
<div class="flex flex-col gap-4">
<!-- Icon picker -->
<div>
<p class="text-xs text-muted mb-2">Icon</p>
<div class="flex flex-wrap gap-2">
{#each ROOM_ICONS as ic}
<button
class="p-2 rounded-lg border-2 transition-colors {pickIcon === ic.key ? 'border-accent bg-accent/10' : 'border-transparent bg-white/5 hover:bg-white/10'}"
onclick={() => pickIcon = ic.key}
title={ic.label}
>
<RoomIcon icon={ic.key} color={pickIcon === ic.key ? pickColor : null} size={18} />
</button>
{/each}
</div>
</div>
<!-- Color picker -->
<div>
<p class="text-xs text-muted mb-2">Farbe</p>
<div class="flex flex-wrap gap-2">
{#each ROOM_COLORS as col}
<button
class="h-7 w-7 rounded-full border-2 transition-all {pickColor === col.hex ? 'border-white scale-110' : 'border-transparent'}"
style="background-color: {col.hex}"
onclick={() => pickColor = col.hex}
title={col.label}
></button>
{/each}
</div>
</div>
<!-- Preview -->
<div class="flex items-center gap-3 rounded-lg bg-white/5 px-3 py-2">
<RoomIcon icon={pickIcon} color={pickColor} size={22} />
<span class="text-sm text-white">{appearanceRoom.name}</span>
</div>
<div class="flex gap-2">
<button class="btn-primary flex-1" onclick={saveAppearance}>Speichern</button>
<button class="btn-ghost flex-1" onclick={() => appearanceRoom = null}>Abbrechen</button>
</div>
</div>
</div>
</div>
{/if}
{#if deleteTarget}
<ConfirmDialog
message="Raum &quot;{deleteTarget.name}&quot; wirklich löschen? Gegenstände bleiben erhalten."

View File

@ -0,0 +1 @@
ALTER TABLE `rooms` ADD `color` text;

View File

@ -0,0 +1 @@
ALTER TABLE `rooms` ADD `color` text;

File diff suppressed because it is too large Load Diff

View File

@ -120,6 +120,13 @@
"when": 1772571259713,
"tag": "0016_inventory",
"breakpoints": true
},
{
"idx": 17,
"version": "6",
"when": 1772831133949,
"tag": "0017_kind_luke_cage",
"breakpoints": true
}
]
}

Binary file not shown.

Binary file not shown.

View File

@ -209,6 +209,7 @@ export const rooms = sqliteTable('rooms', {
name: text('name').notNull(),
groupType: text('group_type').notNull(),
icon: text('icon').notNull(),
color: text('color'),
sortOrder: integer('sort_order').notNull().default(0),
updatedAt: text('updated_at').notNull(),
deletedAt: text('deleted_at'),

View File

@ -181,6 +181,7 @@ function mapRoom(row: typeof rooms.$inferSelect): Room {
name: row.name,
groupType: row.groupType as Room['groupType'],
icon: row.icon,
color: row.color ?? null,
sortOrder: row.sortOrder,
updatedAt: row.updatedAt,
deletedAt: row.deletedAt ?? null,
@ -421,7 +422,7 @@ export async function pushChanges(request: SyncPushRequest, userId: string): Pro
}
for (const rm of rms) {
const row = { id: rm.id, userId, name: rm.name, groupType: rm.groupType, icon: rm.icon, sortOrder: rm.sortOrder, updatedAt: rm.updatedAt, deletedAt: rm.deletedAt, purgedAt: rm.purgedAt ?? null, version: rm.version };
const row = { id: rm.id, userId, name: rm.name, groupType: rm.groupType, icon: rm.icon, color: rm.color ?? null, sortOrder: rm.sortOrder, updatedAt: rm.updatedAt, deletedAt: rm.deletedAt, purgedAt: rm.purgedAt ?? null, version: rm.version };
if (await upsertEntity(rooms, row, conflicts, 'room', userId)) accepted++;
}

View File

@ -95,6 +95,7 @@ export interface Room extends SyncEntity {
name: string;
groupType: RoomGroupType;
icon: string;
color?: string | null;
sortOrder: number;
}