feat(mail): center-align action button in email template

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-14 00:49:41 +02:00
parent 172a6a12d3
commit ef195a6777
14 changed files with 336 additions and 141 deletions

View File

@@ -47,3 +47,26 @@
.bg-custom-background {
background-color: rgb(var(--v-table-header-color));
}
// ---------------------------------------------------------------------------
// Vuexy (Bootstrap 5) parity — spacing + horizontal rules in cards
// (e.g. form-layouts-horizontal “Form separator”)
// ---------------------------------------------------------------------------
.my-6 {
margin-block-end: 1.5rem !important;
margin-block-start: 1.5rem !important;
}
.mx-n6 {
margin-inline-end: -1.5rem !important;
margin-inline-start: -1.5rem !important;
}
// BS: `.card hr { color: var(--bs-card-border-color); }` — Vuetify card border token
.v-card .v-card-text hr {
border: 0;
border-top: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
color: rgba(var(--v-border-color), var(--v-border-opacity));
opacity: 1;
}

View File

@@ -1,3 +1,58 @@
.v-card-subtitle {
color: rgba(var(--v-theme-on-background), 0.55);
}
/* Card border shadow — colored bottom border with hover effect */
.v-card[class*="card-border-shadow-"] {
position: relative;
overflow: visible;
border-block-end: none;
transition: box-shadow 0.3s ease;
&::after {
position: absolute;
border-radius: inherit;
block-size: 100%;
border-block-end: 0.125rem solid var(--card-border-bottom-color, rgba(var(--v-theme-on-surface), 0.12));
content: "";
inline-size: 100%;
inset-block-end: 0;
inset-inline-start: 0;
pointer-events: none;
transition: border-color 0.3s ease, border-width 0.3s ease;
}
&:hover {
box-shadow: 0 0.25rem 1.125rem rgba(var(--v-theme-on-surface), 0.16) !important;
&::after {
border-color: var(--card-hover-border-bottom-color, rgba(var(--v-theme-on-surface), 0.3));
border-block-end-width: 0.1875rem;
}
}
}
.v-card.card-border-shadow-primary {
--card-border-bottom-color: rgba(var(--v-theme-primary), 0.38);
--card-hover-border-bottom-color: rgb(var(--v-theme-primary));
}
.v-card.card-border-shadow-warning {
--card-border-bottom-color: rgba(var(--v-theme-warning), 0.38);
--card-hover-border-bottom-color: rgb(var(--v-theme-warning));
}
.v-card.card-border-shadow-info {
--card-border-bottom-color: rgba(var(--v-theme-info), 0.38);
--card-hover-border-bottom-color: rgb(var(--v-theme-info));
}
.v-card.card-border-shadow-success {
--card-border-bottom-color: rgba(var(--v-theme-success), 0.38);
--card-hover-border-bottom-color: rgb(var(--v-theme-success));
}
.v-card.card-border-shadow-error {
--card-border-bottom-color: rgba(var(--v-theme-error), 0.38);
--card-hover-border-bottom-color: rgb(var(--v-theme-error));
}

View File

