diff --git a/.vscode/settings.json b/.vscode/settings.json index 28d3f25e..045018b5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,5 +19,17 @@ }, "[xml]": { "editor.defaultFormatter": "redhat.vscode-xml" - } + }, + "sqltools.connections": [ + { + "previewLimit": 50, + "server": "localhost", + "port": 5432, + "driver": "PostgreSQL", + "name": "Persistant-Data", + "database": "var", + "username": "persistant-data", + "password": "persistant-data-pw" + } + ] } diff --git a/Persistant-Data.session.sql b/Persistant-Data.session.sql new file mode 100644 index 00000000..e9e19fac --- /dev/null +++ b/Persistant-Data.session.sql @@ -0,0 +1 @@ +SELECT * FROM users \ No newline at end of file diff --git a/apps/hub/app/(app)/_components/Badges.tsx b/apps/hub/app/(app)/_components/Badges.tsx new file mode 100644 index 00000000..4cd467d6 --- /dev/null +++ b/apps/hub/app/(app)/_components/Badges.tsx @@ -0,0 +1,28 @@ +import { Badge } from "@repo/ui"; + +import { Award } from "lucide-react"; +import { getServerSession } from "../../api/auth/[...nextauth]/auth"; + +export const Badges = async () => { + const session = await getServerSession(); + if (!session) return null; + + return ( +
+
+

+ + Verdiente Abzeichen + +

+ {session.user.badges.map((badge) => { + return ( +
+ +
+ ); + })} +
+
+ ); +}; diff --git a/apps/hub/app/(app)/_components/Events.tsx b/apps/hub/app/(app)/_components/Events.tsx index 2fcc767f..03e042f4 100644 --- a/apps/hub/app/(app)/_components/Events.tsx +++ b/apps/hub/app/(app)/_components/Events.tsx @@ -1,6 +1,8 @@ import { getServerSession } from "../../api/auth/[...nextauth]/auth"; import { PrismaClient } from "@repo/db"; import { KursItem } from "../events/_components/item"; +import { RocketIcon } from "lucide-react"; +import { eventCompleted } from "@repo/ui"; export default async () => { const prisma = new PrismaClient(); @@ -39,18 +41,37 @@ export default async () => { }, }); + const filteredEvents = events.filter((event) => { + console.log; + if (eventCompleted(event, event.participants[0])) return false; + if ( + event.type === "OBLIGATED_COURSE" && + !eventCompleted(event, event.participants[0]) + ) + return true; + return false; + }); + + if (!filteredEvents.length) return null; return ( -
- {events.map((event) => { - return ( - - ); - })} +
+
+

+ Laufende Events & Kurse +

+
+
+ {filteredEvents.map((event) => { + return ( + + ); + })} +
); }; diff --git a/apps/hub/app/(app)/admin/event/_components/AppointmentModal.tsx b/apps/hub/app/(app)/admin/event/_components/AppointmentModal.tsx new file mode 100644 index 00000000..2032a60a --- /dev/null +++ b/apps/hub/app/(app)/admin/event/_components/AppointmentModal.tsx @@ -0,0 +1,203 @@ +import { DateInput } from "../../../../_components/ui/DateInput"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Event, Participant, Prisma } from "@repo/db"; +import { + EventAppointmentOptionalDefaults, + EventAppointmentOptionalDefaultsSchema, + ParticipantOptionalDefaultsSchema, +} from "@repo/db/zod"; +import { useSession } from "next-auth/react"; +import { Controller, useForm } from "react-hook-form"; +import { deleteAppoinement, upsertAppointment } from "../action"; +import { Button } from "../../../../_components/ui/Button"; +import { + PaginatedTable, + PaginatedTableRef, +} from "../../../../_components/PaginatedTable"; +import { Ref, RefObject, useRef } from "react"; +import { CellContext } from "@tanstack/react-table"; +import { upsertParticipant } from "../../../events/actions"; +import { Switch } from "../../../../_components/ui/Switch"; + +interface AppointmentModalProps { + event?: Event; + ref: RefObject; + appointmentsTableRef: React.RefObject; +} + +export const AppointmentModal = ({ + event, + ref, + appointmentsTableRef, +}: AppointmentModalProps) => { + const { data: session } = useSession(); + const appointmentForm = useForm({ + resolver: zodResolver(EventAppointmentOptionalDefaultsSchema), + defaultValues: { + eventId: event?.id, + presenterId: session?.user?.id, + }, + }); + const participantTableRef = useRef(null); + const participantForm = useForm({ + resolver: zodResolver(ParticipantOptionalDefaultsSchema), + }); + + return ( + +
+
+ {/* if there is a button in form, it will close the modal */} + +
+

+ Termin {appointmentForm.watch("id")} +

+
{ + if (!event) return; + const createdAppointment = await upsertAppointment(values); + ref.current?.close(); + appointmentsTableRef.current?.refresh(); + })} + className="flex flex-col" + > + + {/* */} +
+ {appointmentForm.watch("id") && ( + ) => { + return ( + <> + + {!row.original.attended && + event?.hasPresenceEvents && ( + + )} + + ); + }, + }, + ]} + prismaModel={"participant"} + filter={{ + eventAppointmentId: appointmentForm.watch("id"), + }} + include={{ User: true }} + leftOfPagination={ +
+ + {appointmentForm.watch("id") && ( + + )} +
+ } + /> + )} +
+
+ + {participantForm.watch("id") && ( +
{ + await upsertParticipant({ + ...data, + statusLog: data.statusLog as Prisma.InputJsonValue[], + }); + participantTableRef.current?.refresh(); + })} + className="space-y-1" + > +

