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 && (