Skip to content

Commit 6087126

Browse files
ErlendSaebrage-andreas
authored andcommitted
Added notification-tab to groups
1 parent 176e64c commit 6087126

14 files changed

Lines changed: 254 additions & 32 deletions

File tree

apps/dashboard/src/app/(internal)/arrangementer/[id]/notification-page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ export const NotificationsPage: FC = () => {
7373
<Stack gap="lg">
7474
<Box>
7575
<Title order={3}>Opprett varsling</Title>
76-
<Button mt="md" onClick={openCreateEventNotificationModal({ eventId: event.id })}>Legg til ny varsling</Button>
76+
<Button mt="md" onClick={openCreateEventNotificationModal({ eventId: event.id })}>
77+
Legg til ny varsling
78+
</Button>
7779
</Box>
7880

7981
<Box>

apps/dashboard/src/app/(internal)/arrangementer/queries.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import type { Pageable } from "@dotkomonline/utils"
66
import { useMemo } from "react"
77
import type { NotificationPayloadType } from "@dotkomonline/rpc"
88

9-
109
interface UseEventAllQueryProps {
1110
filter: EventFilterQuery
1211
page?: Pageable
@@ -95,9 +94,7 @@ export const useFeedbackAnswersGetQuery = (formId: FeedbackFormId | SkipToken) =
9594

9695
export const useNotificationsByPayloadQuery = (payloadType: NotificationPayloadType, payload: string) => {
9796
const trpc = useTRPC()
98-
const { data, ...query } = useQuery(
99-
trpc.notification.findManyByPayload.queryOptions({ payloadType, payload })
100-
)
97+
const { data, ...query } = useQuery(trpc.notification.findManyByPayload.queryOptions({ payloadType, payload }))
10198

10299
return { notifications: useMemo(() => data?.items ?? [], [data]), ...query }
103100
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"use client"
2+
3+
import { GenericTable } from "@/components/GenericTable"
4+
import { DateTooltip } from "@/components/DateTooltip"
5+
import { mapNotificationPayloadTypeToLabel, mapNotificationTypeToLabel, type Notification } from "@dotkomonline/rpc"
6+
import { Anchor, Box, Button, Skeleton, Stack, Title } from "@mantine/core"
7+
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table"
8+
import Link from "next/link"
9+
import { type FC, useMemo } from "react"
10+
import { useNotificationsByPayloadQuery } from "../../arrangementer/queries"
11+
import { openCreateGroupNotificationModal } from "../modals/create-group-notification-modal"
12+
import { useGroupDetailsContext } from "./provider"
13+
14+
export const GroupNotificationPage: FC = () => {
15+
const { group } = useGroupDetailsContext()
16+
const { notifications, isLoading } = useNotificationsByPayloadQuery("GROUP", group.slug)
17+
18+
const columnHelper = createColumnHelper<Notification>()
19+
20+
const columns = useMemo(
21+
() => [
22+
columnHelper.accessor((notification) => notification.title, {
23+
id: "title",
24+
header: () => "Tittel",
25+
sortingFn: "alphanumeric",
26+
cell: (info) => (
27+
<Anchor component={Link} size="sm" href={`/varslinger/${info.row.original.id}`}>
28+
{info.getValue()}
29+
</Anchor>
30+
),
31+
}),
32+
columnHelper.accessor((notification) => notification.shortDescription, {
33+
id: "shortDescription",
34+
header: () => "Kort beskrivelse",
35+
cell: (info) => info.getValue(),
36+
sortingFn: "alphanumeric",
37+
}),
38+
columnHelper.accessor((notification) => notification.type, {
39+
id: "type",
40+
header: () => "Type",
41+
cell: (info) => mapNotificationTypeToLabel(info.getValue()),
42+
sortingFn: "alphanumeric",
43+
}),
44+
columnHelper.accessor((notification) => notification.payloadType, {
45+
id: "payloadType",
46+
header: () => "Payload type",
47+
cell: (info) => mapNotificationPayloadTypeToLabel(info.getValue()),
48+
sortingFn: "alphanumeric",
49+
}),
50+
columnHelper.accessor((notification) => notification.createdAt, {
51+
id: "createdAt",
52+
header: () => "Opprettet",
53+
cell: (info) => <DateTooltip date={info.getValue()} />,
54+
sortingFn: "datetime",
55+
}),
56+
],
57+
[columnHelper]
58+
)
59+
60+
const tableOptions = useMemo(
61+
() => ({
62+
data: notifications,
63+
getCoreRowModel: getCoreRowModel(),
64+
columns,
65+
}),
66+
[notifications, columns]
67+
)
68+
69+
const table = useReactTable(tableOptions)
70+
71+
return (
72+
<Skeleton visible={isLoading}>
73+
<Stack gap="lg">
74+
<Box>
75+
<Title order={3}>Opprett varsling</Title>
76+
<Button mt="md" onClick={openCreateGroupNotificationModal({ groupSlug: group.slug })}>
77+
Legg til ny varsling
78+
</Button>
79+
</Box>
80+
81+
<Box>
82+
<Title order={2}>Varslinger</Title>
83+
<GenericTable table={table} />
84+
</Box>
85+
</Stack>
86+
</Skeleton>
87+
)
88+
}

apps/dashboard/src/app/(internal)/grupper/[id]/page.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"use client"
22

33
import { Box, CloseButton, Group, Tabs, Title } from "@mantine/core"
4-
import { IconCircles, IconListDetails, IconUsers, IconWheelchair } from "@tabler/icons-react"
4+
import { IconCircles, IconListDetails, IconBell, IconUsers, IconWheelchair } from "@tabler/icons-react"
55
import { useRouter, useSearchParams } from "next/navigation"
66
import { GroupEditCard } from "./edit-card"
77
import { GroupEventPage } from "./group-event-page"
8+
import { GroupNotificationPage } from "./group-notification-page"
89
import { GroupMembersPage } from "./members-page"
910
import { useGroupDetailsContext } from "./provider"
1011
import { GroupRolesPage } from "./roles-page"
@@ -34,6 +35,12 @@ const SIDEBAR_LINKS = [
3435
slug: "arrangementer",
3536
component: GroupEventPage,
3637
},
38+
{
39+
icon: IconBell,
40+
label: "Varslinger",
41+
slug: "varslinger",
42+
component: GroupNotificationPage,
43+
},
3744
] as const
3845

3946
export default function GroupDetailsPage() {
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { useFormBuilder } from "@/components/forms/Form"
2+
import { createRichTextInput } from "@/components/forms/RichTextInput/RichTextInput"
3+
import { createSelectInput } from "@/components/forms/SelectInput"
4+
import { createTextInput } from "@/components/forms/TextInput"
5+
import { mapNotificationTypeToLabel, NotificationTypeSchema, NotificationWriteSchema } from "@dotkomonline/rpc"
6+
import { getActiveGroupMembership } from "@dotkomonline/types"
7+
import { type ContextModalProps, modals } from "@mantine/modals"
8+
import type { FC } from "react"
9+
import { useGroupAllQuery, useGroupMembersAllQuery } from "../queries"
10+
import { useCreateGroupNotificationMutation } from "../mutations"
11+
12+
interface CreateGroupNotificationModalProps {
13+
groupSlug: string
14+
}
15+
16+
export const CreateGroupNotificationModal: FC<ContextModalProps<CreateGroupNotificationModalProps>> = ({
17+
context,
18+
id,
19+
innerProps: { groupSlug },
20+
}) => {
21+
const close = () => context.closeModal(id)
22+
const create = useCreateGroupNotificationMutation(groupSlug)
23+
const { members } = useGroupMembersAllQuery(groupSlug)
24+
const { groups } = useGroupAllQuery()
25+
26+
const recipientIds = Array.from(members.entries())
27+
.filter(([, member]) => getActiveGroupMembership(member, groupSlug) !== null)
28+
.map(([userId]) => userId)
29+
30+
const FormComponent = useFormBuilder({
31+
schema: NotificationWriteSchema,
32+
defaultValues: {
33+
recipientIds,
34+
taskId: null,
35+
payloadType: "GROUP",
36+
payload: groupSlug,
37+
},
38+
onSubmit: (data) => {
39+
create.mutate(data)
40+
close()
41+
},
42+
label: "Legg inn ny varsling",
43+
fields: {
44+
title: createTextInput({
45+
label: "Tittel",
46+
placeholder: "Juleball Påmeldingen er åpen!",
47+
required: true,
48+
}),
49+
shortDescription: createTextInput({
50+
label: "Kort beskrivelse",
51+
placeholder: "En kort beskrivelse av varslingen",
52+
required: true,
53+
}),
54+
content: createRichTextInput({
55+
label: "Innhold",
56+
required: true,
57+
}),
58+
type: createSelectInput({
59+
data: Object.values(NotificationTypeSchema.Values).map((type) => ({
60+
value: type,
61+
label: mapNotificationTypeToLabel(type),
62+
})),
63+
label: "Type",
64+
placeholder: "Velg type",
65+
required: true,
66+
}),
67+
actorGroupId: createSelectInput({
68+
label: "Ansvarlig gruppe",
69+
placeholder: "Velg gruppe",
70+
data: groups.map((group) => ({ value: group.slug, label: group.abbreviation })),
71+
searchable: true,
72+
required: true,
73+
}),
74+
},
75+
})
76+
77+
return <FormComponent />
78+
}
79+
80+
export const openCreateGroupNotificationModal =
81+
({ groupSlug }: CreateGroupNotificationModalProps) =>
82+
() =>
83+
modals.openContextModal({
84+
modal: "group/notification/create",
85+
title: "Legg inn ny varsling",
86+
size: "lg",
87+
innerProps: { groupSlug },
88+
})

apps/dashboard/src/app/(internal)/grupper/mutations.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,37 @@ export const useLinkGroupMutation = () => {
282282
)
283283
}
284284

285+
export const useCreateGroupNotificationMutation = (groupSlug: string) => {
286+
const trpc = useTRPC()
287+
const queryClient = useQueryClient()
288+
const notification = useQueryNotification()
289+
return useMutation(
290+
trpc.notification.create.mutationOptions({
291+
onMutate: () => {
292+
notification.loading({
293+
title: "Lager varsling...",
294+
message: "Varslingen blir opprettet.",
295+
})
296+
},
297+
onSuccess: async (data) => {
298+
await queryClient.invalidateQueries({
299+
queryKey: trpc.notification.findManyByPayload.queryKey({ payloadType: "GROUP", payload: groupSlug }),
300+
})
301+
notification.complete({
302+
title: "Varsling opprettet",
303+
message: `Varslingen "${data.title}" har blitt opprettet.`,
304+
})
305+
},
306+
onError: (err) => {
307+
notification.fail({
308+
title: "Feil oppsto",
309+
message: `En feil oppsto under opprettelse av varslingen: ${err.toString()}.`,
310+
})
311+
},
312+
})
313+
)
314+
}
315+
285316
export const useGroupFileUploadMutation = () => {
286317
const trpc = useTRPC()
287318

apps/dashboard/src/app/ModalProvider.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type { FC, PropsWithChildren } from "react"
2525
import { QRCodeScannedModal } from "@/app/(internal)/arrangementer/components/qr-code-scanned-modal"
2626
import { NotifyAttendeesModal } from "@/app/(internal)/arrangementer/components/notify-attendees-modal"
2727
import { CreateGroupMemberModal } from "@/app/(internal)/grupper/modals/create-group-member-modal"
28+
import { CreateGroupNotificationModal } from "@/app/(internal)/grupper/modals/create-group-notification-modal"
2829
import { CreateNotificationModal } from "@/app/(internal)/varslinger/modals/create-notification"
2930
import { CreateEventNotificationModal } from "./(internal)/arrangementer/components/create-event-notification-modal"
3031

@@ -35,7 +36,6 @@ const modals = {
3536
"event/attendance/notify-attendees": NotifyAttendeesModal,
3637
"event/attendance/pool/create": CreatePoolModal,
3738
"event/attendance/pool/update": EditPoolModal,
38-
"event/notification/create": CreateEventNotificationModal,
3939
"jobListing/create": CreateJobListingModal,
4040
"offline/create": CreateOfflineModal,
4141
"attendance/selections/create": CreateAttendanceSelectionsModal,
@@ -54,6 +54,8 @@ const modals = {
5454
"user/membership/update": EditMembershipModal,
5555
"image/upload": UploadImageModal,
5656
"notification/create": CreateNotificationModal,
57+
"event/notification/create": CreateEventNotificationModal,
58+
"group/notification/create": CreateGroupNotificationModal,
5759
} as const
5860

5961
export const ModalProvider: FC<PropsWithChildren> = ({ children }) => (

apps/rpc/src/modules/core.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,12 @@ export async function createServiceLayer(
192192
const contestRepository = getContestRepository()
193193
const notificationRepository = getNotificationRepository()
194194

195-
const notificationService = getNotificationService(notificationRepository, userRepository, attendanceRepository, eventEmitter)
195+
const notificationService = getNotificationService(
196+
notificationRepository,
197+
userRepository,
198+
attendanceRepository,
199+
eventEmitter
200+
)
196201
const membershipService = getMembershipService()
197202
const emailService = isAmazonSesEmailFeatureEnabled(configuration)
198203
? getEmailService(clients.sesClient, clients.sqsClient, configuration)

apps/rpc/src/modules/event/event-service.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,11 @@ export function getEventService(
128128
afterEvent.attendanceId !== null &&
129129
afterEvent.hostingGroups.length > 0
130130
) {
131-
const recipients = await notificationService.retrieveIntendedRecipientIds(handle, "EVENT_UPDATE", afterEvent.id)
131+
const recipients = await notificationService.retrieveIntendedRecipientIds(
132+
handle,
133+
"EVENT_UPDATE",
134+
afterEvent.id
135+
)
132136
await notificationService.create(
133137
handle,
134138
recipients,

apps/rpc/src/modules/mark/personal-mark-service.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,16 +73,7 @@ export function getPersonalMarkService(
7373
const title = mark.weight > 1 ? `Du har fått ${mark.weight} prikker` : `Du har fått en prikk`
7474
const description = `${mark.title} med grunn "${mark.details}"`
7575

76-
await notificationService.create(
77-
handle,
78-
[userId],
79-
"NEW_MARK",
80-
title,
81-
description,
82-
null,
83-
"USER",
84-
userId
85-
)
76+
await notificationService.create(handle, [userId], "NEW_MARK", title, description, null, "USER", userId)
8677
await this.sendReceivedMarkEmail(handle, personalMark)
8778

8879
return personalMark

0 commit comments

Comments
 (0)