Teilnehmer bearbeiten

+ + + {event?.hasPresenceEvents && ( + + )} +
+

Verlauf

+ {participantForm.watch("statusLog").map((s) => { + return ( +
+

{(s as any).event}

+

{new Date((s as any).timestamp).toLocaleString()}

+
+ ); + })} +
+ + + )} +
+
+ ); +}; diff --git a/apps/hub/app/(app)/admin/event/_components/Form.tsx b/apps/hub/app/(app)/admin/event/_components/Form.tsx index f2ed0bd8..e07d7c6d 100644 --- a/apps/hub/app/(app)/admin/event/_components/Form.tsx +++ b/apps/hub/app/(app)/admin/event/_components/Form.tsx @@ -38,10 +38,7 @@ import { import { Select } from "../../../../_components/ui/Select"; import { useSession } from "next-auth/react"; import { MarkdownEditor } from "../../../../_components/ui/MDEditor"; -import { CellContext } from "@tanstack/react-table"; -import { upsertParticipant } from "../../../events/actions"; -import { de } from "date-fns/locale"; -registerLocale("de", de); +import { AppointmentModal } from "./AppointmentModal"; export const Form = ({ event }: { event?: Event }) => { const { data: session } = useSession(); @@ -66,177 +63,10 @@ export const Form = ({ event }: { event?: Event }) => { }); return ( <> - -
-
- {/* if there is a button in form, it will close the modal */} - -
-

- Termin {appointmentForm.watch("id")} -

-
{ - if (!event) return; - const createdAppointment = await upsertAppointment(values); - appointmentModal.current?.close(); - appointmentsTableRef.current?.refresh(); - })} - className="flex flex-col" - > - ( - field.onChange(date)} - selected={field.value} - /> - )} - /> - {/* */} -
- {appointmentForm.watch("id") && ( - ) => { - return ( - <> - - {!row.original.attended && - event?.hasPresenceEvents && ( - - )} - - ); - }, - }, - ]} - prismaModel={"participant"} - filter={{ - eventAppointmentId: appointmentForm.watch("id"), - }} - include={{ User: true }} - leftOfPagination={ -
- - {appointmentForm.watch("id") && ( - - )} -
- } - /> - )} -
-
- - {participantForm.watch("id") && ( -
{ - await upsertParticipant({ - ...data, - statusLog: data.statusLog as Prisma.InputJsonValue[], - }); - participantTableRef.current?.refresh(); - })} - className="space-y-1" - > -

