upd inventory
This commit is contained in:
parent
b068c75616
commit
b4ed2055a2
|
|
@ -1 +1 @@
|
|||
1.2.31
|
||||
1.2.33
|
||||
|
|
@ -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'} />
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
];
|
||||
|
|
@ -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 "{deleteTarget.name}" wirklich löschen? Gegenstände bleiben erhalten."
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE `rooms` ADD `color` text;
|
||||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE `rooms` ADD `color` text;
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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.
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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++;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ export interface Room extends SyncEntity {
|
|||
name: string;
|
||||
groupType: RoomGroupType;
|
||||
icon: string;
|
||||
color?: string | null;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue