feat: add client-side permission gating to all write-action UIs

Gate all mutating UI elements with useCanDo() permission checks:
- BudgetPanel (budget_edit), PackingListPanel (packing_edit)
- DayPlanSidebar, DayDetailPanel (day_edit)
- ReservationsPanel, ReservationModal (reservation_edit)
- CollabNotes, CollabPolls, CollabChat (collab_edit)
- FileManager (file_edit, file_delete, file_upload)
- PlaceFormModal, PlaceInspector, PlacesSidebar (place_edit, file_upload)
- TripFormModal (trip_edit, trip_cover_upload)
- DashboardPage (trip_edit, trip_cover_upload, trip_delete, trip_archive)
- TripMembersModal (member_manage, share_manage)

Also: fix redundant getTripOwnerId queries in trips.ts, remove dead
getTripOwnerId function, fix TripMembersModal grid when share hidden,
fix canRemove logic, guard TripListItem empty actions div.
This commit is contained in:
Gérnyi Márk
2026-03-31 22:06:52 +02:00
parent d74133745a
commit 5f71b85c06
17 changed files with 333 additions and 221 deletions
+1 -1
View File
@@ -410,7 +410,7 @@ function TripListItem({ trip, onEdit, onDelete, onArchive, onClick, t, locale, i
</div>
{/* Actions */}
{(!!trip.is_owner || isAdmin) && (
{(onEdit || onArchive || onDelete) && (
<div style={{ display: 'flex', gap: 4, flexShrink: 0 }} onClick={e => e.stopPropagation()}>
{onEdit && <CardAction onClick={() => onEdit(trip)} icon={<Edit2 size={12} />} label="" />}
{onArchive && <CardAction onClick={() => onArchive(trip.id)} icon={<Archive size={12} />} label="" />}
+5 -2
View File
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react'
import ReactDOM from 'react-dom'
import { useParams, useNavigate } from 'react-router-dom'
import { useTripStore } from '../store/tripStore'
import { useCanDo } from '../store/permissionsStore'
import { useSettingsStore } from '../store/settingsStore'
import { MapView } from '../components/Map/MapView'
import DayPlanSidebar from '../components/Planner/DayPlanSidebar'
@@ -38,6 +39,8 @@ export default function TripPlannerPage(): React.ReactElement | null {
const { settings } = useSettingsStore()
const tripStore = useTripStore()
const { trip, days, places, assignments, packingItems, categories, reservations, budgetItems, files, selectedDayId, isLoading } = tripStore
const can = useCanDo()
const canUploadFiles = can('file_upload', trip)
const [enabledAddons, setEnabledAddons] = useState<Record<string, boolean>>({ packing: true, budget: true, documents: true })
const [tripAccommodations, setTripAccommodations] = useState<Accommodation[]>([])
@@ -584,7 +587,7 @@ export default function TripPlannerPage(): React.ReactElement | null {
onAssignToDay={handleAssignToDay}
onRemoveAssignment={handleRemoveAssignment}
files={files}
onFileUpload={(fd) => tripStore.addFile(tripId, fd)}
onFileUpload={canUploadFiles ? (fd) => tripStore.addFile(tripId, fd) : undefined}
tripMembers={tripMembers}
onSetParticipants={async (assignmentId, dayId, userIds) => {
try {
@@ -688,7 +691,7 @@ export default function TripPlannerPage(): React.ReactElement | null {
<PlaceFormModal isOpen={showPlaceForm} onClose={() => { setShowPlaceForm(false); setEditingPlace(null); setEditingAssignmentId(null); setPrefillCoords(null) }} onSave={handleSavePlace} place={editingPlace} prefillCoords={prefillCoords} assignmentId={editingAssignmentId} dayAssignments={editingAssignmentId ? Object.values(assignments).flat() : []} tripId={tripId} categories={categories} onCategoryCreated={cat => tripStore.addCategory?.(cat)} />
<TripFormModal isOpen={showTripForm} onClose={() => setShowTripForm(false)} onSave={async (data) => { await tripStore.updateTrip(tripId, data); toast.success(t('trip.toast.tripUpdated')) }} trip={trip} />
<TripMembersModal isOpen={showMembersModal} onClose={() => setShowMembersModal(false)} tripId={tripId} tripTitle={trip?.title} />
<ReservationModal isOpen={showReservationModal} onClose={() => { setShowReservationModal(false); setEditingReservation(null) }} onSave={handleSaveReservation} reservation={editingReservation} days={days} places={places} assignments={assignments} selectedDayId={selectedDayId} files={files} onFileUpload={(fd) => tripStore.addFile(tripId, fd)} onFileDelete={(id) => tripStore.deleteFile(tripId, id)} accommodations={tripAccommodations} />
<ReservationModal isOpen={showReservationModal} onClose={() => { setShowReservationModal(false); setEditingReservation(null) }} onSave={handleSaveReservation} reservation={editingReservation} days={days} places={places} assignments={assignments} selectedDayId={selectedDayId} files={files} onFileUpload={canUploadFiles ? (fd) => tripStore.addFile(tripId, fd) : undefined} onFileDelete={(id) => tripStore.deleteFile(tripId, id)} accommodations={tripAccommodations} />
<ConfirmDialog
isOpen={!!deletePlaceId}
onClose={() => setDeletePlaceId(null)}