feat: assign trip members to packing list categories — closes #71
This commit is contained in:
@@ -111,6 +111,8 @@ export const packingApi = {
|
|||||||
update: (tripId: number | string, id: number, data: Record<string, unknown>) => apiClient.put(`/trips/${tripId}/packing/${id}`, data).then(r => r.data),
|
update: (tripId: number | string, id: number, data: Record<string, unknown>) => apiClient.put(`/trips/${tripId}/packing/${id}`, data).then(r => r.data),
|
||||||
delete: (tripId: number | string, id: number) => apiClient.delete(`/trips/${tripId}/packing/${id}`).then(r => r.data),
|
delete: (tripId: number | string, id: number) => apiClient.delete(`/trips/${tripId}/packing/${id}`).then(r => r.data),
|
||||||
reorder: (tripId: number | string, orderedIds: number[]) => apiClient.put(`/trips/${tripId}/packing/reorder`, { orderedIds }).then(r => r.data),
|
reorder: (tripId: number | string, orderedIds: number[]) => apiClient.put(`/trips/${tripId}/packing/reorder`, { orderedIds }).then(r => r.data),
|
||||||
|
getCategoryAssignees: (tripId: number | string) => apiClient.get(`/trips/${tripId}/packing/category-assignees`).then(r => r.data),
|
||||||
|
setCategoryAssignees: (tripId: number | string, categoryName: string, userIds: number[]) => apiClient.put(`/trips/${tripId}/packing/category-assignees/${encodeURIComponent(categoryName)}`, { user_ids: userIds }).then(r => r.data),
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tagsApi = {
|
export const tagsApi = {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useState, useMemo, useRef } from 'react'
|
import { useState, useMemo, useRef, useEffect } from 'react'
|
||||||
import { useTripStore } from '../../store/tripStore'
|
import { useTripStore } from '../../store/tripStore'
|
||||||
import { useToast } from '../shared/Toast'
|
import { useToast } from '../shared/Toast'
|
||||||
import { useTranslation } from '../../i18n'
|
import { useTranslation } from '../../i18n'
|
||||||
|
import { packingApi, tripsApi } from '../../api/client'
|
||||||
import {
|
import {
|
||||||
CheckSquare, Square, Trash2, Plus, ChevronDown, ChevronRight,
|
CheckSquare, Square, Trash2, Plus, ChevronDown, ChevronRight,
|
||||||
Sparkles, X, Pencil, Check, MoreHorizontal, CheckCheck, RotateCcw, Luggage,
|
Sparkles, X, Pencil, Check, MoreHorizontal, CheckCheck, RotateCcw, Luggage, UserPlus,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import type { PackingItem } from '../../types'
|
import type { PackingItem } from '../../types'
|
||||||
|
|
||||||
@@ -186,6 +187,19 @@ function ArtikelZeile({ item, tripId, categories, onCategoryChange }: ArtikelZei
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Kategorie-Gruppe ───────────────────────────────────────────────────────
|
// ── Kategorie-Gruppe ───────────────────────────────────────────────────────
|
||||||
|
interface TripMember {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
avatar?: string | null
|
||||||
|
avatar_url?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CategoryAssignee {
|
||||||
|
user_id: number
|
||||||
|
username: string
|
||||||
|
avatar?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
interface KategorieGruppeProps {
|
interface KategorieGruppeProps {
|
||||||
kategorie: string
|
kategorie: string
|
||||||
items: PackingItem[]
|
items: PackingItem[]
|
||||||
@@ -193,16 +207,32 @@ interface KategorieGruppeProps {
|
|||||||
allCategories: string[]
|
allCategories: string[]
|
||||||
onRename: (oldName: string, newName: string) => Promise<void>
|
onRename: (oldName: string, newName: string) => Promise<void>
|
||||||
onDeleteAll: (items: PackingItem[]) => Promise<void>
|
onDeleteAll: (items: PackingItem[]) => Promise<void>
|
||||||
|
assignees: CategoryAssignee[]
|
||||||
|
tripMembers: TripMember[]
|
||||||
|
onSetAssignees: (category: string, userIds: number[]) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
function KategorieGruppe({ kategorie, items, tripId, allCategories, onRename, onDeleteAll }: KategorieGruppeProps) {
|
function KategorieGruppe({ kategorie, items, tripId, allCategories, onRename, onDeleteAll, assignees, tripMembers, onSetAssignees }: KategorieGruppeProps) {
|
||||||
const [offen, setOffen] = useState(true)
|
const [offen, setOffen] = useState(true)
|
||||||
const [editingName, setEditingName] = useState(false)
|
const [editingName, setEditingName] = useState(false)
|
||||||
const [editKatName, setEditKatName] = useState(kategorie)
|
const [editKatName, setEditKatName] = useState(kategorie)
|
||||||
const [showMenu, setShowMenu] = useState(false)
|
const [showMenu, setShowMenu] = useState(false)
|
||||||
|
const [showAssigneeDropdown, setShowAssigneeDropdown] = useState(false)
|
||||||
|
const assigneeDropdownRef = useRef<HTMLDivElement>(null)
|
||||||
const { togglePackingItem } = useTripStore()
|
const { togglePackingItem } = useTripStore()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showAssigneeDropdown) return
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
if (assigneeDropdownRef.current && !assigneeDropdownRef.current.contains(e.target as Node)) {
|
||||||
|
setShowAssigneeDropdown(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', handleClickOutside)
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||||
|
}, [showAssigneeDropdown])
|
||||||
|
|
||||||
const abgehakt = items.filter(i => i.checked).length
|
const abgehakt = items.filter(i => i.checked).length
|
||||||
const alleAbgehakt = abgehakt === items.length
|
const alleAbgehakt = abgehakt === items.length
|
||||||
const dot = katColor(kategorie, allCategories)
|
const dot = katColor(kategorie, allCategories)
|
||||||
@@ -247,11 +277,98 @@ function KategorieGruppe({ kategorie, items, tripId, allCategories, onRename, on
|
|||||||
style={{ flex: 1, fontSize: 12.5, fontWeight: 600, border: 'none', borderBottom: '2px solid var(--text-primary)', outline: 'none', background: 'transparent', fontFamily: 'inherit', color: 'var(--text-primary)', padding: '0 2px' }}
|
style={{ flex: 1, fontSize: 12.5, fontWeight: 600, border: 'none', borderBottom: '2px solid var(--text-primary)', outline: 'none', background: 'transparent', fontFamily: 'inherit', color: 'var(--text-primary)', padding: '0 2px' }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span style={{ fontSize: 12.5, fontWeight: 700, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.04em', flex: 1 }}>
|
<span style={{ fontSize: 12.5, fontWeight: 700, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.04em' }}>
|
||||||
{kategorie}
|
{kategorie}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Assignee chips */}
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 3, flex: 1, minWidth: 0, marginLeft: 4 }}>
|
||||||
|
{assignees.map(a => (
|
||||||
|
<div key={a.user_id} style={{ position: 'relative' }}
|
||||||
|
onClick={e => { e.stopPropagation(); onSetAssignees(kategorie, assignees.filter(x => x.user_id !== a.user_id).map(x => x.user_id)) }}
|
||||||
|
>
|
||||||
|
<div className="assignee-chip"
|
||||||
|
style={{
|
||||||
|
width: 22, height: 22, borderRadius: '50%', flexShrink: 0, cursor: 'pointer',
|
||||||
|
background: `hsl(${a.username.charCodeAt(0) * 37 % 360}, 55%, 55%)`,
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
fontSize: 10, fontWeight: 700, color: 'white', textTransform: 'uppercase',
|
||||||
|
border: '2px solid var(--bg-card)', transition: 'opacity 0.15s',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{a.username[0]}
|
||||||
|
</div>
|
||||||
|
<div className="assignee-tooltip" style={{
|
||||||
|
position: 'absolute', top: '100%', left: '50%', transform: 'translateX(-50%)',
|
||||||
|
marginTop: 6, padding: '3px 8px', borderRadius: 6, zIndex: 60,
|
||||||
|
background: 'var(--text-primary)', color: 'var(--bg-primary)',
|
||||||
|
fontSize: 10, fontWeight: 600, whiteSpace: 'nowrap',
|
||||||
|
pointerEvents: 'none', opacity: 0, transition: 'opacity 0.15s',
|
||||||
|
}}>
|
||||||
|
{a.username}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div ref={assigneeDropdownRef} style={{ position: 'relative' }}>
|
||||||
|
<button onClick={e => { e.stopPropagation(); setShowAssigneeDropdown(v => !v) }}
|
||||||
|
style={{
|
||||||
|
width: 20, height: 20, borderRadius: '50%', border: '1.5px dashed var(--border-primary)',
|
||||||
|
background: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
color: 'var(--text-faint)', flexShrink: 0, padding: 0, transition: 'all 0.15s',
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--text-muted)'; e.currentTarget.style.color = 'var(--text-muted)' }}
|
||||||
|
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border-primary)'; e.currentTarget.style.color = 'var(--text-faint)' }}
|
||||||
|
>
|
||||||
|
<UserPlus size={10} />
|
||||||
|
</button>
|
||||||
|
{showAssigneeDropdown && (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute', left: 0, top: '100%', marginTop: 4, zIndex: 50,
|
||||||
|
background: 'var(--bg-card)', border: '1px solid var(--border-primary)', borderRadius: 10,
|
||||||
|
boxShadow: '0 4px 16px rgba(0,0,0,0.12)', padding: 4, minWidth: 160,
|
||||||
|
}}>
|
||||||
|
{tripMembers.map(m => {
|
||||||
|
const isAssigned = assignees.some(a => a.user_id === m.id)
|
||||||
|
return (
|
||||||
|
<button key={m.id} onClick={e => {
|
||||||
|
e.stopPropagation()
|
||||||
|
const newIds = isAssigned
|
||||||
|
? assignees.filter(a => a.user_id !== m.id).map(a => a.user_id)
|
||||||
|
: [...assignees.map(a => a.user_id), m.id]
|
||||||
|
onSetAssignees(kategorie, newIds)
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 8, width: '100%',
|
||||||
|
padding: '6px 10px', borderRadius: 8, border: 'none', cursor: 'pointer',
|
||||||
|
background: isAssigned ? 'var(--bg-hover)' : 'transparent',
|
||||||
|
fontFamily: 'inherit', fontSize: 12, color: 'var(--text-primary)',
|
||||||
|
transition: 'background 0.1s',
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => { if (!isAssigned) e.currentTarget.style.background = 'var(--bg-tertiary)' }}
|
||||||
|
onMouseLeave={e => { if (!isAssigned) e.currentTarget.style.background = 'transparent' }}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
width: 20, height: 20, borderRadius: '50%', flexShrink: 0,
|
||||||
|
background: `hsl(${m.username.charCodeAt(0) * 37 % 360}, 55%, 55%)`,
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
fontSize: 10, fontWeight: 700, color: 'white', textTransform: 'uppercase',
|
||||||
|
}}>
|
||||||
|
{m.username[0]}
|
||||||
|
</div>
|
||||||
|
<span style={{ flex: 1 }}>{m.username}</span>
|
||||||
|
{isAssigned && <Check size={12} style={{ color: 'var(--text-muted)' }} />}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{tripMembers.length === 0 && (
|
||||||
|
<div style={{ padding: '8px 10px', fontSize: 11, color: 'var(--text-faint)' }}>{t('packing.noMembers')}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<span style={{
|
<span style={{
|
||||||
fontSize: 11, fontWeight: 600, padding: '1px 8px', borderRadius: 99,
|
fontSize: 11, fontWeight: 600, padding: '1px 8px', borderRadius: 99,
|
||||||
background: alleAbgehakt ? 'rgba(22,163,74,0.12)' : 'var(--bg-tertiary)',
|
background: alleAbgehakt ? 'rgba(22,163,74,0.12)' : 'var(--bg-tertiary)',
|
||||||
@@ -329,6 +446,31 @@ export default function PackingListPanel({ tripId, items }: PackingListPanelProp
|
|||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
// Trip members & category assignees
|
||||||
|
const [tripMembers, setTripMembers] = useState<TripMember[]>([])
|
||||||
|
const [categoryAssignees, setCategoryAssignees] = useState<Record<string, CategoryAssignee[]>>({})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
tripsApi.getMembers(tripId).then(data => {
|
||||||
|
const all: TripMember[] = []
|
||||||
|
if (data.owner) all.push({ id: data.owner.id, username: data.owner.username, avatar: data.owner.avatar_url })
|
||||||
|
if (data.members) all.push(...data.members.map((m: any) => ({ id: m.id, username: m.username, avatar: m.avatar_url })))
|
||||||
|
setTripMembers(all)
|
||||||
|
}).catch(() => {})
|
||||||
|
packingApi.getCategoryAssignees(tripId).then(data => {
|
||||||
|
setCategoryAssignees(data.assignees || {})
|
||||||
|
}).catch(() => {})
|
||||||
|
}, [tripId])
|
||||||
|
|
||||||
|
const handleSetAssignees = async (category: string, userIds: number[]) => {
|
||||||
|
try {
|
||||||
|
const data = await packingApi.setCategoryAssignees(tripId, category, userIds)
|
||||||
|
setCategoryAssignees(prev => ({ ...prev, [category]: data.assignees || [] }))
|
||||||
|
} catch {
|
||||||
|
toast.error(t('packing.toast.saveError'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const allCategories = useMemo(() => {
|
const allCategories = useMemo(() => {
|
||||||
const cats = new Set(items.map(i => i.category || t('packing.defaultCategory')))
|
const cats = new Set(items.map(i => i.category || t('packing.defaultCategory')))
|
||||||
return Array.from(cats).sort()
|
return Array.from(cats).sort()
|
||||||
@@ -546,11 +688,18 @@ export default function PackingListPanel({ tripId, items }: PackingListPanelProp
|
|||||||
allCategories={allCategories}
|
allCategories={allCategories}
|
||||||
onRename={handleRenameCategory}
|
onRename={handleRenameCategory}
|
||||||
onDeleteAll={handleDeleteCategory}
|
onDeleteAll={handleDeleteCategory}
|
||||||
|
assignees={categoryAssignees[kat] || []}
|
||||||
|
tripMembers={tripMembers}
|
||||||
|
onSetAssignees={handleSetAssignees}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<style>{`
|
||||||
|
.assignee-chip:hover + .assignee-tooltip { opacity: 1 !important; }
|
||||||
|
.assignee-chip:hover { opacity: 0.7; }
|
||||||
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -813,6 +813,8 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'packing.menuCheckAll': 'Alle abhaken',
|
'packing.menuCheckAll': 'Alle abhaken',
|
||||||
'packing.menuUncheckAll': 'Alle Haken entfernen',
|
'packing.menuUncheckAll': 'Alle Haken entfernen',
|
||||||
'packing.menuDeleteCat': 'Kategorie löschen',
|
'packing.menuDeleteCat': 'Kategorie löschen',
|
||||||
|
'packing.assignUser': 'Benutzer zuweisen',
|
||||||
|
'packing.noMembers': 'Keine Mitglieder',
|
||||||
'packing.changeCategory': 'Kategorie ändern',
|
'packing.changeCategory': 'Kategorie ändern',
|
||||||
'packing.confirm.clearChecked': 'Möchtest du {count} abgehakte Gegenstände wirklich entfernen?',
|
'packing.confirm.clearChecked': 'Möchtest du {count} abgehakte Gegenstände wirklich entfernen?',
|
||||||
'packing.confirm.deleteCat': 'Möchtest du die Kategorie "{name}" mit {count} Gegenständen wirklich löschen?',
|
'packing.confirm.deleteCat': 'Möchtest du die Kategorie "{name}" mit {count} Gegenständen wirklich löschen?',
|
||||||
|
|||||||
@@ -813,6 +813,8 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
|||||||
'packing.menuCheckAll': 'Check All',
|
'packing.menuCheckAll': 'Check All',
|
||||||
'packing.menuUncheckAll': 'Uncheck All',
|
'packing.menuUncheckAll': 'Uncheck All',
|
||||||
'packing.menuDeleteCat': 'Delete Category',
|
'packing.menuDeleteCat': 'Delete Category',
|
||||||
|
'packing.assignUser': 'Assign user',
|
||||||
|
'packing.noMembers': 'No trip members',
|
||||||
'packing.changeCategory': 'Change Category',
|
'packing.changeCategory': 'Change Category',
|
||||||
'packing.confirm.clearChecked': 'Are you sure you want to remove {count} checked items?',
|
'packing.confirm.clearChecked': 'Are you sure you want to remove {count} checked items?',
|
||||||
'packing.confirm.deleteCat': 'Are you sure you want to delete the category "{name}" with {count} items?',
|
'packing.confirm.deleteCat': 'Are you sure you want to delete the category "{name}" with {count} items?',
|
||||||
|
|||||||
@@ -220,6 +220,15 @@ function runMigrations(db: Database.Database): void {
|
|||||||
try { db.exec('ALTER TABLE users ADD COLUMN mfa_enabled INTEGER DEFAULT 0'); } catch {}
|
try { db.exec('ALTER TABLE users ADD COLUMN mfa_enabled INTEGER DEFAULT 0'); } catch {}
|
||||||
try { db.exec('ALTER TABLE users ADD COLUMN mfa_secret TEXT'); } catch {}
|
try { db.exec('ALTER TABLE users ADD COLUMN mfa_secret TEXT'); } catch {}
|
||||||
},
|
},
|
||||||
|
() => {
|
||||||
|
db.exec(`CREATE TABLE IF NOT EXISTS packing_category_assignees (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||||
|
category_name TEXT NOT NULL,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE(trip_id, category_name, user_id)
|
||||||
|
)`);
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (currentVersion < migrations.length) {
|
if (currentVersion < migrations.length) {
|
||||||
|
|||||||
@@ -91,6 +91,58 @@ router.delete('/:id', authenticate, (req: Request, res: Response) => {
|
|||||||
broadcast(tripId, 'packing:deleted', { itemId: Number(id) }, req.headers['x-socket-id'] as string);
|
broadcast(tripId, 'packing:deleted', { itemId: Number(id) }, req.headers['x-socket-id'] as string);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Category assignees ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
router.get('/category-assignees', authenticate, (req: Request, res: Response) => {
|
||||||
|
const authReq = req as AuthRequest;
|
||||||
|
const { tripId } = req.params;
|
||||||
|
const trip = verifyTripOwnership(tripId, authReq.user.id);
|
||||||
|
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
||||||
|
|
||||||
|
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: row.avatar });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ assignees });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/category-assignees/:categoryName', authenticate, (req: Request, res: Response) => {
|
||||||
|
const authReq = req as AuthRequest;
|
||||||
|
const { tripId, categoryName } = req.params;
|
||||||
|
const { user_ids } = req.body;
|
||||||
|
|
||||||
|
const trip = verifyTripOwnership(tripId, authReq.user.id);
|
||||||
|
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
||||||
|
|
||||||
|
const cat = decodeURIComponent(categoryName);
|
||||||
|
db.prepare('DELETE FROM packing_category_assignees WHERE trip_id = ? AND category_name = ?').run(tripId, cat);
|
||||||
|
|
||||||
|
if (Array.isArray(user_ids) && user_ids.length > 0) {
|
||||||
|
const insert = db.prepare('INSERT OR IGNORE INTO packing_category_assignees (trip_id, category_name, user_id) VALUES (?, ?, ?)');
|
||||||
|
for (const uid of user_ids) insert.run(tripId, cat, uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = 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, cat);
|
||||||
|
|
||||||
|
res.json({ assignees: rows });
|
||||||
|
broadcast(tripId, 'packing:assignees', { category: cat, assignees: rows }, req.headers['x-socket-id'] as string);
|
||||||
|
});
|
||||||
|
|
||||||
router.put('/reorder', authenticate, (req: Request, res: Response) => {
|
router.put('/reorder', authenticate, (req: Request, res: Response) => {
|
||||||
const authReq = req as AuthRequest;
|
const authReq = req as AuthRequest;
|
||||||
const { tripId } = req.params;
|
const { tripId } = req.params;
|
||||||
|
|||||||
Reference in New Issue
Block a user