Compare commits
2 Commits
48199b0ed7
...
616bb5d7f3
| Author | SHA1 | Date | |
|---|---|---|---|
| 616bb5d7f3 | |||
| 6297480b89 |
@@ -198,34 +198,51 @@ export default async function notificationsRoutes(server: FastifyInstance) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await svc.trigger({
|
const devices = await server.db
|
||||||
tenantId,
|
.select({ centralDeviceId: notificationMobilePushDevices.centralDeviceId })
|
||||||
userId: profile.userId,
|
.from(notificationMobilePushDevices)
|
||||||
eventType: "system.test_push",
|
.where(and(
|
||||||
title: "FEDEO Push ist aktiv",
|
eq(notificationMobilePushDevices.tenantId, tenantId),
|
||||||
message: "Diese Testbenachrichtigung wurde einmalig über das Mitarbeiterprofil ausgelöst.",
|
eq(notificationMobilePushDevices.userId, profile.userId),
|
||||||
payload: {
|
isNull(notificationMobilePushDevices.disabledAt)
|
||||||
link: "/",
|
))
|
||||||
icon: "/favicon.ico",
|
|
||||||
},
|
|
||||||
channels: ["push"],
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.created === 0) {
|
if (!devices.length) {
|
||||||
return reply.code(409).send({
|
return reply.code(409).send({
|
||||||
error: "Pushnachrichten sind für diesen Mitarbeiter deaktiviert",
|
error: "Für diesen Mitarbeiter ist kein aktives mobiles Push-Gerät registriert",
|
||||||
result,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.success || result.delivered === 0) {
|
try {
|
||||||
return reply.code(424).send({
|
const result = await pushServerClient.sendPush({
|
||||||
error: "Die Pushnachricht konnte an kein aktives Gerät zugestellt werden",
|
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,
|
result,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return 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",
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
server.post("/notifications/trigger", async (req, reply) => {
|
server.post("/notifications/trigger", async (req, reply) => {
|
||||||
|
|||||||
396
frontend/components/HeroiconPicker.vue
Normal file
396
frontend/components/HeroiconPicker.vue
Normal 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>
|
||||||
|
|
||||||
@@ -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: {
|
workbox: {
|
||||||
navigateFallback: '/',
|
navigateFallback: null,
|
||||||
},
|
},
|
||||||
|
|
||||||
devOptions: {
|
devOptions: {
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ const archiveItem = async () => {
|
|||||||
<UInput v-model="phase.label" class="w-full" placeholder="Phasenname" />
|
<UInput v-model="phase.label" class="w-full" placeholder="Phasenname" />
|
||||||
</UFormField>
|
</UFormField>
|
||||||
<UFormField label="Icon">
|
<UFormField label="Icon">
|
||||||
<UInput v-model="phase.icon" class="w-full" placeholder="i-heroicons-…" />
|
<HeroiconPicker v-model="phase.icon" />
|
||||||
</UFormField>
|
</UFormField>
|
||||||
<UFormField label="Beschreibung" class="xl:col-span-2">
|
<UFormField label="Beschreibung" class="xl:col-span-2">
|
||||||
<UInput v-model="phase.description" class="w-full" placeholder="Optionale Beschreibung" />
|
<UInput v-model="phase.description" class="w-full" placeholder="Optionale Beschreibung" />
|
||||||
|
|||||||
@@ -490,7 +490,7 @@ onMounted(async () => {
|
|||||||
<p class="text-sm font-medium text-gray-900 dark:text-white">Push-Zustellung testen</p>
|
<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">
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
<template v-if="profile.user_id">
|
<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>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
Für einen Push-Test muss der Mitarbeiter zuerst mit einem Benutzerkonto verknüpft werden.
|
Für einen Push-Test muss der Mitarbeiter zuerst mit einem Benutzerkonto verknüpft werden.
|
||||||
|
|||||||
Reference in New Issue
Block a user