Spaces:
Sleeping
Sleeping
File size: 14,628 Bytes
57a889c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 | import { db } from '../db/database';
import { avatarUrl } from './authService';
const BAG_COLORS = ['#6366f1', '#ec4899', '#f97316', '#10b981', '#06b6d4', '#8b5cf6', '#ef4444', '#f59e0b', '#3b82f6', '#84cc16', '#d946ef', '#14b8a6', '#f43f5e', '#a855f7', '#eab308', '#64748b'];
export { verifyTripAccess } from './tripAccess';
// ββ Items ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export function listItems(tripId: string | number) {
return db.prepare(
'SELECT * FROM packing_items WHERE trip_id = ? ORDER BY sort_order ASC, created_at ASC'
).all(tripId);
}
export function createItem(tripId: string | number, data: { name: string; category?: string; checked?: boolean; quantity?: number }) {
const maxOrder = db.prepare('SELECT MAX(sort_order) as max FROM packing_items WHERE trip_id = ?').get(tripId) as { max: number | null };
const sortOrder = (maxOrder.max !== null ? maxOrder.max : -1) + 1;
const qty = Math.max(1, Math.min(999, Number(data.quantity) || 1));
const result = db.prepare(
'INSERT INTO packing_items (trip_id, name, checked, category, sort_order, quantity) VALUES (?, ?, ?, ?, ?, ?)'
).run(tripId, data.name, data.checked ? 1 : 0, data.category || 'Allgemein', sortOrder, qty);
return db.prepare('SELECT * FROM packing_items WHERE id = ?').get(result.lastInsertRowid);
}
export function updateItem(
tripId: string | number,
id: string | number,
data: { name?: string; checked?: number; category?: string; weight_grams?: number | null; bag_id?: number | null; quantity?: number },
bodyKeys: string[]
) {
const item = db.prepare('SELECT * FROM packing_items WHERE id = ? AND trip_id = ?').get(id, tripId);
if (!item) return null;
db.prepare(`
UPDATE packing_items SET
name = COALESCE(?, name),
checked = CASE WHEN ? IS NOT NULL THEN ? ELSE checked END,
category = COALESCE(?, category),
weight_grams = CASE WHEN ? THEN ? ELSE weight_grams END,
bag_id = CASE WHEN ? THEN ? ELSE bag_id END,
quantity = CASE WHEN ? THEN ? ELSE quantity END
WHERE id = ?
`).run(
data.name || null,
data.checked !== undefined ? 1 : null,
data.checked ? 1 : 0,
data.category || null,
bodyKeys.includes('weight_grams') ? 1 : 0,
data.weight_grams ?? null,
bodyKeys.includes('bag_id') ? 1 : 0,
data.bag_id ?? null,
bodyKeys.includes('quantity') ? 1 : 0,
data.quantity ? Math.max(1, Math.min(999, Number(data.quantity))) : 1,
id
);
return db.prepare('SELECT * FROM packing_items WHERE id = ?').get(id);
}
export function deleteItem(tripId: string | number, id: string | number) {
const item = db.prepare('SELECT id FROM packing_items WHERE id = ? AND trip_id = ?').get(id, tripId);
if (!item) return false;
db.prepare('DELETE FROM packing_items WHERE id = ?').run(id);
return true;
}
// ββ Bulk Import ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
interface ImportItem {
name?: string;
checked?: boolean;
category?: string;
weight_grams?: string | number;
bag?: string;
quantity?: number;
}
export function bulkImport(tripId: string | number, items: ImportItem[]) {
const maxOrder = db.prepare('SELECT MAX(sort_order) as max FROM packing_items WHERE trip_id = ?').get(tripId) as { max: number | null };
let sortOrder = (maxOrder.max !== null ? maxOrder.max : -1) + 1;
const stmt = db.prepare('INSERT INTO packing_items (trip_id, name, checked, category, weight_grams, bag_id, sort_order, quantity) VALUES (?, ?, ?, ?, ?, ?, ?, ?)');
const created: any[] = [];
const insertAll = db.transaction(() => {
for (const item of items) {
if (!item.name?.trim()) continue;
const checked = item.checked ? 1 : 0;
const weight = item.weight_grams ? parseInt(String(item.weight_grams)) || null : null;
// Resolve bag by name if provided
let bagId = null;
if (item.bag?.trim()) {
const bagName = item.bag.trim();
const existing = db.prepare('SELECT id FROM packing_bags WHERE trip_id = ? AND name = ?').get(tripId, bagName) as { id: number } | undefined;
if (existing) {
bagId = existing.id;
} else {
const bagCount = (db.prepare('SELECT COUNT(*) as c FROM packing_bags WHERE trip_id = ?').get(tripId) as { c: number }).c;
const newBag = db.prepare('INSERT INTO packing_bags (trip_id, name, color) VALUES (?, ?, ?)').run(tripId, bagName, BAG_COLORS[bagCount % BAG_COLORS.length]);
bagId = newBag.lastInsertRowid;
}
}
const qty = Math.max(1, Math.min(999, Number(item.quantity) || 1));
const result = stmt.run(tripId, item.name.trim(), checked, item.category?.trim() || 'Other', weight, bagId, sortOrder++, qty);
created.push(db.prepare('SELECT * FROM packing_items WHERE id = ?').get(result.lastInsertRowid));
}
});
insertAll();
return created;
}
// ββ Bags βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export function listBags(tripId: string | number) {
const bags = db.prepare('SELECT * FROM packing_bags WHERE trip_id = ? ORDER BY sort_order, id').all(tripId) as any[];
const members = db.prepare(`
SELECT bm.bag_id, bm.user_id, u.username, u.avatar
FROM packing_bag_members bm
JOIN users u ON bm.user_id = u.id
JOIN packing_bags b ON bm.bag_id = b.id
WHERE b.trip_id = ?
`).all(tripId) as { bag_id: number; user_id: number; username: string; avatar: string | null }[];
const membersByBag = new Map<number, typeof members>();
for (const m of members) {
if (!membersByBag.has(m.bag_id)) membersByBag.set(m.bag_id, []);
membersByBag.get(m.bag_id)!.push(m);
}
return bags.map(b => ({
...b,
members: (membersByBag.get(b.id) || []).map(m => ({ ...m, avatar: avatarUrl(m) })),
}));
}
export function setBagMembers(tripId: string | number, bagId: string | number, userIds: number[]) {
const bag = db.prepare('SELECT * FROM packing_bags WHERE id = ? AND trip_id = ?').get(bagId, tripId);
if (!bag) return null;
db.prepare('DELETE FROM packing_bag_members WHERE bag_id = ?').run(bagId);
const ins = db.prepare('INSERT OR IGNORE INTO packing_bag_members (bag_id, user_id) VALUES (?, ?)');
for (const uid of userIds) ins.run(bagId, uid);
const rows = db.prepare(`
SELECT bm.user_id, u.username, u.avatar
FROM packing_bag_members bm JOIN users u ON bm.user_id = u.id
WHERE bm.bag_id = ?
`).all(bagId) as { user_id: number; username: string; avatar: string | null }[];
return rows.map(m => ({ ...m, avatar: avatarUrl(m) }));
}
export function createBag(tripId: string | number, data: { name: string; color?: string }) {
const maxOrder = db.prepare('SELECT MAX(sort_order) as max FROM packing_bags WHERE trip_id = ?').get(tripId) as { max: number | null };
const result = db.prepare('INSERT INTO packing_bags (trip_id, name, color, sort_order) VALUES (?, ?, ?, ?)').run(
tripId, data.name.trim(), data.color || '#6366f1', (maxOrder.max ?? -1) + 1
);
return db.prepare('SELECT * FROM packing_bags WHERE id = ?').get(result.lastInsertRowid);
}
export function updateBag(
tripId: string | number,
bagId: string | number,
data: { name?: string; color?: string; weight_limit_grams?: number | null; user_id?: number | null },
bodyKeys?: string[]
) {
const bag = db.prepare('SELECT * FROM packing_bags WHERE id = ? AND trip_id = ?').get(bagId, tripId);
if (!bag) return null;
db.prepare(`UPDATE packing_bags SET
name = COALESCE(?, name),
color = COALESCE(?, color),
weight_limit_grams = ?,
user_id = CASE WHEN ? THEN ? ELSE user_id END
WHERE id = ?`).run(
data.name?.trim() || null,
data.color || null,
data.weight_limit_grams ?? (bag as any).weight_limit_grams ?? null,
bodyKeys?.includes('user_id') ? 1 : 0,
data.user_id ?? null,
bagId
);
return db.prepare('SELECT b.*, u.username as assigned_username FROM packing_bags b LEFT JOIN users u ON b.user_id = u.id WHERE b.id = ?').get(bagId);
}
export function deleteBag(tripId: string | number, bagId: string | number) {
const bag = db.prepare('SELECT * FROM packing_bags WHERE id = ? AND trip_id = ?').get(bagId, tripId);
if (!bag) return false;
db.prepare('DELETE FROM packing_bags WHERE id = ?').run(bagId);
return true;
}
// ββ List Templates βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
/**
* Read-only template list for trip members (name + item count), so non-admins
* can pick a template to apply. Management (create/edit/delete) stays admin-only
* under /api/admin/packing-templates.
*/
export function listTemplates() {
return db.prepare(`
SELECT pt.id, pt.name,
(SELECT COUNT(*) FROM packing_template_items ti JOIN packing_template_categories tc ON ti.category_id = tc.id WHERE tc.template_id = pt.id) as item_count
FROM packing_templates pt
ORDER BY pt.created_at DESC
`).all() as { id: number; name: string; item_count: number }[];
}
// ββ Apply Template βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export function applyTemplate(tripId: string | number, templateId: string | number) {
const templateItems = db.prepare(`
SELECT ti.name, tc.name as category
FROM packing_template_items ti
JOIN packing_template_categories tc ON ti.category_id = tc.id
WHERE tc.template_id = ?
ORDER BY tc.sort_order, ti.sort_order
`).all(templateId) as { name: string; category: string }[];
if (templateItems.length === 0) return null;
const maxOrder = db.prepare('SELECT MAX(sort_order) as max FROM packing_items WHERE trip_id = ?').get(tripId) as { max: number | null };
let sortOrder = (maxOrder.max !== null ? maxOrder.max : -1) + 1;
const insert = db.prepare('INSERT INTO packing_items (trip_id, name, checked, category, sort_order) VALUES (?, ?, 0, ?, ?)');
const added: any[] = [];
for (const ti of templateItems) {
const result = insert.run(tripId, ti.name, ti.category, sortOrder++);
const item = db.prepare('SELECT * FROM packing_items WHERE id = ?').get(result.lastInsertRowid);
added.push(item);
}
return added;
}
// ββ Save as Template ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export function saveAsTemplate(tripId: string | number, userId: number, templateName: string) {
const items = db.prepare(
'SELECT name, category FROM packing_items WHERE trip_id = ? ORDER BY sort_order ASC'
).all(tripId) as { name: string; category: string }[];
if (items.length === 0) return null;
const result = db.prepare('INSERT INTO packing_templates (name, created_by) VALUES (?, ?)').run(templateName, userId);
const templateId = result.lastInsertRowid;
const categories = [...new Set(items.map(i => i.category || 'Other'))];
const catIdMap = new Map<string, number | bigint>();
for (let i = 0; i < categories.length; i++) {
const catResult = db.prepare('INSERT INTO packing_template_categories (template_id, name, sort_order) VALUES (?, ?, ?)').run(templateId, categories[i], i);
catIdMap.set(categories[i], catResult.lastInsertRowid);
}
const itemsByCategory = new Map<string, number>();
for (const item of items) {
const catId = catIdMap.get(item.category || 'Other')!;
const order = itemsByCategory.get(item.category || 'Other') || 0;
db.prepare('INSERT INTO packing_template_items (category_id, name, sort_order) VALUES (?, ?, ?)').run(catId, item.name, order);
itemsByCategory.set(item.category || 'Other', order + 1);
}
return { id: Number(templateId), name: templateName, categoryCount: categories.length, itemCount: items.length };
}
// ββ Category Assignees βββββββββββββββββββββββββββββββββββββββββββββββββββββ
export function getCategoryAssignees(tripId: string | number) {
const rows = db.prepare(`
SELECT pca.category_name, pca.user_id, u.username, u.avatar
FROM packing_category_assignees pca
JOIN users u ON pca.user_id = u.id
WHERE pca.trip_id = ?
`).all(tripId);
// Group by category
const assignees: Record<string, { user_id: number; username: string; avatar: string | null }[]> = {};
for (const row of rows as any[]) {
if (!assignees[row.category_name]) assignees[row.category_name] = [];
assignees[row.category_name].push({ user_id: row.user_id, username: row.username, avatar: avatarUrl(row) });
}
return assignees;
}
export function updateCategoryAssignees(tripId: string | number, categoryName: string, userIds: number[] | undefined) {
db.prepare('DELETE FROM packing_category_assignees WHERE trip_id = ? AND category_name = ?').run(tripId, categoryName);
if (Array.isArray(userIds) && userIds.length > 0) {
const insert = db.prepare('INSERT OR IGNORE INTO packing_category_assignees (trip_id, category_name, user_id) VALUES (?, ?, ?)');
for (const uid of userIds) insert.run(tripId, categoryName, uid);
}
const updated = db.prepare(`
SELECT pca.user_id, u.username, u.avatar
FROM packing_category_assignees pca
JOIN users u ON pca.user_id = u.id
WHERE pca.trip_id = ? AND pca.category_name = ?
`).all(tripId, categoryName) as { user_id: number; username: string; avatar: string | null }[];
return updated.map(m => ({ ...m, avatar: avatarUrl(m) }));
}
// ββ Reorder ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export function reorderItems(tripId: string | number, orderedIds: number[]) {
const update = db.prepare('UPDATE packing_items SET sort_order = ? WHERE id = ? AND trip_id = ?');
const updateMany = db.transaction((ids: number[]) => {
ids.forEach((id, index) => {
update.run(index, id, tripId);
});
});
updateMany(orderedIds);
}
|