Compare commits

..

2 Commits

Author SHA1 Message Date
616bb5d7f3 fix: send employee test push via mobile service
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 43s
Build and Push Docker Images / build-website (push) Successful in 23s
Build and Push Docker Images / build-central-services-admin (push) Successful in 23s
Build and Push Docker Images / build-frontend (push) Successful in 1m12s
Build and Push Docker Images / build-central-services-api (push) Successful in 22s
Build and Push Docker Images / build-docs (push) Successful in 22s
2026-09-09 13:08:17 +02:00
6297480b89 Heroicons-Picker für Projektphasen ergänzen 2026-09-09 13:07:22 +02:00
5 changed files with 439 additions and 25 deletions

View File

@@ -198,34 +198,51 @@ export default async function notificationsRoutes(server: FastifyInstance) {
})
}
const result = await svc.trigger({
tenantId,
userId: profile.userId,
eventType: "system.test_push",
title: "FEDEO Push ist aktiv",
message: "Diese Testbenachrichtigung wurde einmalig über das Mitarbeiterprofil ausgelöst.",
payload: {
link: "/",
icon: "/favicon.ico",
},
channels: ["push"],
})
const devices = await server.db
.select({ centralDeviceId: notificationMobilePushDevices.centralDeviceId })
.from(notificationMobilePushDevices)
.where(and(
eq(notificationMobilePushDevices.tenantId, tenantId),
eq(notificationMobilePushDevices.userId, profile.userId),
isNull(notificationMobilePushDevices.disabledAt)
))
if (result.created === 0) {
if (!devices.length) {
return reply.code(409).send({
error: "Pushnachrichten sind für diesen Mitarbeiter deaktiviert",
result,
error: "Für diesen Mitarbeiter ist kein aktives mobiles Push-Gerät registriert",
})
}
if (!result.success || result.delivered === 0) {
return reply.code(424).send({
error: "Die Pushnachricht konnte an kein aktives Gerät zugestellt werden",
result,
try {
const result = await pushServerClient.sendPush({
idempotencyKey: `profile-mobile-test:${tenantId}:${profile.userId}:${Date.now()}`,
devices: devices.map((device) => device.centralDeviceId),
priority: "high",
ttlSeconds: 600,
notification: {
title: "FEDEO Push ist aktiv",
body: "Diese Testbenachrichtigung wurde einmalig über das Mitarbeiterprofil ausgelöst.",
},
data: {
type: "system.test_mobile_push",
link: "/",
},
})
if (result.accepted === 0) {
return reply.code(502).send({
error: "Der zentrale Push-Server hat kein Gerät zur Zustellung angenommen",
result,
})
}
return result
} catch (error: any) {
server.log.error({ err: error, profileId, userId: profile.userId }, "Mitarbeiter-Test-Push fehlgeschlagen")
return reply.code(502).send({
error: error?.message || "Der zentrale Push-Server konnte die Nachricht nicht annehmen",
})
}
return result
})
server.post("/notifications/trigger", async (req, reply) => {

View File

@@ -0,0 +1,396 @@
<script setup>
const props = defineProps({
modelValue: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:modelValue'])
const heroiconNames = [
'academic-cap',
'adjustments-horizontal',
'adjustments-vertical',
'archive-box',
'archive-box-arrow-down',
'archive-box-x-mark',
'arrow-down',
'arrow-down-circle',
'arrow-down-left',
'arrow-down-on-square',
'arrow-down-on-square-stack',
'arrow-down-right',
'arrow-down-tray',
'arrow-left',
'arrow-left-circle',
'arrow-left-end-on-rectangle',
'arrow-left-on-rectangle',
'arrow-left-start-on-rectangle',
'arrow-long-down',
'arrow-long-left',
'arrow-long-right',
'arrow-long-up',
'arrow-path',
'arrow-path-rounded-square',
'arrow-right',
'arrow-right-circle',
'arrow-right-end-on-rectangle',
'arrow-right-on-rectangle',
'arrow-right-start-on-rectangle',
'arrow-small-down',
'arrow-small-left',
'arrow-small-right',
'arrow-small-up',
'arrow-top-right-on-square',
'arrow-trending-down',
'arrow-trending-up',
'arrow-turn-down-left',
'arrow-turn-down-right',
'arrow-turn-left-down',
'arrow-turn-left-up',
'arrow-turn-right-down',
'arrow-turn-right-up',
'arrow-turn-up-left',
'arrow-turn-up-right',
'arrow-up',
'arrow-up-circle',
'arrow-up-left',
'arrow-up-on-square',
'arrow-up-on-square-stack',
'arrow-up-right',
'arrow-up-tray',
'arrow-uturn-down',
'arrow-uturn-left',
'arrow-uturn-right',
'arrow-uturn-up',
'arrows-pointing-in',
'arrows-pointing-out',
'arrows-right-left',
'arrows-up-down',
'at-symbol',
'backspace',
'backward',
'banknotes',
'bars-2',
'bars-3',
'bars-3-bottom-left',
'bars-3-bottom-right',
'bars-3-center-left',
'bars-4',
'bars-arrow-down',
'bars-arrow-up',
'battery-0',
'battery-100',
'battery-50',
'beaker',
'bell',
'bell-alert',
'bell-slash',
'bell-snooze',
'bold',
'bolt',
'bolt-slash',
'book-open',
'bookmark',
'bookmark-slash',
'bookmark-square',
'briefcase',
'bug-ant',
'building-library',
'building-office',
'building-office-2',
'building-storefront',
'cake',
'calculator',
'calendar',
'calendar-date-range',
'calendar-days',
'camera',
'chart-bar',
'chart-bar-square',
'chart-pie',
'chat-bubble-bottom-center',
'chat-bubble-bottom-center-text',
'chat-bubble-left',
'chat-bubble-left-ellipsis',
'chat-bubble-left-right',
'chat-bubble-oval-left',
'chat-bubble-oval-left-ellipsis',
'check',
'check-badge',
'check-circle',
'chevron-double-down',
'chevron-double-left',
'chevron-double-right',
'chevron-double-up',
'chevron-down',
'chevron-left',
'chevron-right',
'chevron-up',
'chevron-up-down',
'circle-stack',
'clipboard',
'clipboard-document',
'clipboard-document-check',
'clipboard-document-list',
'clock',
'cloud',
'cloud-arrow-down',
'cloud-arrow-up',
'code-bracket',
'code-bracket-square',
'cog',
'cog-6-tooth',
'cog-8-tooth',
'command-line',
'computer-desktop',
'cpu-chip',
'credit-card',
'cube',
'cube-transparent',
'currency-bangladeshi',
'currency-dollar',
'currency-euro',
'currency-pound',
'currency-rupee',
'currency-yen',
'cursor-arrow-rays',
'cursor-arrow-ripple',
'device-phone-mobile',
'device-tablet',
'divide',
'document',
'document-arrow-down',
'document-arrow-up',
'document-chart-bar',
'document-check',
'document-currency-bangladeshi',
'document-currency-dollar',
'document-currency-euro',
'document-currency-pound',
'document-currency-rupee',
'document-currency-yen',
'document-duplicate',
'document-magnifying-glass',
'document-minus',
'document-plus',
'document-text',
'ellipsis-horizontal',
'ellipsis-horizontal-circle',
'ellipsis-vertical',
'envelope',
'envelope-open',
'equals',
'exclamation-circle',
'exclamation-triangle',
'eye',
'eye-dropper',
'eye-slash',
'face-frown',
'face-smile',
'film',
'finger-print',
'fire',
'flag',
'folder',
'folder-arrow-down',
'folder-minus',
'folder-open',
'folder-plus',
'forward',
'funnel',
'gif',
'gift',
'gift-top',
'globe-alt',
'globe-americas',
'globe-asia-australia',
'globe-europe-africa',
'h1',
'h2',
'h3',
'hand-raised',
'hand-thumb-down',
'hand-thumb-up',
'hashtag',
'heart',
'home',
'home-modern',
'identification',
'inbox',
'inbox-arrow-down',
'inbox-stack',
'information-circle',
'italic',
'key',
'language',
'lifebuoy',
'light-bulb',
'link',
'link-slash',
'list-bullet',
'lock-closed',
'lock-open',
'magnifying-glass',
'magnifying-glass-circle',
'magnifying-glass-minus',
'magnifying-glass-plus',
'map',
'map-pin',
'megaphone',
'microphone',
'minus',
'minus-circle',
'minus-small',
'moon',
'musical-note',
'newspaper',
'no-symbol',
'numbered-list',
'paint-brush',
'paper-airplane',
'paper-clip',
'pause',
'pause-circle',
'pencil',
'pencil-square',
'percent-badge',
'phone',
'phone-arrow-down-left',
'phone-arrow-up-right',
'phone-x-mark',
'photo',
'play',
'play-circle',
'play-pause',
'plus',
'plus-circle',
'plus-small',
'power',
'presentation-chart-bar',
'presentation-chart-line',
'printer',
'puzzle-piece',
'qr-code',
'question-mark-circle',
'queue-list',
'radio',
'receipt-percent',
'receipt-refund',
'rectangle-group',
'rectangle-stack',
'rocket-launch',
'rss',
'scale',
'scissors',
'server',
'server-stack',
'share',
'shield-check',
'shield-exclamation',
'shopping-bag',
'shopping-cart',
'signal',
'signal-slash',
'slash',
'sparkles',
'speaker-wave',
'speaker-x-mark',
'square-2-stack',
'square-3-stack-3d',
'squares-2x2',
'squares-plus',
'star',
'stop',
'stop-circle',
'strikethrough',
'sun',
'swatch',
'table-cells',
'tag',
'ticket',
'trash',
'trophy',
'truck',
'tv',
'underline',
'user',
'user-circle',
'user-group',
'user-minus',
'user-plus',
'users',
'variable',
'video-camera',
'video-camera-slash',
'view-columns',
'viewfinder-circle',
'wallet',
'wifi',
'window',
'wrench',
'wrench-screwdriver',
'x-circle',
'x-mark',
]
const iconOptions = heroiconNames.map(name => ({
label: name,
value: `i-heroicons-${name}`
}))
const selectedIcon = computed({
get: () => props.modelValue,
set: value => emit('update:modelValue', value || '')
})
const selectedName = computed(() => {
return iconOptions.find(option => option.value === selectedIcon.value)?.label
|| selectedIcon.value.replace(/^i-heroicons-/, '')
})
</script>
<template>
<div class="flex items-center gap-2">
<div class="flex size-9 shrink-0 items-center justify-center rounded-md border border-default">
<UIcon
:name="selectedIcon || 'i-heroicons-question-mark-circle'"
class="size-5"
:class="{ 'text-muted': !selectedIcon }"
/>
</div>
<USelectMenu
v-model="selectedIcon"
:items="iconOptions"
value-key="value"
label-key="label"
:search-input="{ placeholder: 'Heroicon suchen ' }"
:filter-fields="['label', 'value']"
placeholder="Heroicon auswählen"
class="min-w-0 flex-1"
clear-search-on-close
>
<template #default>
<span class="truncate">{{ selectedName || 'Heroicon auswählen' }}</span>
</template>
<template #item="{ item }">
<div class="flex min-w-0 items-center gap-3">
<UIcon :name="item.value" class="size-5 shrink-0" />
<span class="truncate">{{ item.label }}</span>
</div>
</template>
</USelectMenu>
<UButton
v-if="selectedIcon"
icon="i-heroicons-x-mark"
color="neutral"
variant="ghost"
aria-label="Icon entfernen"
@click="selectedIcon = ''"
/>
</div>
</template>

View File

@@ -119,9 +119,10 @@ export default defineNuxtConfig({
]
},
/* WICHTIG FÜR SAFARI / iOS */
// FEDEO wird serverseitig gerendert; es gibt daher keine vorab gecachte
// index.html, die Workbox als Navigations-Fallback verwenden könnte.
workbox: {
navigateFallback: '/',
navigateFallback: null,
},
devOptions: {

View File

@@ -316,7 +316,7 @@ const archiveItem = async () => {
<UInput v-model="phase.label" class="w-full" placeholder="Phasenname" />
</UFormField>
<UFormField label="Icon">
<UInput v-model="phase.icon" class="w-full" placeholder="i-heroicons-…" />
<HeroiconPicker v-model="phase.icon" />
</UFormField>
<UFormField label="Beschreibung" class="xl:col-span-2">
<UInput v-model="phase.description" class="w-full" placeholder="Optionale Beschreibung" />

View File

@@ -490,7 +490,7 @@ onMounted(async () => {
<p class="text-sm font-medium text-gray-900 dark:text-white">Push-Zustellung testen</p>
<p class="mt-1 text-sm text-gray-500">
<template v-if="profile.user_id">
Sendet einmalig eine Testnachricht an alle aktiven Push-Geräte dieses Mitarbeiters.
Sendet einmalig eine Testnachricht an alle aktiven mobilen Push-Geräte dieses Mitarbeiters.
</template>
<template v-else>
Für einen Push-Test muss der Mitarbeiter zuerst mit einem Benutzerkonto verknüpft werden.