diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 2b98c6f2a..f72fad943 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -15,7 +15,7 @@ import { type RegisterRequest, type LoginRequest, type ForgotPasswordRequest, type ResetPasswordRequest, type ChangePasswordRequest, type MfaVerifyLoginRequest, type MfaEnableRequest, type McpTokenCreateRequest, - type TripAddMemberRequest, type AssignmentReorderRequest, + type TripAddMemberRequest, type TripTransferOwnershipRequest, type AssignmentReorderRequest, type PackingReorderRequest, type PackingCreateBagRequest, type TodoReorderRequest, type TripCreateRequest, type TripUpdateRequest, type TripCopyRequest, type DayCreateRequest, type DayUpdateRequest, type DayReorderRequest, @@ -340,6 +340,7 @@ export const tripsApi = { getMembers: (id: number | string) => apiClient.get(`/trips/${id}/members`).then(r => r.data), addMember: (id: number | string, identifier: string) => apiClient.post(`/trips/${id}/members`, { identifier } satisfies TripAddMemberRequest).then(r => r.data), removeMember: (id: number | string, userId: number) => apiClient.delete(`/trips/${id}/members/${userId}`).then(r => r.data), + transferOwnership: (id: number | string, newOwnerId: number) => apiClient.post(`/trips/${id}/transfer`, { newOwnerId } satisfies TripTransferOwnershipRequest).then(r => r.data), copy: (id: number | string, data?: TripCopyRequest) => apiClient.post(`/trips/${id}/copy`, data || {}).then(r => r.data), bundle: (id: number | string) => apiClient.get(`/trips/${id}/bundle`).then(r => r.data), } diff --git a/client/src/components/Trips/TripMembersModal.tsx b/client/src/components/Trips/TripMembersModal.tsx index 2862a548f..a167baa96 100644 --- a/client/src/components/Trips/TripMembersModal.tsx +++ b/client/src/components/Trips/TripMembersModal.tsx @@ -177,6 +177,7 @@ export default function TripMembersModal({ isOpen, onClose, tripId, tripTitle }: const [selectedUserId, setSelectedUserId] = useState('') const [adding, setAdding] = useState(false) const [removingId, setRemovingId] = useState(null) + const [transferringId, setTransferringId] = useState(null) const toast = useToast() const { user } = useAuthStore() const { t } = useTranslation() @@ -227,6 +228,21 @@ export default function TripMembersModal({ isOpen, onClose, tripId, tripTitle }: } } + const handleTransfer = async (newOwnerId, username) => { + if (!confirm(t('members.confirmTransfer', { name: username }))) return + setTransferringId(newOwnerId) + try { + await tripsApi.transferOwnership(tripId, newOwnerId) + // The current user just dropped from owner to member — reload so the trip + // state and permissions everywhere reflect the new ownership. + onClose() + window.location.reload() + } catch (err: unknown) { + toast.error(getApiErrorMessage(err, t('members.transferError'))) + setTransferringId(null) + } + } + const handleRemove = async (userId, isSelf) => { const msg = isSelf ? t('members.confirmLeave') @@ -347,6 +363,18 @@ export default function TripMembersModal({ isOpen, onClose, tripId, tripTitle }: )} + {isCurrentOwner && member.role !== 'owner' && ( + + )} {canRemove && (