Teilnehmer bearbeiten

- - - {event?.hasPresenceEvents && ( - - )} -
-

Verlauf

- {participantForm.watch("statusLog").map((s) => { - return ( -
-

{(s as any).event}

-

{new Date((s as any).timestamp).toLocaleString()}

-
- ); - })} -
- - - )} -
-
+
{ setLoading(true); @@ -322,92 +152,94 @@ export const Form = ({ event }: { event?: Event }) => { />
-
-
-
-

- Termine -

- {event && ( - - )} -
+ {form.watch("hasPresenceEvents") ? ( +
+
+
+

+ Termine +

+ {event && ( + + )} +
- - new Date(date.appointmentDate).toLocaleDateString(), - }, - { - header: "Presenter", - accessorKey: "presenter", - cell: ({ row }) => ( -
- - {(row.original as any).Presenter.firstname}{" "} - {(row.original as any).Presenter.lastname} - -
- ), - }, - { - header: "Teilnehmer", - accessorKey: "Participants", - cell: ({ row }) => ( -
- - - {row.original.Participants.length} - -
- ), - }, - { - header: "Aktionen", - cell: ({ row }) => { - return ( -
- -
- ); + + new Date(date.appointmentDate).toLocaleDateString(), }, - }, - ]} - /> + { + header: "Presenter", + accessorKey: "presenter", + cell: ({ row }) => ( +
+ + {(row.original as any).Presenter.firstname}{" "} + {(row.original as any).Presenter.lastname} + +
+ ), + }, + { + header: "Teilnehmer", + accessorKey: "Participants", + cell: ({ row }) => ( +
+ + + {row.original.Participants.length} + +
+ ), + }, + { + header: "Aktionen", + cell: ({ row }) => { + return ( +
+ +
+ ); + }, + }, + ]} + /> +
-
+ ) : null}
diff --git a/apps/hub/app/(app)/events/_components/modalBtn.tsx b/apps/hub/app/(app)/events/_components/modalBtn.tsx index 57cb0ac5..572b3cf9 100644 --- a/apps/hub/app/(app)/events/_components/modalBtn.tsx +++ b/apps/hub/app/(app)/events/_components/modalBtn.tsx @@ -8,7 +8,7 @@ import { import { Event, EventAppointment, Participant, prisma, User } from "@repo/db"; import { cn } from "../../../../helper/cn"; import { inscribeToMoodleCourse, upsertParticipant } from "../actions"; -import { Clock10Icon, Cross } from "lucide-react"; +import { Check, Clock10Icon, Cross, EyeIcon } from "lucide-react"; import { useForm } from "react-hook-form"; import { EventAppointmentOptionalDefaults, @@ -22,6 +22,7 @@ import { Select } from "../../../_components/ui/Select"; import toast from "react-hot-toast"; import { useRouter } from "next/navigation"; import { JsonArray } from "../../../../../../packages/database/generated/client/runtime/library"; +import { eventCompleted } from "@repo/ui"; interface ModalBtnProps { title: string; @@ -90,10 +91,25 @@ const ModalBtn = ({ className={cn( "btn btn-outline btn-info btn-wide", event.type === "OBLIGATED_COURSE" && "btn-secondary", + eventCompleted(event, participant) && "btn-success", )} onClick={openModal} > - Anmelden + {participant && !eventCompleted(event, participant) && ( + <> + Anzeigen + + )} + {!participant && ( + <> + Anmelden + + )} + {eventCompleted(event, participant) && ( + <> + Abgeschlossen + + )}
diff --git a/apps/hub/app/(app)/page.tsx b/apps/hub/app/(app)/page.tsx index c1853ec0..1557459a 100644 --- a/apps/hub/app/(app)/page.tsx +++ b/apps/hub/app/(app)/page.tsx @@ -3,6 +3,7 @@ import { ArrowRight, NotebookText, Award, RocketIcon } from "lucide-react"; import Link from "next/link"; import Events from "./_components/Events"; import StatsClientWrapper from "./_components/StatsClientWrapper"; +import { Badges } from "./_components/Badges"; /* ✔️ Einlog-Zeit @@ -36,22 +37,7 @@ export default function Home() {
-
-
-

- - Verdiente Abzeichen - -

- Badges -
-
-
- -
-

- Laufende Events & Kurse -

+
diff --git a/apps/hub/app/_components/ui/DateInput.tsx b/apps/hub/app/_components/ui/DateInput.tsx new file mode 100644 index 00000000..a479bd65 --- /dev/null +++ b/apps/hub/app/_components/ui/DateInput.tsx @@ -0,0 +1,37 @@ +import DatePicker, { DatePickerProps, registerLocale } from "react-datepicker"; +import { + Control, + Controller, + FieldValues, + Path, + PathValue, +} from "react-hook-form"; +import { de } from "date-fns/locale"; +registerLocale("de", de); + +interface DateInputProps + extends Omit { + control: Control; + name: Path; +} + +export const DateInput = ({ + control, + name, + ...props +}: DateInputProps) => { + return ( + ( + field.onChange(date)} + selected={field.value} + {...(props as any)} + /> + )} + /> + ); +}; diff --git a/packages/ui/package.json b/packages/ui/package.json index 7b732477..b3d2bf57 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -3,9 +3,7 @@ "version": "0.0.0", "private": true, "exports": { - "./button": "./src/button.tsx", - "./card": "./src/card.tsx", - "./code": "./src/code.tsx" + ".": "./src/index.ts" }, "scripts": { "lint": "eslint . --max-warnings 0", @@ -14,6 +12,7 @@ }, "devDependencies": { "@repo/eslint-config": "*", + "@repo/db": "*", "@repo/typescript-config": "*", "@turbo/gen": "^1.12.4", "@types/node": "^20.11.24", diff --git a/packages/ui/src/.d.ts b/packages/ui/src/.d.ts new file mode 100644 index 00000000..5685bb40 --- /dev/null +++ b/packages/ui/src/.d.ts @@ -0,0 +1,4 @@ +declare module "*.png" { + const value: string; + export = value; +} diff --git a/packages/ui/src/Badge/Badge.tsx b/packages/ui/src/Badge/Badge.tsx new file mode 100644 index 00000000..d8aaf25f --- /dev/null +++ b/packages/ui/src/Badge/Badge.tsx @@ -0,0 +1,28 @@ +import { BADGES } from "@repo/db"; +import P1 from "./p-1.png"; +import P2 from "./p-2.png"; +import P3 from "./p-3.png"; +import D1 from "./d-1.png"; +import D2 from "./d-2.png"; +import D3 from "./d-3.png"; +import DAY1 from "./day-1-member.png"; + +const BadgeImage = { + [BADGES.P1]: P1, + [BADGES.P2]: P2, + [BADGES.P3]: P3, + [BADGES.D1]: D1, + [BADGES.D2]: D2, + [BADGES.D3]: D3, + [BADGES.DAY1]: DAY1, +}; + +export const Badge = ({ name }: { name: BADGES }) => { + const image = BadgeImage[name]; + + return ( + + + + ); +}; diff --git a/packages/ui/src/Badge/d-1.png b/packages/ui/src/Badge/d-1.png new file mode 100644 index 00000000..8fa5ec32 Binary files /dev/null and b/packages/ui/src/Badge/d-1.png differ diff --git a/packages/ui/src/Badge/d-2.png b/packages/ui/src/Badge/d-2.png new file mode 100644 index 00000000..d65b164c Binary files /dev/null and b/packages/ui/src/Badge/d-2.png differ diff --git a/packages/ui/src/Badge/d-3.png b/packages/ui/src/Badge/d-3.png new file mode 100644 index 00000000..e48113d7 Binary files /dev/null and b/packages/ui/src/Badge/d-3.png differ diff --git a/packages/ui/src/Badge/day-1-member.png b/packages/ui/src/Badge/day-1-member.png new file mode 100644 index 00000000..a0c383f8 Binary files /dev/null and b/packages/ui/src/Badge/day-1-member.png differ diff --git a/packages/ui/src/Badge/p-1.png b/packages/ui/src/Badge/p-1.png new file mode 100644 index 00000000..1e979605 Binary files /dev/null and b/packages/ui/src/Badge/p-1.png differ diff --git a/packages/ui/src/Badge/p-2.png b/packages/ui/src/Badge/p-2.png new file mode 100644 index 00000000..0ebe38bf Binary files /dev/null and b/packages/ui/src/Badge/p-2.png differ diff --git a/packages/ui/src/Badge/p-3.png b/packages/ui/src/Badge/p-3.png new file mode 100644 index 00000000..d83d0db0 Binary files /dev/null and b/packages/ui/src/Badge/p-3.png differ diff --git a/packages/ui/src/button.tsx b/packages/ui/src/button.tsx deleted file mode 100644 index 61fbd73b..00000000 --- a/packages/ui/src/button.tsx +++ /dev/null @@ -1,20 +0,0 @@ -"use client"; - -import { ReactNode } from "react"; - -interface ButtonProps { - children: ReactNode; - className?: string; - appName: string; -} - -export const Button = ({ children, className, appName }: ButtonProps) => { - return ( - - ); -}; diff --git a/packages/ui/src/card.tsx b/packages/ui/src/card.tsx deleted file mode 100644 index f9dc6828..00000000 --- a/packages/ui/src/card.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { type JSX } from "react"; - -export function Card({ - className, - title, - children, - href, -}: { - className?: string; - title: string; - children: React.ReactNode; - href: string; -}): JSX.Element { - return ( - -

- {title} -> -

-

{children}

-
- ); -} diff --git a/packages/ui/src/code.tsx b/packages/ui/src/code.tsx deleted file mode 100644 index 6659630f..00000000 --- a/packages/ui/src/code.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { type JSX } from "react"; - -export function Code({ - children, - className, -}: { - children: React.ReactNode; - className?: string; -}): JSX.Element { - return {children}; -} diff --git a/apps/hub/helper/event.ts b/packages/ui/src/helper/event.ts similarity index 67% rename from apps/hub/helper/event.ts rename to packages/ui/src/helper/event.ts index 11537fd7..bcc6236f 100644 --- a/apps/hub/helper/event.ts +++ b/packages/ui/src/helper/event.ts @@ -1,9 +1,7 @@ import { Event, Participant } from "@repo/db"; -export const participantCompleted = ( - event: Event, - participant: Participant, -) => { +export const eventCompleted = (event: Event, participant?: Participant) => { + if (!participant) return false; if (event.finisherMoodleCourseId && !participant.finisherMoodleCurseCompleted) return false; if (event.hasPresenceEvents && !participant.attended) return false; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts new file mode 100644 index 00000000..5c19ae83 --- /dev/null +++ b/packages/ui/src/index.ts @@ -0,0 +1,2 @@ +export * from "./Badge/Badge"; +export * from "./helper/event"; diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index fa5b1b3f..4fe4652a 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -1,8 +1,9 @@ { "extends": "@repo/typescript-config/react-library.json", "compilerOptions": { - "outDir": "dist" + "outDir": "dist", + "allowImportingTsExtensions": false }, - "include": ["src"], + "include": ["src", "src/.d.ts"], "exclude": ["node_modules", "dist"] }