@@ -80,33 +80,37 @@ const shiftFillPercent = computed(() => {
md="3"
>
<VCard
class="cursor-pointer"
hover
:class="[
'cursor-pointer h-100',
stats.persons_approved_without_shift > 0 ? 'card-border-shadow-warning' : 'card-border-shadow-success',
]"
@click="navigateTo('events-id-persons')"
>
<VCardText class="d-flex align-center gap-x-3">
<VAvatar
:color="stats.persons_approved_without_shift > 0 ? 'warning' : 'success'"
variant="tonal"
size="44"
rounded
>
<VIcon
icon="tabler-user-exclamation"
size="28"
/>
</VAvatar>
<div>
<h4 class="text-h4">
<VCardText>
<div class="d-flex align-center mb-1">
<VAvatar
:color="stats.persons_approved_without_shift > 0 ? 'warning' : 'success'"
variant="tonal"
size="44"
rounded
class="me-4"
>
<VIcon
icon="tabler-user-exclamation"
size="28"
/>
</VAvatar>
<h4 class="text-h4 mb-0">
{{ stats.persons_approved_without_shift }}
</h4>
<p class="text-body-2 text-medium-emphasis mb-0">
goedgekeurd zonder shift
</p>
<p class="text-caption text-disabled mb-0">
van {{ stats.persons_approved }} goedgekeurde personen
</p>
</div>
<p class="mb-1">
Goedgekeurd zonder shift
</p>
<p class="mb-0">
<span class="text-heading fw-medium me-2">{{ stats.persons_approved }}</span>
<span class="text-body-secondary text-sm">goedgekeurde personen</span>
</p>
</VCardText>
</VCard>
</VCol>
@@ -118,30 +122,36 @@ const shiftFillPercent = computed(() => {
md="3"
>
<VCard
class="cursor-pointer"
hover
:class="[
'cursor-pointer h-100',
stats.persons_pending > 0 ? 'card-border-shadow-warning' : 'card-border-shadow-success',
]"
@click="navigateTo('events-id-persons')"
>
<VCardText class="d-flex align-center gap-x-3">
<VAvatar
:color="stats.persons_pending > 0 ? 'warning' : 'success'"
variant="tonal"
size="44"
rounded
>
<VIcon
icon="tabler-clock"
size="28"
/>
</VAvatar>
<div>
<h4 class="text-h4">
<VCardText>
<div class="d-flex align-center mb-1">
<VAvatar
:color="stats.persons_pending > 0 ? 'warning' : 'success'"
variant="tonal"
size="44"
rounded
class="me-4"
>
<VIcon
icon="tabler-clock"
size="28"
/>
</VAvatar>
<h4 class="text-h4 mb-0">
{{ stats.persons_pending }}
</h4>
<p class="text-body-2 text-medium-emphasis mb-0">
wachtende goedkeuringen
</p>
</div>
<p class="mb-1">
Wachtende goedkeuringen
</p>
<p class="mb-0">
<span class="text-body-secondary text-sm">te beoordelen aanmeldingen</span>
</p>
</VCardText>
</VCard>
</VCol>
@@ -153,30 +163,36 @@ const shiftFillPercent = computed(() => {
md="3"
>
<VCard
class="cursor-pointer"
hover
:class="[
'cursor-pointer h-100',
stats.pending_identity_matches > 0 ? 'card-border-shadow-info' : 'card-border-shadow-success',
]"
@click="navigateTo('events-id-persons')"
>
<VCardText class="d-flex align-center gap-x-3">
<VAvatar
:color="stats.pending_identity_matches > 0 ? 'info' : 'success'"
variant="tonal"
size="44"
rounded
>
<VIcon
icon="tabler-user-search"
size="28"
/>
</VAvatar>
<div>
<h4 class="text-h4">
<VCardText>
<div class="d-flex align-center mb-1">
<VAvatar
:color="stats.pending_identity_matches > 0 ? 'info' : 'success'"
variant="tonal"
size="44"
rounded
class="me-4"
>
<VIcon
icon="tabler-user-search"
size="28"
/>
</VAvatar>
<h4 class="text-h4 mb-0">
{{ stats.pending_identity_matches }}
</h4>
<p class="text-body-2 text-medium-emphasis mb-0">
onopgeloste matches
</p>
</div>
<p class="mb-1">
Onopgeloste matches
</p>
<p class="mb-0">
<span class="text-body-secondary text-sm">identiteitscontrole nodig</span>
</p>
</VCardText>
</VCard>
</VCol>
@@ -188,36 +204,37 @@ const shiftFillPercent = computed(() => {
md="3"
>
<VCard
class="cursor-pointer"
hover
:class="[
'cursor-pointer h-100',
`card-border-shadow-${shiftFillColor}`,
]"
@click="navigateTo('events-id-sections')"
>
<VCardText class="d-flex align-center gap-x-3">
<VAvatar
:color="shiftFillColor"
variant="tonal"
size="44"
rounded
>
<VIcon
icon="tabler-calendar-check"
size="28"
/>
</VAvatar>
<div class="flex-grow-1">
<h4 class="text-h4">
<VCardText>
<div class="d-flex align-center mb-1">
<VAvatar
:color="shiftFillColor"
variant="tonal"
size="44"
rounded
class="me-4"
>
<VIcon
icon="tabler-calendar-check"
size="28"
/>
</VAvatar>
<h4 class="text-h4 mb-0">
{{ stats.shifts_filled }}/{{ stats.shifts_total }}
</h4>
<p class="text-body-2 text-medium-emphasis mb-1">
shifts gevuld
</p>
<VProgressLinear
:model-value="shiftFillPercent"
:color="shiftFillColor"
height="6"
rounded
/>
</div>
<p class="mb-1">
Shifts gevuld
</p>
<p class="mb-0">
<span class="text-heading fw-medium me-2">{{ shiftFillPercent }}%</span>
<span class="text-body-secondary text-sm">bezetting</span>
</p>
</VCardText>
</VCard>
</VCol>

View File

@@ -3,6 +3,7 @@ import { useOrganisationStore } from '@/stores/useOrganisationStore'
import PersonTagsTab from '@/components/organisation/PersonTagsTab.vue'
import RegistrationFieldTemplatesTab from '@/components/organisation/RegistrationFieldTemplatesTab.vue'
import CrowdTypesManager from '@/components/organisations/CrowdTypesManager.vue'
import EmailBrandingTab from '@/components/organisation/EmailBrandingTab.vue'
const route = useRoute()
const router = useRouter()
@@ -14,6 +15,7 @@ const tabs = [
{ value: 'tags', label: 'Tags & Vaardigheden', icon: 'tabler-tag' },
{ value: 'templates', label: 'Registratieveld-templates', icon: 'tabler-forms' },
{ value: 'crowd-types', label: 'Crowd types', icon: 'tabler-users-group' },
{ value: 'email-branding', label: 'E-mail opmaak', icon: 'tabler-mail' },
]
const activeTab = computed({
@@ -67,6 +69,9 @@ const activeTab = computed({
<VWindowItem value="crowd-types">
<CrowdTypesManager :org-id="orgId" />
</VWindowItem>
<VWindowItem value="email-branding">
<EmailBrandingTab :org-id="orgId" />
</VWindowItem>
</VWindow>
</div>
</template>

View File

@@ -4,6 +4,11 @@ export interface Organisation {
slug: string
billing_status: 'trial' | 'active' | 'suspended' | 'cancelled'
settings: Record<string, unknown> | null
email_logo_url: string | null
email_primary_color: string | null
email_reply_to: string | null
email_sender_name: string | null
email_footer_text: string | null
created_at: string
}
@@ -19,6 +24,11 @@ export interface UpdateOrganisationPayload {
slug?: string
billing_status?: Organisation['billing_status']
settings?: Record<string, unknown>
email_logo_url?: string | null
email_primary_color?: string | null
email_reply_to?: string | null
email_sender_name?: string | null
email_footer_text?: string | null
}
export interface CrowdType {