feat(portal): auth persistence, shift visibility, profile page, and UI polish

- Fix session persistence: add loading state to App.vue, hydrate portal store
  in router guards so page refresh preserves auth + event context
- Fix shift visibility for festivals: query child event time slots so shifts
  on sub-events appear in the portal
- Add profile page with editable personal info and password change
- Add backend endpoints: PUT /portal/profile and PUT /portal/password
- Fix registration form: make first_name/last_name editable for logged-in users
- Restyle login page: remove Vuexy illustration, center form with Crewli branding
- Improve dashboard StatusCard with action cards, icons, and upcoming shift count
- Enhance shift cards with status border colors and availability progress bars

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 10:19:14 +02:00
parent 838bee4d60
commit 59ad09fad2
17 changed files with 1145 additions and 254 deletions

View File

@@ -4,6 +4,8 @@ const props = defineProps<{
eventName: string
registeredAt?: string | null
nextShiftSummary?: string | null
upcomingCount?: number
availableCount?: number | null
}>()
const registeredLabel = computed(() => {
@@ -88,6 +90,7 @@ const registeredLabel = computed(() => {
</div>
</div>
<!-- Quick action cards -->
<VRow class="mb-6">
<VCol
cols="12"
@@ -95,15 +98,23 @@ const registeredLabel = computed(() => {
>
<VCard
:to="{ name: 'portal-my-shifts' }"
variant="outlined"
class="pa-4 h-100 text-decoration-none"
class="h-100 text-decoration-none portal-action-card"
elevation="1"
>
<div class="text-subtitle-2 text-medium-emphasis mb-1">
Mijn Diensten
</div>
<div class="text-body-2">
Rooster bekijken
</div>
<VCardText class="d-flex flex-column align-center text-center pa-4">
<VIcon
icon="tabler-calendar-check"
size="28"
color="primary"
class="mb-2"
/>
<div class="text-subtitle-2 font-weight-bold mb-1">
Mijn Diensten
</div>
<div class="text-caption text-medium-emphasis">
Rooster bekijken
</div>
</VCardText>
</VCard>
</VCol>
<VCol
@@ -112,15 +123,23 @@ const registeredLabel = computed(() => {
>
<VCard
:to="{ name: 'portal-claim-shifts' }"
variant="outlined"
class="pa-4 h-100 text-decoration-none"
class="h-100 text-decoration-none portal-action-card"
elevation="1"
>
<div class="text-subtitle-2 text-medium-emphasis mb-1">
Diensten claimen
</div>
<div class="text-body-2">
Schrijf je in voor diensten
</div>
<VCardText class="d-flex flex-column align-center text-center pa-4">
<VIcon
icon="tabler-calendar-plus"
size="28"
color="primary"
class="mb-2"
/>
<div class="text-subtitle-2 font-weight-bold mb-1">
Diensten Claimen
</div>
<div class="text-caption text-medium-emphasis">
Schrijf je in
</div>
</VCardText>
</VCard>
</VCol>
<VCol
@@ -129,21 +148,30 @@ const registeredLabel = computed(() => {
>
<VCard
:to="{ name: 'portal-profile' }"
variant="outlined"
class="pa-4 h-100 text-decoration-none"
class="h-100 text-decoration-none portal-action-card"
elevation="1"
>
<div class="text-subtitle-2 text-medium-emphasis mb-1">
Profiel
</div>
<div class="text-body-2">
Gegevens bekijken
</div>
<VCardText class="d-flex flex-column align-center text-center pa-4">
<VIcon
icon="tabler-user"
size="28"
color="primary"
class="mb-2"
/>
<div class="text-subtitle-2 font-weight-bold mb-1">
Mijn Profiel
</div>
<div class="text-caption text-medium-emphasis">
Gegevens bekijken
</div>
</VCardText>
</VCard>
</VCol>
</VRow>
<!-- Upcoming shift -->
<div class="text-subtitle-1 font-weight-bold mb-2">
Komende shift
Komende dienst
</div>
<p
v-if="nextShiftSummary"
@@ -155,8 +183,57 @@ const registeredLabel = computed(() => {
v-else
class="text-body-2 text-medium-emphasis mb-0"
>
Er is nog geen shift ingepland. Je coördinator houdt je op de hoogte.
Nog geen diensten ingepland.
<RouterLink
:to="{ name: 'portal-claim-shifts' }"
class="text-primary font-weight-medium"
>
Diensten claimen
</RouterLink>
</p>
<!-- Quick stats -->
<div
v-if="upcomingCount !== undefined || availableCount !== null"
class="d-flex flex-wrap gap-4 mt-4 pt-4"
style="border-top: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));"
>
<div
v-if="upcomingCount !== undefined"
class="text-body-2"
>
<VIcon
icon="tabler-calendar-check"
size="16"
class="me-1"
/>
Diensten ingepland: <strong>{{ upcomingCount }}</strong>
</div>
<RouterLink
v-if="availableCount !== null && availableCount !== undefined"
:to="{ name: 'portal-claim-shifts' }"
class="text-body-2 text-primary text-decoration-none"
>
<VIcon
icon="tabler-calendar-plus"
size="16"
class="me-1"
/>
Beschikbare diensten bekijken
</RouterLink>
</div>
</template>
</VCard>
</template>
<style scoped>
.portal-action-card {
transition: transform 0.15s ease, box-shadow 0.15s ease;
cursor: pointer;
}
.portal-action-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
}
</style>