style(app): apply eslint --fix to Tier 2 (TypeScript plumbing)
WS-3 session 1b-i Tier 2. Scope: composables, lib, stores, plugins, types, utils, navigation, main.ts. Mechanical fixes only — predominantly newline-before-return, arrow-parens, antfu/if-newline, padding-line-between-statements, plus one unicorn/prefer-includes (.some(p => x === p) → .includes(x)) in router guards. Excludes (per session prompt): - apps/app/vite.config.ts (Tier 3) - apps/app/themeConfig.ts (Tier 3) - apps/app/vitest.config.ts (Tier 3) - All .vue files (already in Tier 1) Hand-reviewed diffs for the three auth/router-critical files before committing: - src/lib/axios.ts: reviewed clean. Pure mechanical (quote-props on Accept header, curly-strip on single-statement ifs, one blank line before impersonationStore.clearState()). No type-import changes, no logic touched. - src/stores/useAuthStore.ts: reviewed clean. curly-strip + padding before returns. The initialize()/doInitialize() race-condition guard on isInitialized is preserved verbatim. - src/plugins/1.router/guards.ts: reviewed clean. if-newline reformat + one .some() → .includes() rewrite that's behaviorally identical for primitive equality on the guestOnlyPaths string array. Tests + typecheck verified green post-fix: - apps/app vitest: 49 passed (unchanged) - apps/app vue-tsc: clean (unchanged) Lint baseline progression: - Pre-Tier-2: 422 problems (post-Tier-1) - Post-Tier-2: 246 problems Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,7 @@ export function useUpdateProfile() {
|
||||
'/me/profile',
|
||||
payload,
|
||||
)
|
||||
|
||||
return data
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -57,6 +58,7 @@ export function useChangePassword() {
|
||||
'/me/change-password',
|
||||
payload,
|
||||
)
|
||||
|
||||
return data
|
||||
},
|
||||
})
|
||||
@@ -69,6 +71,7 @@ export function useChangeEmail() {
|
||||
'/me/change-email',
|
||||
payload,
|
||||
)
|
||||
|
||||
return data
|
||||
},
|
||||
})
|
||||
@@ -81,6 +84,7 @@ export function useAdminChangeEmail(orgId: Ref<string>) {
|
||||
`/organisations/${orgId.value}/members/${userId}/change-email`,
|
||||
{ new_email: newEmail },
|
||||
)
|
||||
|
||||
return data
|
||||
},
|
||||
})
|
||||
|
||||
@@ -39,6 +39,7 @@ export function useAdminOrganisations(params: Ref<Record<string, string | number
|
||||
'/admin/organisations',
|
||||
{ params: params.value },
|
||||
)
|
||||
|
||||
return data
|
||||
},
|
||||
})
|
||||
@@ -51,6 +52,7 @@ export function useAdminOrganisation(id: Ref<string>) {
|
||||
const { data } = await apiClient.get<ApiResponse<AdminOrganisationDetail>>(
|
||||
`/admin/organisations/${id.value}`,
|
||||
)
|
||||
|
||||
return data.data
|
||||
},
|
||||
enabled: () => !!id.value,
|
||||
@@ -66,6 +68,7 @@ export function useUpdateAdminOrganisation() {
|
||||
`/admin/organisations/${id}`,
|
||||
payload,
|
||||
)
|
||||
|
||||
return data.data
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -83,6 +86,7 @@ export function useCreateOrganisation() {
|
||||
'/organisations',
|
||||
payload,
|
||||
)
|
||||
|
||||
return data.data
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -114,6 +118,7 @@ export function useAdminUsers(params: Ref<Record<string, string | number | undef
|
||||
'/admin/users',
|
||||
{ params: params.value },
|
||||
)
|
||||
|
||||
return data
|
||||
},
|
||||
})
|
||||
@@ -126,6 +131,7 @@ export function useAdminUser(id: Ref<string>) {
|
||||
const { data } = await apiClient.get<ApiResponse<AdminUser>>(
|
||||
`/admin/users/${id.value}`,
|
||||
)
|
||||
|
||||
return data.data
|
||||
},
|
||||
enabled: () => !!id.value,
|
||||
@@ -141,6 +147,7 @@ export function useUpdateAdminUser() {
|
||||
`/admin/users/${id}`,
|
||||
payload,
|
||||
)
|
||||
|
||||
return data.data
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -171,6 +178,7 @@ export function usePlatformStats() {
|
||||
const { data } = await apiClient.get<{ data: PlatformStats }>(
|
||||
'/admin/stats',
|
||||
)
|
||||
|
||||
return data.data
|
||||
},
|
||||
})
|
||||
@@ -186,6 +194,7 @@ export function useAdminActivityLog(params: Ref<Record<string, string | number |
|
||||
'/admin/activity-log',
|
||||
{ params: params.value },
|
||||
)
|
||||
|
||||
return data
|
||||
},
|
||||
})
|
||||
|
||||
@@ -10,9 +10,10 @@ export function useLogin() {
|
||||
return useMutation({
|
||||
mutationFn: async (credentials: LoginCredentials) => {
|
||||
const { data } = await apiClient.post<LoginResponse>('/auth/login', credentials)
|
||||
|
||||
return data
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
onSuccess: data => {
|
||||
// Token is set automatically via httpOnly Set-Cookie header
|
||||
authStore.setUser(data.data.user)
|
||||
queryClient.setQueryData(['auth', 'me'], data.data.user)
|
||||
@@ -27,12 +28,14 @@ export function useMe() {
|
||||
queryKey: ['auth', 'me'],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get<{ success: boolean; data: MeResponse }>('/auth/me')
|
||||
|
||||
return data.data
|
||||
},
|
||||
staleTime: Infinity,
|
||||
staleTime: Number.POSITIVE_INFINITY,
|
||||
enabled: () => authStore.isAuthenticated,
|
||||
select: (data) => {
|
||||
select: data => {
|
||||
authStore.setUser(data)
|
||||
|
||||
return data
|
||||
},
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ export function useCompanies(orgId: Ref<string>) {
|
||||
return data.data
|
||||
},
|
||||
enabled: () => !!orgId.value,
|
||||
staleTime: Infinity,
|
||||
staleTime: Number.POSITIVE_INFINITY,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import type { Ref } from 'vue'
|
||||
import { apiClient } from '@/lib/axios'
|
||||
import type { CrowdList, CreateCrowdListDto, UpdateCrowdListDto } from '@/types/crowdList'
|
||||
import type { CreateCrowdListDto, CrowdList, UpdateCrowdListDto } from '@/types/crowdList'
|
||||
import type { Person } from '@/types/person'
|
||||
|
||||
interface ApiResponse<T> {
|
||||
|
||||
@@ -31,7 +31,7 @@ export function useCrowdTypeList(orgId: Ref<string>) {
|
||||
return data.data
|
||||
},
|
||||
enabled: () => !!orgId.value,
|
||||
staleTime: Infinity,
|
||||
staleTime: Number.POSITIVE_INFINITY,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ export function useCreateCrowdType(orgId: Ref<string>) {
|
||||
`/organisations/${orgId.value}/crowd-types`,
|
||||
payload,
|
||||
)
|
||||
|
||||
return data.data
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -61,6 +62,7 @@ export function useUpdateCrowdType(orgId: Ref<string>) {
|
||||
`/organisations/${orgId.value}/crowd-types/${id}`,
|
||||
payload,
|
||||
)
|
||||
|
||||
return data.data
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
||||
@@ -145,12 +145,18 @@ export function useEmailLogs(orgId: Ref<string>, filters: Ref<EmailLogFilters>)
|
||||
per_page: filters.value.perPage,
|
||||
}
|
||||
|
||||
if (filters.value.search) params.search = filters.value.search
|
||||
if (filters.value.status) params.status = filters.value.status
|
||||
if (filters.value.templateType) params.template_type = filters.value.templateType
|
||||
if (filters.value.eventId) params.event_id = filters.value.eventId
|
||||
if (filters.value.from) params.from = filters.value.from
|
||||
if (filters.value.to) params.to = filters.value.to
|
||||
if (filters.value.search)
|
||||
params.search = filters.value.search
|
||||
if (filters.value.status)
|
||||
params.status = filters.value.status
|
||||
if (filters.value.templateType)
|
||||
params.template_type = filters.value.templateType
|
||||
if (filters.value.eventId)
|
||||
params.event_id = filters.value.eventId
|
||||
if (filters.value.from)
|
||||
params.from = filters.value.from
|
||||
if (filters.value.to)
|
||||
params.to = filters.value.to
|
||||
|
||||
const { data } = await apiClient.get<ApiResponse<PaginatedResponse<EmailLog>>>(
|
||||
`/organisations/${orgId.value}/email-logs`,
|
||||
|
||||
@@ -34,6 +34,7 @@ export function useEventList(orgId: Ref<string>) {
|
||||
`/organisations/${orgId.value}/events`,
|
||||
{ params: { include_children: true } },
|
||||
)
|
||||
|
||||
return data.data
|
||||
},
|
||||
enabled: () => !!orgId.value,
|
||||
@@ -47,6 +48,7 @@ export function useEventDetail(orgId: Ref<string>, id: Ref<string>) {
|
||||
const { data } = await apiClient.get<ApiResponse<EventItem>>(
|
||||
`/organisations/${orgId.value}/events/${id.value}`,
|
||||
)
|
||||
|
||||
return data.data
|
||||
},
|
||||
enabled: () => !!orgId.value && !!id.value,
|
||||
@@ -60,6 +62,7 @@ export function useEventChildren(orgId: Ref<string>, eventId: Ref<string>) {
|
||||
const { data } = await apiClient.get<PaginatedResponse<EventItem>>(
|
||||
`/organisations/${orgId.value}/events/${eventId.value}/children`,
|
||||
)
|
||||
|
||||
return data.data
|
||||
},
|
||||
enabled: () => !!orgId.value && !!eventId.value,
|
||||
@@ -75,6 +78,7 @@ export function useCreateEvent(orgId: Ref<string>) {
|
||||
`/organisations/${orgId.value}/events`,
|
||||
payload,
|
||||
)
|
||||
|
||||
return data.data
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -92,6 +96,7 @@ export function useCreateSubEvent(orgId: Ref<string>, parentEventId: Ref<string>
|
||||
`/organisations/${orgId.value}/events`,
|
||||
payload,
|
||||
)
|
||||
|
||||
return data.data
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -124,6 +129,7 @@ export function useUpdateEvent(orgId: Ref<string>, id: Ref<string>) {
|
||||
`/organisations/${orgId.value}/events/${id.value}`,
|
||||
payload,
|
||||
)
|
||||
|
||||
return data.data
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -142,6 +148,7 @@ export function useTransitionEventStatus(orgId: Ref<string>, eventId: Ref<string
|
||||
`/organisations/${orgId.value}/events/${eventId.value}/transition`,
|
||||
{ status },
|
||||
)
|
||||
|
||||
return data.data
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -158,6 +165,7 @@ export function useUploadEventImage(orgId: Ref<string>, eventId: Ref<string>) {
|
||||
return useMutation({
|
||||
mutationFn: async ({ file, type }: { file: File; type: 'banner' | 'logo' }) => {
|
||||
const formData = new FormData()
|
||||
|
||||
formData.append('image', file)
|
||||
formData.append('type', type)
|
||||
|
||||
@@ -182,6 +190,7 @@ export function useEventStats(orgId: Ref<string>, eventId: Ref<string>) {
|
||||
const { data } = await apiClient.get<{ data: EventStats }>(
|
||||
`/organisations/${orgId.value}/events/${eventId.value}/stats`,
|
||||
)
|
||||
|
||||
return data.data
|
||||
},
|
||||
enabled: () => !!orgId.value && !!eventId.value,
|
||||
|
||||
@@ -53,6 +53,7 @@ export function useMembersList(scope: MemberScope, orgId: Ref<string>) {
|
||||
const url = endpointList(scope, orgId.value)
|
||||
if (scope === 'platform') {
|
||||
const { data } = await apiClient.get<ApiResponse<{ members: Member[] }>>(url)
|
||||
|
||||
return {
|
||||
data: data.data.members,
|
||||
meta: {
|
||||
@@ -63,6 +64,7 @@ export function useMembersList(scope: MemberScope, orgId: Ref<string>) {
|
||||
} satisfies MemberListResponse
|
||||
}
|
||||
const { data } = await apiClient.get<MemberListResponse>(url)
|
||||
|
||||
return data
|
||||
},
|
||||
enabled: () => !!orgId.value,
|
||||
@@ -75,6 +77,7 @@ export function useInviteMember(scope: MemberScope, orgId: Ref<string>) {
|
||||
return useMutation({
|
||||
mutationFn: async (payload: InviteMemberPayload) => {
|
||||
const { data } = await apiClient.post<ApiResponse<Member>>(endpointInvite(scope, orgId.value), payload)
|
||||
|
||||
return data.data
|
||||
},
|
||||
onSuccess: () => invalidateScopedKeys(scope, orgId, queryClient),
|
||||
@@ -90,6 +93,7 @@ export function useUpdateMemberRole(scope: MemberScope, orgId: Ref<string>) {
|
||||
endpointMember(scope, orgId.value, userId),
|
||||
{ role },
|
||||
)
|
||||
|
||||
return data.data
|
||||
},
|
||||
onSuccess: () => invalidateScopedKeys(scope, orgId, queryClient),
|
||||
@@ -125,6 +129,7 @@ export function useInvitationDetail(token: Ref<string>) {
|
||||
queryKey: ['invitation', token],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get<ApiResponse<InvitationDetailResponse>>(`/invitations/${token.value}`)
|
||||
|
||||
return data.data
|
||||
},
|
||||
enabled: () => !!token.value,
|
||||
@@ -136,6 +141,7 @@ export function useAcceptInvitation() {
|
||||
return useMutation({
|
||||
mutationFn: async ({ token, ...payload }: AcceptInvitationPayload & { token: string }) => {
|
||||
const { data } = await apiClient.post<AcceptInvitationResponse>(`/invitations/${token}/accept`, payload)
|
||||
|
||||
return data
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, type Ref } from 'vue'
|
||||
import { type Ref, computed } from 'vue'
|
||||
import { apiClient } from '@/lib/axios'
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
import type {
|
||||
@@ -30,6 +30,7 @@ export function useOrganisationList() {
|
||||
queryKey: ['organisations'],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get<PaginatedResponse<Organisation>>('/organisations')
|
||||
|
||||
return data.data
|
||||
},
|
||||
})
|
||||
@@ -40,6 +41,7 @@ export function useOrganisationDetail(id: Ref<string>) {
|
||||
queryKey: ['organisations', id],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get<ApiResponse<Organisation>>(`/organisations/${id.value}`)
|
||||
|
||||
return data.data
|
||||
},
|
||||
enabled: () => !!id.value,
|
||||
@@ -54,6 +56,7 @@ export function useMyOrganisation() {
|
||||
queryKey: ['organisations', id],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get<ApiResponse<Organisation>>(`/organisations/${id.value}`)
|
||||
|
||||
return data.data
|
||||
},
|
||||
enabled: () => !!id.value,
|
||||
@@ -66,6 +69,7 @@ export function useUpdateOrganisation() {
|
||||
return useMutation({
|
||||
mutationFn: async ({ id, ...payload }: UpdateOrganisationPayload & { id: string }) => {
|
||||
const { data } = await apiClient.put<ApiResponse<Organisation>>(`/organisations/${id}`, payload)
|
||||
|
||||
return data.data
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
@@ -81,6 +85,7 @@ export function useOrganisationDashboardStats(id: Ref<string>) {
|
||||
queryKey: ['organisation-dashboard-stats', id],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get<ApiResponse<OrganisationDashboardStats>>(`/organisations/${id.value}/dashboard-stats`)
|
||||
|
||||
return data.data
|
||||
},
|
||||
enabled: () => !!id.value,
|
||||
|
||||
@@ -20,7 +20,7 @@ export function usePersonTags(orgId: Ref<string>) {
|
||||
return data.data
|
||||
},
|
||||
enabled: () => !!orgId.value,
|
||||
staleTime: Infinity,
|
||||
staleTime: Number.POSITIVE_INFINITY,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export function usePersonTagCategories(orgId: Ref<string>) {
|
||||
return data.data
|
||||
},
|
||||
enabled: () => !!orgId.value,
|
||||
staleTime: Infinity,
|
||||
staleTime: Number.POSITIVE_INFINITY,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export function useRegistrationFieldTemplates(orgId: Ref<string>) {
|
||||
return data.data
|
||||
},
|
||||
enabled: () => !!orgId.value,
|
||||
staleTime: Infinity,
|
||||
staleTime: Number.POSITIVE_INFINITY,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export function useRegistrationFormFields(orgId: Ref<string>, eventId: Ref<strin
|
||||
return data.data
|
||||
},
|
||||
enabled: () => !!orgId.value && !!eventId.value,
|
||||
staleTime: Infinity,
|
||||
staleTime: Number.POSITIVE_INFINITY,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -87,22 +87,23 @@ export function useReorderRegistrationFormFields(orgId: Ref<string>, eventId: Re
|
||||
ids: orderedIds,
|
||||
})
|
||||
},
|
||||
onMutate: async (orderedIds) => {
|
||||
onMutate: async orderedIds => {
|
||||
await queryClient.cancelQueries({ queryKey: ['registration-form-fields', eventId.value] })
|
||||
previousFields = queryClient.getQueryData<RegistrationFormField[]>(['registration-form-fields', eventId.value])
|
||||
|
||||
if (previousFields) {
|
||||
const byId = new Map(previousFields.map(f => [f.id, f]))
|
||||
|
||||
const reordered = orderedIds
|
||||
.map(id => byId.get(id))
|
||||
.filter((f): f is RegistrationFormField => !!f)
|
||||
|
||||
queryClient.setQueryData(['registration-form-fields', eventId.value], reordered)
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
if (previousFields) {
|
||||
if (previousFields)
|
||||
queryClient.setQueryData(['registration-form-fields', eventId.value], previousFields)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -111,24 +111,26 @@ export function useReorderSections(orgId: Ref<string>, eventId: Ref<string>) {
|
||||
sections: orderedIds,
|
||||
})
|
||||
},
|
||||
onMutate: async (orderedIds) => {
|
||||
onMutate: async orderedIds => {
|
||||
await queryClient.cancelQueries({ queryKey: ['sections', eventId.value] })
|
||||
previousSections = queryClient.getQueryData<FestivalSection[]>(['sections', eventId.value])
|
||||
|
||||
// Optimistically update query cache so watch doesn't snap back
|
||||
if (previousSections) {
|
||||
const byId = new Map(previousSections.map(s => [s.id, s]))
|
||||
|
||||
const reordered = orderedIds
|
||||
.map(id => byId.get(id))
|
||||
.filter((s): s is FestivalSection => !!s)
|
||||
|
||||
queryClient.setQueryData(['sections', eventId.value], reordered)
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
if (previousSections) {
|
||||
if (previousSections)
|
||||
queryClient.setQueryData(['sections', eventId.value], previousSections)
|
||||
}
|
||||
},
|
||||
|
||||
// No onSuccess invalidation — query cache and v-model are already in sync
|
||||
})
|
||||
}
|
||||
|
||||
@@ -37,10 +37,14 @@ export function useShiftAssignmentList(
|
||||
queryKey: ['shift-assignments', eventId, filters],
|
||||
queryFn: async () => {
|
||||
const params: Record<string, string> = {}
|
||||
if (filters?.value?.shift_id) params.shift_id = filters.value.shift_id
|
||||
if (filters?.value?.person_id) params.person_id = filters.value.person_id
|
||||
if (filters?.value?.section_id) params.section_id = filters.value.section_id
|
||||
if (filters?.value?.status) params.status = filters.value.status
|
||||
if (filters?.value?.shift_id)
|
||||
params.shift_id = filters.value.shift_id
|
||||
if (filters?.value?.person_id)
|
||||
params.person_id = filters.value.person_id
|
||||
if (filters?.value?.section_id)
|
||||
params.section_id = filters.value.section_id
|
||||
if (filters?.value?.status)
|
||||
params.status = filters.value.status
|
||||
|
||||
const { data } = await apiClient.get<PaginatedResponse<ShiftAssignment>>(
|
||||
`/organisations/${orgId.value}/events/${eventId.value}/shift-assignments`,
|
||||
|
||||
@@ -21,8 +21,10 @@ export function useTimeSlotList(orgId: Ref<string>, eventId: Ref<string>, option
|
||||
queryKey: ['time-slots', eventId, includeParent, includeChildren],
|
||||
queryFn: async () => {
|
||||
const params: Record<string, string> = {}
|
||||
if (includeParent?.value) params.include_parent = 'true'
|
||||
if (includeChildren?.value) params.include_children = 'true'
|
||||
if (includeParent?.value)
|
||||
params.include_parent = 'true'
|
||||
if (includeChildren?.value)
|
||||
params.include_children = 'true'
|
||||
|
||||
const { data } = await apiClient.get<PaginatedResponse<TimeSlot>>(
|
||||
`/organisations/${orgId.value}/events/${eventId.value}/time-slots`,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { computed, type Ref } from 'vue'
|
||||
import { type Ref, computed } from 'vue'
|
||||
import type { EventItem } from '@/types/event'
|
||||
import type { FestivalSection } from '@/types/section'
|
||||
import type { TimeSlot } from '@/types/timeSlot'
|
||||
@@ -24,20 +24,24 @@ export function useTimeSlotDropdown(
|
||||
section: Ref<FestivalSection | null | undefined>,
|
||||
) {
|
||||
const scenario = computed<DropdownScenario>(() => {
|
||||
if (!event.value) return 'flat'
|
||||
if (!event.value)
|
||||
return 'flat'
|
||||
|
||||
const isSubEvent = !!event.value.parent_event_id
|
||||
const hasChildren = event.value.has_children
|
||||
const isCrossEvent = section.value?.type === 'cross_event'
|
||||
|
||||
// Flat event — no hierarchy
|
||||
if (!isSubEvent && !hasChildren) return 'flat'
|
||||
if (!isSubEvent && !hasChildren)
|
||||
return 'flat'
|
||||
|
||||
// Cross_event section — needs all time slots
|
||||
if (isCrossEvent) return 'cross_event'
|
||||
if (isCrossEvent)
|
||||
return 'cross_event'
|
||||
|
||||
// Standard section on sub-event — own + parent
|
||||
if (isSubEvent) return 'sub_event_standard'
|
||||
if (isSubEvent)
|
||||
return 'sub_event_standard'
|
||||
|
||||
// Standard section on festival level — own only
|
||||
return 'festival_standard'
|
||||
@@ -61,12 +65,12 @@ export function useTimeSlotDropdown(
|
||||
case 'cross_event':
|
||||
return {
|
||||
main: `${sectionName} is een festival-brede sectie — actief bij elk programmaonderdeel. Je kunt tijdsloten kiezen van ${festivalName} en van alle programmaonderdelen.`,
|
||||
tip: `Plan diensten per programmaonderdeel (bijv. een showavond) of festival-breed (bijv. opbouw, nachtsecurity).`,
|
||||
tip: 'Plan diensten per programmaonderdeel (bijv. een showavond) of festival-breed (bijv. opbouw, nachtsecurity).',
|
||||
}
|
||||
case 'festival_standard':
|
||||
return {
|
||||
main: `Kies een tijdslot van ${eventName} voor deze dienst.`,
|
||||
tip: `Alleen tijdsloten op festivalniveau zijn beschikbaar voor deze sectie.`,
|
||||
tip: 'Alleen tijdsloten op festivalniveau zijn beschikbaar voor deze sectie.',
|
||||
}
|
||||
default:
|
||||
return null
|
||||
@@ -93,9 +97,8 @@ export function useTimeSlotDropdown(
|
||||
* in the template by comparing adjacent items' groupName.
|
||||
*/
|
||||
function sortedItems(timeSlots: TimeSlot[]): TimeSlotDropdownItem[] {
|
||||
if (scenario.value === 'flat') {
|
||||
if (scenario.value === 'flat')
|
||||
return timeSlots.map(ts => toDropdownItem(ts, false, ''))
|
||||
}
|
||||
|
||||
// Classify each slot into a group and determine isOwn per group
|
||||
const groups = new Map<string, { slots: TimeSlot[]; isOwn: boolean }>()
|
||||
@@ -105,6 +108,7 @@ export function useTimeSlotDropdown(
|
||||
const isOwn = scenario.value === 'sub_event_standard'
|
||||
? ts.source === 'sub_event'
|
||||
: ts.source === 'own'
|
||||
|
||||
groups.set(key, { slots: [], isOwn })
|
||||
}
|
||||
groups.get(key)!.slots.push(ts)
|
||||
@@ -112,17 +116,19 @@ export function useTimeSlotDropdown(
|
||||
|
||||
// Own group first, then others alphabetically
|
||||
const sorted = [...groups.entries()].sort(([nameA, a], [nameB, b]) => {
|
||||
if (a.isOwn && !b.isOwn) return -1
|
||||
if (!a.isOwn && b.isOwn) return 1
|
||||
if (a.isOwn && !b.isOwn)
|
||||
return -1
|
||||
if (!a.isOwn && b.isOwn)
|
||||
return 1
|
||||
|
||||
return nameA.localeCompare(nameB)
|
||||
})
|
||||
|
||||
const items: TimeSlotDropdownItem[] = []
|
||||
for (const [groupName, { slots, isOwn }] of sorted) {
|
||||
const isDimmed = scenario.value === 'sub_event_standard' && !isOwn
|
||||
for (const ts of slots) {
|
||||
for (const ts of slots)
|
||||
items.push(toDropdownItem(ts, isDimmed, groupName))
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
@@ -140,6 +146,7 @@ export function useTimeSlotDropdown(
|
||||
|
||||
function toDropdownItem(ts: TimeSlot, isDimmed: boolean, groupName: string): TimeSlotDropdownItem {
|
||||
const timeRange = `${ts.start_time} – ${ts.end_time}`
|
||||
|
||||
return {
|
||||
id: ts.id,
|
||||
name: ts.name,
|
||||
|
||||
@@ -4,10 +4,12 @@ import { isAxiosError } from 'axios'
|
||||
* Human-readable message from Laravel API validation / exception responses.
|
||||
*/
|
||||
export function getApiErrorMessage(error: unknown, fallback: string): string {
|
||||
if (!isAxiosError(error)) return fallback
|
||||
if (!isAxiosError(error))
|
||||
return fallback
|
||||
|
||||
const data = error.response?.data as Record<string, unknown> | undefined
|
||||
if (!data) return fallback
|
||||
if (!data)
|
||||
return fallback
|
||||
|
||||
const errors = data.errors
|
||||
if (errors && typeof errors === 'object' && errors !== null && !Array.isArray(errors)) {
|
||||
@@ -15,12 +17,14 @@ export function getApiErrorMessage(error: unknown, fallback: string): string {
|
||||
if (Array.isArray(value) && value.length > 0 && typeof value[0] === 'string')
|
||||
return value[0]
|
||||
|
||||
if (typeof value === 'string') return value
|
||||
if (typeof value === 'string')
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
const message = data.message
|
||||
if (typeof message === 'string' && message.trim() !== '') return message
|
||||
if (typeof message === 'string' && message.trim() !== '')
|
||||
return message
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ const apiClient: AxiosInstance = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_URL,
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout: 30000,
|
||||
@@ -17,9 +17,8 @@ apiClient.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
const orgStore = useOrganisationStore()
|
||||
|
||||
if (orgStore.activeOrganisationId) {
|
||||
if (orgStore.activeOrganisationId)
|
||||
config.headers['X-Organisation-Id'] = orgStore.activeOrganisationId
|
||||
}
|
||||
|
||||
// Add impersonation header when active
|
||||
// Lazy import to avoid circular dependency with store
|
||||
@@ -27,18 +26,16 @@ apiClient.interceptors.request.use(
|
||||
if (impersonationData) {
|
||||
try {
|
||||
const parsed = JSON.parse(impersonationData) as { targetUserId?: string }
|
||||
if (parsed.targetUserId) {
|
||||
if (parsed.targetUserId)
|
||||
config.headers['X-Impersonate-User'] = parsed.targetUserId
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Invalid data — ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
if (import.meta.env.DEV)
|
||||
console.log(`🚀 ${config.method?.toUpperCase()} ${config.url}`, config.data)
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
@@ -47,16 +44,14 @@ apiClient.interceptors.request.use(
|
||||
|
||||
apiClient.interceptors.response.use(
|
||||
response => {
|
||||
if (import.meta.env.DEV) {
|
||||
if (import.meta.env.DEV)
|
||||
console.log(`✅ ${response.status} ${response.config.url}`, response.data)
|
||||
}
|
||||
|
||||
return response
|
||||
},
|
||||
error => {
|
||||
if (import.meta.env.DEV) {
|
||||
if (import.meta.env.DEV)
|
||||
console.error(`❌ ${error.response?.status} ${error.config?.url}`, error.response?.data)
|
||||
}
|
||||
|
||||
const status = error.response?.status
|
||||
const notificationStore = useNotificationStore()
|
||||
@@ -65,6 +60,7 @@ apiClient.interceptors.response.use(
|
||||
if (status === 403 && error.response?.data?.impersonation_ended) {
|
||||
import('@/stores/useImpersonationStore').then(({ useImpersonationStore }) => {
|
||||
const impersonationStore = useImpersonationStore()
|
||||
|
||||
impersonationStore.clearState()
|
||||
window.location.href = '/platform'
|
||||
})
|
||||
@@ -76,9 +72,8 @@ apiClient.interceptors.response.use(
|
||||
// Lazy import to avoid circular dependency
|
||||
import('@/stores/useAuthStore').then(({ useAuthStore }) => {
|
||||
const authStore = useAuthStore()
|
||||
if (authStore.isInitialized) {
|
||||
if (authStore.isInitialized)
|
||||
authStore.handleUnauthorized()
|
||||
}
|
||||
})
|
||||
}
|
||||
else if (status === 403) {
|
||||
@@ -90,9 +85,8 @@ apiClient.interceptors.response.use(
|
||||
else if (status === 422) {
|
||||
// Show validation message to user; still reject so component onError handlers can react
|
||||
const message = error.response?.data?.message
|
||||
if (message && typeof message === 'string') {
|
||||
if (message && typeof message === 'string')
|
||||
notificationStore.show(message, 'error')
|
||||
}
|
||||
}
|
||||
else if (status === 503) {
|
||||
notificationStore.show('Service temporarily unavailable. Please try again later.', 'error')
|
||||
|
||||
@@ -4,12 +4,18 @@
|
||||
*/
|
||||
export function dutchPlural(word: string): string {
|
||||
// Words ending in -ie: add -s (editie → edities, locatie → locaties)
|
||||
if (word.endsWith('ie')) return `${word}s`
|
||||
if (word.endsWith('ie'))
|
||||
return `${word}s`
|
||||
|
||||
// Words ending in -e: add -s (ronde → rondes)
|
||||
if (word.endsWith('e')) return `${word}s`
|
||||
if (word.endsWith('e'))
|
||||
return `${word}s`
|
||||
|
||||
// Double vowel before final consonant(s): single vowel + en (onderdeel → onderdelen)
|
||||
const match = word.match(/^(.*)([aeiou])\2([^aeiou]+)$/i)
|
||||
if (match) return `${match[1]}${match[2]}${match[3]}en`
|
||||
if (match)
|
||||
return `${match[1]}${match[2]}${match[3]}en`
|
||||
|
||||
// Default: add -en (dag → dagen)
|
||||
return `${word}en`
|
||||
}
|
||||
|
||||
@@ -7,9 +7,8 @@ export function setupGuards(router: Router) {
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// Wait for initialization to complete (only blocks on first navigation)
|
||||
if (!authStore.isInitialized) {
|
||||
if (!authStore.isInitialized)
|
||||
await authStore.initialize()
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('🔒 Router Guard:', {
|
||||
@@ -24,40 +23,55 @@ export function setupGuards(router: Router) {
|
||||
// Allow public routes (login, auth pages, 404) — but redirect authenticated users away from login
|
||||
if (isPublic) {
|
||||
const guestOnlyPaths = ['/login', '/forgot-password', '/reset-password', '/verify-email-change']
|
||||
if (authStore.isAuthenticated && guestOnlyPaths.some(p => to.path === p)) {
|
||||
if (import.meta.env.DEV) console.log('🔄 Redirecting logged-in user away from login page')
|
||||
if (authStore.isAuthenticated && guestOnlyPaths.includes(to.path)) {
|
||||
if (import.meta.env.DEV)
|
||||
console.log('🔄 Redirecting logged-in user away from login page')
|
||||
|
||||
return { name: 'dashboard' }
|
||||
}
|
||||
if (import.meta.env.DEV) console.log('✅ Public route, allowing access')
|
||||
if (import.meta.env.DEV)
|
||||
console.log('✅ Public route, allowing access')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Routes that opt out of auth (e.g. invitations)
|
||||
if (to.meta.requiresAuth === false) {
|
||||
if (import.meta.env.DEV) console.log('✅ Route does not require auth')
|
||||
if (import.meta.env.DEV)
|
||||
console.log('✅ Route does not require auth')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Not authenticated → redirect to login with return URL
|
||||
if (!authStore.isAuthenticated) {
|
||||
if (import.meta.env.DEV) console.log('🚫 Not authenticated, redirecting to login')
|
||||
if (import.meta.env.DEV)
|
||||
console.log('🚫 Not authenticated, redirecting to login')
|
||||
|
||||
return { path: '/login', query: { to: to.fullPath } }
|
||||
}
|
||||
|
||||
// MFA enforcement — redirect to security settings if MFA setup is required
|
||||
if (authStore.mfaSetupRequired && to.path !== '/account-settings') {
|
||||
if (import.meta.env.DEV) console.log('🔒 MFA setup required, redirecting to security settings')
|
||||
if (import.meta.env.DEV)
|
||||
console.log('🔒 MFA setup required, redirecting to security settings')
|
||||
|
||||
return { path: '/account-settings', query: { tab: 'security' } }
|
||||
}
|
||||
|
||||
// Platform admin routes — require super_admin role
|
||||
if (to.path.startsWith('/platform')) {
|
||||
if (!authStore.isSuperAdmin) {
|
||||
if (import.meta.env.DEV) console.log('🚫 Not a super admin, redirecting to dashboard')
|
||||
if (import.meta.env.DEV)
|
||||
console.log('🚫 Not a super admin, redirecting to dashboard')
|
||||
|
||||
return { name: 'dashboard' }
|
||||
}
|
||||
|
||||
// Platform routes don't require organisation selection
|
||||
if (import.meta.env.DEV) console.log('✅ Super admin access to platform route')
|
||||
if (import.meta.env.DEV)
|
||||
console.log('✅ Super admin access to platform route')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -66,16 +80,21 @@ export function setupGuards(router: Router) {
|
||||
const isSelectOrgPage = to.path === '/select-organisation'
|
||||
|
||||
if (isSelectOrgPage) {
|
||||
if (import.meta.env.DEV) console.log('✅ Organisation selection page')
|
||||
if (import.meta.env.DEV)
|
||||
console.log('✅ Organisation selection page')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// If user has organisations but none selected → redirect to selection
|
||||
if (authStore.organisations.length > 0 && !orgStore.hasOrganisation) {
|
||||
if (import.meta.env.DEV) console.log('🔄 No organisation selected, redirecting')
|
||||
if (import.meta.env.DEV)
|
||||
console.log('🔄 No organisation selected, redirecting')
|
||||
|
||||
return { path: '/select-organisation' }
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) console.log('✅ Access granted')
|
||||
if (import.meta.env.DEV)
|
||||
console.log('✅ Access granted')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
const currentOrganisation = computed(() => {
|
||||
const orgStore = useOrganisationStore()
|
||||
|
||||
return organisations.value.find(o => o.id === orgStore.activeOrganisationId)
|
||||
?? organisations.value[0]
|
||||
?? null
|
||||
@@ -43,13 +44,13 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
// Auto-select first organisation if none is active
|
||||
const orgStore = useOrganisationStore()
|
||||
if (!orgStore.activeOrganisationId && me.organisations.length > 0) {
|
||||
if (!orgStore.activeOrganisationId && me.organisations.length > 0)
|
||||
orgStore.setActiveOrganisation(me.organisations[0].id)
|
||||
}
|
||||
}
|
||||
|
||||
function setActiveOrganisation(id: string) {
|
||||
const orgStore = useOrganisationStore()
|
||||
|
||||
orgStore.setActiveOrganisation(id)
|
||||
}
|
||||
|
||||
@@ -61,19 +62,20 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
mfaSetupRequired.value = false
|
||||
|
||||
const orgStore = useOrganisationStore()
|
||||
|
||||
orgStore.clear()
|
||||
}
|
||||
|
||||
function handleUnauthorized() {
|
||||
clearState()
|
||||
|
||||
// Do NOT reset isInitialized — the full page reload (below) resets all JS state.
|
||||
// Resetting it here causes a race condition: the async 401 interceptor fires
|
||||
// after doInitialize() sets isInitialized=true, putting the app back into
|
||||
// a loading state that never resolves.
|
||||
|
||||
if (typeof window !== 'undefined' && window.location.pathname !== '/login') {
|
||||
if (typeof window !== 'undefined' && window.location.pathname !== '/login')
|
||||
window.location.href = '/login'
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
@@ -95,6 +97,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
async function refreshUser(): Promise<void> {
|
||||
try {
|
||||
const { data } = await apiClient.get<{ success: boolean; data: MeResponse }>('/auth/me')
|
||||
|
||||
setUser(data.data)
|
||||
}
|
||||
catch {
|
||||
@@ -110,16 +113,18 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
let initializePromise: Promise<void> | null = null
|
||||
|
||||
function initialize(): Promise<void> {
|
||||
if (isInitialized.value) return Promise.resolve()
|
||||
if (!initializePromise) {
|
||||
if (isInitialized.value)
|
||||
return Promise.resolve()
|
||||
if (!initializePromise)
|
||||
initializePromise = doInitialize()
|
||||
}
|
||||
|
||||
return initializePromise
|
||||
}
|
||||
|
||||
async function doInitialize(): Promise<void> {
|
||||
try {
|
||||
const { data } = await apiClient.get<{ success: boolean; data: MeResponse }>('/auth/me')
|
||||
|
||||
setUser(data.data)
|
||||
}
|
||||
catch {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { apiClient } from '@/lib/axios'
|
||||
import type { AdminUser, ImpersonationSession, ImpersonationStartResponse, ImpersonationStatusResponse, StartImpersonationPayload } from '@/types/admin'
|
||||
import type { AdminUser, ImpersonationStartResponse, ImpersonationStatusResponse, StartImpersonationPayload } from '@/types/admin'
|
||||
|
||||
const SESSION_STORAGE_KEY = 'crewli_impersonation'
|
||||
const BROADCAST_CHANNEL_NAME = 'crewli_impersonation_sync'
|
||||
@@ -27,12 +27,11 @@ export const useImpersonationStore = defineStore('impersonation', () => {
|
||||
const expiresAt = computed(() => state.value?.expiresAt ? new Date(state.value.expiresAt) : null)
|
||||
|
||||
function persistState(): void {
|
||||
if (state.value) {
|
||||
if (state.value)
|
||||
sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(state.value))
|
||||
}
|
||||
else {
|
||||
|
||||
else
|
||||
sessionStorage.removeItem(SESSION_STORAGE_KEY)
|
||||
}
|
||||
}
|
||||
|
||||
async function start(
|
||||
@@ -69,12 +68,12 @@ export const useImpersonationStore = defineStore('impersonation', () => {
|
||||
// Call stop WITHOUT the X-Impersonate-User header
|
||||
// The interceptor won't add it because we clear state first
|
||||
const currentState = state.value
|
||||
|
||||
state.value = null
|
||||
persistState()
|
||||
|
||||
if (currentState) {
|
||||
if (currentState)
|
||||
await apiClient.post('/admin/stop-impersonation')
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// Even if API call fails, state is already cleared
|
||||
@@ -130,7 +129,8 @@ export const useImpersonationStore = defineStore('impersonation', () => {
|
||||
}
|
||||
|
||||
function listenForBroadcasts(): void {
|
||||
if (broadcastChannel) return
|
||||
if (broadcastChannel)
|
||||
return
|
||||
|
||||
try {
|
||||
broadcastChannel = new BroadcastChannel(BROADCAST_CHANNEL_NAME)
|
||||
|
||||
@@ -25,7 +25,8 @@ export const useShiftDetailStore = defineStore('shiftDetail', () => {
|
||||
|
||||
function toggleAssignmentSelection(id: string) {
|
||||
const idx = selectedAssignmentIds.value.indexOf(id)
|
||||
if (idx === -1) selectedAssignmentIds.value.push(id)
|
||||
if (idx === -1)
|
||||
selectedAssignmentIds.value.push(id)
|
||||
else selectedAssignmentIds.value.splice(idx, 1)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Member } from './member'
|
||||
|
||||
export type BillingStatus = 'trial' | 'active' | 'suspended' | 'cancelled'
|
||||
|
||||
export interface AdminOrganisation {
|
||||
@@ -97,8 +99,6 @@ export interface StartImpersonationPayload {
|
||||
mfa_method: 'totp' | 'email' | 'backup_code'
|
||||
}
|
||||
|
||||
import type { Member } from './member'
|
||||
|
||||
export interface AdminOrganisationDetail {
|
||||
organisation: AdminOrganisation
|
||||
members: Member[]
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface EventItem {
|
||||
name: string
|
||||
slug: string
|
||||
status: EventStatus
|
||||
|
||||
/** Valid next statuses from the API state machine (detail + transition responses). */
|
||||
allowed_transitions?: EventStatus[]
|
||||
event_type: EventTypeEnum
|
||||
|
||||
@@ -78,6 +78,7 @@ export interface FormSchemaSummary {
|
||||
is_published: boolean
|
||||
version: number
|
||||
updated_at: string | null
|
||||
|
||||
// Both counts are always present in the index response because the
|
||||
// controller calls ->withCount(['fields', 'submissions']).
|
||||
submissions_count: number
|
||||
@@ -121,8 +122,10 @@ export interface FormSchema {
|
||||
fields_count: number
|
||||
submissions_count: number | null
|
||||
has_submissions: boolean | null
|
||||
|
||||
// TODO(PR-b3): replace with FormField[] once organizer field types land
|
||||
fields: unknown[]
|
||||
|
||||
// TODO(PR-b3): replace with FormSchemaSection[] once organizer section types land
|
||||
sections: unknown[]
|
||||
created_at: string | null
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TimeSlot, PersonType, TimeSlotSource } from '@/types/timeSlot'
|
||||
import type { PersonType, TimeSlot, TimeSlotSource } from '@/types/timeSlot'
|
||||
|
||||
export type { TimeSlot, PersonType, TimeSlotSource }
|
||||
export type { CreateTimeSlotPayload, UpdateTimeSlotPayload } from '@/types/timeSlot'
|
||||
@@ -71,7 +71,6 @@ export interface CreateSectionPayload {
|
||||
|
||||
export interface UpdateSectionPayload extends Partial<CreateSectionPayload> {}
|
||||
|
||||
|
||||
export interface CreateShiftPayload {
|
||||
time_slot_id: string
|
||||
location_id?: string
|
||||
|
||||
Reference in New Issue
Block a user