feat(portal): shift claiming and my-shifts for volunteer portal
Backend: PortalShiftController with 4 endpoints (available-shifts, my-shifts, claim, cancel) delegating to ShiftAssignmentService. 24 PHPUnit tests covering happy paths, auth, conflicts, and edge cases. Frontend: claim-shifts and my-shifts pages with TanStack Query composable, conflict detection, confirmation dialogs, and cancel flow. Navigation and dashboard cards wired up for approved volunteers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
73
apps/portal/src/composables/api/usePortalShifts.ts
Normal file
73
apps/portal/src/composables/api/usePortalShifts.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||
import type { Ref } from 'vue'
|
||||
import { apiClient } from '@/lib/axios'
|
||||
import type { AvailableShiftsDay, MyShiftsResponse } from '@/types/portal-shift'
|
||||
|
||||
interface ApiResponse<T> {
|
||||
data: T
|
||||
}
|
||||
|
||||
export function useAvailableShifts(eventId: Ref<string | null>) {
|
||||
return useQuery({
|
||||
queryKey: ['available-shifts', eventId],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get<ApiResponse<AvailableShiftsDay[]>>(
|
||||
`/portal/events/${eventId.value}/available-shifts`,
|
||||
)
|
||||
|
||||
return data.data
|
||||
},
|
||||
enabled: () => !!eventId.value,
|
||||
})
|
||||
}
|
||||
|
||||
export function useMyShifts(eventId: Ref<string | null>) {
|
||||
return useQuery({
|
||||
queryKey: ['my-shifts', eventId],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get<ApiResponse<MyShiftsResponse>>(
|
||||
`/portal/events/${eventId.value}/my-shifts`,
|
||||
)
|
||||
|
||||
return data.data
|
||||
},
|
||||
enabled: () => !!eventId.value,
|
||||
})
|
||||
}
|
||||
|
||||
export function useClaimShift(eventId: Ref<string | null>) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (shiftId: string) => {
|
||||
const { data } = await apiClient.post<ApiResponse<{ assignment_id: string; status: string; message: string }>>(
|
||||
`/portal/events/${eventId.value}/shifts/${shiftId}/claim`,
|
||||
)
|
||||
|
||||
return data.data
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['available-shifts', eventId] })
|
||||
queryClient.invalidateQueries({ queryKey: ['my-shifts', eventId] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useCancelAssignment(eventId: Ref<string | null>) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ assignmentId, reason }: { assignmentId: string; reason?: string }) => {
|
||||
const { data } = await apiClient.post<ApiResponse<{ message: string }>>(
|
||||
`/portal/events/${eventId.value}/assignments/${assignmentId}/cancel`,
|
||||
reason ? { reason } : {},
|
||||
)
|
||||
|
||||
return data.data
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['available-shifts', eventId] })
|
||||
queryClient.invalidateQueries({ queryKey: ['my-shifts', eventId] })
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user