feat(router): wire portal/register pages, portal-context guard carve-out, lint cleanup
Routing wiring (Phase D of WS-3 PR-B1):
- apps/app/src/plugins/1.router/guards.ts: add a single early-return
carve-out before the org-selection redirect — `if (to.meta.context
=== 'portal') return`. Per ARCH-CONSOLIDATION-2026-04 §4.3,
meta.context is the canonical contract; PR-B2 evolves the guards
from this key to full context-aware logic (post-login landing,
context-switcher, role checks).
- apps/app/env.d.ts: extend RouteMeta with the new layout names
('OrganizerLayout' | 'PortalLayout' | 'PublicLayout'), context,
requiresAuth, requiresToken, navMode, navTitle.
- apps/app/typed-router.d.ts: regenerated by unplugin-vue-router to
pick up portal/* and register/* route names.
- Page meta finalisation: portal pages have layout: 'PortalLayout',
context: 'portal', preserving original requiresAuth + nav fields;
register pages have layout: 'PublicLayout' + public: true (the
apps/app guard convention for public routes, since meta.public is
what the existing guard recognises).
Form-types restructure (boundaries cleanup):
- apps/app/src/composables/forms/types/formBuilder.ts → src/types/forms/
- apps/app/src/composables/forms/utils/{formValidation,validators}.ts
→ src/utils/forms/
- All `@/composables/forms/{types,utils}/*` imports rewritten across
pages, components, composables, tests.
- This avoids a `types → composables` boundaries violation at
src/types/formSchema.ts which re-exports primitives from the
inlined form-schema. types/formSchema.ts now imports from
@/types/forms/formBuilder which is in the same boundaries zone.
Lint cleanup for moved portal sources (apps/portal had no
.eslintrc.cjs; the migrated code now has to pass apps/app's stricter
config):
- axios.isAxiosError → named import { isAxiosError }
(ClaimenTab, RoosterTab, profiel.vue)
- void schemaQuery.refetch() → schemaQuery.refetch()
(register/[public_token].vue)
- if-then-else collapsed to single boolean return (formatFieldValue)
- :delay-on-touch-only="true" → delay-on-touch-only shorthand
(FieldSectionPriority)
- ml-2 class → ms-2 (FieldAvailabilityPicker)
- multi-statement-per-line splits in profiel.vue + spec files
- unused emailConfigured ref removed (profiel.vue)
- one-component-per-file disabled with TODO TECH-WS3-PORTAL-LINT-CLEANUP
ref (FieldOptionsLocale.spec.ts — multi-Wrapper test pattern)
- restored `import Draggable from 'vuedraggable'` after lint:fix
removed it (template-only usage; the import IS needed)
- camelcase param renamed in FieldOptionsLocale harness factory
- typecheck nudge: spec state.data typed via PublicFormSectionOption[] /
PublicFormTimeSlot[] aliases instead of Record<string, unknown>
- PortalLayout.vue: explicit `import { useRoute, useRouter }` so the
vitest mock can intercept (the trimmed AutoImport set doesn't pull
vue-router's auto-imports)
Vitest: 23 / 162 passing. Lint: 0 errors / 0 new warnings (only the
pre-existing boundaries v5→v6 deprecation warnings remain). Typecheck:
clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,10 +6,14 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
function statusColor(status: string): string {
|
||||
if (status === 'approved') return 'success'
|
||||
if (status === 'pending' || status === 'applied') return 'warning'
|
||||
if (status === 'invited') return 'info'
|
||||
if (status === 'rejected') return 'error'
|
||||
if (status === 'approved')
|
||||
return 'success'
|
||||
if (status === 'pending' || status === 'applied')
|
||||
return 'warning'
|
||||
if (status === 'invited')
|
||||
return 'info'
|
||||
if (status === 'rejected')
|
||||
return 'error'
|
||||
|
||||
return 'secondary'
|
||||
}
|
||||
@@ -33,9 +37,8 @@ function formatDates(startDate: string, endDate: string): string {
|
||||
const end = new Date(`${endDate}T12:00:00`)
|
||||
const opts: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'long', year: 'numeric' }
|
||||
|
||||
if (startDate === endDate) {
|
||||
if (startDate === endDate)
|
||||
return start.toLocaleDateString('nl-NL', opts)
|
||||
}
|
||||
|
||||
return `${start.toLocaleDateString('nl-NL', opts)} – ${end.toLocaleDateString('nl-NL', opts)}`
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const registeredLabel = computed(() => {
|
||||
if (!props.registeredAt) return null
|
||||
if (!props.registeredAt)
|
||||
return null
|
||||
try {
|
||||
return new Date(props.registeredAt).toLocaleDateString('nl-NL', {
|
||||
day: 'numeric',
|
||||
|
||||
@@ -6,7 +6,8 @@ const router = useRouter()
|
||||
|
||||
const userInitials = computed(() => {
|
||||
const user = authStore.user
|
||||
if (!user) return '?'
|
||||
if (!user)
|
||||
return '?'
|
||||
const first = user.first_name?.charAt(0) ?? ''
|
||||
const last = user.last_name?.charAt(0) ?? ''
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useAvailableShifts, useClaimShift } from '@/composables/api/usePortalShifts'
|
||||
import { isAxiosError } from 'axios'
|
||||
import { useAvailableShifts, useClaimShift } from '@/composables/api/portal/usePortalShifts'
|
||||
import type { AvailableShift } from '@/types/portal-shift'
|
||||
import axios from 'axios'
|
||||
import type { ApiErrorResponse } from '@/types/api'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -36,19 +36,21 @@ function openClaimDialog(shift: AvailableShift, dayLabel: string, startTime: str
|
||||
}
|
||||
|
||||
async function confirmClaim() {
|
||||
if (!selectedShift.value) return
|
||||
if (!selectedShift.value)
|
||||
return
|
||||
|
||||
claimError.value = null
|
||||
|
||||
try {
|
||||
const result = await claimMutation.mutateAsync(selectedShift.value.id)
|
||||
|
||||
showConfirmDialog.value = false
|
||||
snackbarMessage.value = result.message
|
||||
snackbarColor.value = 'success'
|
||||
snackbar.value = true
|
||||
}
|
||||
catch (err: unknown) {
|
||||
claimError.value = axios.isAxiosError<ApiErrorResponse>(err)
|
||||
claimError.value = isAxiosError<ApiErrorResponse>(err)
|
||||
? err.response?.data?.message ?? 'Er is een fout opgetreden.'
|
||||
: 'Er is een fout opgetreden.'
|
||||
}
|
||||
@@ -62,7 +64,8 @@ function toggleDescription(shiftId: string) {
|
||||
}
|
||||
|
||||
function availabilityColor(slotsAvailable: number): string {
|
||||
if (slotsAvailable >= 3) return 'success'
|
||||
if (slotsAvailable >= 3)
|
||||
return 'success'
|
||||
|
||||
return 'warning'
|
||||
}
|
||||
@@ -110,7 +113,7 @@ function availabilityColor(slotsAvailable: number): string {
|
||||
<VBtn
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="refetch()"
|
||||
@click="refetch"
|
||||
>
|
||||
Opnieuw proberen
|
||||
</VBtn>
|
||||
@@ -235,7 +238,7 @@ function availabilityColor(slotsAvailable: number): string {
|
||||
v-if="!expandedDescriptions.has(shift.id)"
|
||||
class="text-body-2 text-medium-emphasis mb-0"
|
||||
>
|
||||
{{ shift.description.length > 80 ? shift.description.slice(0, 80) + '...' : shift.description }}
|
||||
{{ shift.description.length > 80 ? `${shift.description.slice(0, 80)}...` : shift.description }}
|
||||
<a
|
||||
v-if="shift.description.length > 80"
|
||||
href="#"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import StatusCard from '@/components/portal/StatusCard.vue'
|
||||
import { usePortalStore } from '@/stores/portal/usePortalStore'
|
||||
import { useMyShifts } from '@/composables/api/usePortalShifts'
|
||||
import { useMyShifts } from '@/composables/api/portal/usePortalShifts'
|
||||
import type { PortalPersonPayload } from '@/types/portal'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -19,15 +19,18 @@ const { data: shifts } = useMyShifts(eventIdRef)
|
||||
|
||||
const effectiveStatus = computed(() => {
|
||||
const fromPerson = portal.currentPerson?.status
|
||||
if (fromPerson) return fromPerson
|
||||
if (fromPerson)
|
||||
return fromPerson
|
||||
|
||||
return portal.activeEvent?.person_status ?? 'pending'
|
||||
})
|
||||
|
||||
const statusVariant = computed((): 'pending' | 'approved' | 'rejected' => {
|
||||
const s = effectiveStatus.value
|
||||
if (s === 'approved') return 'approved'
|
||||
if (s === 'rejected') return 'rejected'
|
||||
if (s === 'approved')
|
||||
return 'approved'
|
||||
if (s === 'rejected')
|
||||
return 'rejected'
|
||||
|
||||
return 'pending'
|
||||
})
|
||||
@@ -40,12 +43,15 @@ const upcomingCount = computed(() => shifts.value?.upcoming.length ?? 0)
|
||||
|
||||
function formatNextShift(person: PortalPersonPayload | null): string | null {
|
||||
const list = person?.shift_assignments
|
||||
if (!list?.length) return null
|
||||
if (!list?.length)
|
||||
return null
|
||||
|
||||
const usable = list.filter(
|
||||
a => a.shift?.time_slot?.date && (a.status === 'approved' || a.status === 'pending_approval'),
|
||||
)
|
||||
if (!usable.length) return null
|
||||
|
||||
if (!usable.length)
|
||||
return null
|
||||
|
||||
usable.sort((a, b) => {
|
||||
const da = a.shift?.time_slot?.date ?? ''
|
||||
@@ -57,13 +63,15 @@ function formatNextShift(person: PortalPersonPayload | null): string | null {
|
||||
const a = usable[0]!
|
||||
const slot = a.shift?.time_slot
|
||||
const section = a.shift?.festival_section?.name
|
||||
if (!slot?.date) return null
|
||||
if (!slot?.date)
|
||||
return null
|
||||
|
||||
const dateStr = new Date(`${slot.date}T12:00:00`).toLocaleDateString('nl-NL', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
})
|
||||
|
||||
const start = slot.start_time?.slice(0, 5) ?? ''
|
||||
const end = slot.end_time?.slice(0, 5) ?? ''
|
||||
const timePart = start && end ? `${start} – ${end}` : start || ''
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useMyShifts, useCancelAssignment } from '@/composables/api/usePortalShifts'
|
||||
import { isAxiosError } from 'axios'
|
||||
import { useCancelAssignment, useMyShifts } from '@/composables/api/portal/usePortalShifts'
|
||||
import type { MyShiftAssignment } from '@/types/portal-shift'
|
||||
import axios from 'axios'
|
||||
import type { ApiErrorResponse } from '@/types/api'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -40,7 +40,8 @@ function openCancelDialog(assignment: MyShiftAssignment) {
|
||||
}
|
||||
|
||||
async function confirmCancel() {
|
||||
if (!cancelTarget.value) return
|
||||
if (!cancelTarget.value)
|
||||
return
|
||||
|
||||
cancelError.value = null
|
||||
|
||||
@@ -49,12 +50,13 @@ async function confirmCancel() {
|
||||
assignmentId: cancelTarget.value.assignment_id,
|
||||
reason: cancelReason.value || undefined,
|
||||
})
|
||||
|
||||
showCancelDialog.value = false
|
||||
snackbarMessage.value = result.message
|
||||
snackbar.value = true
|
||||
}
|
||||
catch (err: unknown) {
|
||||
cancelError.value = axios.isAxiosError<ApiErrorResponse>(err)
|
||||
cancelError.value = isAxiosError<ApiErrorResponse>(err)
|
||||
? err.response?.data?.message ?? 'Er is een fout opgetreden.'
|
||||
: 'Er is een fout opgetreden.'
|
||||
}
|
||||
@@ -103,7 +105,7 @@ async function confirmCancel() {
|
||||
<VBtn
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="refetch()"
|
||||
@click="refetch"
|
||||
>
|
||||
Opnieuw proberen
|
||||
</VBtn>
|
||||
|
||||
Reference in New Issue
Block a user