diff --git a/api/app/Http/Resources/Api/V1/ShiftResource.php b/api/app/Http/Resources/Api/V1/ShiftResource.php index 378e16d8..867db36f 100644 --- a/api/app/Http/Resources/Api/V1/ShiftResource.php +++ b/api/app/Http/Resources/Api/V1/ShiftResource.php @@ -37,6 +37,8 @@ final class ShiftResource extends JsonResource 'status' => $this->status, 'filled_slots' => $this->filled_slots, 'fill_rate' => $this->fill_rate, + 'is_overbooked' => $this->filled_slots > $this->slots_total, + 'overbooking_count' => max(0, $this->filled_slots - $this->slots_total), 'effective_start_time' => $this->effective_start_time ? Carbon::parse($this->effective_start_time)->format('H:i') : null, 'effective_end_time' => $this->effective_end_time ? Carbon::parse($this->effective_end_time)->format('H:i') : null, 'created_at' => $this->created_at->toIso8601String(), diff --git a/api/resources/views/mail/layouts/crewli.blade.php b/api/resources/views/mail/layouts/crewli.blade.php index 9a829fae..cdcead7e 100644 --- a/api/resources/views/mail/layouts/crewli.blade.php +++ b/api/resources/views/mail/layouts/crewli.blade.php @@ -41,7 +41,7 @@ {{-- Action button --}} @hasSection('action') -
+
@yield('action')
@endif diff --git a/apps/admin/auto-imports.d.ts b/apps/admin/auto-imports.d.ts index 51748e90..a3880173 100644 --- a/apps/admin/auto-imports.d.ts +++ b/apps/admin/auto-imports.d.ts @@ -113,6 +113,7 @@ declare global { const prefixWithPlus: typeof import('./src/@core/utils/formatters')['prefixWithPlus'] const provide: typeof import('vue')['provide'] const provideLocal: typeof import('@vueuse/core')['provideLocal'] + const queryClientConfig: typeof import('./src/lib/query-client')['queryClientConfig'] const reactify: typeof import('@vueuse/core')['reactify'] const reactifyObject: typeof import('@vueuse/core')['reactifyObject'] const reactive: typeof import('vue')['reactive'] @@ -483,6 +484,7 @@ declare module 'vue' { readonly prefixWithPlus: UnwrapRef readonly provide: UnwrapRef readonly provideLocal: UnwrapRef + readonly queryClientConfig: UnwrapRef readonly reactify: UnwrapRef readonly reactifyObject: UnwrapRef readonly reactive: UnwrapRef diff --git a/apps/admin/src/@core/scss/template/_utilities.scss b/apps/admin/src/@core/scss/template/_utilities.scss index 46419137..1bfde240 100644 --- a/apps/admin/src/@core/scss/template/_utilities.scss +++ b/apps/admin/src/@core/scss/template/_utilities.scss @@ -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; +} diff --git a/apps/app/components.d.ts b/apps/app/components.d.ts index 65797d4c..bd624a63 100644 --- a/apps/app/components.d.ts +++ b/apps/app/components.d.ts @@ -25,6 +25,7 @@ declare module 'vue' { AppStepper: typeof import('./src/@core/components/AppStepper.vue')['default'] AppTextarea: typeof import('./src/@core/components/app-form-elements/AppTextarea.vue')['default'] AppTextField: typeof import('./src/@core/components/app-form-elements/AppTextField.vue')['default'] + AssignPersonDialog: typeof import('./src/components/shifts/AssignPersonDialog.vue')['default'] AssignShiftDialog: typeof import('./src/components/sections/AssignShiftDialog.vue')['default'] CardStatisticsHorizontal: typeof import('./src/@core/components/cards/CardStatisticsHorizontal.vue')['default'] CardStatisticsVertical: typeof import('./src/@core/components/cards/CardStatisticsVertical.vue')['default'] @@ -56,25 +57,34 @@ declare module 'vue' { EditOrganisationDialog: typeof import('./src/components/organisations/EditOrganisationDialog.vue')['default'] EditPersonDialog: typeof import('./src/components/persons/EditPersonDialog.vue')['default'] EditSectionDialog: typeof import('./src/components/sections/EditSectionDialog.vue')['default'] + EmailBrandingTab: typeof import('./src/components/organisation/EmailBrandingTab.vue')['default'] EnableOneTimePasswordDialog: typeof import('./src/components/dialogs/EnableOneTimePasswordDialog.vue')['default'] ErrorHeader: typeof import('./src/components/ErrorHeader.vue')['default'] EventMetricCards: typeof import('./src/components/events/EventMetricCards.vue')['default'] EventTabsNav: typeof import('./src/components/events/EventTabsNav.vue')['default'] I18n: typeof import('./src/@core/components/I18n.vue')['default'] + ImportFromEventDialog: typeof import('./src/components/event/ImportFromEventDialog.vue')['default'] InviteMemberDialog: typeof import('./src/components/members/InviteMemberDialog.vue')['default'] MoreBtn: typeof import('./src/@core/components/MoreBtn.vue')['default'] Notifications: typeof import('./src/@core/components/Notifications.vue')['default'] OrganisationSwitcher: typeof import('./src/components/layout/OrganisationSwitcher.vue')['default'] PaymentProvidersDialog: typeof import('./src/components/dialogs/PaymentProvidersDialog.vue')['default'] PersonDetailPanel: typeof import('./src/components/persons/PersonDetailPanel.vue')['default'] + PersonTagsTab: typeof import('./src/components/organisation/PersonTagsTab.vue')['default'] ProductDescriptionEditor: typeof import('./src/@core/components/ProductDescriptionEditor.vue')['default'] + RegistrationFieldCard: typeof import('./src/components/event/RegistrationFieldCard.vue')['default'] + RegistrationFieldFormDialog: typeof import('./src/components/event/RegistrationFieldFormDialog.vue')['default'] + RegistrationFieldTemplatesTab: typeof import('./src/components/organisation/RegistrationFieldTemplatesTab.vue')['default'] + RegistrationLinkCard: typeof import('./src/components/events/RegistrationLinkCard.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] ScrollToTop: typeof import('./src/@core/components/ScrollToTop.vue')['default'] SectionsShiftsPanel: typeof import('./src/components/sections/SectionsShiftsPanel.vue')['default'] ShareProjectDialog: typeof import('./src/components/dialogs/ShareProjectDialog.vue')['default'] + ShiftDetailPanel: typeof import('./src/components/shifts/ShiftDetailPanel.vue')['default'] Shortcuts: typeof import('./src/@core/components/Shortcuts.vue')['default'] TablePagination: typeof import('./src/@core/components/TablePagination.vue')['default'] + TemplatePickerDialog: typeof import('./src/components/event/TemplatePickerDialog.vue')['default'] ThemeSwitcher: typeof import('./src/@core/components/ThemeSwitcher.vue')['default'] TiptapEditor: typeof import('./src/@core/components/TiptapEditor.vue')['default'] TwoFactorAuthDialog: typeof import('./src/components/dialogs/TwoFactorAuthDialog.vue')['default'] diff --git a/apps/app/env.d.ts b/apps/app/env.d.ts index 5bf2874e..552bc501 100644 --- a/apps/app/env.d.ts +++ b/apps/app/env.d.ts @@ -1,4 +1,15 @@ import 'vue-router' + +interface ImportMetaEnv { + readonly VITE_API_URL: string + readonly VITE_APP_NAME: string + readonly VITE_PORTAL_URL: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} + declare module 'vue-router' { interface RouteMeta { action?: string diff --git a/apps/app/src/@core/scss/template/_utilities.scss b/apps/app/src/@core/scss/template/_utilities.scss index 46419137..1bfde240 100644 --- a/apps/app/src/@core/scss/template/_utilities.scss +++ b/apps/app/src/@core/scss/template/_utilities.scss @@ -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; +} diff --git a/apps/app/src/@core/scss/template/libs/vuetify/components/_cards.scss b/apps/app/src/@core/scss/template/libs/vuetify/components/_cards.scss index 13d71501..75c4dd8c 100644 --- a/apps/app/src/@core/scss/template/libs/vuetify/components/_cards.scss +++ b/apps/app/src/@core/scss/template/libs/vuetify/components/_cards.scss @@ -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)); +} diff --git a/apps/app/src/components/events/EventMetricCards.vue b/apps/app/src/components/events/EventMetricCards.vue index 6a48f232..3231e432 100644 --- a/apps/app/src/components/events/EventMetricCards.vue +++ b/apps/app/src/components/events/EventMetricCards.vue @@ -80,33 +80,37 @@ const shiftFillPercent = computed(() => { md="3" > - - - - -
-

+ +
+ + + +

{{ stats.persons_approved_without_shift }}

-

- goedgekeurd zonder shift -

-

- van {{ stats.persons_approved }} goedgekeurde personen -

+

+ Goedgekeurd zonder shift +

+

+ {{ stats.persons_approved }} + goedgekeurde personen +

@@ -118,30 +122,36 @@ const shiftFillPercent = computed(() => { md="3" > - - - - -
-

+ +
+ + + +

{{ stats.persons_pending }}

-

- wachtende goedkeuringen -

+

+ Wachtende goedkeuringen +

+

+ te beoordelen aanmeldingen +

@@ -153,30 +163,36 @@ const shiftFillPercent = computed(() => { md="3" > - - - - -
-

+ +
+ + + +

{{ stats.pending_identity_matches }}

-

- onopgeloste matches -

+

+ Onopgeloste matches +

+

+ identiteitscontrole nodig +

@@ -188,36 +204,37 @@ const shiftFillPercent = computed(() => { md="3" > - - - - -
-

+ +
+ + + +

{{ stats.shifts_filled }}/{{ stats.shifts_total }}

-

- shifts gevuld -

-
+

+ Shifts gevuld +

+

+ {{ shiftFillPercent }}% + bezetting +

diff --git a/apps/app/src/pages/organisation/settings.vue b/apps/app/src/pages/organisation/settings.vue index 1d15afe1..85bbd449 100644 --- a/apps/app/src/pages/organisation/settings.vue +++ b/apps/app/src/pages/organisation/settings.vue @@ -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({ + + +

diff --git a/apps/app/src/types/organisation.ts b/apps/app/src/types/organisation.ts index 80c4665a..2b0319d3 100644 --- a/apps/app/src/types/organisation.ts +++ b/apps/app/src/types/organisation.ts @@ -4,6 +4,11 @@ export interface Organisation { slug: string billing_status: 'trial' | 'active' | 'suspended' | 'cancelled' settings: Record | 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 + 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 { diff --git a/apps/app/themeConfig.ts b/apps/app/themeConfig.ts index c91a0f2a..374bcd56 100644 --- a/apps/app/themeConfig.ts +++ b/apps/app/themeConfig.ts @@ -1,47 +1,53 @@ -import { breakpointsVuetifyV3 } from '@vueuse/core' -import { h } from 'vue' -import { VIcon } from 'vuetify/components/VIcon' -import { defineThemeConfig } from '@core' -import { Skins } from '@core/enums' +import { breakpointsVuetifyV3 } from "@vueuse/core"; +import { h } from "vue"; +import { VIcon } from "vuetify/components/VIcon"; +import { defineThemeConfig } from "@core"; +import { Skins } from "@core/enums"; -import { AppContentLayoutNav, ContentWidth, FooterType, NavbarType } from '@layouts/enums' +import { + AppContentLayoutNav, + ContentWidth, + FooterType, + NavbarType, +} from "@layouts/enums"; export const { themeConfig, layoutConfig } = defineThemeConfig({ app: { - title: 'Crewli', + title: "Crewli", logo: h( - 'span', + "span", { - class: 'crewli-mark text-h5 font-weight-bold', - style: 'line-height: 1.2; letter-spacing: -0.02em; color: rgb(var(--v-theme-primary));', + class: "crewli-mark text-h5 font-weight-bold", + style: + "line-height: 1.2; letter-spacing: -0.02em; color: rgb(var(--v-theme-primary));", }, - 'C', + "C", ), contentWidth: ContentWidth.Boxed, contentLayoutNav: AppContentLayoutNav.Vertical, overlayNavFromBreakpoint: breakpointsVuetifyV3.lg - 1, // 1 for matching with vuetify breakpoint. Docs: https://next.vuetifyjs.com/en/features/display-and-platform/ i18n: { enable: false, - defaultLocale: 'en', + defaultLocale: "en", langConfig: [ { - label: 'English', - i18nLang: 'en', + label: "English", + i18nLang: "en", isRTL: false, }, { - label: 'French', - i18nLang: 'fr', + label: "French", + i18nLang: "fr", isRTL: false, }, { - label: 'Arabic', - i18nLang: 'ar', + label: "Arabic", + i18nLang: "ar", isRTL: true, }, ], }, - theme: 'system', + theme: "system", skin: Skins.Default, iconRenderer: VIcon, }, @@ -52,12 +58,12 @@ export const { themeConfig, layoutConfig } = defineThemeConfig({ footer: { type: FooterType.Static }, verticalNav: { isVerticalNavCollapsed: false, - defaultNavItemIconProps: { icon: 'tabler-circle' }, + defaultNavItemIconProps: { icon: "tabler-circle" }, isVerticalNavSemiDark: true, }, horizontalNav: { - type: 'sticky', - transition: 'slide-y-reverse-transition', + type: "sticky", + transition: "slide-y-reverse-transition", popoverOffset: 6, }, @@ -66,11 +72,11 @@ export const { themeConfig, layoutConfig } = defineThemeConfig({ // Such as: chevronDown: { icon: 'tabler-chevron-down', color:'primary', size: '24' }, */ icons: { - chevronDown: { icon: 'tabler-chevron-down' }, - chevronRight: { icon: 'tabler-chevron-right', size: 20 }, - close: { icon: 'tabler-x', size: 20 }, - verticalNavPinned: { icon: 'tabler-circle-dot', size: 20 }, - verticalNavUnPinned: { icon: 'tabler-circle', size: 20 }, - sectionTitlePlaceholder: { icon: 'tabler-minus' }, + chevronDown: { icon: "tabler-chevron-down" }, + chevronRight: { icon: "tabler-chevron-right", size: 20 }, + close: { icon: "tabler-x", size: 20 }, + verticalNavPinned: { icon: "tabler-circle-dot", size: 20 }, + verticalNavUnPinned: { icon: "tabler-circle", size: 20 }, + sectionTitlePlaceholder: { icon: "tabler-minus" }, }, -}) +}); diff --git a/apps/portal/src/@core/scss/template/_utilities.scss b/apps/portal/src/@core/scss/template/_utilities.scss index 46419137..1bfde240 100644 --- a/apps/portal/src/@core/scss/template/_utilities.scss +++ b/apps/portal/src/@core/scss/template/_utilities.scss @@ -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; +} diff --git a/apps/portal/themeConfig.ts b/apps/portal/themeConfig.ts index 5aa17473..c5f749c7 100644 --- a/apps/portal/themeConfig.ts +++ b/apps/portal/themeConfig.ts @@ -1,42 +1,50 @@ -import { breakpointsVuetifyV3 } from '@vueuse/core' -import { VIcon } from 'vuetify/components/VIcon' -import { defineThemeConfig } from '@core' -import { Skins } from '@core/enums' +import { breakpointsVuetifyV3 } from "@vueuse/core"; +import { VIcon } from "vuetify/components/VIcon"; +import { defineThemeConfig } from "@core"; +import { Skins } from "@core/enums"; // ❗ Logo SVG must be imported with ?raw suffix -import logo from '@images/logo.svg?raw' +import logo from "@images/logo.svg?raw"; -import { AppContentLayoutNav, ContentWidth, FooterType, NavbarType } from '@layouts/enums' +import { + AppContentLayoutNav, + ContentWidth, + FooterType, + NavbarType, +} from "@layouts/enums"; export const { themeConfig, layoutConfig } = defineThemeConfig({ app: { - title: 'Crewli Portal', - logo: h('div', { innerHTML: logo, style: 'line-height:0; color: rgb(var(--v-global-theme-primary))' }), + title: "Crewli Portal", + logo: h("div", { + innerHTML: logo, + style: "line-height:0; color: rgb(var(--v-global-theme-primary))", + }), contentWidth: ContentWidth.Boxed, contentLayoutNav: AppContentLayoutNav.Vertical, overlayNavFromBreakpoint: breakpointsVuetifyV3.lg - 1, // 1 for matching with vuetify breakpoint. Docs: https://next.vuetifyjs.com/en/features/display-and-platform/ i18n: { enable: false, - defaultLocale: 'en', + defaultLocale: "en", langConfig: [ { - label: 'English', - i18nLang: 'en', + label: "English", + i18nLang: "en", isRTL: false, }, { - label: 'French', - i18nLang: 'fr', + label: "French", + i18nLang: "fr", isRTL: false, }, { - label: 'Arabic', - i18nLang: 'ar', + label: "Arabic", + i18nLang: "ar", isRTL: true, }, ], }, - theme: 'light', + theme: "light", skin: Skins.Default, iconRenderer: VIcon, }, @@ -47,12 +55,12 @@ export const { themeConfig, layoutConfig } = defineThemeConfig({ footer: { type: FooterType.Static }, verticalNav: { isVerticalNavCollapsed: false, - defaultNavItemIconProps: { icon: 'tabler-circle' }, + defaultNavItemIconProps: { icon: "tabler-circle" }, isVerticalNavSemiDark: false, }, horizontalNav: { - type: 'sticky', - transition: 'slide-y-reverse-transition', + type: "sticky", + transition: "slide-y-reverse-transition", popoverOffset: 6, }, @@ -61,11 +69,11 @@ export const { themeConfig, layoutConfig } = defineThemeConfig({ // Such as: chevronDown: { icon: 'tabler-chevron-down', color:'primary', size: '24' }, */ icons: { - chevronDown: { icon: 'tabler-chevron-down' }, - chevronRight: { icon: 'tabler-chevron-right', size: 20 }, - close: { icon: 'tabler-x', size: 20 }, - verticalNavPinned: { icon: 'tabler-circle-dot', size: 20 }, - verticalNavUnPinned: { icon: 'tabler-circle', size: 20 }, - sectionTitlePlaceholder: { icon: 'tabler-minus' }, + chevronDown: { icon: "tabler-chevron-down" }, + chevronRight: { icon: "tabler-chevron-right", size: 20 }, + close: { icon: "tabler-x", size: 20 }, + verticalNavPinned: { icon: "tabler-circle-dot", size: 20 }, + verticalNavUnPinned: { icon: "tabler-circle", size: 20 }, + sectionTitlePlaceholder: { icon: "tabler-minus" }, }, -}) +});