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({
|
||||
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",
|
||||
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",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
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: {
|
||||
navigateFallback: '/',
|
||||
navigateFallback: null,
|
||||
},
|
||||
|
||||
devOptions: {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user