v2.0.8 #156
@@ -1,6 +1,7 @@
|
|||||||
import { DISCORD_ROLES, MissionLog, NotificationPayload, prisma } from "@repo/db";
|
import { DISCORD_ROLES, MissionLog, NotificationPayload, prisma } from "@repo/db";
|
||||||
import { io } from "index";
|
import { io } from "index";
|
||||||
import cron from "node-cron";
|
import cron from "node-cron";
|
||||||
|
import { setUserStandardNamePermissions } from "routes/helper";
|
||||||
import { changeMemberRoles } from "routes/member";
|
import { changeMemberRoles } from "routes/member";
|
||||||
|
|
||||||
const removeMission = async (id: number, reason: string) => {
|
const removeMission = async (id: number, reason: string) => {
|
||||||
@@ -141,21 +142,12 @@ const removeConnectedAircrafts = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
const removePermissionsForBannedUsers = async () => {
|
const removePermissionsForBannedUsers = async () => {
|
||||||
const activePenalties = await prisma.penalty.findMany({
|
const removePermissionsPenaltys = await prisma.penalty.findMany({
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
removePermissionApplied: false,
|
||||||
{
|
User: {
|
||||||
type: "BAN",
|
DiscordAccount: { isNot: null },
|
||||||
suspended: false,
|
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: "TIME_BAN",
|
|
||||||
suspended: false,
|
|
||||||
until: {
|
|
||||||
gt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
User: {
|
User: {
|
||||||
@@ -167,16 +159,33 @@ const removePermissionsForBannedUsers = async () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const penalty of activePenalties) {
|
const addPermissionsPenaltys = await prisma.penalty.findMany({
|
||||||
const user = penalty.User;
|
where: {
|
||||||
|
addPermissionApplied: false,
|
||||||
|
User: {
|
||||||
|
DiscordAccount: { isNot: null },
|
||||||
|
},
|
||||||
|
OR: [{ suspended: true }, { until: { lt: new Date().toISOString() } }],
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
User: {
|
||||||
|
include: {
|
||||||
|
DiscordAccount: true,
|
||||||
|
FormerDiscordAccounts: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const penalty of removePermissionsPenaltys) {
|
||||||
|
const user = penalty.User;
|
||||||
|
console.log(`Removing roles for user ${user.id} due to penalty ${penalty.id}`);
|
||||||
|
|
||||||
if (user.DiscordAccount) {
|
|
||||||
await changeMemberRoles(
|
await changeMemberRoles(
|
||||||
user.DiscordAccount.discordId,
|
user.DiscordAccount!.discordId,
|
||||||
[DISCORD_ROLES.PILOT, DISCORD_ROLES.DISPATCHER],
|
[DISCORD_ROLES.PILOT, DISCORD_ROLES.DISPATCHER],
|
||||||
"remove",
|
"remove",
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
for (const formerAccount of user.FormerDiscordAccounts) {
|
for (const formerAccount of user.FormerDiscordAccounts) {
|
||||||
await changeMemberRoles(
|
await changeMemberRoles(
|
||||||
@@ -185,15 +194,29 @@ const removePermissionsForBannedUsers = async () => {
|
|||||||
"remove",
|
"remove",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
await prisma.penalty.update({
|
||||||
|
where: { id: penalty.id },
|
||||||
|
data: { removePermissionApplied: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const penalty of addPermissionsPenaltys) {
|
||||||
|
console.log(`Restoring roles for user ${penalty.userId} due to penalty ${penalty.id}`);
|
||||||
|
await setUserStandardNamePermissions({
|
||||||
|
memberId: penalty.User.DiscordAccount!.discordId,
|
||||||
|
userId: penalty.userId,
|
||||||
|
});
|
||||||
|
await prisma.penalty.update({
|
||||||
|
where: { id: penalty.id },
|
||||||
|
data: { addPermissionApplied: true },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
cron.schedule("*/5 * * * *", async () => {
|
removePermissionsForBannedUsers();
|
||||||
await removePermissionsForBannedUsers();
|
|
||||||
});
|
|
||||||
|
|
||||||
cron.schedule("*/1 * * * *", async () => {
|
cron.schedule("*/1 * * * *", async () => {
|
||||||
try {
|
try {
|
||||||
|
await removePermissionsForBannedUsers();
|
||||||
await removeClosedMissions();
|
await removeClosedMissions();
|
||||||
await removeConnectedAircrafts();
|
await removeConnectedAircrafts();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -7,22 +7,25 @@ const router: Router = Router();
|
|||||||
export const eventCompleted = (event: Event, participant?: Participant) => {
|
export const eventCompleted = (event: Event, participant?: Participant) => {
|
||||||
if (!participant) return false;
|
if (!participant) return false;
|
||||||
if (event.finisherMoodleCourseId && !participant.finisherMoodleCurseCompleted) return false;
|
if (event.finisherMoodleCourseId && !participant.finisherMoodleCurseCompleted) return false;
|
||||||
if (event.hasPresenceEvents && !participant.attended) return false;
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
router.post("/set-standard-name", async (req, res) => {
|
export const setUserStandardNamePermissions = async ({
|
||||||
const { memberId, userId } = req.body;
|
memberId,
|
||||||
|
userId,
|
||||||
|
}: {
|
||||||
|
memberId: string;
|
||||||
|
userId: string;
|
||||||
|
}) => {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: userId,
|
id: userId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!user) {
|
if (!user) {
|
||||||
res.status(404).json({ error: "User not found" });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const participant = await prisma.participant.findMany({
|
const participant = await prisma.participant.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -73,6 +76,13 @@ router.post("/set-standard-name", async (req, res) => {
|
|||||||
await changeMemberRoles(memberId, [DISCORD_ROLES.PILOT], isPilot ? "add" : "remove");
|
await changeMemberRoles(memberId, [DISCORD_ROLES.PILOT], isPilot ? "add" : "remove");
|
||||||
await changeMemberRoles(memberId, [DISCORD_ROLES.DISPATCHER], isDispatcher ? "add" : "remove");
|
await changeMemberRoles(memberId, [DISCORD_ROLES.DISPATCHER], isDispatcher ? "add" : "remove");
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
router.post("/set-standard-name", async (req, res) => {
|
||||||
|
const { memberId, userId } = req.body;
|
||||||
|
|
||||||
|
await setUserStandardNamePermissions({ memberId, userId });
|
||||||
|
res.status(200).json({ message: "Standard name and permissions set" });
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ export const getMember = async (memberId: string) => {
|
|||||||
const guild = client.guilds.cache.get(GUILD_ID);
|
const guild = client.guilds.cache.get(GUILD_ID);
|
||||||
if (!guild) throw new Error("Guild not found");
|
if (!guild) throw new Error("Guild not found");
|
||||||
try {
|
try {
|
||||||
return guild.members.cache.get(memberId) ?? (await guild.members.fetch(memberId));
|
let member = guild.members.cache.get(memberId);
|
||||||
|
if (!member) {
|
||||||
|
member = await guild.members.fetch(memberId);
|
||||||
|
}
|
||||||
|
return member;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching member:", error);
|
console.error("Error fetching member:", error);
|
||||||
throw new Error("Member not found");
|
throw new Error("Member not found");
|
||||||
|
|||||||
@@ -75,5 +75,6 @@ USER nextjs
|
|||||||
|
|
||||||
# Expose the application port
|
# Expose the application port
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
ENV HOST=0.0.0.0
|
||||||
|
|
||||||
CMD ["node", "apps/dispatch/server.js"]
|
CMD ["node", "apps/dispatch/server.js"]
|
||||||
32
apps/dispatch/app/(app)/_components/Navbar.tsx
Normal file
32
apps/dispatch/app/(app)/_components/Navbar.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { ExitIcon } from "@radix-ui/react-icons";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { prisma } from "@repo/db";
|
||||||
|
import { ChangelogWrapper } from "_components/navbar/ChangelogWrapper";
|
||||||
|
import ModeSwitchDropdown from "_components/navbar/ModeSwitchDropdown";
|
||||||
|
|
||||||
|
export default async function Navbar({ children }: { children: React.ReactNode }) {
|
||||||
|
const latestChangelog = await prisma.changelog.findFirst({
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "desc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div className="navbar bg-base-100 flex justify-between gap-5 shadow-sm">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div>
|
||||||
|
<p className="text-xl font-semibold normal-case">VAR Operations Center</p>
|
||||||
|
<ChangelogWrapper latestChangelog={latestChangelog} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{children}
|
||||||
|
<ModeSwitchDropdown className="dropdown-center" btnClassName="btn-ghost" />
|
||||||
|
<Link href={"/logout"}>
|
||||||
|
<button className="btn btn-ghost">
|
||||||
|
<ExitIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
"use client";
|
"use client";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useDispatchConnectionStore } from "../../../../../_store/dispatch/connectionStore";
|
import { useDispatchConnectionStore } from "../../../../_store/dispatch/connectionStore";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { Prisma } from "@repo/db";
|
import { Prisma } from "@repo/db";
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import { Connection } from "./_components/Connection";
|
|
||||||
import { Audio } from "../../../../_components/Audio/Audio";
|
|
||||||
import { ExitIcon, ExternalLinkIcon } from "@radix-ui/react-icons";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { Settings } from "./_components/Settings";
|
|
||||||
import AdminPanel from "_components/navbar/AdminPanel";
|
|
||||||
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
|
||||||
import { WarningAlert } from "_components/navbar/PageAlert";
|
|
||||||
import { Radar } from "lucide-react";
|
|
||||||
import { ChangelogWrapper } from "_components/navbar/ChangelogWrapper";
|
|
||||||
import { prisma } from "@repo/db";
|
|
||||||
|
|
||||||
export default async function Navbar() {
|
|
||||||
const session = await getServerSession();
|
|
||||||
const latestChangelog = await prisma.changelog.findFirst({
|
|
||||||
orderBy: {
|
|
||||||
createdAt: "desc",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="navbar bg-base-100 flex justify-between gap-5 shadow-sm">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div>
|
|
||||||
<p className="text-xl font-semibold normal-case">VAR Leitstelle</p>
|
|
||||||
<ChangelogWrapper latestChangelog={latestChangelog} />
|
|
||||||
</div>
|
|
||||||
{session?.user.permissions.includes("ADMIN_KICK") && <AdminPanel />}
|
|
||||||
</div>
|
|
||||||
<WarningAlert />
|
|
||||||
<div className="flex items-center gap-5">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Audio />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Connection />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Settings />
|
|
||||||
<Link href={"/tracker"} target="_blank" rel="noopener noreferrer">
|
|
||||||
<button className="btn btn-ghost">
|
|
||||||
<Radar size={19} /> Tracker
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href={process.env.NEXT_PUBLIC_HUB_URL || "#!"}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<button className="btn btn-ghost">
|
|
||||||
<ExternalLinkIcon className="h-4 w-4" /> HUB
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
<Link href={"/logout"}>
|
|
||||||
<button className="btn btn-ghost">
|
|
||||||
<ExitIcon className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { MoonIcon, SunIcon } from "@radix-ui/react-icons";
|
|
||||||
|
|
||||||
interface ThemeSwapProps {
|
|
||||||
isDark: boolean;
|
|
||||||
toggleTheme: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ThemeSwap: React.FC<ThemeSwapProps> = ({
|
|
||||||
isDark,
|
|
||||||
toggleTheme,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<label className="swap swap-rotate">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="theme-controller"
|
|
||||||
checked={isDark}
|
|
||||||
onChange={toggleTheme}
|
|
||||||
/>
|
|
||||||
<MoonIcon className="swap-off h-5 w-5 fill-current" />
|
|
||||||
<SunIcon className="swap-on h-5 w-5 fill-current" />
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Navbar from "./_components/navbar/Navbar";
|
|
||||||
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
||||||
import { Error } from "_components/Error";
|
import { Error } from "_components/Error";
|
||||||
|
import Navbar from "(app)/_components/Navbar";
|
||||||
|
import { Audio } from "_components/Audio/Audio";
|
||||||
|
import { Connection } from "./_components/navbar/Connection";
|
||||||
|
import { Settings } from "./_components/navbar/Settings";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "VAR: Disponent",
|
title: "VAR: Disponent",
|
||||||
@@ -26,7 +29,11 @@ export default async function RootLayout({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar />
|
<Navbar>
|
||||||
|
<Audio />
|
||||||
|
<Connection />
|
||||||
|
<Settings />
|
||||||
|
</Navbar>
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
import { Connection } from "./_components/Connection";
|
|
||||||
import { Audio } from "_components/Audio/Audio";
|
|
||||||
import { ExitIcon, ExternalLinkIcon } from "@radix-ui/react-icons";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { Settings } from "./_components/Settings";
|
|
||||||
import { WarningAlert } from "_components/navbar/PageAlert";
|
|
||||||
import { Radar } from "lucide-react";
|
|
||||||
import { prisma } from "@repo/db";
|
|
||||||
import { ChangelogWrapper } from "_components/navbar/ChangelogWrapper";
|
|
||||||
|
|
||||||
export default async function Navbar() {
|
|
||||||
const latestChangelog = await prisma.changelog.findFirst({
|
|
||||||
orderBy: {
|
|
||||||
createdAt: "desc",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<div className="navbar bg-base-100 flex justify-between gap-5 shadow-sm">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div>
|
|
||||||
<p className="text-xl font-semibold normal-case">VAR Operations Center</p>
|
|
||||||
<ChangelogWrapper latestChangelog={latestChangelog} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<WarningAlert />
|
|
||||||
<div className="flex items-center gap-5">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Audio />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Connection />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Settings />
|
|
||||||
<Link href={"/tracker"} target="_blank" rel="noopener noreferrer">
|
|
||||||
<button className="btn btn-ghost">
|
|
||||||
<Radar size={19} /> Tracker
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href={process.env.NEXT_PUBLIC_HUB_URL || "#!"}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<button className="btn btn-ghost">
|
|
||||||
<ExternalLinkIcon className="h-4 w-4" /> HUB
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
<Link href={"/logout"}>
|
|
||||||
<button className="btn btn-ghost">
|
|
||||||
<ExitIcon className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { MoonIcon, SunIcon } from "@radix-ui/react-icons";
|
|
||||||
|
|
||||||
interface ThemeSwapProps {
|
|
||||||
isDark: boolean;
|
|
||||||
toggleTheme: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ThemeSwap: React.FC<ThemeSwapProps> = ({
|
|
||||||
isDark,
|
|
||||||
toggleTheme,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<label className="swap swap-rotate">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="theme-controller"
|
|
||||||
checked={isDark}
|
|
||||||
onChange={toggleTheme}
|
|
||||||
/>
|
|
||||||
<MoonIcon className="swap-off h-5 w-5 fill-current" />
|
|
||||||
<SunIcon className="swap-on h-5 w-5 fill-current" />
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Navbar from "./_components/navbar/Navbar";
|
|
||||||
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
||||||
import { Error } from "_components/Error";
|
import { Error } from "_components/Error";
|
||||||
|
import Navbar from "(app)/_components/Navbar";
|
||||||
|
import { Audio } from "_components/Audio/Audio";
|
||||||
|
import { Connection } from "./_components/navbar/Connection";
|
||||||
|
import { Settings } from "./_components/navbar/Settings";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "VAR: Pilot",
|
title: "VAR: Pilot",
|
||||||
@@ -26,7 +29,11 @@ export default async function RootLayout({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar />
|
<Navbar>
|
||||||
|
<Audio />
|
||||||
|
<Connection />
|
||||||
|
<Settings />
|
||||||
|
</Navbar>
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { cn } from "@repo/shared-components";
|
import { cn } from "@repo/shared-components";
|
||||||
import { ArrowLeftRight, Plane, Radar, Workflow } from "lucide-react";
|
import { ArrowLeftRight, ExternalLinkIcon, Plane, Radar, Workflow } from "lucide-react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
export default function ModeSwitchDropdown({ className }: { className?: string }) {
|
export default function ModeSwitchDropdown({
|
||||||
|
className,
|
||||||
|
btnClassName,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
btnClassName?: string;
|
||||||
|
}) {
|
||||||
const path = usePathname();
|
const path = usePathname();
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("dropdown z-999999", className)}>
|
<div className={cn("dropdown z-999999", className)}>
|
||||||
<div tabIndex={0} role="button" className="btn m-1">
|
<div tabIndex={0} role="button" className={cn("btn", btnClassName)}>
|
||||||
<ArrowLeftRight size={22} /> {path.includes("pilot") && "Pilot"}
|
<ArrowLeftRight size={22} /> {path.includes("pilot") && "Pilot"}
|
||||||
{path.includes("dispatch") && "Leitstelle"}
|
{path.includes("dispatch") && "Leitstelle"}
|
||||||
</div>
|
</div>
|
||||||
@@ -39,6 +45,15 @@ export default function ModeSwitchDropdown({ className }: { className?: string }
|
|||||||
<Radar size={22} /> Tracker
|
<Radar size={22} /> Tracker
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
href={process.env.NEXT_PUBLIC_HUB_URL || "#!"}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<ExternalLinkIcon size={22} /> HUB
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export const removeRolesFromMember = async (memberId: string, roleIds: string[])
|
|||||||
console.error("Error removing roles from member:", error);
|
console.error("Error removing roles from member:", error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setStandardName = async ({
|
export const setStandardName = async ({
|
||||||
memberId,
|
memberId,
|
||||||
userId,
|
userId,
|
||||||
|
|||||||
@@ -66,5 +66,6 @@ USER nextjs
|
|||||||
|
|
||||||
# Expose the application port
|
# Expose the application port
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
ENV HOST=0.0.0.0
|
||||||
|
|
||||||
CMD ["node", "apps/hub/server.js"]
|
CMD ["node", "apps/hub/server.js"]
|
||||||
@@ -23,15 +23,6 @@ const page = async () => {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Appointments: {
|
|
||||||
include: {
|
|
||||||
Participants: {
|
|
||||||
where: {
|
|
||||||
appointmentCancelled: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,23 +38,13 @@ const page = async () => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="col-span-full">
|
<div className="col-span-full">
|
||||||
<p className="text-xl font-semibold text-left flex items-center gap-2 mb-2 mt-5">
|
<p className="mb-2 mt-5 flex items-center gap-2 text-left text-xl font-semibold">
|
||||||
<RocketIcon className="w-4 h-4" /> Laufende Events & Kurse
|
<RocketIcon className="h-4 w-4" /> Laufende Events & Kurse
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-6 gap-4">
|
<div className="grid grid-cols-6 gap-4">
|
||||||
{filteredEvents.map((event) => {
|
{filteredEvents.map((event) => {
|
||||||
return (
|
return <EventCard user={user} event={event} key={event.id} />;
|
||||||
<EventCard
|
|
||||||
appointments={event.Appointments}
|
|
||||||
selectedAppointments={event.Appointments.filter((a) =>
|
|
||||||
a.Participants.find((p) => p.userId == user.id),
|
|
||||||
)}
|
|
||||||
user={user}
|
|
||||||
event={event}
|
|
||||||
key={event.id}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,18 +20,18 @@ const PathsOptions = ({
|
|||||||
<div className="flex gap-6">
|
<div className="flex gap-6">
|
||||||
{/* Disponent Card */}
|
{/* Disponent Card */}
|
||||||
<div
|
<div
|
||||||
className={`cursor-pointer border rounded-lg p-6 w-80 transition-colors ${
|
className={`w-80 cursor-pointer rounded-lg border p-6 transition-colors ${
|
||||||
selected === "disponent" ? "border-info ring-2 ring-info" : "border-base-300"
|
selected === "disponent" ? "border-info ring-info ring-2" : "border-base-300"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setSelected("disponent")}
|
onClick={() => setSelected("disponent")}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-pressed={selected === "disponent"}
|
aria-pressed={selected === "disponent"}
|
||||||
>
|
>
|
||||||
<h1 className="font-semibold text-lg mb-2 flex gap-2 justify-center items-center">
|
<h1 className="mb-2 flex items-center justify-center gap-2 text-lg font-semibold">
|
||||||
Disponent <Workflow />
|
Disponent <Workflow />
|
||||||
</h1>
|
</h1>
|
||||||
<div className="text-sm text-base-content/70">
|
<div className="text-base-content/70 text-sm">
|
||||||
Denkt sich realistische Einsatzszenarien aus, koordiniert deren Ablauf und ist die
|
Denkt sich realistische Einsatzszenarien aus, koordiniert deren Ablauf und ist die
|
||||||
zentrale Schnittstelle zwischen Piloten und bodengebundenen Rettungsmitteln. Er trägt
|
zentrale Schnittstelle zwischen Piloten und bodengebundenen Rettungsmitteln. Er trägt
|
||||||
die Verantwortung für einen reibungslosen Ablauf und der erfolgreichen Durchführung der
|
die Verantwortung für einen reibungslosen Ablauf und der erfolgreichen Durchführung der
|
||||||
@@ -43,18 +43,18 @@ const PathsOptions = ({
|
|||||||
</div>
|
</div>
|
||||||
{/* Pilot Card */}
|
{/* Pilot Card */}
|
||||||
<div
|
<div
|
||||||
className={`cursor-pointer border rounded-lg p-6 w-80 transition-colors ${
|
className={`w-80 cursor-pointer rounded-lg border p-6 transition-colors ${
|
||||||
selected === "pilot" ? "border-info ring-2 ring-info" : "border-base-300"
|
selected === "pilot" ? "border-info ring-info ring-2" : "border-base-300"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setSelected("pilot")}
|
onClick={() => setSelected("pilot")}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-pressed={selected === "pilot"}
|
aria-pressed={selected === "pilot"}
|
||||||
>
|
>
|
||||||
<h1 className="font-semibold text-lg mb-2 flex gap-2 justify-center items-center">
|
<h1 className="mb-2 flex items-center justify-center gap-2 text-lg font-semibold">
|
||||||
Pilot <Plane />
|
Pilot <Plane />
|
||||||
</h1>
|
</h1>
|
||||||
<div className="text-sm text-base-content/70">
|
<div className="text-base-content/70 text-sm">
|
||||||
Fliegt die vom Disponenten erstellten Einsätze und transportiert die Med-Crew sicher zum
|
Fliegt die vom Disponenten erstellten Einsätze und transportiert die Med-Crew sicher zum
|
||||||
Einsatzort. Er übernimmt die navigatorische Vorbereitung, achtet auf Wetterentwicklungen
|
Einsatzort. Er übernimmt die navigatorische Vorbereitung, achtet auf Wetterentwicklungen
|
||||||
und sorgt für die Sicherheit seiner Crew im Flug.
|
und sorgt für die Sicherheit seiner Crew im Flug.
|
||||||
@@ -76,17 +76,7 @@ const EventSelect = ({ pathSelected }: { pathSelected: "disponent" | "pilot" })
|
|||||||
const user = useSession().data?.user;
|
const user = useSession().data?.user;
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
return events?.map((event) => {
|
return events?.map((event) => {
|
||||||
return (
|
return <EventCard user={user} event={event} key={event.id} />;
|
||||||
<EventCard
|
|
||||||
appointments={event.Appointments}
|
|
||||||
selectedAppointments={event.Appointments.filter((a) =>
|
|
||||||
a.Participants.find((p) => p.userId == user.id),
|
|
||||||
)}
|
|
||||||
user={user}
|
|
||||||
event={event}
|
|
||||||
key={event.id}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -107,14 +97,14 @@ export const FirstPath = () => {
|
|||||||
return (
|
return (
|
||||||
<dialog ref={modalRef} className="modal">
|
<dialog ref={modalRef} className="modal">
|
||||||
<div className="modal-box w-11/12 max-w-5xl">
|
<div className="modal-box w-11/12 max-w-5xl">
|
||||||
<h3 className="flex items-center gap-2 text-lg font-bold mb-10">
|
<h3 className="mb-10 flex items-center gap-2 text-lg font-bold">
|
||||||
{session?.user.migratedFromV1
|
{session?.user.migratedFromV1
|
||||||
? "Hallo, hier hat sich einiges geändert!"
|
? "Hallo, hier hat sich einiges geändert!"
|
||||||
: "Wähle deinen Einstieg!"}
|
: "Wähle deinen Einstieg!"}
|
||||||
</h3>
|
</h3>
|
||||||
<h2 className="text-2xl font-bold mb-4 text-center">Willkommen bei Virtual Air Rescue!</h2>
|
<h2 className="mb-4 text-center text-2xl font-bold">Willkommen bei Virtual Air Rescue!</h2>
|
||||||
{session?.user.migratedFromV1 ? (
|
{session?.user.migratedFromV1 ? (
|
||||||
<p className="mb-8 text-base text-base-content/80 text-center">
|
<p className="text-base-content/80 mb-8 text-center text-base">
|
||||||
Dein Account wurde erfolgreich auf das neue System migriert. Herzlich Willkommen im
|
Dein Account wurde erfolgreich auf das neue System migriert. Herzlich Willkommen im
|
||||||
neuen HUB! Um die Erfahrung für alle Nutzer zu steigern haben wir uns dazu entschlossen,
|
neuen HUB! Um die Erfahrung für alle Nutzer zu steigern haben wir uns dazu entschlossen,
|
||||||
dass alle Nutzer einen Test absolvieren müssen:{" "}
|
dass alle Nutzer einen Test absolvieren müssen:{" "}
|
||||||
@@ -129,12 +119,12 @@ export const FirstPath = () => {
|
|||||||
ausprobieren, wenn du möchtest.
|
ausprobieren, wenn du möchtest.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col items-center justify-center m-20">
|
<div className="m-20 flex flex-col items-center justify-center">
|
||||||
{page === "path" && <PathsOptions selected={selected} setSelected={setSelected} />}
|
{page === "path" && <PathsOptions selected={selected} setSelected={setSelected} />}
|
||||||
{page === "event-select" && (
|
{page === "event-select" && (
|
||||||
<div className="flex flex-col gap-3 min-w-[800px]">
|
<div className="flex min-w-[800px] flex-col gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-left text-gray-400 text-sm">Wähle dein Einführungs-Event aus:</p>
|
<p className="text-left text-sm text-gray-400">Wähle dein Einführungs-Event aus:</p>
|
||||||
</div>
|
</div>
|
||||||
<EventSelect pathSelected={selected!} />
|
<EventSelect pathSelected={selected!} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,23 +2,17 @@ import Image from "next/image";
|
|||||||
import { DiscordLogoIcon, InstagramLogoIcon, ReaderIcon } from "@radix-ui/react-icons";
|
import { DiscordLogoIcon, InstagramLogoIcon, ReaderIcon } from "@radix-ui/react-icons";
|
||||||
import YoutubeSvg from "./youtube_wider.svg";
|
import YoutubeSvg from "./youtube_wider.svg";
|
||||||
import FacebookSvg from "./facebook.svg";
|
import FacebookSvg from "./facebook.svg";
|
||||||
import { ChangelogModalBtn } from "@repo/shared-components";
|
|
||||||
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
|
||||||
import { updateUser } from "(app)/settings/actions";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import { ChangelogWrapper } from "(app)/_components/ChangelogWrapper";
|
import { ChangelogWrapper } from "(app)/_components/ChangelogWrapper";
|
||||||
import { prisma } from "@repo/db";
|
import { prisma } from "@repo/db";
|
||||||
|
|
||||||
export const Footer = async () => {
|
export const Footer = async () => {
|
||||||
const session = await getServerSession();
|
|
||||||
const latestChangelog = await prisma.changelog.findFirst({
|
const latestChangelog = await prisma.changelog.findFirst({
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: "desc",
|
createdAt: "desc",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const autoOpen = !session?.user.changelogAck && !!latestChangelog;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="footer bg-base-200 mt-4 flex items-center justify-between rounded-lg p-4 shadow-md">
|
<footer className="footer bg-base-200 mt-4 flex items-center justify-between rounded-lg p-4 shadow-md">
|
||||||
{/* Left: Impressum & Datenschutz */}
|
{/* Left: Impressum & Datenschutz */}
|
||||||
@@ -39,7 +33,7 @@ export const Footer = async () => {
|
|||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className="tooltip tooltip-top" data-tip="Discord">
|
<div className="tooltip tooltip-top" data-tip="Discord">
|
||||||
<a
|
<a
|
||||||
href="https://discord.gg/yn7uXmmNnG"
|
href="https://discord.gg/virtualairrescue"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="hover:text-primary"
|
className="hover:text-primary"
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { prisma } from "@repo/db";
|
||||||
|
import { ParticipantForm } from "../../../_components/ParticipantForm";
|
||||||
|
import { Error } from "_components/Error";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { PersonIcon } from "@radix-ui/react-icons";
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
|
||||||
|
export default async function Page({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ id: string; participantId: string }>;
|
||||||
|
}) {
|
||||||
|
const { id: eventId, participantId } = await params;
|
||||||
|
console.log(eventId, participantId);
|
||||||
|
|
||||||
|
const event = await prisma.event.findUnique({
|
||||||
|
where: { id: parseInt(eventId) },
|
||||||
|
});
|
||||||
|
|
||||||
|
const participant = await prisma.participant.findUnique({
|
||||||
|
where: { id: parseInt(participantId) },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!participant) {
|
||||||
|
return <Error title="Teilnehmer nicht gefunden" statusCode={404} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: participant?.userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!event) return <Error title="Event nicht gefunden" statusCode={404} />;
|
||||||
|
|
||||||
|
if (!participant || !user) {
|
||||||
|
return <Error title="Teilnehmer nicht gefunden" statusCode={404} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="my-3">
|
||||||
|
<div className="text-left">
|
||||||
|
<Link href={`/admin/event/${event.id}`} className="link-hover l-0 text-gray-500">
|
||||||
|
<ArrowLeft className="mb-1 mr-1 inline h-4 w-4" />
|
||||||
|
Zurück zum Event
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<p className="text-left text-2xl font-semibold">
|
||||||
|
<PersonIcon className="mr-2 inline h-5 w-5" /> Event-Übersicht für{" "}
|
||||||
|
{`${user.firstname} ${user.lastname} #${user.publicId}`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ParticipantForm event={event} participant={participant} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
import { Event, Participant, Prisma } from "@repo/db";
|
|
||||||
import { EventAppointmentOptionalDefaults, InputJsonValueType } from "@repo/db/zod";
|
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
|
||||||
import { useSession } from "next-auth/react";
|
|
||||||
import { RefObject, useRef } from "react";
|
|
||||||
import { UseFormReturn } from "react-hook-form";
|
|
||||||
import { PaginatedTable, PaginatedTableRef } from "../../../../_components/PaginatedTable";
|
|
||||||
import { Button } from "../../../../_components/ui/Button";
|
|
||||||
import { DateInput } from "../../../../_components/ui/DateInput";
|
|
||||||
import { upsertParticipant } from "../../../events/actions";
|
|
||||||
import { deleteAppoinement, upsertAppointment } from "../action";
|
|
||||||
import { handleParticipantFinished } from "../../../../../helper/events";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
|
|
||||||
interface AppointmentModalProps {
|
|
||||||
event?: Event;
|
|
||||||
ref: RefObject<HTMLDialogElement | null>;
|
|
||||||
participantModal: RefObject<HTMLDialogElement | null>;
|
|
||||||
appointmentsTableRef: React.RefObject<PaginatedTableRef | null>;
|
|
||||||
appointmentForm: UseFormReturn<EventAppointmentOptionalDefaults>;
|
|
||||||
participantForm: UseFormReturn<Participant>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AppointmentModal = ({
|
|
||||||
event,
|
|
||||||
ref,
|
|
||||||
participantModal,
|
|
||||||
appointmentsTableRef,
|
|
||||||
appointmentForm,
|
|
||||||
participantForm,
|
|
||||||
}: AppointmentModalProps) => {
|
|
||||||
const { data: session } = useSession();
|
|
||||||
|
|
||||||
const participantTableRef = useRef<PaginatedTableRef>(null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<dialog ref={ref} className="modal">
|
|
||||||
<div className="modal-box min-h-[500px] min-w-[900px]">
|
|
||||||
<form method="dialog">
|
|
||||||
{/* if there is a button in form, it will close the modal */}
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
|
|
||||||
onClick={() => ref.current?.close()}
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form
|
|
||||||
onSubmit={appointmentForm.handleSubmit(async (values) => {
|
|
||||||
if (!event) return;
|
|
||||||
await upsertAppointment(values);
|
|
||||||
ref.current?.close();
|
|
||||||
appointmentsTableRef.current?.refresh();
|
|
||||||
})}
|
|
||||||
className="flex flex-col"
|
|
||||||
>
|
|
||||||
<div className="mr-7 flex justify-between">
|
|
||||||
<h3 className="text-lg font-bold">Termin {appointmentForm.watch("id")}</h3>
|
|
||||||
<DateInput
|
|
||||||
value={new Date(appointmentForm.watch("appointmentDate") || Date.now())}
|
|
||||||
onChange={(date) => appointmentForm.setValue("appointmentDate", date)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<PaginatedTable
|
|
||||||
supressQuery={appointmentForm.watch("id") === undefined}
|
|
||||||
ref={participantTableRef}
|
|
||||||
columns={
|
|
||||||
[
|
|
||||||
{
|
|
||||||
accessorKey: "User.firstname",
|
|
||||||
header: "Vorname",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "User.lastname",
|
|
||||||
header: "Nachname",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "enscriptionDate",
|
|
||||||
header: "Einschreibedatum",
|
|
||||||
cell: ({ row }) => {
|
|
||||||
return <span>{new Date(row.original.enscriptionDate).toLocaleString()}</span>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Anwesend",
|
|
||||||
cell: ({ row }) => {
|
|
||||||
if (row.original.attended) {
|
|
||||||
return <span className="text-green-500">Ja</span>;
|
|
||||||
} else if (row.original.appointmentCancelled) {
|
|
||||||
return <span className="text-red-500">Nein (Termin abgesagt)</span>;
|
|
||||||
} else {
|
|
||||||
return <span>?</span>;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Aktion",
|
|
||||||
|
|
||||||
cell: ({ row }) => {
|
|
||||||
return (
|
|
||||||
<div className="space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
participantForm.reset(row.original);
|
|
||||||
participantModal.current?.showModal();
|
|
||||||
}}
|
|
||||||
className="btn btn-outline btn-sm"
|
|
||||||
>
|
|
||||||
anzeigen
|
|
||||||
</button>
|
|
||||||
{!row.original.attended && event?.hasPresenceEvents && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onSubmit={() => {}}
|
|
||||||
onClick={async () => {
|
|
||||||
await upsertParticipant({
|
|
||||||
eventId: event!.id,
|
|
||||||
userId: row.original.userId,
|
|
||||||
attended: true,
|
|
||||||
appointmentCancelled: false,
|
|
||||||
});
|
|
||||||
if (!event.finisherMoodleCourseId?.length) {
|
|
||||||
toast(
|
|
||||||
"Teilnehmer hat das event abgeschlossen, workflow ausgeführt",
|
|
||||||
);
|
|
||||||
await handleParticipantFinished(row.original.id.toString());
|
|
||||||
}
|
|
||||||
participantTableRef.current?.refresh();
|
|
||||||
}}
|
|
||||||
className="btn btn-outline btn-info btn-sm"
|
|
||||||
>
|
|
||||||
Anwesend
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{!row.original.appointmentCancelled && event?.hasPresenceEvents && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onSubmit={() => {}}
|
|
||||||
onClick={async () => {
|
|
||||||
await upsertParticipant({
|
|
||||||
eventId: event!.id,
|
|
||||||
userId: row.original.userId,
|
|
||||||
attended: false,
|
|
||||||
appointmentCancelled: true,
|
|
||||||
statusLog: [
|
|
||||||
...(row.original.statusLog as InputJsonValueType[]),
|
|
||||||
{
|
|
||||||
event: "Gefehlt an Event",
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
user: `${session?.user?.firstname} ${session?.user?.lastname} - ${session?.user?.publicId}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
participantTableRef.current?.refresh();
|
|
||||||
}}
|
|
||||||
className="btn btn-outline btn-error btn-sm"
|
|
||||||
>
|
|
||||||
abwesend
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as ColumnDef<Participant>[]
|
|
||||||
}
|
|
||||||
prismaModel={"participant"}
|
|
||||||
getFilter={() =>
|
|
||||||
({
|
|
||||||
eventAppointmentId: appointmentForm.watch("id")!,
|
|
||||||
}) as Prisma.ParticipantWhereInput
|
|
||||||
}
|
|
||||||
include={{ User: true }}
|
|
||||||
leftOfPagination={
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button type="submit" className="btn btn-primary">
|
|
||||||
Speichern
|
|
||||||
</Button>
|
|
||||||
{appointmentForm.watch("id") && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onSubmit={() => {}}
|
|
||||||
onClick={async () => {
|
|
||||||
await deleteAppoinement(appointmentForm.watch("id")!);
|
|
||||||
ref.current?.close();
|
|
||||||
appointmentsTableRef.current?.refresh();
|
|
||||||
}}
|
|
||||||
className="btn btn-error btn-outline"
|
|
||||||
>
|
|
||||||
Löschen
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,70 +1,37 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { BADGES, Event, EVENT_TYPE, Participant, PERMISSION, Prisma, User } from "@repo/db";
|
import { BADGES, Event, EVENT_TYPE, Participant, PERMISSION, Prisma } from "@repo/db";
|
||||||
import {
|
import { EventOptionalDefaults, EventOptionalDefaultsSchema } from "@repo/db/zod";
|
||||||
EventAppointmentOptionalDefaults,
|
import { Bot, FileText, UserIcon } from "lucide-react";
|
||||||
EventAppointmentOptionalDefaultsSchema,
|
|
||||||
EventOptionalDefaults,
|
|
||||||
EventOptionalDefaultsSchema,
|
|
||||||
ParticipantSchema,
|
|
||||||
} from "@repo/db/zod";
|
|
||||||
import { Bot, Calendar, FileText, UserIcon } from "lucide-react";
|
|
||||||
import { useSession } from "next-auth/react";
|
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { useRef } from "react";
|
|
||||||
import "react-datepicker/dist/react-datepicker.css";
|
import "react-datepicker/dist/react-datepicker.css";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { PaginatedTable, PaginatedTableRef } from "../../../../_components/PaginatedTable";
|
|
||||||
import { Button } from "../../../../_components/ui/Button";
|
import { Button } from "../../../../_components/ui/Button";
|
||||||
import { Input } from "../../../../_components/ui/Input";
|
import { Input } from "../../../../_components/ui/Input";
|
||||||
import { MarkdownEditor } from "../../../../_components/ui/MDEditor";
|
import { MarkdownEditor } from "../../../../_components/ui/MDEditor";
|
||||||
import { Select } from "../../../../_components/ui/Select";
|
import { Select } from "../../../../_components/ui/Select";
|
||||||
import { Switch } from "../../../../_components/ui/Switch";
|
import { Switch } from "../../../../_components/ui/Switch";
|
||||||
import { deleteEvent, upsertEvent } from "../action";
|
import { deleteEvent, upsertEvent } from "../action";
|
||||||
import { AppointmentModal } from "./AppointmentModal";
|
|
||||||
import { ParticipantModal } from "./ParticipantModal";
|
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
import { PaginatedTable } from "_components/PaginatedTable";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import { User } from "next-auth";
|
||||||
|
|
||||||
export const Form = ({ event }: { event?: Event }) => {
|
export const Form = ({ event }: { event?: Event }) => {
|
||||||
const { data: session } = useSession();
|
|
||||||
const form = useForm<EventOptionalDefaults>({
|
const form = useForm<EventOptionalDefaults>({
|
||||||
resolver: zodResolver(EventOptionalDefaultsSchema),
|
resolver: zodResolver(EventOptionalDefaultsSchema),
|
||||||
defaultValues: event
|
defaultValues: event
|
||||||
? {
|
? {
|
||||||
...event,
|
...event,
|
||||||
discordRoleId: event.discordRoleId ?? undefined,
|
discordRoleId: event.discordRoleId ?? undefined,
|
||||||
maxParticipants: event.maxParticipants ?? undefined,
|
|
||||||
finisherMoodleCourseId: event.finisherMoodleCourseId ?? undefined,
|
finisherMoodleCourseId: event.finisherMoodleCourseId ?? undefined,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
const appointmentForm = useForm<EventAppointmentOptionalDefaults>({
|
|
||||||
resolver: zodResolver(EventAppointmentOptionalDefaultsSchema),
|
|
||||||
defaultValues: {
|
|
||||||
eventId: event?.id,
|
|
||||||
presenterId: session?.user?.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const participantForm = useForm<Participant>({
|
|
||||||
resolver: zodResolver(ParticipantSchema),
|
|
||||||
});
|
|
||||||
const appointmentsTableRef = useRef<PaginatedTableRef>(null);
|
|
||||||
const appointmentModal = useRef<HTMLDialogElement>(null);
|
|
||||||
const participantModal = useRef<HTMLDialogElement>(null);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppointmentModal
|
|
||||||
participantModal={participantModal}
|
|
||||||
participantForm={participantForm}
|
|
||||||
appointmentForm={appointmentForm}
|
|
||||||
ref={appointmentModal}
|
|
||||||
appointmentsTableRef={appointmentsTableRef}
|
|
||||||
event={event}
|
|
||||||
/>
|
|
||||||
<ParticipantModal participantForm={participantForm} ref={participantModal} />
|
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(async (values) => {
|
onSubmit={form.handleSubmit(async (values) => {
|
||||||
await upsertEvent(values, event?.id);
|
await upsertEvent(values, event?.id);
|
||||||
@@ -139,117 +106,11 @@ export const Form = ({ event }: { event?: Event }) => {
|
|||||||
label="Discord Rolle für eingeschriebene Teilnehmer"
|
label="Discord Rolle für eingeschriebene Teilnehmer"
|
||||||
className="input-sm"
|
className="input-sm"
|
||||||
/>
|
/>
|
||||||
<Input
|
|
||||||
form={form}
|
|
||||||
label="Maximale Teilnehmer (Nur für live Events)"
|
|
||||||
className="input-sm"
|
|
||||||
{...form.register("maxParticipants", {
|
|
||||||
valueAsNumber: true,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
<Switch form={form} name="hasPresenceEvents" label="Hat Live Event" />
|
|
||||||
<div className="divider w-full" />
|
<div className="divider w-full" />
|
||||||
|
|
||||||
<Switch form={form} name="hidden" label="Event verstecken" />
|
<Switch form={form} name="hidden" label="Event verstecken" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{form.watch("hasPresenceEvents") ? (
|
|
||||||
<div className="card bg-base-200 col-span-6 shadow-xl">
|
|
||||||
<div className="card-body">
|
|
||||||
<PaginatedTable
|
|
||||||
ref={appointmentsTableRef}
|
|
||||||
prismaModel={"eventAppointment"}
|
|
||||||
getFilter={() =>
|
|
||||||
({
|
|
||||||
eventId: event?.id,
|
|
||||||
}) as Prisma.EventAppointmentWhereInput
|
|
||||||
}
|
|
||||||
include={{
|
|
||||||
Presenter: true,
|
|
||||||
Participants: true,
|
|
||||||
}}
|
|
||||||
leftOfSearch={
|
|
||||||
<h2 className="card-title">
|
|
||||||
<Calendar className="h-5 w-5" /> Termine
|
|
||||||
</h2>
|
|
||||||
}
|
|
||||||
rightOfSearch={
|
|
||||||
event && (
|
|
||||||
<button
|
|
||||||
className="btn btn-primary btn-outline"
|
|
||||||
onClick={() => {
|
|
||||||
appointmentModal.current?.showModal();
|
|
||||||
appointmentForm.reset({
|
|
||||||
id: undefined,
|
|
||||||
eventId: event.id,
|
|
||||||
presenterId: session?.user?.id,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Hinzufügen
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
columns={
|
|
||||||
[
|
|
||||||
{
|
|
||||||
header: "Datum",
|
|
||||||
accessorKey: "appointmentDate",
|
|
||||||
accessorFn: (date) => new Date(date.appointmentDate).toLocaleString(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Presenter",
|
|
||||||
accessorKey: "presenter",
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="ml-2">
|
|
||||||
{row.original.Presenter.firstname} {row.original.Presenter.lastname}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Teilnehmer",
|
|
||||||
accessorKey: "Participants",
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="flex items-center">
|
|
||||||
<UserIcon className="h-5 w-5" />
|
|
||||||
<span className="ml-2">{row.original.Participants.length}</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Aktionen",
|
|
||||||
cell: ({ row }) => {
|
|
||||||
return (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
onSubmit={() => false}
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
appointmentForm.reset(row.original);
|
|
||||||
appointmentModal.current?.showModal();
|
|
||||||
}}
|
|
||||||
className="btn btn-sm btn-outline"
|
|
||||||
>
|
|
||||||
Bearbeiten
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as ColumnDef<
|
|
||||||
EventAppointmentOptionalDefaults & {
|
|
||||||
Presenter: User;
|
|
||||||
Participants: Participant[];
|
|
||||||
}
|
|
||||||
>[]
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{!form.watch("hasPresenceEvents") ? (
|
|
||||||
<div className="card bg-base-200 col-span-6 shadow-xl">
|
<div className="card bg-base-200 col-span-6 shadow-xl">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
{
|
{
|
||||||
@@ -259,7 +120,6 @@ export const Form = ({ event }: { event?: Event }) => {
|
|||||||
<UserIcon className="h-5 w-5" /> Teilnehmer
|
<UserIcon className="h-5 w-5" /> Teilnehmer
|
||||||
</h2>
|
</h2>
|
||||||
}
|
}
|
||||||
ref={appointmentsTableRef}
|
|
||||||
prismaModel={"participant"}
|
prismaModel={"participant"}
|
||||||
showSearch
|
showSearch
|
||||||
getFilter={(searchTerm) =>
|
getFilter={(searchTerm) =>
|
||||||
@@ -334,19 +194,18 @@ export const Form = ({ event }: { event?: Event }) => {
|
|||||||
header: "Aktionen",
|
header: "Aktionen",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2">
|
<Link
|
||||||
|
href={`/admin/event/${event?.id}/participant/${row.original.id}`}
|
||||||
|
className="flex gap-2"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
onSubmit={() => false}
|
onSubmit={() => false}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
|
||||||
participantForm.reset(row.original);
|
|
||||||
participantModal.current?.showModal();
|
|
||||||
}}
|
|
||||||
className="btn btn-sm btn-outline"
|
className="btn btn-sm btn-outline"
|
||||||
>
|
>
|
||||||
Bearbeiten
|
Ansehen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</Link>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -356,7 +215,6 @@ export const Form = ({ event }: { event?: Event }) => {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
|
||||||
<div className="card bg-base-200 col-span-6 shadow-xl">
|
<div className="card bg-base-200 col-span-6 shadow-xl">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<div className="flex w-full gap-4">
|
<div className="flex w-full gap-4">
|
||||||
|
|||||||
213
apps/hub/app/(app)/admin/event/_components/ParticipantForm.tsx
Normal file
213
apps/hub/app/(app)/admin/event/_components/ParticipantForm.tsx
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
"use client";
|
||||||
|
import { Participant, Event, ParticipantLog, Prisma } from "@repo/db";
|
||||||
|
import { Users, Activity, Bug } from "lucide-react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { InputJsonValueType, ParticipantOptionalDefaultsSchema } from "@repo/db/zod";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Switch } from "_components/ui/Switch";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "_components/ui/Button";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { upsertParticipant } from "(app)/events/actions";
|
||||||
|
import { deleteParticipant } from "../action";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
interface ParticipantFormProps {
|
||||||
|
event: Event;
|
||||||
|
participant: Participant;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkEventCompleted = (participant: Participant, event: Event): boolean => {
|
||||||
|
return event.finisherMoodleCourseId ? participant.finisherMoodleCurseCompleted : false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ParticipantForm = ({ event, participant }: ParticipantFormProps) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const upsertParticipantMutation = useMutation({
|
||||||
|
mutationKey: ["upsertParticipant"],
|
||||||
|
mutationFn: async (newData: Prisma.ParticipantUncheckedCreateInput) => {
|
||||||
|
const data = await upsertParticipant(newData);
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["participants", event.id] });
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteParticipantMutation = useMutation({
|
||||||
|
mutationKey: ["deleteParticipant"],
|
||||||
|
mutationFn: async (participantId: number) => {
|
||||||
|
await deleteParticipant(participantId);
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["participants", event.id] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventCompleted = checkEventCompleted(participant, event);
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
resolver: zodResolver(ParticipantOptionalDefaultsSchema),
|
||||||
|
defaultValues: participant,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleEventFinished = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await upsertParticipantMutation.mutateAsync({
|
||||||
|
eventId: event.id,
|
||||||
|
userId: participant.userId,
|
||||||
|
});
|
||||||
|
toast.success("Event als beendet markiert");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckMoodle = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
toast.success("Moodle-Check durchgeführt");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(async (formData) => {
|
||||||
|
const data = await upsertParticipantMutation.mutateAsync({
|
||||||
|
...formData,
|
||||||
|
statusLog: participant.statusLog as unknown as Prisma.ParticipantCreatestatusLogInput,
|
||||||
|
eventId: event.id,
|
||||||
|
userId: participant.userId,
|
||||||
|
});
|
||||||
|
form.reset(data);
|
||||||
|
toast.success("Teilnehmer aktualisiert");
|
||||||
|
})}
|
||||||
|
className="bg-base-100 flex flex-wrap gap-6 p-6"
|
||||||
|
>
|
||||||
|
{/* Status Section */}
|
||||||
|
<div className="card bg-base-200 shadow">
|
||||||
|
<div className="card-body">
|
||||||
|
<h3 className="card-title text-sm">
|
||||||
|
<Bug className="mr-2 inline h-5 w-5" />
|
||||||
|
Debug
|
||||||
|
</h3>
|
||||||
|
<Switch form={form} name={"attended"} label="Anwesend" />
|
||||||
|
<Switch form={form} name={"appointmentCancelled"} label="Termin Abgesagt" />
|
||||||
|
<Switch
|
||||||
|
form={form}
|
||||||
|
name={"finisherMoodleCurseCompleted"}
|
||||||
|
label="Moodle Kurs abgeschlossen"
|
||||||
|
/>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Info Card */}
|
||||||
|
<div className="card bg-base-200 min-w-[200px] flex-1 shadow-lg">
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title text-lg">
|
||||||
|
<Users className="h-5 w-5" /> Informationen
|
||||||
|
</h2>
|
||||||
|
<div className="divider" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600">Kurs-status</p>
|
||||||
|
<p className="font-semibold">{eventCompleted ? "Abgeschlossen" : "In Bearbeitung"}</p>
|
||||||
|
<p className="text-sm text-gray-600">Einschreibedatum</p>
|
||||||
|
<p className="font-semibold">
|
||||||
|
{participant?.enscriptionDate
|
||||||
|
? new Date(participant.enscriptionDate).toLocaleDateString()
|
||||||
|
: "-"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="divider" />
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleEventFinished}
|
||||||
|
isLoading={isLoading}
|
||||||
|
className="btn btn-sm btn-success"
|
||||||
|
>
|
||||||
|
Event beendet
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleCheckMoodle}
|
||||||
|
isLoading={isLoading}
|
||||||
|
className="btn btn-sm btn-info"
|
||||||
|
>
|
||||||
|
Moodle-Check
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Activity Log */}
|
||||||
|
<div className="card bg-base-200 min-w-[300px] flex-1 shadow-lg">
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title text-lg">
|
||||||
|
<Activity className="h-5 w-5" /> Aktivitätslog
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="timeline timeline-vertical">
|
||||||
|
<table className="table-sm table w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Event</th>
|
||||||
|
<th>User</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{participant?.statusLog &&
|
||||||
|
Array.isArray(participant.statusLog) &&
|
||||||
|
(participant.statusLog as InputJsonValueType[])
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.map((log: InputJsonValueType, index: number) => {
|
||||||
|
const logEntry = log as unknown as ParticipantLog;
|
||||||
|
return (
|
||||||
|
<tr key={index}>
|
||||||
|
<td>
|
||||||
|
{logEntry.timestamp
|
||||||
|
? new Date(logEntry.timestamp).toLocaleDateString()
|
||||||
|
: "-"}
|
||||||
|
</td>
|
||||||
|
<td>{logEntry.event || "-"}</td>
|
||||||
|
<td>{logEntry.user || "-"}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card bg-base-200 w-full shadow-xl">
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="flex w-full gap-4">
|
||||||
|
<Button
|
||||||
|
disabled={!form.formState.isDirty}
|
||||||
|
isLoading={form.formState.isSubmitting}
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-primary flex-1"
|
||||||
|
>
|
||||||
|
Speichern
|
||||||
|
</Button>
|
||||||
|
{event && (
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
await deleteParticipantMutation.mutateAsync(participant.id);
|
||||||
|
redirect(`/admin/event/${event.id}`);
|
||||||
|
}}
|
||||||
|
className="btn btn-error"
|
||||||
|
>
|
||||||
|
Austragen
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { prisma, Prisma, Event, Participant } from "@repo/db";
|
import { prisma, Prisma, Event, Participant } from "@repo/db";
|
||||||
|
|
||||||
|
//############# Event //#############
|
||||||
export const upsertEvent = async (event: Prisma.EventCreateInput, id?: Event["id"]) => {
|
export const upsertEvent = async (event: Prisma.EventCreateInput, id?: Event["id"]) => {
|
||||||
const newEvent = id
|
const newEvent = id
|
||||||
? await prisma.event.update({
|
? await prisma.event.update({
|
||||||
@@ -11,34 +12,22 @@ export const upsertEvent = async (event: Prisma.EventCreateInput, id?: Event["id
|
|||||||
: await prisma.event.create({ data: event });
|
: await prisma.event.create({ data: event });
|
||||||
return newEvent;
|
return newEvent;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteEvent = async (id: Event["id"]) => {
|
export const deleteEvent = async (id: Event["id"]) => {
|
||||||
await prisma.event.delete({ where: { id: id } });
|
await prisma.event.delete({ where: { id: id } });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const upsertAppointment = async (
|
//############# Participant //#############
|
||||||
eventAppointment: Prisma.EventAppointmentUncheckedCreateInput,
|
|
||||||
) => {
|
export const upsertParticipant = async (participant: Prisma.ParticipantUncheckedCreateInput) => {
|
||||||
const newEventAppointment = eventAppointment.id
|
const newParticipant = participant.id
|
||||||
? await prisma.eventAppointment.update({
|
? await prisma.participant.update({
|
||||||
where: { id: eventAppointment.id },
|
where: { id: participant.id },
|
||||||
data: eventAppointment,
|
data: participant,
|
||||||
})
|
})
|
||||||
: await prisma.eventAppointment.create({ data: eventAppointment });
|
: await prisma.participant.create({ data: participant });
|
||||||
return newEventAppointment;
|
return newParticipant;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteAppoinement = async (id: Event["id"]) => {
|
|
||||||
await prisma.eventAppointment.delete({ where: { id: id } });
|
|
||||||
prisma.eventAppointment.findMany({
|
|
||||||
where: {
|
|
||||||
eventId: id,
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
// TODO: add order by in relation to table selected column
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
export const deleteParticipant = async (id: Participant["id"]) => {
|
export const deleteParticipant = async (id: Participant["id"]) => {
|
||||||
await prisma.participant.delete({ where: { id: id } });
|
await prisma.participant.delete({ where: { id: id } });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ export const reportColumns: ColumnDef<Report & { Sender?: User; Reported: User }
|
|||||||
return (
|
return (
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
{row.getValue("reviewed") ? (
|
{row.getValue("reviewed") ? (
|
||||||
<Check className="text-green-500 w-5 h-5" />
|
<Check className="h-5 w-5 text-green-500" />
|
||||||
) : (
|
) : (
|
||||||
<X className="text-red-500 w-5 h-5" />
|
<X className="h-5 w-5 text-red-500" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -31,13 +31,13 @@ export const reportColumns: ColumnDef<Report & { Sender?: User; Reported: User }
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "reportedUserRole",
|
accessorKey: "reportedUserRole",
|
||||||
header: "Rolle des gemeldeten Nutzers",
|
header: "Rolle",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const role = row.getValue("reportedUserRole") as string | undefined;
|
const role = row.getValue("reportedUserRole") as string | undefined;
|
||||||
const Icon = role ? (role.startsWith("LST") ? Workflow : Plane) : ShieldQuestion;
|
const Icon = role ? (role.startsWith("LST") ? Workflow : Plane) : ShieldQuestion;
|
||||||
return (
|
return (
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<Icon className="w-4 h-4" />
|
<Icon className="h-4 w-4" />
|
||||||
{role || "Unbekannt"}
|
{role || "Unbekannt"}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -62,7 +62,7 @@ export const reportColumns: ColumnDef<Report & { Sender?: User; Reported: User }
|
|||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Link href={`/admin/report/${row.original.id}`}>
|
<Link href={`/admin/report/${row.original.id}`}>
|
||||||
<button className="btn btn-sm btn-outline btn-info flex items-center gap-2">
|
<button className="btn btn-sm btn-outline btn-info flex items-center gap-2">
|
||||||
<Eye className="w-4 h-4" /> Anzeigen
|
<Eye className="h-4 w-4" /> Anzeigen
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
|
|||||||
137
apps/hub/app/(app)/admin/user/[id]/_components/AccountLog.tsx
Normal file
137
apps/hub/app/(app)/admin/user/[id]/_components/AccountLog.tsx
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
"use client";
|
||||||
|
import { Log, Prisma, User } from "@repo/db";
|
||||||
|
import { cn } from "@repo/shared-components";
|
||||||
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import { PaginatedTable, PaginatedTableRef } from "_components/PaginatedTable";
|
||||||
|
import { Printer } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
|
||||||
|
export const AccountLog = ({ sameIPLogs, userId }: { sameIPLogs: Log[]; userId: string }) => {
|
||||||
|
const [onlyImportant, setOnlyImportant] = useState(true);
|
||||||
|
const tableRef = useRef<PaginatedTableRef>(null);
|
||||||
|
return (
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="card-title flex justify-between">
|
||||||
|
<h2 className="flex items-center gap-2">
|
||||||
|
<Printer className="h-5 w-5" /> Account Log
|
||||||
|
</h2>
|
||||||
|
<p className="text-end text-sm text-gray-500">
|
||||||
|
Hier werden Logs angezeigt, die dem Nutzer zugeordnet sind oder von der selben IP stammen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<PaginatedTable
|
||||||
|
ref={tableRef}
|
||||||
|
rightOfPagination={
|
||||||
|
<div className="ml-4 flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="ImportantOnly"
|
||||||
|
checked={onlyImportant}
|
||||||
|
onChange={() => {
|
||||||
|
setOnlyImportant(!onlyImportant);
|
||||||
|
tableRef.current?.refresh();
|
||||||
|
}}
|
||||||
|
className="checkbox checkbox-sm"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="ImportantOnly"
|
||||||
|
className="min-w-[210px] cursor-pointer select-none text-sm"
|
||||||
|
>
|
||||||
|
Unauffällige Einträge ausblenden
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
getFilter={(searchTerm) => {
|
||||||
|
return {
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
OR: [
|
||||||
|
{ ip: { contains: searchTerm } },
|
||||||
|
{ browser: { contains: searchTerm } },
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
in: sameIPLogs
|
||||||
|
.filter((log) => log.id.toString().includes(searchTerm))
|
||||||
|
.map((log) => log.id),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
onlyImportant
|
||||||
|
? {
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
in: sameIPLogs
|
||||||
|
.filter((log) => log.id.toString().includes(searchTerm))
|
||||||
|
.map((log) => log.id),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: {
|
||||||
|
in: ["REGISTER", "PROFILE_CHANGE"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
],
|
||||||
|
} as Prisma.LogWhereInput;
|
||||||
|
}}
|
||||||
|
include={{
|
||||||
|
User: true,
|
||||||
|
}}
|
||||||
|
prismaModel={"log"}
|
||||||
|
columns={
|
||||||
|
[
|
||||||
|
{
|
||||||
|
header: "Zeitstempel",
|
||||||
|
accessorKey: "timestamp",
|
||||||
|
cell: (info) => new Date(info.getValue<string>()).toLocaleString("de-DE"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Aktion",
|
||||||
|
accessorKey: "action",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const action = row.original.type;
|
||||||
|
|
||||||
|
if (action !== "PROFILE_CHANGE") {
|
||||||
|
return <span className="text-blue-500">{action}</span>;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<span className="text-yellow-500">{`${row.original.field} von "${row.original.oldValue}" zu "${row.original.newValue}"`}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "IP-Adresse",
|
||||||
|
accessorKey: "ip",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Browser",
|
||||||
|
accessorKey: "browser",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Benutzer",
|
||||||
|
accessorKey: "userId",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={`/admin/user/${row.original.userId}`}
|
||||||
|
className={cn("link", userId !== row.original.userId && "text-red-400")}
|
||||||
|
>
|
||||||
|
{row.original.User
|
||||||
|
? `${row.original.User.firstname} ${row.original.User.lastname} - ${row.original.User.publicId}`
|
||||||
|
: "Unbekannt"}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as ColumnDef<Log & { User: User }>[]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -691,6 +691,29 @@ export const AdminForm = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{user.isDeleted && (
|
||||||
|
<div role="alert" className="alert alert-warning alert-outline flex flex-col">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<TriangleAlert />
|
||||||
|
{openBans.map((ban) => (
|
||||||
|
<div key={ban.id}>
|
||||||
|
<h3 className="text-lg font-semibold">Account gelöscht</h3>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{openTimebans.map((timeban) => (
|
||||||
|
<div key={timeban.id}>
|
||||||
|
<h3 className="text-lg font-semibold">
|
||||||
|
Dieser Account ist als gelöscht markiert, der Nutzer kann sich nicht mehr
|
||||||
|
anmelden.
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-400">
|
||||||
|
Achtung! Die Strafe(n) sind aktiv, die Rechte des Nutzers müssen nicht angepasst werden!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{(user.CanonicalUser || (user.Duplicates && user.Duplicates.length > 0)) && (
|
{(user.CanonicalUser || (user.Duplicates && user.Duplicates.length > 0)) && (
|
||||||
<div role="alert" className="alert alert-error alert-outline flex flex-col">
|
<div role="alert" className="alert alert-error alert-outline flex flex-col">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PersonIcon } from "@radix-ui/react-icons";
|
import { PersonIcon } from "@radix-ui/react-icons";
|
||||||
import { prisma } from "@repo/db";
|
import { Log, prisma } from "@repo/db";
|
||||||
import {
|
import {
|
||||||
AdminForm,
|
AdminForm,
|
||||||
ConnectionHistory,
|
ConnectionHistory,
|
||||||
@@ -9,6 +9,9 @@ import {
|
|||||||
} from "./_components/forms";
|
} from "./_components/forms";
|
||||||
import { Error } from "../../../../_components/Error";
|
import { Error } from "../../../../_components/Error";
|
||||||
import { getUserPenaltys } from "@repo/shared-components";
|
import { getUserPenaltys } from "@repo/shared-components";
|
||||||
|
import { PaginatedTable } from "_components/PaginatedTable";
|
||||||
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import { AccountLog } from "./_components/AccountLog";
|
||||||
|
|
||||||
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
|
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
@@ -35,6 +38,26 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userLog = await prisma.log.findMany({
|
||||||
|
where: {
|
||||||
|
userId: user?.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const sameIpLogs = await prisma.log.findMany({
|
||||||
|
where: {
|
||||||
|
ip: {
|
||||||
|
in: userLog.map((log) => log.ip).filter((ip): ip is string => ip !== null),
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
not: user?.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
User: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const formerDiscordAccounts = await prisma.formerDiscordAccount.findMany({
|
const formerDiscordAccounts = await prisma.formerDiscordAccount.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
@@ -153,9 +176,12 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-6">
|
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-6">
|
||||||
|
<AccountLog sameIPLogs={sameIpLogs} userId={user.id} />
|
||||||
|
</div>
|
||||||
|
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-3">
|
||||||
<UserReports user={user} />
|
<UserReports user={user} />
|
||||||
</div>
|
</div>
|
||||||
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-6">
|
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-3">
|
||||||
<UserPenalties user={user} />
|
<UserPenalties user={user} />
|
||||||
</div>
|
</div>
|
||||||
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-6">
|
<div className="card bg-base-200 col-span-6 mb-4 shadow-xl xl:col-span-6">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { prisma, Prisma } from "@repo/db";
|
|||||||
import bcrypt from "bcryptjs";
|
import bcrypt from "bcryptjs";
|
||||||
import { sendMailByTemplate } from "../../../../helper/mail";
|
import { sendMailByTemplate } from "../../../../helper/mail";
|
||||||
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
||||||
|
import { logAction } from "(auth)/login/_components/action";
|
||||||
|
|
||||||
export const getUser = async (where: Prisma.UserWhereInput) => {
|
export const getUser = async (where: Prisma.UserWhereInput) => {
|
||||||
return await prisma.user.findMany({
|
return await prisma.user.findMany({
|
||||||
@@ -58,10 +59,14 @@ export const deletePilotHistory = async (id: number) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
export const deleteUser = async (id: string) => {
|
export const deleteUser = async (id: string) => {
|
||||||
return await prisma.user.delete({
|
await logAction("ACCOUNT_DELETED");
|
||||||
|
return await prisma.user.update({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
id: id,
|
||||||
},
|
},
|
||||||
|
data: {
|
||||||
|
isDeleted: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ const AdminUserPage = () => {
|
|||||||
if (activePenaltys.length > 0) {
|
if (activePenaltys.length > 0) {
|
||||||
return <span className="font-bold text-red-600">AKTIVE STRAFE</span>;
|
return <span className="font-bold text-red-600">AKTIVE STRAFE</span>;
|
||||||
}
|
}
|
||||||
|
if (props.row.original.isDeleted) {
|
||||||
|
return <span className="font-bold text-yellow-600">GELÖSCHT</span>;
|
||||||
|
}
|
||||||
if (props.row.original.permissions.length === 0) {
|
if (props.row.original.permissions.length === 0) {
|
||||||
return <span className="text-gray-700">Keine</span>;
|
return <span className="text-gray-700">Keine</span>;
|
||||||
} else if (props.row.original.permissions.includes("ADMIN_USER_ADVANCED")) {
|
} else if (props.row.original.permissions.includes("ADMIN_USER_ADVANCED")) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { DrawingPinFilledIcon } from "@radix-ui/react-icons";
|
import { DrawingPinFilledIcon } from "@radix-ui/react-icons";
|
||||||
import { Event, Participant, EventAppointment, User } from "@repo/db";
|
import { Event, Participant, User } from "@repo/db";
|
||||||
import ModalBtn from "./Modal";
|
import ModalBtn from "./Modal";
|
||||||
import MDEditor from "@uiw/react-md-editor";
|
import MDEditor from "@uiw/react-md-editor";
|
||||||
import { Badge } from "@repo/shared-components";
|
import { Badge } from "@repo/shared-components";
|
||||||
@@ -8,25 +8,18 @@ import { Badge } from "@repo/shared-components";
|
|||||||
export const EventCard = ({
|
export const EventCard = ({
|
||||||
user,
|
user,
|
||||||
event,
|
event,
|
||||||
selectedAppointments,
|
|
||||||
appointments,
|
|
||||||
}: {
|
}: {
|
||||||
user: User;
|
user: User;
|
||||||
event: Event & {
|
event: Event & {
|
||||||
Appointments: EventAppointment[];
|
|
||||||
Participants: Participant[];
|
Participants: Participant[];
|
||||||
};
|
};
|
||||||
selectedAppointments: EventAppointment[];
|
|
||||||
appointments: (EventAppointment & {
|
|
||||||
Participants: { userId: string }[];
|
|
||||||
})[];
|
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="col-span-full">
|
<div className="col-span-full">
|
||||||
<div className="card bg-base-200 shadow-xl mb-4">
|
<div className="card bg-base-200 mb-4 shadow-xl">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<h2 className="card-title">{event.name}</h2>
|
<h2 className="card-title">{event.name}</h2>
|
||||||
<div className="absolute top-0 right-0 m-4">
|
<div className="absolute right-0 top-0 m-4">
|
||||||
{event.type === "COURSE" && (
|
{event.type === "COURSE" && (
|
||||||
<span className="badge badge-info badge-outline">Zusatzqualifikation</span>
|
<span className="badge badge-info badge-outline">Zusatzqualifikation</span>
|
||||||
)}
|
)}
|
||||||
@@ -36,7 +29,7 @@ export const EventCard = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-6 gap-4">
|
<div className="grid grid-cols-6 gap-4">
|
||||||
<div className="col-span-4">
|
<div className="col-span-4">
|
||||||
<div className="text-left text-balance" data-color-mode="dark">
|
<div className="text-balance text-left" data-color-mode="dark">
|
||||||
<MDEditor.Markdown
|
<MDEditor.Markdown
|
||||||
source={event.descriptionShort}
|
source={event.descriptionShort}
|
||||||
style={{
|
style={{
|
||||||
@@ -45,21 +38,21 @@ export const EventCard = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex col-span-2 justify-end">
|
<div className="col-span-2 flex justify-end">
|
||||||
{event.finishedBadges.map((b) => {
|
{event.finishedBadges.map((b) => {
|
||||||
return <Badge badge={b} key={b} />;
|
return <Badge badge={b} key={b} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-actions flex justify-between items-center mt-5">
|
<div className="card-actions mt-5 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-600 text-left flex items-center gap-2">
|
<p className="flex items-center gap-2 text-left text-gray-600">
|
||||||
<DrawingPinFilledIcon /> <b>Teilnahmevoraussetzungen: </b>
|
<DrawingPinFilledIcon /> <b>Teilnahmevoraussetzungen: </b>
|
||||||
{!event.requiredBadges.length && "Keine"}
|
{!event.requiredBadges.length && "Keine"}
|
||||||
</p>
|
</p>
|
||||||
{!!event.requiredBadges.length && (
|
{!!event.requiredBadges.length && (
|
||||||
<div className="flex ml-6">
|
<div className="ml-6 flex">
|
||||||
<b className="text-gray-600 text-left mr-2">Abzeichen:</b>
|
<b className="mr-2 text-left text-gray-600">Abzeichen:</b>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{event.requiredBadges.map((badge) => (
|
{event.requiredBadges.map((badge) => (
|
||||||
<div className="badge badge-secondary badge-outline" key={badge}>
|
<div className="badge badge-secondary badge-outline" key={badge}>
|
||||||
@@ -71,11 +64,9 @@ export const EventCard = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ModalBtn
|
<ModalBtn
|
||||||
selectedAppointments={selectedAppointments}
|
|
||||||
user={user}
|
user={user}
|
||||||
event={event}
|
event={event}
|
||||||
title={event.name}
|
title={event.name}
|
||||||
dates={appointments}
|
|
||||||
participant={event.Participants[0]}
|
participant={event.Participants[0]}
|
||||||
modalId={`${event.name}_modal.${event.id}`}
|
modalId={`${event.name}_modal.${event.id}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,56 +1,30 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { CheckCircledIcon, EnterIcon, DrawingPinFilledIcon } from "@radix-ui/react-icons";
|
import { CheckCircledIcon, EnterIcon, DrawingPinFilledIcon } from "@radix-ui/react-icons";
|
||||||
import { Event, EventAppointment, Participant, User } from "@repo/db";
|
import { Event, Participant, User } from "@repo/db";
|
||||||
import { cn } from "@repo/shared-components";
|
import { cn } from "@repo/shared-components";
|
||||||
import { inscribeToMoodleCourse, upsertParticipant } from "../actions";
|
import { inscribeToMoodleCourse, upsertParticipant } from "../actions";
|
||||||
import {
|
import {
|
||||||
BookCheck,
|
BookCheck,
|
||||||
Calendar,
|
|
||||||
Check,
|
Check,
|
||||||
CirclePlay,
|
CirclePlay,
|
||||||
Clock10Icon,
|
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
Info,
|
Info,
|
||||||
TriangleAlert,
|
TriangleAlert,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import {
|
|
||||||
InputJsonValueType,
|
|
||||||
ParticipantOptionalDefaults,
|
|
||||||
ParticipantOptionalDefaultsSchema,
|
|
||||||
} from "@repo/db/zod";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { Select } from "../../../_components/ui/Select";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { handleParticipantEnrolled } from "../../../../helper/events";
|
|
||||||
import { eventCompleted } from "@repo/shared-components";
|
import { eventCompleted } from "@repo/shared-components";
|
||||||
import MDEditor from "@uiw/react-md-editor";
|
import MDEditor from "@uiw/react-md-editor";
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import { formatDate } from "date-fns";
|
|
||||||
|
|
||||||
interface ModalBtnProps {
|
interface ModalBtnProps {
|
||||||
title: string;
|
title: string;
|
||||||
event: Event;
|
event: Event;
|
||||||
dates: (EventAppointment & {
|
|
||||||
Participants: { userId: string }[];
|
|
||||||
})[];
|
|
||||||
selectedAppointments: EventAppointment[];
|
|
||||||
participant?: Participant;
|
participant?: Participant;
|
||||||
user: User;
|
user: User;
|
||||||
modalId: string;
|
modalId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalBtn = ({
|
const ModalBtn = ({ title, modalId, participant, event, user }: ModalBtnProps) => {
|
||||||
title,
|
|
||||||
dates,
|
|
||||||
modalId,
|
|
||||||
participant,
|
|
||||||
selectedAppointments,
|
|
||||||
event,
|
|
||||||
user,
|
|
||||||
}: ModalBtnProps) => {
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
||||||
const handleOpen = () => {
|
const handleOpen = () => {
|
||||||
@@ -66,12 +40,6 @@ const ModalBtn = ({
|
|||||||
modal?.removeEventListener("close", handleClose);
|
modal?.removeEventListener("close", handleClose);
|
||||||
};
|
};
|
||||||
}, [modalId]);
|
}, [modalId]);
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const canSelectDate =
|
|
||||||
event.hasPresenceEvents &&
|
|
||||||
!participant?.attended &&
|
|
||||||
(selectedAppointments.length === 0 || participant?.appointmentCancelled);
|
|
||||||
|
|
||||||
const openModal = () => {
|
const openModal = () => {
|
||||||
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
||||||
@@ -82,29 +50,6 @@ const ModalBtn = ({
|
|||||||
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
||||||
modal?.close();
|
modal?.close();
|
||||||
};
|
};
|
||||||
const selectAppointmentForm = useForm<ParticipantOptionalDefaults>({
|
|
||||||
resolver: zodResolver(ParticipantOptionalDefaultsSchema),
|
|
||||||
defaultValues: {
|
|
||||||
eventId: event.id,
|
|
||||||
userId: user.id,
|
|
||||||
...participant,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const selectedAppointment = selectedAppointments[0];
|
|
||||||
const selectedDate = dates.find(
|
|
||||||
(date) =>
|
|
||||||
date.id === selectAppointmentForm.watch("eventAppointmentId") || selectedAppointment?.id,
|
|
||||||
);
|
|
||||||
const ownIndexInParticipantList = selectedDate?.Participants?.findIndex(
|
|
||||||
(p) => p.userId === user.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
const ownPlaceInParticipantList =
|
|
||||||
typeof ownIndexInParticipantList === "number"
|
|
||||||
? ownIndexInParticipantList === -1
|
|
||||||
? (selectedDate?.Participants?.length ?? 0) + 1
|
|
||||||
: ownIndexInParticipantList + 1
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const missingRequirements =
|
const missingRequirements =
|
||||||
event.requiredBadges?.length > 0 &&
|
event.requiredBadges?.length > 0 &&
|
||||||
@@ -163,79 +108,6 @@ const ModalBtn = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{event.hasPresenceEvents && (
|
|
||||||
<div className="bg-base-300 flex min-w-[300px] flex-1 flex-col gap-2 rounded-lg p-3 shadow">
|
|
||||||
<h2 className="flex gap-2 text-lg font-bold">
|
|
||||||
<Calendar /> Termine
|
|
||||||
</h2>
|
|
||||||
<div className="flex flex-1 flex-col items-center justify-center">
|
|
||||||
{!!dates.length && !selectedDate && (
|
|
||||||
<>
|
|
||||||
<p className="text-info text-center">Melde dich zu einem Termin an</p>
|
|
||||||
<Select
|
|
||||||
form={selectAppointmentForm}
|
|
||||||
options={dates.map((date) => ({
|
|
||||||
label: `${formatDate(date.appointmentDate, "dd.MM.yyyy HH:mm")} - (${date.Participants.length}/${event.maxParticipants})`,
|
|
||||||
value: date.id,
|
|
||||||
}))}
|
|
||||||
name="eventAppointmentId"
|
|
||||||
label={""}
|
|
||||||
placeholder="Wähle einen Termin"
|
|
||||||
className="min-w-[250px]"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{selectedAppointment && !participant?.appointmentCancelled && (
|
|
||||||
<div className="flex flex-col items-center justify-center gap-2">
|
|
||||||
<span>Dein ausgewählter Termin (Deutsche Zeit)</span>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
className="input input-border pointer-events-none min-w-[250px]"
|
|
||||||
style={{ anchorName: "--rdp" } as React.CSSProperties}
|
|
||||||
>
|
|
||||||
{new Date(selectedAppointment.appointmentDate).toLocaleString("de-DE", {
|
|
||||||
year: "numeric",
|
|
||||||
month: "2-digit",
|
|
||||||
day: "2-digit",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
})}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{participant?.attended ? (
|
|
||||||
<p className="flex items-center justify-center gap-2 py-4">
|
|
||||||
<CheckCircledIcon className="text-success" />
|
|
||||||
Du hast an dem Presenztermin teilgenommen
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<p className="flex items-center justify-center gap-2 py-4">
|
|
||||||
Bitte erscheine ~5 minuten vor dem Termin im Discord
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!dates.length && (
|
|
||||||
<p className="text-error text-center">Aktuell sind keine Termine verfügbar</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!!selectedDate &&
|
|
||||||
!!event.maxParticipants &&
|
|
||||||
!!ownPlaceInParticipantList &&
|
|
||||||
!!(ownPlaceInParticipantList > event.maxParticipants) && (
|
|
||||||
<p
|
|
||||||
role="alert"
|
|
||||||
className="alert alert-error alert-outline my-5 flex items-center justify-center gap-2 border py-4"
|
|
||||||
>
|
|
||||||
<TriangleAlert className="h-6 w-6 shrink-0 stroke-current" fill="none" />
|
|
||||||
Dieser Termin ist ausgebucht, wahrscheinlich wirst du nicht teilnehmen
|
|
||||||
können. (Listenplatz: {ownPlaceInParticipantList} , max.{" "}
|
|
||||||
{event.maxParticipants})
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{event.finisherMoodleCourseId && (
|
{event.finisherMoodleCourseId && (
|
||||||
<div className="bg-base-300 flex min-w-[300px] flex-1 flex-col gap-2 rounded-lg p-3 shadow">
|
<div className="bg-base-300 flex min-w-[300px] flex-1 flex-col gap-2 rounded-lg p-3 shadow">
|
||||||
<h2 className="flex gap-2 text-lg font-bold">
|
<h2 className="flex gap-2 text-lg font-bold">
|
||||||
@@ -261,64 +133,6 @@ const ModalBtn = ({
|
|||||||
{!event.requiredBadges.length && "Keine"}
|
{!event.requiredBadges.length && "Keine"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-action">
|
|
||||||
{!!canSelectDate && (
|
|
||||||
<button
|
|
||||||
className={cn(
|
|
||||||
"btn btn-info btn-outline btn-wide",
|
|
||||||
event.type === "COURSE" && "btn-secondary",
|
|
||||||
)}
|
|
||||||
onClick={async () => {
|
|
||||||
const data = selectAppointmentForm.getValues();
|
|
||||||
if (!data.eventAppointmentId) return;
|
|
||||||
|
|
||||||
const participant = await upsertParticipant({
|
|
||||||
...data,
|
|
||||||
enscriptionDate: new Date(),
|
|
||||||
statusLog: data.statusLog?.filter((log) => log !== null),
|
|
||||||
appointmentCancelled: false,
|
|
||||||
});
|
|
||||||
await handleParticipantEnrolled(participant.id.toString());
|
|
||||||
|
|
||||||
router.refresh();
|
|
||||||
closeModal();
|
|
||||||
}}
|
|
||||||
disabled={!selectAppointmentForm.watch("eventAppointmentId")}
|
|
||||||
>
|
|
||||||
<EnterIcon /> Anmelden
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{selectedAppointment &&
|
|
||||||
!participant?.appointmentCancelled &&
|
|
||||||
!participant?.attended && (
|
|
||||||
<button
|
|
||||||
onClick={async () => {
|
|
||||||
await upsertParticipant({
|
|
||||||
eventId: event.id,
|
|
||||||
userId: participant!.userId,
|
|
||||||
appointmentCancelled: true,
|
|
||||||
statusLog: [
|
|
||||||
...(participant?.statusLog as unknown as InputJsonValueType[]),
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
appointmentId: selectedAppointment.id,
|
|
||||||
appointmentDate: selectedAppointment.appointmentDate,
|
|
||||||
},
|
|
||||||
user: `${user?.firstname} ${user?.lastname} - ${user?.publicId}`,
|
|
||||||
event: "Termin abgesagt",
|
|
||||||
timestamp: new Date(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
toast.success("Termin abgesagt");
|
|
||||||
router.refresh();
|
|
||||||
}}
|
|
||||||
className="btn btn-error btn-outline btn-wide"
|
|
||||||
>
|
|
||||||
Termin Absagen
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button className="modal-backdrop" onClick={closeModal}>
|
<button className="modal-backdrop" onClick={closeModal}>
|
||||||
@@ -335,7 +149,6 @@ const MoodleCourseIndicator = ({
|
|||||||
completed,
|
completed,
|
||||||
moodleCourseId,
|
moodleCourseId,
|
||||||
event,
|
event,
|
||||||
participant,
|
|
||||||
user,
|
user,
|
||||||
}: {
|
}: {
|
||||||
user: User;
|
user: User;
|
||||||
@@ -345,13 +158,6 @@ const MoodleCourseIndicator = ({
|
|||||||
event: Event;
|
event: Event;
|
||||||
}) => {
|
}) => {
|
||||||
const courseUrl = `${process.env.NEXT_PUBLIC_MOODLE_URL}/course/view.php?id=${moodleCourseId}`;
|
const courseUrl = `${process.env.NEXT_PUBLIC_MOODLE_URL}/course/view.php?id=${moodleCourseId}`;
|
||||||
if (event.hasPresenceEvents && !participant?.attended)
|
|
||||||
return (
|
|
||||||
<p className="flex items-center justify-center gap-2 py-4">
|
|
||||||
<Clock10Icon className="text-error" />
|
|
||||||
Abschlusstest erst nach Teilnahme verfügbar
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
if (completed)
|
if (completed)
|
||||||
return (
|
return (
|
||||||
<p className="flex items-center justify-center gap-2 py-4">
|
<p className="flex items-center justify-center gap-2 py-4">
|
||||||
|
|||||||
@@ -10,63 +10,19 @@ const page = async () => {
|
|||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
const events = await prisma.event.findMany({
|
const events = await prisma.event.findMany({
|
||||||
|
orderBy: {
|
||||||
|
id: "desc",
|
||||||
|
},
|
||||||
where: {
|
where: {
|
||||||
hidden: false,
|
hidden: false,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
Appointments: {
|
|
||||||
where: {
|
|
||||||
appointmentDate: {
|
|
||||||
gte: new Date(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Participants: {
|
Participants: {
|
||||||
where: {
|
where: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
orderBy: {
|
|
||||||
id: "desc",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const appointments = await prisma.eventAppointment.findMany({
|
|
||||||
where: {
|
|
||||||
appointmentDate: {
|
|
||||||
gte: new Date(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
Participants: {
|
|
||||||
select: {
|
|
||||||
enscriptionDate: true,
|
|
||||||
id: true,
|
|
||||||
userId: true,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
appointmentCancelled: false,
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
enscriptionDate: "asc",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_count: {
|
|
||||||
select: {
|
|
||||||
Participants: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const userAppointments = await prisma.eventAppointment.findMany({
|
|
||||||
where: {
|
|
||||||
Participants: {
|
|
||||||
some: {
|
|
||||||
userId: user.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -78,15 +34,7 @@ const page = async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{events.map((event) => {
|
{events.map((event) => {
|
||||||
return (
|
return <EventCard user={user} event={event} key={event.id} />;
|
||||||
<EventCard
|
|
||||||
appointments={appointments}
|
|
||||||
selectedAppointments={userAppointments}
|
|
||||||
user={user}
|
|
||||||
event={event}
|
|
||||||
key={event.id}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function () {
|
|||||||
image={Discord}
|
image={Discord}
|
||||||
title="Discord Server"
|
title="Discord Server"
|
||||||
BtnIcon={<DiscordLogoIcon />}
|
BtnIcon={<DiscordLogoIcon />}
|
||||||
btnHref="https://discord.com/invite/x6FAMY7DW6"
|
btnHref="https://discord.gg/virtualairrescue"
|
||||||
btnLabel="Beitreten"
|
btnLabel="Beitreten"
|
||||||
description="Tritt unserem Discordserver bei, um mit der Community in Kontakt zu bleiben, Unterstützung zu erhalten und über die neuesten Updates informiert zu werden. Wenn du beigetreten bist kannst du in deinen Einstellungen dein VAR-Konto mit deinem Discordkonto verknüpfen und eine Rolle zu erhalten.
|
description="Tritt unserem Discordserver bei, um mit der Community in Kontakt zu bleiben, Unterstützung zu erhalten und über die neuesten Updates informiert zu werden. Wenn du beigetreten bist kannst du in deinen Einstellungen dein VAR-Konto mit deinem Discordkonto verknüpfen und eine Rolle zu erhalten.
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import toast from "react-hot-toast";
|
|||||||
import { CircleAlert, Trash2 } from "lucide-react";
|
import { CircleAlert, Trash2 } from "lucide-react";
|
||||||
import { deleteUser, sendVerificationLink } from "(app)/admin/user/action";
|
import { deleteUser, sendVerificationLink } from "(app)/admin/user/action";
|
||||||
import { setStandardName } from "../../../../helper/discord";
|
import { setStandardName } from "../../../../helper/discord";
|
||||||
|
import { logAction } from "(auth)/login/_components/action";
|
||||||
|
|
||||||
export const ProfileForm = ({
|
export const ProfileForm = ({
|
||||||
user,
|
user,
|
||||||
@@ -101,6 +102,28 @@ export const ProfileForm = ({
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (user.firstname !== values.firstname) {
|
||||||
|
await logAction("PROFILE_CHANGE", {
|
||||||
|
field: "firstname",
|
||||||
|
oldValue: user.firstname,
|
||||||
|
newValue: values.firstname,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (user.lastname !== values.lastname) {
|
||||||
|
await logAction("PROFILE_CHANGE", {
|
||||||
|
field: "lastname",
|
||||||
|
oldValue: user.lastname,
|
||||||
|
newValue: values.lastname,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (user.email !== values.email) {
|
||||||
|
await logAction("PROFILE_CHANGE", {
|
||||||
|
field: "email",
|
||||||
|
oldValue: user.email,
|
||||||
|
newValue: values.email,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
if (user.email !== values.email) {
|
if (user.email !== values.email) {
|
||||||
await sendVerificationLink(user.id);
|
await sendVerificationLink(user.id);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Toaster, toast } from "react-hot-toast";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Button } from "../../../_components/ui/Button";
|
import { Button } from "../../../_components/ui/Button";
|
||||||
import { useErrorBoundary } from "react-error-boundary";
|
import { useErrorBoundary } from "react-error-boundary";
|
||||||
|
import { logAction } from "./action";
|
||||||
|
|
||||||
export const Login = () => {
|
export const Login = () => {
|
||||||
const { showBoundary } = useErrorBoundary();
|
const { showBoundary } = useErrorBoundary();
|
||||||
@@ -46,6 +47,10 @@ export const Login = () => {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("data", data);
|
||||||
|
|
||||||
|
await logAction("LOGIN");
|
||||||
redirect(searchParams.get("redirect") || "/");
|
redirect(searchParams.get("redirect") || "/");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showBoundary(error);
|
showBoundary(error);
|
||||||
|
|||||||
87
apps/hub/app/(auth)/login/_components/action.ts
Normal file
87
apps/hub/app/(auth)/login/_components/action.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
"use server";
|
||||||
|
import { LOG_TYPE, prisma } from "@repo/db";
|
||||||
|
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
import { cookies, headers } from "next/headers";
|
||||||
|
import { sendReportEmbed } from "../../../../helper/discord";
|
||||||
|
|
||||||
|
export async function getOrSetDeviceId() {
|
||||||
|
const store = await cookies();
|
||||||
|
let deviceId = store.get("device_id")?.value;
|
||||||
|
|
||||||
|
if (!deviceId) {
|
||||||
|
deviceId = randomUUID();
|
||||||
|
store.set("device_id", deviceId, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: "lax",
|
||||||
|
path: "/",
|
||||||
|
maxAge: 60 * 60 * 24 * 365, // 1 Jahr
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logAction = async (
|
||||||
|
type: LOG_TYPE,
|
||||||
|
otherValues?: {
|
||||||
|
field?: string;
|
||||||
|
oldValue?: string;
|
||||||
|
newValue?: string;
|
||||||
|
userId?: string;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const headersList = await headers();
|
||||||
|
const user = await getServerSession();
|
||||||
|
|
||||||
|
const ip =
|
||||||
|
headersList.get("X-Forwarded-For") ||
|
||||||
|
headersList.get("Forwarded") ||
|
||||||
|
headersList.get("X-Real-IP");
|
||||||
|
|
||||||
|
const deviceId = await getOrSetDeviceId();
|
||||||
|
if (type == "LOGIN" || type == "REGISTER") {
|
||||||
|
const existingLogs = await prisma.log.findMany({
|
||||||
|
where: {
|
||||||
|
type: "LOGIN",
|
||||||
|
userId: {
|
||||||
|
not: user?.user.id,
|
||||||
|
},
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
ip: ip,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deviceId: deviceId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (existingLogs.length > 0 && user?.user.id) {
|
||||||
|
// Möglicherweise ein doppelter Account, Report erstellen
|
||||||
|
const report = await prisma.report.create({
|
||||||
|
data: {
|
||||||
|
text: `Möglicher doppelter Account erkannt bei Login-Versuch.\n\nÜbereinstimmende Logs:\n${existingLogs
|
||||||
|
.map((log) => `- Log ID: ${log.id}, IP: ${log.ip}, Zeitstempel: ${log.timestamp}`)
|
||||||
|
.join("\n")}`,
|
||||||
|
reportedUserId: user?.user.id,
|
||||||
|
reportedUserRole: "LOGIN - Doppelter Account Verdacht",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await sendReportEmbed(report.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.log.create({
|
||||||
|
data: {
|
||||||
|
type,
|
||||||
|
browser: headersList.get("user-agent") || "unknown",
|
||||||
|
userId: user?.user.id || otherValues?.userId,
|
||||||
|
deviceId: deviceId,
|
||||||
|
ip,
|
||||||
|
...otherValues,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -11,6 +11,7 @@ export const resetPassword = async (email: string) => {
|
|||||||
let user = await prisma.user.findFirst({
|
let user = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
email,
|
email,
|
||||||
|
isDeleted: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const oldUser = (v1User as OldUser[]).find((u) => u.email.toLowerCase() === email);
|
const oldUser = (v1User as OldUser[]).find((u) => u.email.toLowerCase() === email);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useState } from "react";
|
|||||||
import { Button } from "../../../_components/ui/Button";
|
import { Button } from "../../../_components/ui/Button";
|
||||||
import { sendVerificationLink } from "(app)/admin/user/action";
|
import { sendVerificationLink } from "(app)/admin/user/action";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
import { logAction } from "(auth)/login/_components/action";
|
||||||
|
|
||||||
export const Register = () => {
|
export const Register = () => {
|
||||||
const schema = z
|
const schema = z
|
||||||
@@ -93,6 +94,9 @@ export const Register = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await sendVerificationLink(user.id);
|
await sendVerificationLink(user.id);
|
||||||
|
await logAction("REGISTER", {
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
await signIn("credentials", {
|
await signIn("credentials", {
|
||||||
callbackUrl: "/",
|
callbackUrl: "/",
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
|||||||
@@ -24,9 +24,6 @@ export const register = async ({ password, ...user }: Omit<Prisma.UserCreateInpu
|
|||||||
where: {
|
where: {
|
||||||
email: user.email,
|
email: user.email,
|
||||||
},
|
},
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingOldUser = (v1User as OldUser[]).find(
|
const existingOldUser = (v1User as OldUser[]).find(
|
||||||
@@ -34,10 +31,16 @@ export const register = async ({ password, ...user }: Omit<Prisma.UserCreateInpu
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
|
if (existingUser.isDeleted) {
|
||||||
|
return {
|
||||||
|
error: "Diese E-Mail-Adresse kann nicht verwendet werden.",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
return {
|
return {
|
||||||
error: "Ein Nutzer mit dieser E-Mail-Adresse existiert bereits.",
|
error: "Ein Nutzer mit dieser E-Mail-Adresse existiert bereits.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (existingOldUser) {
|
if (existingOldUser) {
|
||||||
return {
|
return {
|
||||||
@@ -53,5 +56,6 @@ export const register = async ({ password, ...user }: Omit<Prisma.UserCreateInpu
|
|||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return newUser;
|
return newUser;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Button } from "@repo/shared-components";
|
|||||||
import { formatTimeRange } from "../../helper/timerange";
|
import { formatTimeRange } from "../../helper/timerange";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { error } from "console";
|
||||||
|
|
||||||
interface BookingTimelineModalProps {
|
interface BookingTimelineModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -318,12 +319,7 @@ export const BookingTimelineModal = ({
|
|||||||
{canDeleteBooking(booking.User.publicId) && (
|
{canDeleteBooking(booking.User.publicId) && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => deleteBooking(booking.id)}
|
onClick={() => deleteBooking(booking.id)}
|
||||||
className={`btn btn-xs ${
|
className={`btn btn-xs btn-error`}
|
||||||
currentUser?.permissions.includes("ADMIN_EVENT") &&
|
|
||||||
booking.User.publicId !== currentUser.publicId
|
|
||||||
? "btn-error"
|
|
||||||
: "btn-neutral"
|
|
||||||
}`}
|
|
||||||
title="Buchung löschen"
|
title="Buchung löschen"
|
||||||
>
|
>
|
||||||
<Trash2 size={12} />
|
<Trash2 size={12} />
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ interface PaginatedTableProps<TData, TWhere extends object>
|
|||||||
leftOfSearch?: React.ReactNode;
|
leftOfSearch?: React.ReactNode;
|
||||||
rightOfSearch?: React.ReactNode;
|
rightOfSearch?: React.ReactNode;
|
||||||
leftOfPagination?: React.ReactNode;
|
leftOfPagination?: React.ReactNode;
|
||||||
|
rightOfPagination?: React.ReactNode;
|
||||||
supressQuery?: boolean;
|
supressQuery?: boolean;
|
||||||
ref?: Ref<PaginatedTableRef>;
|
ref?: Ref<PaginatedTableRef>;
|
||||||
}
|
}
|
||||||
@@ -37,6 +38,7 @@ export function PaginatedTable<TData, TWhere extends object>({
|
|||||||
leftOfSearch,
|
leftOfSearch,
|
||||||
rightOfSearch,
|
rightOfSearch,
|
||||||
leftOfPagination,
|
leftOfPagination,
|
||||||
|
rightOfPagination,
|
||||||
supressQuery,
|
supressQuery,
|
||||||
...restProps
|
...restProps
|
||||||
}: PaginatedTableProps<TData, TWhere>) {
|
}: PaginatedTableProps<TData, TWhere>) {
|
||||||
@@ -159,10 +161,9 @@ export function PaginatedTable<TData, TWhere extends object>({
|
|||||||
<SortableTable data={data} prismaModel={prismaModel} setOrderBy={setOrderBy} {...restProps} />
|
<SortableTable data={data} prismaModel={prismaModel} setOrderBy={setOrderBy} {...restProps} />
|
||||||
<div className="items-between flex">
|
<div className="items-between flex">
|
||||||
{leftOfPagination}
|
{leftOfPagination}
|
||||||
<>
|
|
||||||
<RowsPerPage rowsPerPage={rowsPerPage} setRowsPerPage={setRowsPerPage} />
|
<RowsPerPage rowsPerPage={rowsPerPage} setRowsPerPage={setRowsPerPage} />
|
||||||
|
{rightOfPagination}
|
||||||
<Pagination totalPages={Math.ceil(total / rowsPerPage)} page={page} setPage={setPage} />
|
<Pagination totalPages={Math.ceil(total / rowsPerPage)} page={page} setPage={setPage} />
|
||||||
</>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export const RowsPerPage = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<select
|
<select
|
||||||
className="select w-32"
|
className="select select-sm w-32"
|
||||||
value={rowsPerPage}
|
value={rowsPerPage}
|
||||||
onChange={(e) => setRowsPerPage(Number(e.target.value))}
|
onChange={(e) => setRowsPerPage(Number(e.target.value))}
|
||||||
>
|
>
|
||||||
@@ -122,11 +122,15 @@ export const Pagination = ({
|
|||||||
if (totalPages === 0) return null;
|
if (totalPages === 0) return null;
|
||||||
return (
|
return (
|
||||||
<div className="join w-full justify-end">
|
<div className="join w-full justify-end">
|
||||||
<button className="join-item btn" disabled={page === 0} onClick={() => setPage(page - 1)}>
|
<button
|
||||||
<ArrowLeft size={16} />
|
className="join-item btn btn-sm"
|
||||||
|
disabled={page === 0}
|
||||||
|
onClick={() => setPage(page - 1)}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={14} />
|
||||||
</button>
|
</button>
|
||||||
<select
|
<select
|
||||||
className="select join-item w-16"
|
className="select select-sm join-item w-16"
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(e) => setPage(Number(e.target.value))}
|
onChange={(e) => setPage(Number(e.target.value))}
|
||||||
>
|
>
|
||||||
@@ -137,11 +141,11 @@ export const Pagination = ({
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
className="join-item btn"
|
className="join-item btn btn-sm"
|
||||||
disabled={page === totalPages - 1}
|
disabled={page === totalPages - 1}
|
||||||
onClick={() => page < totalPages && setPage(page + 1)}
|
onClick={() => page < totalPages && setPage(page + 1)}
|
||||||
>
|
>
|
||||||
<ArrowRight size={16} />
|
<ArrowRight size={14} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export const options: AuthOptions = {
|
|||||||
contains: credentials.email,
|
contains: credentials.email,
|
||||||
mode: "insensitive",
|
mode: "insensitive",
|
||||||
},
|
},
|
||||||
|
isDeleted: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const v1User = (oldUser as OldUser[]).find(
|
const v1User = (oldUser as OldUser[]).find(
|
||||||
@@ -87,6 +88,7 @@ export const options: AuthOptions = {
|
|||||||
const dbUser = await prisma.user.findUnique({
|
const dbUser = await prisma.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: token?.sub,
|
id: token?.sub,
|
||||||
|
isDeleted: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!dbUser) {
|
if (!dbUser) {
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ import { getServerSession } from "../../auth/[...nextauth]/auth";
|
|||||||
// DELETE /api/booking/[id] - Delete a booking
|
// DELETE /api/booking/[id] - Delete a booking
|
||||||
export const DELETE = async (req: NextRequest, { params }: { params: { id: string } }) => {
|
export const DELETE = async (req: NextRequest, { params }: { params: { id: string } }) => {
|
||||||
try {
|
try {
|
||||||
console.log(params);
|
|
||||||
const session = await getServerSession();
|
const session = await getServerSession();
|
||||||
if (!session?.user) {
|
if (!session?.user) {
|
||||||
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
|
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const bookingId = params.id;
|
const bookingId = (await params).id;
|
||||||
|
|
||||||
|
console.log("Attempting to delete booking with ID:", bookingId);
|
||||||
|
|
||||||
// Find the booking
|
// Find the booking
|
||||||
const booking = await prisma.booking.findUnique({
|
const booking = await prisma.booking.findUnique({
|
||||||
@@ -40,14 +41,14 @@ export const DELETE = async (req: NextRequest, { params }: { params: { id: strin
|
|||||||
};
|
};
|
||||||
|
|
||||||
// PUT /api/booking/[id] - Update a booking
|
// PUT /api/booking/[id] - Update a booking
|
||||||
export const PUT = async (req: NextRequest, { params }: { params: { id: string } }) => {
|
export const PATCH = async (req: NextRequest, { params }: { params: { id: string } }) => {
|
||||||
try {
|
try {
|
||||||
const session = await getServerSession();
|
const session = await getServerSession();
|
||||||
if (!session?.user) {
|
if (!session?.user) {
|
||||||
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
|
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const bookingId = params.id;
|
const bookingId = (await params).id;
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
const { type, stationId, startTime, endTime } = body;
|
const { type, stationId, startTime, endTime } = body;
|
||||||
|
|
||||||
|
|||||||
@@ -24,15 +24,6 @@ export async function GET(request: Request): Promise<NextResponse> {
|
|||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Appointments: {
|
|
||||||
include: {
|
|
||||||
Participants: {
|
|
||||||
where: {
|
|
||||||
appointmentCancelled: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,16 @@ export const removeRolesFromMember = async (memberId: string, roleIds: string[])
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sendReportEmbed = async (reportId: number) => {
|
||||||
|
discordAxiosClient
|
||||||
|
.post("/report/admin-embed", {
|
||||||
|
reportId,
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error sending report embed:", error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const setStandardName = async ({
|
export const setStandardName = async ({
|
||||||
memberId,
|
memberId,
|
||||||
userId,
|
userId,
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { Event, EventAppointment, Participant, Prisma } from "@repo/db";
|
import { Event, Participant, Prisma } from "@repo/db";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
export const getEvents = async (filter: Prisma.EventWhereInput) => {
|
export const getEvents = async (filter: Prisma.EventWhereInput) => {
|
||||||
const { data } = await axios.get<
|
const { data } = await axios.get<
|
||||||
(Event & {
|
(Event & {
|
||||||
Appointments: (EventAppointment & {
|
|
||||||
Appointments: EventAppointment[];
|
|
||||||
Participants: Participant[];
|
|
||||||
})[];
|
|
||||||
Participants: Participant[];
|
Participants: Participant[];
|
||||||
})[]
|
})[]
|
||||||
>(`/api/event`, {
|
>(`/api/event`, {
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { Booking } from "@repo/db";
|
|||||||
export const formatTimeRange = (booking: Booking, options?: { includeDate?: boolean }) => {
|
export const formatTimeRange = (booking: Booking, options?: { includeDate?: boolean }) => {
|
||||||
const start = new Date(booking.startTime);
|
const start = new Date(booking.startTime);
|
||||||
const end = new Date(booking.endTime);
|
const end = new Date(booking.endTime);
|
||||||
const timeRange = `${start.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" })} - ${end.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" })}`;
|
const timeRange = `${start.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit", timeZone: "Europe/Berlin" })} - ${end.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit", timeZone: "Europe/Berlin" })}`;
|
||||||
if (options?.includeDate) {
|
if (options?.includeDate) {
|
||||||
return `${start.toLocaleDateString("de-DE")} ${timeRange}`;
|
return `${start.toLocaleDateString("de-DE", { timeZone: "Europe/Berlin" })} ${timeRange}`;
|
||||||
}
|
}
|
||||||
return timeRange;
|
return timeRange;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"node": ">=18",
|
"node": ">=18",
|
||||||
"pnpm": ">=10"
|
"pnpm": ">=10"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.28.0",
|
"packageManager": "pnpm@10.28.2",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|||||||
0
packages/database/.tsignore
Normal file
0
packages/database/.tsignore
Normal file
@@ -1,7 +1,29 @@
|
|||||||
export { prisma } from "./prisma/client"; // exports instance of prisma
|
export { prisma } from "./prisma/client"; // Prisma instance
|
||||||
export * from "./generated/client"; // exports generated types from prisma
|
|
||||||
|
|
||||||
|
// ✅ NUR TYPES aus dem Prisma Client
|
||||||
|
export type * from "./generated/client";
|
||||||
|
|
||||||
|
export {
|
||||||
|
LOG_TYPE,
|
||||||
|
BOOKING_TYPE,
|
||||||
|
BADGES,
|
||||||
|
Country,
|
||||||
|
BosUse,
|
||||||
|
EVENT_TYPE,
|
||||||
|
HeliportType,
|
||||||
|
GlobalColor,
|
||||||
|
PERMISSION,
|
||||||
|
MissionState,
|
||||||
|
PenaltyType,
|
||||||
|
missionType,
|
||||||
|
HpgState,
|
||||||
|
HpgValidationState,
|
||||||
|
KEYWORD_CATEGORY,
|
||||||
|
} from "./generated/client"; // Prisma helpers
|
||||||
|
|
||||||
|
// Zod
|
||||||
import * as zodTypes from "./generated/zod";
|
import * as zodTypes from "./generated/zod";
|
||||||
|
|
||||||
export const zod = zodTypes;
|
export const zod = zodTypes;
|
||||||
|
|
||||||
|
// JSON helpers
|
||||||
export * from "./prisma/json";
|
export * from "./prisma/json";
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"zod-prisma-types": "^3.2.4"
|
"zod-prisma-types": "^3.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.1.0",
|
||||||
"prisma": "^6.12.0"
|
"prisma": "^6.12.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,27 @@
|
|||||||
import { User } from "../../generated/client";
|
import { User } from "../../generated/client";
|
||||||
|
|
||||||
|
// USer History
|
||||||
|
|
||||||
|
interface UserDeletedEvent {
|
||||||
|
type: "USER_DELETED";
|
||||||
|
reason: string;
|
||||||
|
date: string;
|
||||||
|
by: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserProfileUpdatedEvent {
|
||||||
|
type: "USER_PROFILE_UPDATED";
|
||||||
|
changes: {
|
||||||
|
field: string;
|
||||||
|
oldValue: string;
|
||||||
|
newValue: string;
|
||||||
|
};
|
||||||
|
date: string;
|
||||||
|
by: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserHistoryEvent = UserDeletedEvent | UserProfileUpdatedEvent;
|
||||||
|
|
||||||
export interface PublicUser {
|
export interface PublicUser {
|
||||||
firstname: string;
|
firstname: string;
|
||||||
lastname: string;
|
lastname: string;
|
||||||
@@ -17,8 +39,9 @@ export const DISCORD_ROLES = {
|
|||||||
|
|
||||||
export const getPublicUser = (
|
export const getPublicUser = (
|
||||||
user: User,
|
user: User,
|
||||||
options = {
|
options?: {
|
||||||
ignorePrivacy: false,
|
ignorePrivacy?: boolean;
|
||||||
|
fullLastName?: boolean;
|
||||||
},
|
},
|
||||||
): PublicUser => {
|
): PublicUser => {
|
||||||
const lastName = user.lastname
|
const lastName = user.lastname
|
||||||
@@ -27,11 +50,22 @@ export const getPublicUser = (
|
|||||||
.map((part) => `${part[0] || ""}.`)
|
.map((part) => `${part[0] || ""}.`)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
|
if (options?.fullLastName) {
|
||||||
return {
|
return {
|
||||||
firstname: user.firstname,
|
firstname: user.firstname,
|
||||||
lastname: user.settingsHideLastname && !options.ignorePrivacy ? "" : lastName.trim(), // Only take the first letter of each section of the last name
|
lastname: user.settingsHideLastname && !options.ignorePrivacy ? "" : user.lastname,
|
||||||
fullName:
|
fullName:
|
||||||
`${user.firstname} ${user.settingsHideLastname && !options.ignorePrivacy ? "" : lastName}`.trim(),
|
`${user.firstname} ${user.settingsHideLastname && !options.ignorePrivacy ? "" : user.lastname}`.trim(),
|
||||||
|
publicId: user.publicId,
|
||||||
|
badges: user.badges,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
firstname: user.firstname,
|
||||||
|
lastname: user.settingsHideLastname && !options?.ignorePrivacy ? "" : lastName.trim(), // Only take the first letter of each section of the last name
|
||||||
|
fullName:
|
||||||
|
`${user.firstname} ${user.settingsHideLastname && !options?.ignorePrivacy ? "" : lastName}`.trim(),
|
||||||
publicId: user.publicId,
|
publicId: user.publicId,
|
||||||
badges: user.badges,
|
badges: user.badges,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,25 +5,10 @@ enum EVENT_TYPE {
|
|||||||
EVENT
|
EVENT
|
||||||
}
|
}
|
||||||
|
|
||||||
model EventAppointment {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
eventId Int
|
|
||||||
appointmentDate DateTime
|
|
||||||
presenterId String
|
|
||||||
// relations:
|
|
||||||
Users User[] @relation("EventAppointmentUser")
|
|
||||||
Participants Participant[]
|
|
||||||
Event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
|
|
||||||
Presenter User @relation(fields: [presenterId], references: [id])
|
|
||||||
}
|
|
||||||
|
|
||||||
model Participant {
|
model Participant {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
userId String @map(name: "user_id")
|
userId String @map(name: "user_id")
|
||||||
finisherMoodleCurseCompleted Boolean @default(false)
|
finisherMoodleCurseCompleted Boolean @default(false)
|
||||||
attended Boolean @default(false)
|
|
||||||
appointmentCancelled Boolean @default(false)
|
|
||||||
eventAppointmentId Int?
|
|
||||||
enscriptionDate DateTime @default(now())
|
enscriptionDate DateTime @default(now())
|
||||||
|
|
||||||
statusLog Json[] @default([])
|
statusLog Json[] @default([])
|
||||||
@@ -31,7 +16,6 @@ model Participant {
|
|||||||
// relations:
|
// relations:
|
||||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
Event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
|
Event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
|
||||||
EventAppointment EventAppointment? @relation(fields: [eventAppointmentId], references: [id])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model Event {
|
model Event {
|
||||||
@@ -41,8 +25,6 @@ model Event {
|
|||||||
description String
|
description String
|
||||||
type EVENT_TYPE @default(EVENT)
|
type EVENT_TYPE @default(EVENT)
|
||||||
discordRoleId String? @default("")
|
discordRoleId String? @default("")
|
||||||
hasPresenceEvents Boolean @default(false)
|
|
||||||
maxParticipants Int? @default(0)
|
|
||||||
finisherMoodleCourseId String? @default("")
|
finisherMoodleCourseId String? @default("")
|
||||||
finishedBadges BADGES[] @default([])
|
finishedBadges BADGES[] @default([])
|
||||||
requiredBadges BADGES[] @default([])
|
requiredBadges BADGES[] @default([])
|
||||||
@@ -51,7 +33,6 @@ model Event {
|
|||||||
|
|
||||||
// relations:
|
// relations:
|
||||||
Participants Participant[]
|
Participants Participant[]
|
||||||
Appointments EventAppointment[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model File {
|
model File {
|
||||||
|
|||||||
23
packages/database/prisma/schema/log.prisma
Normal file
23
packages/database/prisma/schema/log.prisma
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
model Log {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
type LOG_TYPE
|
||||||
|
userId String?
|
||||||
|
browser String?
|
||||||
|
deviceId String?
|
||||||
|
ip String?
|
||||||
|
field String?
|
||||||
|
oldValue String?
|
||||||
|
newValue String?
|
||||||
|
timestamp DateTime @default(now())
|
||||||
|
|
||||||
|
User User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@map(name: "logs")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LOG_TYPE {
|
||||||
|
LOGIN
|
||||||
|
PROFILE_CHANGE
|
||||||
|
REGISTER
|
||||||
|
ACCOUNT_DELETED
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `appointmentCancelled` on the `Participant` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `attended` on the `Participant` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `eventAppointmentId` on the `Participant` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the `EventAppointment` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
- You are about to drop the `_EventAppointmentUser` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "EventAppointment" DROP CONSTRAINT "EventAppointment_eventId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "EventAppointment" DROP CONSTRAINT "EventAppointment_presenterId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "Participant" DROP CONSTRAINT "Participant_eventAppointmentId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "_EventAppointmentUser" DROP CONSTRAINT "_EventAppointmentUser_A_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "_EventAppointmentUser" DROP CONSTRAINT "_EventAppointmentUser_B_fkey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Participant" DROP COLUMN "appointmentCancelled",
|
||||||
|
DROP COLUMN "attended",
|
||||||
|
DROP COLUMN "eventAppointmentId";
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "EventAppointment";
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "_EventAppointmentUser";
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `hasPresenceEvents` on the `Event` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Event" DROP COLUMN "hasPresenceEvents";
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `maxParticipants` on the `Event` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Event" DROP COLUMN "maxParticipants";
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "users" ADD COLUMN "is_deleted" BOOLEAN NOT NULL DEFAULT false;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "LOG_TYLE" AS ENUM ('LOGIN', 'PROFILE_CHANGE');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "logs" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"type" "LOG_TYLE" NOT NULL,
|
||||||
|
"userId" TEXT,
|
||||||
|
"ip" TEXT,
|
||||||
|
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "logs_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "logs" ADD CONSTRAINT "logs_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Changed the type of `type` on the `logs` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "LOG_TYPE" AS ENUM ('LOGIN', 'PROFILE_CHANGE', 'REGISTER');
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "logs" ADD COLUMN "browser" TEXT,
|
||||||
|
DROP COLUMN "type",
|
||||||
|
ADD COLUMN "type" "LOG_TYPE" NOT NULL;
|
||||||
|
|
||||||
|
-- DropEnum
|
||||||
|
DROP TYPE "LOG_TYLE";
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "logs" ADD COLUMN "deviceId" TEXT;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "logs" ADD COLUMN "field" TEXT,
|
||||||
|
ADD COLUMN "newValue" TEXT,
|
||||||
|
ADD COLUMN "oldValue" TEXT;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "LOG_TYPE" ADD VALUE 'ACCOUNT_DELETED';
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Penalty" ADD COLUMN "addPermissionApplied" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ADD COLUMN "removePermissionApplied" BOOLEAN NOT NULL DEFAULT false;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- The primary key for the `FormerDiscordAccount` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "FormerDiscordAccount_discord_id_key";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "FormerDiscordAccount" DROP CONSTRAINT "FormerDiscordAccount_pkey",
|
||||||
|
ADD COLUMN "id" SERIAL NOT NULL,
|
||||||
|
ADD CONSTRAINT "FormerDiscordAccount_pkey" PRIMARY KEY ("id");
|
||||||
@@ -10,10 +10,14 @@ model Penalty {
|
|||||||
|
|
||||||
suspended Boolean @default(false)
|
suspended Boolean @default(false)
|
||||||
|
|
||||||
|
// For Chronjob to know if permissions were already applied/removed
|
||||||
|
removePermissionApplied Boolean @default(false)
|
||||||
|
addPermissionApplied Boolean @default(false)
|
||||||
|
|
||||||
timestamp DateTime @default(now())
|
timestamp DateTime @default(now())
|
||||||
|
|
||||||
// relations:
|
// relations:
|
||||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
User User @relation("User", fields: [userId], references: [id], onDelete: Cascade)
|
||||||
CreatedUser User? @relation("CreatedPenalties", fields: [createdUserId], references: [id])
|
CreatedUser User? @relation("CreatedPenalties", fields: [createdUserId], references: [id])
|
||||||
Report Report? @relation(fields: [reportId], references: [id])
|
Report Report? @relation(fields: [reportId], references: [id])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,5 @@ model Report {
|
|||||||
Sender User? @relation("SentReports", fields: [senderUserId], references: [id])
|
Sender User? @relation("SentReports", fields: [senderUserId], references: [id])
|
||||||
Reported User @relation("ReceivedReports", fields: [reportedUserId], references: [id], onDelete: Cascade)
|
Reported User @relation("ReceivedReports", fields: [reportedUserId], references: [id], onDelete: Cascade)
|
||||||
Reviewer User? @relation("ReviewedReports", fields: [reviewerUserId], references: [id])
|
Reviewer User? @relation("ReviewedReports", fields: [reviewerUserId], references: [id])
|
||||||
Penalty Penalty[]
|
Penalties Penalty[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,11 +66,12 @@ model User {
|
|||||||
Duplicates User[] @relation("CanonicalUser")
|
Duplicates User[] @relation("CanonicalUser")
|
||||||
duplicateDetectedAt DateTime? @map(name: "duplicate_detected_at")
|
duplicateDetectedAt DateTime? @map(name: "duplicate_detected_at")
|
||||||
duplicateReason String? @map(name: "duplicate_reason")
|
duplicateReason String? @map(name: "duplicate_reason")
|
||||||
|
|
||||||
|
isDeleted Boolean @default(false) @map(name: "is_deleted")
|
||||||
|
|
||||||
// relations:
|
// relations:
|
||||||
oauthTokens OAuthToken[]
|
oauthTokens OAuthToken[]
|
||||||
participants Participant[]
|
participants Participant[]
|
||||||
EventAppointmentUser EventAppointment[] @relation("EventAppointmentUser")
|
|
||||||
EventAppointment EventAppointment[]
|
|
||||||
SentMessages ChatMessage[] @relation("SentMessages")
|
SentMessages ChatMessage[] @relation("SentMessages")
|
||||||
ReceivedMessages ChatMessage[] @relation("ReceivedMessages")
|
ReceivedMessages ChatMessage[] @relation("ReceivedMessages")
|
||||||
SentReports Report[] @relation("SentReports")
|
SentReports Report[] @relation("SentReports")
|
||||||
@@ -81,10 +82,11 @@ model User {
|
|||||||
ConnectedDispatcher ConnectedDispatcher[]
|
ConnectedDispatcher ConnectedDispatcher[]
|
||||||
ConnectedAircraft ConnectedAircraft[]
|
ConnectedAircraft ConnectedAircraft[]
|
||||||
PositionLog PositionLog[]
|
PositionLog PositionLog[]
|
||||||
Penaltys Penalty[]
|
Penaltys Penalty[] @relation("User")
|
||||||
CreatedPenalties Penalty[] @relation("CreatedPenalties")
|
CreatedPenalties Penalty[] @relation("CreatedPenalties")
|
||||||
Bookings Booking[]
|
Logs Log[]
|
||||||
|
|
||||||
|
Bookings Booking[]
|
||||||
DiscordAccount DiscordAccount?
|
DiscordAccount DiscordAccount?
|
||||||
FormerDiscordAccounts FormerDiscordAccount[]
|
FormerDiscordAccounts FormerDiscordAccount[]
|
||||||
|
|
||||||
@@ -92,14 +94,13 @@ model User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model FormerDiscordAccount {
|
model FormerDiscordAccount {
|
||||||
discordId String @unique @map(name: "discord_id")
|
id Int @id @default(autoincrement())
|
||||||
|
discordId String @map(name: "discord_id")
|
||||||
userId String @map(name: "user_id")
|
userId String @map(name: "user_id")
|
||||||
removedAt DateTime @default(now()) @map(name: "removed_at")
|
removedAt DateTime @default(now()) @map(name: "removed_at")
|
||||||
|
|
||||||
DiscordAccount DiscordAccount? @relation(fields: [discordId], references: [discordId], onDelete: SetNull)
|
DiscordAccount DiscordAccount? @relation(fields: [discordId], references: [discordId], onDelete: SetNull)
|
||||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@id([discordId, userId])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model DiscordAccount {
|
model DiscordAccount {
|
||||||
@@ -119,7 +120,7 @@ model DiscordAccount {
|
|||||||
// Related User
|
// Related User
|
||||||
userId String? @unique
|
userId String? @unique
|
||||||
User User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
User User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||||
formerDiscordAccount FormerDiscordAccount?
|
formerDiscordAccount FormerDiscordAccount[]
|
||||||
|
|
||||||
@@map(name: "discord_accounts")
|
@@map(name: "discord_accounts")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { ReactNode, useState } from "react";
|
import { ReactNode, useState } from "react";
|
||||||
import { cn } from "../helper/cn";
|
import { cn } from "../helper/cn";
|
||||||
|
import { Button } from "./Button";
|
||||||
|
|
||||||
export const PenaltyDropdown = ({
|
export const PenaltyDropdown = ({
|
||||||
onClick,
|
onClick,
|
||||||
@@ -79,10 +80,10 @@ export const PenaltyDropdown = ({
|
|||||||
<option value="1y">1 Jahr</option>
|
<option value="1y">1 Jahr</option>
|
||||||
</select>
|
</select>
|
||||||
)}
|
)}
|
||||||
<button
|
<Button
|
||||||
className={cn("btn btn-square btn-soft tooltip tooltip-bottom w-full", btnClassName)}
|
className={cn("btn btn-square btn-soft tooltip tooltip-bottom w-full", btnClassName)}
|
||||||
data-tip={btnTip}
|
data-tip={btnTip}
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
let untilDate: Date | null = null;
|
let untilDate: Date | null = null;
|
||||||
if (until !== "default") {
|
if (until !== "default") {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -124,11 +125,11 @@ export const PenaltyDropdown = ({
|
|||||||
untilDate = null;
|
untilDate = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onClick({ reason, until: untilDate });
|
await onClick({ reason, until: untilDate });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Icon} {btnName}
|
{Icon} {btnName}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,5 @@ import { Event, Participant } from "@repo/db";
|
|||||||
export const eventCompleted = (event: Event, participant?: Participant) => {
|
export const eventCompleted = (event: Event, participant?: Participant) => {
|
||||||
if (!participant) return false;
|
if (!participant) return false;
|
||||||
if (event.finisherMoodleCourseId && !participant.finisherMoodleCurseCompleted) return false;
|
if (event.finisherMoodleCourseId && !participant.finisherMoodleCurseCompleted) return false;
|
||||||
if (event.hasPresenceEvents && !participant.attended) return false;
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|||||||
194
pnpm-lock.yaml
generated
194
pnpm-lock.yaml
generated
@@ -11,17 +11,25 @@ overrides:
|
|||||||
form-data@>=4.0.0 <4.0.4: '>=4.0.4'
|
form-data@>=4.0.0 <4.0.4: '>=4.0.4'
|
||||||
js-yaml@>=4.0.0 <4.1.1: '>=4.1.1'
|
js-yaml@>=4.0.0 <4.1.1: '>=4.1.1'
|
||||||
jws@<3.2.3: '>=3.2.3'
|
jws@<3.2.3: '>=3.2.3'
|
||||||
|
lodash@>=4.0.0 <=4.17.22: '>=4.17.23'
|
||||||
mdast-util-to-hast@>=13.0.0 <13.2.1: '>=13.2.1'
|
mdast-util-to-hast@>=13.0.0 <13.2.1: '>=13.2.1'
|
||||||
next-auth@<4.24.12: '>=4.24.12'
|
next-auth@<4.24.12: '>=4.24.12'
|
||||||
next@>=15.0.0 <=15.4.4: '>=15.4.5'
|
next@>=15.0.0 <=15.4.4: '>=15.4.5'
|
||||||
next@>=15.0.0-canary.0 <15.4.7: '>=15.4.7'
|
next@>=15.0.0-canary.0 <15.4.7: '>=15.4.7'
|
||||||
next@>=15.4.0-canary.0 <15.4.8: '>=15.4.8'
|
next@>=15.4.0-canary.0 <15.4.8: '>=15.4.8'
|
||||||
next@>=15.4.0-canary.0 <15.4.9: '>=15.4.9'
|
next@>=15.4.0-canary.0 <15.4.9: '>=15.4.9'
|
||||||
|
next@>=15.6.0-canary.0 <16.1.5: '>=16.1.5'
|
||||||
|
next@>=16.0.0-beta.0 <16.1.5: '>=16.1.5'
|
||||||
|
next@>=16.1.0-canary.0 <16.1.5: '>=16.1.5'
|
||||||
nodemailer@<7.0.7: '>=7.0.7'
|
nodemailer@<7.0.7: '>=7.0.7'
|
||||||
nodemailer@<=7.0.10: '>=7.0.11'
|
nodemailer@<=7.0.10: '>=7.0.11'
|
||||||
playwright@<1.55.1: '>=1.55.1'
|
playwright@<1.55.1: '>=1.55.1'
|
||||||
preact@>=10.26.5 <10.26.10: '>=10.26.10'
|
preact@>=10.26.5 <10.26.10: '>=10.26.10'
|
||||||
qs@<6.14.1: '>=6.14.1'
|
qs@<6.14.1: '>=6.14.1'
|
||||||
|
tar@<7.5.7: '>=7.5.7'
|
||||||
|
tar@<=7.5.2: '>=7.5.3'
|
||||||
|
tar@<=7.5.3: '>=7.5.4'
|
||||||
|
undici@<6.23.0: '>=6.23.0'
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
@@ -130,7 +138,7 @@ importers:
|
|||||||
version: 0.5.8(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.15.3(@types/dom-mediacapture-record@1.0.22))
|
version: 0.5.8(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.15.3(@types/dom-mediacapture-record@1.0.22))
|
||||||
'@next-auth/prisma-adapter':
|
'@next-auth/prisma-adapter':
|
||||||
specifier: ^1.0.7
|
specifier: ^1.0.7
|
||||||
version: 1.0.7(@prisma/client@6.12.0(prisma@6.12.0(typescript@5.8.3))(typescript@5.8.3))(next-auth@4.24.13(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(nodemailer@7.0.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))
|
version: 1.0.7(@prisma/client@6.12.0(prisma@6.12.0(typescript@5.8.3))(typescript@5.8.3))(next-auth@4.24.13(next@16.1.1(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(nodemailer@7.0.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))
|
||||||
'@radix-ui/react-icons':
|
'@radix-ui/react-icons':
|
||||||
specifier: ^1.3.2
|
specifier: ^1.3.2
|
||||||
version: 1.3.2(react@19.1.0)
|
version: 1.3.2(react@19.1.0)
|
||||||
@@ -211,10 +219,10 @@ importers:
|
|||||||
version: 0.525.0(react@19.1.0)
|
version: 0.525.0(react@19.1.0)
|
||||||
next:
|
next:
|
||||||
specifier: '>=15.4.9'
|
specifier: '>=15.4.9'
|
||||||
version: 16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 16.1.1(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
next-auth:
|
next-auth:
|
||||||
specifier: '>=4.24.12'
|
specifier: '>=4.24.12'
|
||||||
version: 4.24.13(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(nodemailer@7.0.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 4.24.13(next@16.1.1(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(nodemailer@7.0.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
npm:
|
npm:
|
||||||
specifier: ^11.4.2
|
specifier: ^11.4.2
|
||||||
version: 11.4.2
|
version: 11.4.2
|
||||||
@@ -430,8 +438,8 @@ importers:
|
|||||||
specifier: ^9.0.2
|
specifier: ^9.0.2
|
||||||
version: 9.0.2
|
version: 9.0.2
|
||||||
lodash:
|
lodash:
|
||||||
specifier: ^4.17.21
|
specifier: '>=4.17.23'
|
||||||
version: 4.17.21
|
version: 4.17.23
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.525.0
|
specifier: ^0.525.0
|
||||||
version: 0.525.0(react@19.1.0)
|
version: 0.525.0(react@19.1.0)
|
||||||
@@ -588,6 +596,9 @@ importers:
|
|||||||
specifier: ^3.2.4
|
specifier: ^3.2.4
|
||||||
version: 3.2.4(@prisma/client@6.12.0(prisma@6.12.0(typescript@5.9.3))(typescript@5.9.3))(prisma@6.12.0(typescript@5.9.3))
|
version: 3.2.4(@prisma/client@6.12.0(prisma@6.12.0(typescript@5.9.3))(typescript@5.9.3))(prisma@6.12.0(typescript@5.9.3))
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^25.1.0
|
||||||
|
version: 25.1.0
|
||||||
prisma:
|
prisma:
|
||||||
specifier: ^6.12.0
|
specifier: ^6.12.0
|
||||||
version: 6.12.0(typescript@5.9.3)
|
version: 6.12.0(typescript@5.9.3)
|
||||||
@@ -1109,89 +1120,105 @@ packages:
|
|||||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@img/sharp-linux-arm64@0.34.5':
|
'@img/sharp-linux-arm64@0.34.5':
|
||||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linux-arm@0.34.5':
|
'@img/sharp-linux-arm@0.34.5':
|
||||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linux-ppc64@0.34.5':
|
'@img/sharp-linux-ppc64@0.34.5':
|
||||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linux-riscv64@0.34.5':
|
'@img/sharp-linux-riscv64@0.34.5':
|
||||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linux-s390x@0.34.5':
|
'@img/sharp-linux-s390x@0.34.5':
|
||||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linux-x64@0.34.5':
|
'@img/sharp-linux-x64@0.34.5':
|
||||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@img/sharp-wasm32@0.34.5':
|
'@img/sharp-wasm32@0.34.5':
|
||||||
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||||
@@ -1315,24 +1342,28 @@ packages:
|
|||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@next/swc-linux-arm64-musl@16.1.1':
|
'@next/swc-linux-arm64-musl@16.1.1':
|
||||||
resolution: {integrity: sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==}
|
resolution: {integrity: sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@next/swc-linux-x64-gnu@16.1.1':
|
'@next/swc-linux-x64-gnu@16.1.1':
|
||||||
resolution: {integrity: sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==}
|
resolution: {integrity: sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@next/swc-linux-x64-musl@16.1.1':
|
'@next/swc-linux-x64-musl@16.1.1':
|
||||||
resolution: {integrity: sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==}
|
resolution: {integrity: sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@next/swc-win32-arm64-msvc@16.1.1':
|
'@next/swc-win32-arm64-msvc@16.1.1':
|
||||||
resolution: {integrity: sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==}
|
resolution: {integrity: sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==}
|
||||||
@@ -1655,24 +1686,28 @@ packages:
|
|||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.11':
|
'@tailwindcss/oxide-linux-arm64-musl@4.1.11':
|
||||||
resolution: {integrity: sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==}
|
resolution: {integrity: sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.11':
|
'@tailwindcss/oxide-linux-x64-gnu@4.1.11':
|
||||||
resolution: {integrity: sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==}
|
resolution: {integrity: sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-musl@4.1.11':
|
'@tailwindcss/oxide-linux-x64-musl@4.1.11':
|
||||||
resolution: {integrity: sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==}
|
resolution: {integrity: sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@tailwindcss/oxide-wasm32-wasi@4.1.11':
|
'@tailwindcss/oxide-wasm32-wasi@4.1.11':
|
||||||
resolution: {integrity: sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==}
|
resolution: {integrity: sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==}
|
||||||
@@ -2161,6 +2196,9 @@ packages:
|
|||||||
'@types/node@22.15.34':
|
'@types/node@22.15.34':
|
||||||
resolution: {integrity: sha512-8Y6E5WUupYy1Dd0II32BsWAx5MWdcnRd8L84Oys3veg1YrYtNtzgO4CFhiBg6MDSjk7Ay36HYOnU7/tuOzIzcw==}
|
resolution: {integrity: sha512-8Y6E5WUupYy1Dd0II32BsWAx5MWdcnRd8L84Oys3veg1YrYtNtzgO4CFhiBg6MDSjk7Ay36HYOnU7/tuOzIzcw==}
|
||||||
|
|
||||||
|
'@types/node@25.1.0':
|
||||||
|
resolution: {integrity: sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==}
|
||||||
|
|
||||||
'@types/nodemailer@6.4.17':
|
'@types/nodemailer@6.4.17':
|
||||||
resolution: {integrity: sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==}
|
resolution: {integrity: sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==}
|
||||||
|
|
||||||
@@ -2323,41 +2361,49 @@ packages:
|
|||||||
resolution: {integrity: sha512-UYA0MA8ajkEDCFRQdng/FVx3F6szBvk3EPnkTTQuuO9lV1kPGuTB+V9TmbDxy5ikaEgyWKxa4CI3ySjklZ9lFA==}
|
resolution: {integrity: sha512-UYA0MA8ajkEDCFRQdng/FVx3F6szBvk3EPnkTTQuuO9lV1kPGuTB+V9TmbDxy5ikaEgyWKxa4CI3ySjklZ9lFA==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-arm64-musl@1.9.2':
|
'@unrs/resolver-binding-linux-arm64-musl@1.9.2':
|
||||||
resolution: {integrity: sha512-P/CO3ODU9YJIHFqAkHbquKtFst0COxdphc8TKGL5yCX75GOiVpGqd1d15ahpqu8xXVsqP4MGFP2C3LRZnnL5MA==}
|
resolution: {integrity: sha512-P/CO3ODU9YJIHFqAkHbquKtFst0COxdphc8TKGL5yCX75GOiVpGqd1d15ahpqu8xXVsqP4MGFP2C3LRZnnL5MA==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-ppc64-gnu@1.9.2':
|
'@unrs/resolver-binding-linux-ppc64-gnu@1.9.2':
|
||||||
resolution: {integrity: sha512-uKStFlOELBxBum2s1hODPtgJhY4NxYJE9pAeyBgNEzHgTqTiVBPjfTlPFJkfxyTjQEuxZbbJlJnMCrRgD7ubzw==}
|
resolution: {integrity: sha512-uKStFlOELBxBum2s1hODPtgJhY4NxYJE9pAeyBgNEzHgTqTiVBPjfTlPFJkfxyTjQEuxZbbJlJnMCrRgD7ubzw==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-riscv64-gnu@1.9.2':
|
'@unrs/resolver-binding-linux-riscv64-gnu@1.9.2':
|
||||||
resolution: {integrity: sha512-LkbNnZlhINfY9gK30AHs26IIVEZ9PEl9qOScYdmY2o81imJYI4IMnJiW0vJVtXaDHvBvxeAgEy5CflwJFIl3tQ==}
|
resolution: {integrity: sha512-LkbNnZlhINfY9gK30AHs26IIVEZ9PEl9qOScYdmY2o81imJYI4IMnJiW0vJVtXaDHvBvxeAgEy5CflwJFIl3tQ==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-riscv64-musl@1.9.2':
|
'@unrs/resolver-binding-linux-riscv64-musl@1.9.2':
|
||||||
resolution: {integrity: sha512-vI+e6FzLyZHSLFNomPi+nT+qUWN4YSj8pFtQZSFTtmgFoxqB6NyjxSjAxEC1m93qn6hUXhIsh8WMp+fGgxCoRg==}
|
resolution: {integrity: sha512-vI+e6FzLyZHSLFNomPi+nT+qUWN4YSj8pFtQZSFTtmgFoxqB6NyjxSjAxEC1m93qn6hUXhIsh8WMp+fGgxCoRg==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-s390x-gnu@1.9.2':
|
'@unrs/resolver-binding-linux-s390x-gnu@1.9.2':
|
||||||
resolution: {integrity: sha512-sSO4AlAYhSM2RAzBsRpahcJB1msc6uYLAtP6pesPbZtptF8OU/CbCPhSRW6cnYOGuVmEmWVW5xVboAqCnWTeHQ==}
|
resolution: {integrity: sha512-sSO4AlAYhSM2RAzBsRpahcJB1msc6uYLAtP6pesPbZtptF8OU/CbCPhSRW6cnYOGuVmEmWVW5xVboAqCnWTeHQ==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-x64-gnu@1.9.2':
|
'@unrs/resolver-binding-linux-x64-gnu@1.9.2':
|
||||||
resolution: {integrity: sha512-jkSkwch0uPFva20Mdu8orbQjv2A3G88NExTN2oPTI1AJ+7mZfYW3cDCTyoH6OnctBKbBVeJCEqh0U02lTkqD5w==}
|
resolution: {integrity: sha512-jkSkwch0uPFva20Mdu8orbQjv2A3G88NExTN2oPTI1AJ+7mZfYW3cDCTyoH6OnctBKbBVeJCEqh0U02lTkqD5w==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-x64-musl@1.9.2':
|
'@unrs/resolver-binding-linux-x64-musl@1.9.2':
|
||||||
resolution: {integrity: sha512-Uk64NoiTpQbkpl+bXsbeyOPRpUoMdcUqa+hDC1KhMW7aN1lfW8PBlBH4mJ3n3Y47dYE8qi0XTxy1mBACruYBaw==}
|
resolution: {integrity: sha512-Uk64NoiTpQbkpl+bXsbeyOPRpUoMdcUqa+hDC1KhMW7aN1lfW8PBlBH4mJ3n3Y47dYE8qi0XTxy1mBACruYBaw==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@unrs/resolver-binding-wasm32-wasi@1.9.2':
|
'@unrs/resolver-binding-wasm32-wasi@1.9.2':
|
||||||
resolution: {integrity: sha512-EpBGwkcjDicjR/ybC0g8wO5adPNdVuMrNalVgYcWi+gYtC1XYNuxe3rufcO7dA76OHGeVabcO6cSkPJKVcbCXQ==}
|
resolution: {integrity: sha512-EpBGwkcjDicjR/ybC0g8wO5adPNdVuMrNalVgYcWi+gYtC1XYNuxe3rufcO7dA76OHGeVabcO6cSkPJKVcbCXQ==}
|
||||||
@@ -3810,24 +3856,28 @@ packages:
|
|||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
lightningcss-linux-arm64-musl@1.30.1:
|
lightningcss-linux-arm64-musl@1.30.1:
|
||||||
resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
|
resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
lightningcss-linux-x64-gnu@1.30.1:
|
lightningcss-linux-x64-gnu@1.30.1:
|
||||||
resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
|
resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
lightningcss-linux-x64-musl@1.30.1:
|
lightningcss-linux-x64-musl@1.30.1:
|
||||||
resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
|
resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
lightningcss-win32-arm64-msvc@1.30.1:
|
lightningcss-win32-arm64-msvc@1.30.1:
|
||||||
resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
|
resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
|
||||||
@@ -3899,8 +3949,8 @@ packages:
|
|||||||
lodash.snakecase@4.1.1:
|
lodash.snakecase@4.1.1:
|
||||||
resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
|
resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
|
||||||
|
|
||||||
lodash@4.17.21:
|
lodash@4.17.23:
|
||||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
|
||||||
|
|
||||||
loglevel@1.9.1:
|
loglevel@1.9.1:
|
||||||
resolution: {integrity: sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==}
|
resolution: {integrity: sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==}
|
||||||
@@ -4144,15 +4194,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
|
||||||
minizlib@3.0.2:
|
minizlib@3.1.0:
|
||||||
resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==}
|
resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
mkdirp@3.0.1:
|
|
||||||
resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
moment@2.30.1:
|
moment@2.30.1:
|
||||||
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
|
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
|
||||||
|
|
||||||
@@ -4187,7 +4232,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==}
|
resolution: {integrity: sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@auth/core': 0.34.3
|
'@auth/core': 0.34.3
|
||||||
next: '>=15.4.9'
|
next: '>=16.1.5'
|
||||||
nodemailer: '>=7.0.11'
|
nodemailer: '>=7.0.11'
|
||||||
react: ^17.0.2 || ^18 || ^19
|
react: ^17.0.2 || ^18 || ^19
|
||||||
react-dom: ^17.0.2 || ^18 || ^19
|
react-dom: ^17.0.2 || ^18 || ^19
|
||||||
@@ -5102,8 +5147,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==}
|
resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
tar@7.4.3:
|
tar@7.5.7:
|
||||||
resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}
|
resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
tdigest@0.1.2:
|
tdigest@0.1.2:
|
||||||
@@ -5290,9 +5335,12 @@ packages:
|
|||||||
undici-types@6.21.0:
|
undici-types@6.21.0:
|
||||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||||
|
|
||||||
undici@6.21.3:
|
undici-types@7.16.0:
|
||||||
resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==}
|
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
||||||
engines: {node: '>=18.17'}
|
|
||||||
|
undici@7.19.2:
|
||||||
|
resolution: {integrity: sha512-4VQSpGEGsWzk0VYxyB/wVX/Q7qf9t5znLRgs0dzszr9w9Fej/8RVNQ+S20vdXSAyra/bJ7ZQfGv6ZMj7UEbzSg==}
|
||||||
|
engines: {node: '>=20.18.1'}
|
||||||
|
|
||||||
unified@11.0.5:
|
unified@11.0.5:
|
||||||
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
|
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
|
||||||
@@ -5603,7 +5651,7 @@ snapshots:
|
|||||||
'@babel/parser': 7.27.7
|
'@babel/parser': 7.27.7
|
||||||
'@babel/template': 7.27.2
|
'@babel/template': 7.27.2
|
||||||
'@babel/types': 7.27.7
|
'@babel/types': 7.27.7
|
||||||
debug: 4.4.1
|
debug: 4.4.1(supports-color@5.5.0)
|
||||||
globals: 11.12.0
|
globals: 11.12.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -5645,7 +5693,7 @@ snapshots:
|
|||||||
discord-api-types: 0.38.11
|
discord-api-types: 0.38.11
|
||||||
magic-bytes.js: 1.12.1
|
magic-bytes.js: 1.12.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
undici: 6.21.3
|
undici: 7.19.2
|
||||||
|
|
||||||
'@discordjs/util@1.1.1': {}
|
'@discordjs/util@1.1.1': {}
|
||||||
|
|
||||||
@@ -5852,7 +5900,7 @@ snapshots:
|
|||||||
'@eslint/eslintrc@3.3.1':
|
'@eslint/eslintrc@3.3.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv: 6.12.6
|
ajv: 6.12.6
|
||||||
debug: 4.4.1
|
debug: 4.4.1(supports-color@5.5.0)
|
||||||
espree: 10.4.0
|
espree: 10.4.0
|
||||||
globals: 14.0.0
|
globals: 14.0.0
|
||||||
ignore: 5.3.2
|
ignore: 5.3.2
|
||||||
@@ -6091,11 +6139,6 @@ snapshots:
|
|||||||
'@prisma/client': 6.12.0(prisma@6.12.0(typescript@5.8.3))(typescript@5.8.3)
|
'@prisma/client': 6.12.0(prisma@6.12.0(typescript@5.8.3))(typescript@5.8.3)
|
||||||
next-auth: 4.24.13(next@16.1.1(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(nodemailer@7.0.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
next-auth: 4.24.13(next@16.1.1(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(nodemailer@7.0.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
|
|
||||||
'@next-auth/prisma-adapter@1.0.7(@prisma/client@6.12.0(prisma@6.12.0(typescript@5.8.3))(typescript@5.8.3))(next-auth@4.24.13(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(nodemailer@7.0.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))':
|
|
||||||
dependencies:
|
|
||||||
'@prisma/client': 6.12.0(prisma@6.12.0(typescript@5.8.3))(typescript@5.8.3)
|
|
||||||
next-auth: 4.24.13(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(nodemailer@7.0.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
|
||||||
|
|
||||||
'@next/env@16.1.1': {}
|
'@next/env@16.1.1': {}
|
||||||
|
|
||||||
'@next/eslint-plugin-next@15.4.2':
|
'@next/eslint-plugin-next@15.4.2':
|
||||||
@@ -6347,7 +6390,7 @@ snapshots:
|
|||||||
'@sapphire/shapeshift@4.0.0':
|
'@sapphire/shapeshift@4.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-deep-equal: 3.1.3
|
fast-deep-equal: 3.1.3
|
||||||
lodash: 4.17.21
|
lodash: 4.17.23
|
||||||
|
|
||||||
'@sapphire/snowflake@3.5.3': {}
|
'@sapphire/snowflake@3.5.3': {}
|
||||||
|
|
||||||
@@ -6422,7 +6465,7 @@ snapshots:
|
|||||||
'@tailwindcss/oxide@4.1.11':
|
'@tailwindcss/oxide@4.1.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
detect-libc: 2.0.4
|
detect-libc: 2.0.4
|
||||||
tar: 7.4.3
|
tar: 7.5.7
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@tailwindcss/oxide-android-arm64': 4.1.11
|
'@tailwindcss/oxide-android-arm64': 4.1.11
|
||||||
'@tailwindcss/oxide-darwin-arm64': 4.1.11
|
'@tailwindcss/oxide-darwin-arm64': 4.1.11
|
||||||
@@ -7678,9 +7721,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
||||||
|
'@types/node@25.1.0':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 7.16.0
|
||||||
|
|
||||||
'@types/nodemailer@6.4.17':
|
'@types/nodemailer@6.4.17':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.29
|
'@types/node': 22.15.34
|
||||||
|
|
||||||
'@types/parse-json@4.0.2': {}
|
'@types/parse-json@4.0.2': {}
|
||||||
|
|
||||||
@@ -7749,7 +7796,7 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.37.0
|
'@typescript-eslint/types': 8.37.0
|
||||||
'@typescript-eslint/typescript-estree': 8.37.0(typescript@5.8.3)
|
'@typescript-eslint/typescript-estree': 8.37.0(typescript@5.8.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.37.0
|
'@typescript-eslint/visitor-keys': 8.37.0
|
||||||
debug: 4.4.1
|
debug: 4.4.1(supports-color@5.5.0)
|
||||||
eslint: 9.31.0(jiti@2.4.2)
|
eslint: 9.31.0(jiti@2.4.2)
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -7759,7 +7806,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/tsconfig-utils': 8.37.0(typescript@5.8.3)
|
'@typescript-eslint/tsconfig-utils': 8.37.0(typescript@5.8.3)
|
||||||
'@typescript-eslint/types': 8.37.0
|
'@typescript-eslint/types': 8.37.0
|
||||||
debug: 4.4.1
|
debug: 4.4.1(supports-color@5.5.0)
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -7778,7 +7825,7 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.37.0
|
'@typescript-eslint/types': 8.37.0
|
||||||
'@typescript-eslint/typescript-estree': 8.37.0(typescript@5.8.3)
|
'@typescript-eslint/typescript-estree': 8.37.0(typescript@5.8.3)
|
||||||
'@typescript-eslint/utils': 8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)
|
'@typescript-eslint/utils': 8.37.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)
|
||||||
debug: 4.4.1
|
debug: 4.4.1(supports-color@5.5.0)
|
||||||
eslint: 9.31.0(jiti@2.4.2)
|
eslint: 9.31.0(jiti@2.4.2)
|
||||||
ts-api-utils: 2.1.0(typescript@5.8.3)
|
ts-api-utils: 2.1.0(typescript@5.8.3)
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
@@ -7793,7 +7840,7 @@ snapshots:
|
|||||||
'@typescript-eslint/tsconfig-utils': 8.37.0(typescript@5.8.3)
|
'@typescript-eslint/tsconfig-utils': 8.37.0(typescript@5.8.3)
|
||||||
'@typescript-eslint/types': 8.37.0
|
'@typescript-eslint/types': 8.37.0
|
||||||
'@typescript-eslint/visitor-keys': 8.37.0
|
'@typescript-eslint/visitor-keys': 8.37.0
|
||||||
debug: 4.4.1
|
debug: 4.4.1(supports-color@5.5.0)
|
||||||
fast-glob: 3.3.3
|
fast-glob: 3.3.3
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
minimatch: 9.0.5
|
minimatch: 9.0.5
|
||||||
@@ -8324,7 +8371,7 @@ snapshots:
|
|||||||
concurrently@9.2.0:
|
concurrently@9.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
lodash: 4.17.21
|
lodash: 4.17.23
|
||||||
rxjs: 7.8.2
|
rxjs: 7.8.2
|
||||||
shell-quote: 1.8.3
|
shell-quote: 1.8.3
|
||||||
supports-color: 8.1.1
|
supports-color: 8.1.1
|
||||||
@@ -8424,10 +8471,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
debug@4.4.1:
|
|
||||||
dependencies:
|
|
||||||
ms: 2.1.3
|
|
||||||
|
|
||||||
debug@4.4.1(supports-color@5.5.0):
|
debug@4.4.1(supports-color@5.5.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
@@ -8491,7 +8534,7 @@ snapshots:
|
|||||||
lodash.snakecase: 4.1.1
|
lodash.snakecase: 4.1.1
|
||||||
magic-bytes.js: 1.12.1
|
magic-bytes.js: 1.12.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
undici: 6.21.3
|
undici: 7.19.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
@@ -8770,7 +8813,7 @@ snapshots:
|
|||||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.31.0(jiti@2.4.2)):
|
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.31.0(jiti@2.4.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nolyfill/is-core-module': 1.0.39
|
'@nolyfill/is-core-module': 1.0.39
|
||||||
debug: 4.4.1
|
debug: 4.4.1(supports-color@5.5.0)
|
||||||
eslint: 9.31.0(jiti@2.4.2)
|
eslint: 9.31.0(jiti@2.4.2)
|
||||||
get-tsconfig: 4.10.1
|
get-tsconfig: 4.10.1
|
||||||
is-bun-module: 2.0.0
|
is-bun-module: 2.0.0
|
||||||
@@ -9754,7 +9797,7 @@ snapshots:
|
|||||||
|
|
||||||
lodash.snakecase@4.1.1: {}
|
lodash.snakecase@4.1.1: {}
|
||||||
|
|
||||||
lodash@4.17.21: {}
|
lodash@4.17.23: {}
|
||||||
|
|
||||||
loglevel@1.9.1: {}
|
loglevel@1.9.1: {}
|
||||||
|
|
||||||
@@ -10184,12 +10227,10 @@ snapshots:
|
|||||||
|
|
||||||
minipass@7.1.2: {}
|
minipass@7.1.2: {}
|
||||||
|
|
||||||
minizlib@3.0.2:
|
minizlib@3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
minipass: 7.1.2
|
minipass: 7.1.2
|
||||||
|
|
||||||
mkdirp@3.0.1: {}
|
|
||||||
|
|
||||||
moment@2.30.1: {}
|
moment@2.30.1: {}
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
@@ -10223,23 +10264,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
nodemailer: 7.0.11
|
nodemailer: 7.0.11
|
||||||
|
|
||||||
next-auth@4.24.13(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(nodemailer@7.0.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
|
||||||
dependencies:
|
|
||||||
'@babel/runtime': 7.27.6
|
|
||||||
'@panva/hkdf': 1.2.1
|
|
||||||
cookie: 0.7.2
|
|
||||||
jose: 4.15.9
|
|
||||||
next: 16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
|
||||||
oauth: 0.9.15
|
|
||||||
openid-client: 5.7.1
|
|
||||||
preact: 10.28.2
|
|
||||||
preact-render-to-string: 5.2.6(preact@10.28.2)
|
|
||||||
react: 19.1.0
|
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
|
||||||
uuid: 8.3.2
|
|
||||||
optionalDependencies:
|
|
||||||
nodemailer: 7.0.11
|
|
||||||
|
|
||||||
next-remove-imports@1.0.12(webpack@5.99.9):
|
next-remove-imports@1.0.12(webpack@5.99.9):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.27.7
|
'@babel/core': 7.27.7
|
||||||
@@ -10275,32 +10299,6 @@ snapshots:
|
|||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- babel-plugin-macros
|
- babel-plugin-macros
|
||||||
|
|
||||||
next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
|
||||||
dependencies:
|
|
||||||
'@next/env': 16.1.1
|
|
||||||
'@swc/helpers': 0.5.15
|
|
||||||
baseline-browser-mapping: 2.9.14
|
|
||||||
caniuse-lite: 1.0.30001726
|
|
||||||
postcss: 8.4.31
|
|
||||||
react: 19.1.0
|
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
|
||||||
styled-jsx: 5.1.6(react@19.1.0)
|
|
||||||
optionalDependencies:
|
|
||||||
'@next/swc-darwin-arm64': 16.1.1
|
|
||||||
'@next/swc-darwin-x64': 16.1.1
|
|
||||||
'@next/swc-linux-arm64-gnu': 16.1.1
|
|
||||||
'@next/swc-linux-arm64-musl': 16.1.1
|
|
||||||
'@next/swc-linux-x64-gnu': 16.1.1
|
|
||||||
'@next/swc-linux-x64-musl': 16.1.1
|
|
||||||
'@next/swc-win32-arm64-msvc': 16.1.1
|
|
||||||
'@next/swc-win32-x64-msvc': 16.1.1
|
|
||||||
'@opentelemetry/api': 1.9.0
|
|
||||||
'@playwright/test': 1.52.0
|
|
||||||
sharp: 0.34.5
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@babel/core'
|
|
||||||
- babel-plugin-macros
|
|
||||||
|
|
||||||
node-cron@4.2.1: {}
|
node-cron@4.2.1: {}
|
||||||
|
|
||||||
node-releases@2.0.19: {}
|
node-releases@2.0.19: {}
|
||||||
@@ -10616,7 +10614,7 @@ snapshots:
|
|||||||
dayjs: 1.11.19
|
dayjs: 1.11.19
|
||||||
element-resize-detector: 1.2.4
|
element-resize-detector: 1.2.4
|
||||||
interactjs: 1.10.27
|
interactjs: 1.10.27
|
||||||
lodash: 4.17.21
|
lodash: 4.17.23
|
||||||
memoize-one: 6.0.0
|
memoize-one: 6.0.0
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
react-dom: 19.1.0(react@19.1.0)
|
react-dom: 19.1.0(react@19.1.0)
|
||||||
@@ -11238,11 +11236,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@babel/core': 7.27.7
|
'@babel/core': 7.27.7
|
||||||
|
|
||||||
styled-jsx@5.1.6(react@19.1.0):
|
|
||||||
dependencies:
|
|
||||||
client-only: 0.0.1
|
|
||||||
react: 19.1.0
|
|
||||||
|
|
||||||
stylis@4.2.0: {}
|
stylis@4.2.0: {}
|
||||||
|
|
||||||
supports-color@5.5.0:
|
supports-color@5.5.0:
|
||||||
@@ -11271,13 +11264,12 @@ snapshots:
|
|||||||
|
|
||||||
tapable@2.2.2: {}
|
tapable@2.2.2: {}
|
||||||
|
|
||||||
tar@7.4.3:
|
tar@7.5.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@isaacs/fs-minipass': 4.0.1
|
'@isaacs/fs-minipass': 4.0.1
|
||||||
chownr: 3.0.0
|
chownr: 3.0.0
|
||||||
minipass: 7.1.2
|
minipass: 7.1.2
|
||||||
minizlib: 3.0.2
|
minizlib: 3.1.0
|
||||||
mkdirp: 3.0.1
|
|
||||||
yallist: 5.0.0
|
yallist: 5.0.0
|
||||||
|
|
||||||
tdigest@0.1.2:
|
tdigest@0.1.2:
|
||||||
@@ -11463,7 +11455,9 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@6.21.0: {}
|
undici-types@6.21.0: {}
|
||||||
|
|
||||||
undici@6.21.3: {}
|
undici-types@7.16.0: {}
|
||||||
|
|
||||||
|
undici@7.19.2: {}
|
||||||
|
|
||||||
unified@11.0.5:
|
unified@11.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -11704,7 +11698,7 @@ snapshots:
|
|||||||
'@prisma/client': 6.12.0(prisma@6.12.0(typescript@5.9.3))(typescript@5.9.3)
|
'@prisma/client': 6.12.0(prisma@6.12.0(typescript@5.9.3))(typescript@5.9.3)
|
||||||
'@prisma/generator-helper': 6.8.2
|
'@prisma/generator-helper': 6.8.2
|
||||||
code-block-writer: 12.0.0
|
code-block-writer: 12.0.0
|
||||||
lodash: 4.17.21
|
lodash: 4.17.23
|
||||||
prisma: 6.12.0(typescript@5.9.3)
|
prisma: 6.12.0(typescript@5.9.3)
|
||||||
zod: 3.25.49
|
zod: 3.25.49
|
||||||
|
|
||||||
|
|||||||
@@ -18,14 +18,22 @@ overrides:
|
|||||||
form-data@>=4.0.0 <4.0.4: '>=4.0.4'
|
form-data@>=4.0.0 <4.0.4: '>=4.0.4'
|
||||||
js-yaml@>=4.0.0 <4.1.1: '>=4.1.1'
|
js-yaml@>=4.0.0 <4.1.1: '>=4.1.1'
|
||||||
jws@<3.2.3: '>=3.2.3'
|
jws@<3.2.3: '>=3.2.3'
|
||||||
|
lodash@>=4.0.0 <=4.17.22: '>=4.17.23'
|
||||||
mdast-util-to-hast@>=13.0.0 <13.2.1: '>=13.2.1'
|
mdast-util-to-hast@>=13.0.0 <13.2.1: '>=13.2.1'
|
||||||
next-auth@<4.24.12: '>=4.24.12'
|
next-auth@<4.24.12: '>=4.24.12'
|
||||||
next@>=15.0.0 <=15.4.4: '>=15.4.5'
|
next@>=15.0.0 <=15.4.4: '>=15.4.5'
|
||||||
next@>=15.0.0-canary.0 <15.4.7: '>=15.4.7'
|
next@>=15.0.0-canary.0 <15.4.7: '>=15.4.7'
|
||||||
next@>=15.4.0-canary.0 <15.4.8: '>=15.4.8'
|
next@>=15.4.0-canary.0 <15.4.8: '>=15.4.8'
|
||||||
next@>=15.4.0-canary.0 <15.4.9: '>=15.4.9'
|
next@>=15.4.0-canary.0 <15.4.9: '>=15.4.9'
|
||||||
|
next@>=15.6.0-canary.0 <16.1.5: '>=16.1.5'
|
||||||
|
next@>=16.0.0-beta.0 <16.1.5: '>=16.1.5'
|
||||||
|
next@>=16.1.0-canary.0 <16.1.5: '>=16.1.5'
|
||||||
nodemailer@<7.0.7: '>=7.0.7'
|
nodemailer@<7.0.7: '>=7.0.7'
|
||||||
nodemailer@<=7.0.10: '>=7.0.11'
|
nodemailer@<=7.0.10: '>=7.0.11'
|
||||||
playwright@<1.55.1: '>=1.55.1'
|
playwright@<1.55.1: '>=1.55.1'
|
||||||
preact@>=10.26.5 <10.26.10: '>=10.26.10'
|
preact@>=10.26.5 <10.26.10: '>=10.26.10'
|
||||||
qs@<6.14.1: '>=6.14.1'
|
qs@<6.14.1: '>=6.14.1'
|
||||||
|
tar@<7.5.7: '>=7.5.7'
|
||||||
|
tar@<=7.5.2: '>=7.5.3'
|
||||||
|
tar@<=7.5.3: '>=7.5.4'
|
||||||
|
undici@<6.23.0: '>=6.23.0'
|
||||||
|
|||||||
Reference in New Issue
Block a user