mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 21:31:46 +00:00
b20db1428d
- Budget table column alignment: the NAME data cell had `display: flex` directly on the <td>, which pulled it out of the table-layout and desynced the column widths between data rows and the AddItemRow. Moved the flex wrapper into a <div> inside the cell. Closes #759 - Packing list: template-apply and bulk-import handlers called `window.location.reload()` to refresh the list, which re-rendered the whole trip loading screen. Both flows now merge the returned items into the trip store instead. Closes #760 - Journey timeline: move-up / move-down arrows were rendered on skeleton suggestions — skeletons are places from the linked trip and don't participate in sort order. Skip canReorder when entry.type === 'skeleton'. Closes #763 - Journey public view: the synthetic `[Trip Photos]` and `Gallery` entries produced by syncTripPhotos were leaking into the public timeline and map. The owner view already strips these in JourneyDetailPage — apply the same filter on JourneyPublicPage. Gallery photos still come from every entry so a shared gallery keeps showing the trip-synced photos. Closes #764 - Journey thumbnails: public gallery grid was loading the original asset for every tile. `photoUrl()` now takes an optional kind and the grid requests `thumbnail`; the lightbox still opens the original. Synology thumbnail default bumped from `sm` (240px) to `m` (320px) because `sm` looked pixelated on retina. Closes #761
104 lines
3.7 KiB
TypeScript
104 lines
3.7 KiB
TypeScript
import React, { useEffect, useRef, useState } from 'react'
|
|
import { Package } from 'lucide-react'
|
|
import { adminApi, packingApi } from '../../api/client'
|
|
import { useTripStore } from '../../store/tripStore'
|
|
import { useToast } from '../shared/Toast'
|
|
import { useTranslation } from '../../i18n'
|
|
|
|
interface Template {
|
|
id: number
|
|
name: string
|
|
item_count: number
|
|
}
|
|
|
|
interface ApplyTemplateButtonProps {
|
|
tripId: number
|
|
style: React.CSSProperties
|
|
className?: string
|
|
}
|
|
|
|
// Dropdown-Button um ein Packing-Template auf den aktuellen Trip anzuwenden.
|
|
// Rendert nichts wenn keine Templates existieren.
|
|
export default function ApplyTemplateButton({ tripId, style, className }: ApplyTemplateButtonProps): React.ReactElement | null {
|
|
const [templates, setTemplates] = useState<Template[]>([])
|
|
const [open, setOpen] = useState(false)
|
|
const [applying, setApplying] = useState(false)
|
|
const dropRef = useRef<HTMLDivElement>(null)
|
|
const toast = useToast()
|
|
const { t } = useTranslation()
|
|
|
|
useEffect(() => {
|
|
adminApi.packingTemplates().then(d => setTemplates(d.templates || [])).catch(() => {})
|
|
}, [tripId])
|
|
|
|
useEffect(() => {
|
|
if (!open) return
|
|
const handler = (e: MouseEvent) => {
|
|
if (dropRef.current && !dropRef.current.contains(e.target as Node)) setOpen(false)
|
|
}
|
|
document.addEventListener('mousedown', handler)
|
|
return () => document.removeEventListener('mousedown', handler)
|
|
}, [open])
|
|
|
|
const handleApply = async (templateId: number) => {
|
|
setApplying(true)
|
|
try {
|
|
const data = await packingApi.applyTemplate(tripId, templateId)
|
|
useTripStore.setState(s => ({ packingItems: [...s.packingItems, ...(data.items || [])] }))
|
|
toast.success(t('packing.templateApplied', { count: data.count }))
|
|
setOpen(false)
|
|
} catch {
|
|
toast.error(t('packing.templateError'))
|
|
} finally {
|
|
setApplying(false)
|
|
}
|
|
}
|
|
|
|
if (templates.length === 0) return null
|
|
|
|
return (
|
|
<div ref={dropRef} style={{ position: 'relative' }}>
|
|
<button
|
|
onClick={() => setOpen(v => !v)}
|
|
disabled={applying}
|
|
className={className ?? 'hover:opacity-[0.88]'}
|
|
style={style}
|
|
>
|
|
<Package size={14} strokeWidth={2.5} />
|
|
<span className="hidden sm:inline">{t('packing.applyTemplate')}</span>
|
|
</button>
|
|
{open && (
|
|
<div
|
|
className="trek-menu-enter"
|
|
style={{
|
|
position: 'absolute', right: 0, top: '100%', marginTop: 6, 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: 220,
|
|
transformOrigin: 'top right',
|
|
}}
|
|
>
|
|
{templates.map(tmpl => (
|
|
<button key={tmpl.id} onClick={() => handleApply(tmpl.id)}
|
|
style={{
|
|
display: 'flex', alignItems: 'center', gap: 8, width: '100%',
|
|
padding: '8px 12px', borderRadius: 8, border: 'none', cursor: 'pointer',
|
|
background: 'transparent', fontFamily: 'inherit', fontSize: 12, color: 'var(--text-primary)',
|
|
}}
|
|
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-tertiary)'}
|
|
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
|
|
>
|
|
<Package size={13} style={{ color: 'var(--text-faint)' }} />
|
|
<div style={{ flex: 1, textAlign: 'left' }}>
|
|
<div style={{ fontWeight: 600 }}>{tmpl.name}</div>
|
|
<div style={{ fontSize: 10, color: 'var(--text-faint)' }}>
|
|
{tmpl.item_count} {t('admin.packingTemplates.items')}
|
|
</div>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|