Participants, context menus, budget rename, file types, UI polish

- Assignment participants: toggle who joins each activity
  - Chips with hover-to-remove (strikethrough effect)
  - Add button with dropdown for available members
  - Avatars in day plan sidebar
  - Side-by-side with reservation in place inspector
- Right-click context menus for places, notes in day plan + places list
- Budget categories can now be renamed (pencil icon inline edit)
- Admin: configurable allowed file types (stored in app_settings)
- File manager shows allowed types dynamically
- Hotel picker: select place + save button (no auto-close)
- Edit pencil opens full hotel popup with all options
- Place inspector: opening hours + files side by side on desktop
- Address clamped to 2 lines, coordinates hidden on mobile
- Category shows icon only on mobile
- Rating hidden on mobile in place inspector
- Time validation: "10" becomes "10:00"
- Climate weather: full hourly data from archive API
- CustomSelect: grouped headers support (isHeader)
- Various responsive fixes
This commit is contained in:
Maurice
2026-03-24 23:25:02 +01:00
parent e1cd9655fb
commit 785f0a7684
16 changed files with 480 additions and 102 deletions
+38
View File
@@ -43,6 +43,10 @@ export default function AdminPage() {
// Registration toggle
const [allowRegistration, setAllowRegistration] = useState(true)
// File types
const [allowedFileTypes, setAllowedFileTypes] = useState('jpg,jpeg,png,gif,webp,heic,pdf,doc,docx,xls,xlsx,txt,csv')
const [savingFileTypes, setSavingFileTypes] = useState(false)
// API Keys
const [mapsKey, setMapsKey] = useState('')
const [weatherKey, setWeatherKey] = useState('')
@@ -91,6 +95,7 @@ export default function AdminPage() {
try {
const config = await authApi.getAppConfig()
setAllowRegistration(config.allow_registration)
if (config.allowed_file_types) setAllowedFileTypes(config.allowed_file_types)
} catch (err) {
// ignore
}
@@ -493,6 +498,39 @@ export default function AdminPage() {
</div>
</div>
{/* Allowed File Types */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="px-6 py-4 border-b border-slate-100">
<h2 className="font-semibold text-slate-900">{t('admin.fileTypes')}</h2>
<p className="text-xs text-slate-400 mt-1">{t('admin.fileTypesHint')}</p>
</div>
<div className="p-6">
<input
type="text"
value={allowedFileTypes}
onChange={e => setAllowedFileTypes(e.target.value)}
placeholder="jpg,png,pdf,doc,docx,xls,xlsx,txt,csv"
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent"
/>
<p className="text-xs text-slate-400 mt-2">{t('admin.fileTypesFormat')}</p>
<button
onClick={async () => {
setSavingFileTypes(true)
try {
await authApi.updateAppSettings({ allowed_file_types: allowedFileTypes })
toast.success(t('admin.fileTypesSaved'))
} catch { toast.error(t('common.error')) }
finally { setSavingFileTypes(false) }
}}
disabled={savingFileTypes}
className="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg text-sm hover:bg-slate-700 disabled:bg-slate-400 mt-3"
>
{savingFileTypes ? <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> : <Save className="w-4 h-4" />}
{t('common.save')}
</button>
</div>
</div>
{/* API Keys */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="px-6 py-4 border-b border-slate-100">
+26 -1
View File
@@ -21,7 +21,7 @@ import { useToast } from '../components/shared/Toast'
import { Map, X, PanelLeftClose, PanelLeftOpen, PanelRightClose, PanelRightOpen } from 'lucide-react'
import { useTranslation } from '../i18n'
import { joinTrip, leaveTrip, addListener, removeListener } from '../api/websocket'
import { addonsApi, accommodationsApi } from '../api/client'
import { addonsApi, accommodationsApi, authApi, tripsApi, assignmentsApi } from '../api/client'
const MIN_SIDEBAR = 200
const MAX_SIDEBAR = 520
@@ -37,6 +37,8 @@ export default function TripPlannerPage() {
const [enabledAddons, setEnabledAddons] = useState({ packing: true, budget: true, documents: true })
const [tripAccommodations, setTripAccommodations] = useState([])
const [allowedFileTypes, setAllowedFileTypes] = useState(null)
const [tripMembers, setTripMembers] = useState([])
const loadAccommodations = useCallback(() => {
if (tripId) accommodationsApi.list(tripId).then(d => setTripAccommodations(d.accommodations || [])).catch(() => {})
@@ -48,6 +50,9 @@ export default function TripPlannerPage() {
data.addons.forEach(a => { map[a.id] = true })
setEnabledAddons({ packing: !!map.packing, budget: !!map.budget, documents: !!map.documents })
}).catch(() => {})
authApi.getAppConfig().then(config => {
if (config.allowed_file_types) setAllowedFileTypes(config.allowed_file_types)
}).catch(() => {})
}, [])
const TRIP_TABS = [
@@ -104,6 +109,11 @@ export default function TripPlannerPage() {
tripStore.loadTrip(tripId).catch(() => { toast.error(t('trip.toast.loadError')); navigate('/dashboard') })
tripStore.loadFiles(tripId)
loadAccommodations()
tripsApi.getMembers(tripId).then(d => {
// Combine owner + members into one list
const all = [d.owner, ...(d.members || [])].filter(Boolean)
setTripMembers(all)
}).catch(() => {})
}
}, [tripId])
@@ -582,6 +592,20 @@ export default function TripPlannerPage() {
onRemoveAssignment={handleRemoveAssignment}
files={files}
onFileUpload={(fd) => tripStore.addFile(tripId, fd)}
tripMembers={tripMembers}
onSetParticipants={async (assignmentId, dayId, userIds) => {
try {
const data = await assignmentsApi.setParticipants(tripId, assignmentId, userIds)
useTripStore.setState(state => ({
assignments: {
...state.assignments,
[String(dayId)]: (state.assignments[String(dayId)] || []).map(a =>
a.id === assignmentId ? { ...a, participants: data.participants } : a
),
}
}))
} catch {}
}}
/>
)}
@@ -645,6 +669,7 @@ export default function TripPlannerPage() {
places={places}
reservations={reservations}
tripId={tripId}
allowedFileTypes={allowedFileTypes}
/>
</div>
)}