From b16b719c740053181bf5dc6dcd26f3080ab04047 Mon Sep 17 00:00:00 2001 From: PxlLoewe <72106766+PxlLoewe@users.noreply.github.com> Date: Sat, 27 Dec 2025 15:33:00 +0100 Subject: [PATCH] Redesigned Search, removed Unused Admin Route --- .../app/(app)/_components/RecentFlights.tsx | 20 +++--- apps/hub/app/(app)/admin/changelog/page.tsx | 12 +++- .../event/_components/AppointmentModal.tsx | 10 +-- .../(app)/admin/event/_components/Form.tsx | 30 +++++--- apps/hub/app/(app)/admin/heliport/page.tsx | 14 +++- apps/hub/app/(app)/admin/keyword/page.tsx | 17 +++-- .../(app)/admin/report/_components/form.tsx | 10 +-- .../(app)/admin/station/_components/Form.tsx | 17 +++-- apps/hub/app/(app)/admin/station/page.tsx | 16 +++-- .../admin/user/[id]/_components/forms.tsx | 33 +++++---- apps/hub/app/(app)/admin/user/page.tsx | 18 ++++- apps/hub/app/(app)/logbook/page.tsx | 22 +++--- apps/hub/app/_components/PaginatedTable.tsx | 42 ++++++------ .../app/_components/pagiantedTableActions.ts | 68 ++++++------------- apps/hub/app/api/admin/user/search/route.ts | 33 --------- apps/hub/app/vatsim/page.tsx | 25 +++++-- 16 files changed, 209 insertions(+), 178 deletions(-) delete mode 100644 apps/hub/app/api/admin/user/search/route.ts diff --git a/apps/hub/app/(app)/_components/RecentFlights.tsx b/apps/hub/app/(app)/_components/RecentFlights.tsx index 9999c414..d9be2342 100644 --- a/apps/hub/app/(app)/_components/RecentFlights.tsx +++ b/apps/hub/app/(app)/_components/RecentFlights.tsx @@ -1,5 +1,5 @@ "use client"; -import { Mission, MissionAlertLog, MissionLog, Station } from "@repo/db"; +import { Mission, MissionAlertLog, MissionLog, Prisma, Station } from "@repo/db"; import { ColumnDef } from "@tanstack/react-table"; import { PaginatedTable } from "_components/PaginatedTable"; import { ArrowRight, NotebookText } from "lucide-react"; @@ -12,20 +12,22 @@ export const RecentFlights = () => {

- Logbook + Logbook - Zum vollständigen Logbook + Zum vollständigen Logbook

+ ({ + User: { id: session.data?.user.id }, + Mission: { + state: { in: ["finished", "archived"] }, + }, + }) as Prisma.MissionOnStationUsersWhereInput + } include={{ Station: true, User: true, diff --git a/apps/hub/app/(app)/admin/changelog/page.tsx b/apps/hub/app/(app)/admin/changelog/page.tsx index b768e357..f24bc0e3 100644 --- a/apps/hub/app/(app)/admin/changelog/page.tsx +++ b/apps/hub/app/(app)/admin/changelog/page.tsx @@ -3,7 +3,7 @@ import { DatabaseBackupIcon } from "lucide-react"; import { PaginatedTable } from "../../../_components/PaginatedTable"; import Link from "next/link"; import { ColumnDef } from "@tanstack/react-table"; -import { Keyword } from "@repo/db"; +import { Keyword, Prisma } from "@repo/db"; export default () => { return ( @@ -12,7 +12,15 @@ export default () => { stickyHeaders initialOrderBy={[{ id: "title", desc: true }]} prismaModel="changelog" - searchFields={["title"]} + showSearch + getFilter={(search) => + ({ + OR: [ + { title: { contains: search, mode: "insensitive" } }, + { description: { contains: search, mode: "insensitive" } }, + ], + }) as Prisma.ChangelogWhereInput + } columns={ [ { diff --git a/apps/hub/app/(app)/admin/event/_components/AppointmentModal.tsx b/apps/hub/app/(app)/admin/event/_components/AppointmentModal.tsx index 37c37b49..f2e69dcf 100644 --- a/apps/hub/app/(app)/admin/event/_components/AppointmentModal.tsx +++ b/apps/hub/app/(app)/admin/event/_components/AppointmentModal.tsx @@ -1,4 +1,4 @@ -import { Event, Participant } from "@repo/db"; +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"; @@ -167,9 +167,11 @@ export const AppointmentModal = ({ ] as ColumnDef[] } prismaModel={"participant"} - filter={{ - eventAppointmentId: appointmentForm.watch("id"), - }} + getFilter={() => + ({ + eventAppointmentId: appointmentForm.watch("id")!, + }) as Prisma.ParticipantWhereInput + } include={{ User: true }} leftOfPagination={
diff --git a/apps/hub/app/(app)/admin/event/_components/Form.tsx b/apps/hub/app/(app)/admin/event/_components/Form.tsx index aab1c5b5..0059d196 100644 --- a/apps/hub/app/(app)/admin/event/_components/Form.tsx +++ b/apps/hub/app/(app)/admin/event/_components/Form.tsx @@ -1,6 +1,6 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; -import { BADGES, Event, EVENT_TYPE, Participant, PERMISSION, User } from "@repo/db"; +import { BADGES, Event, EVENT_TYPE, Participant, PERMISSION, Prisma, User } from "@repo/db"; import { EventAppointmentOptionalDefaults, EventAppointmentOptionalDefaultsSchema, @@ -159,9 +159,11 @@ export const Form = ({ event }: { event?: Event }) => { + ({ + eventId: event?.id, + }) as Prisma.EventAppointmentWhereInput + } include={{ Presenter: true, Participants: true, @@ -257,12 +259,24 @@ export const Form = ({ event }: { event?: Event }) => { Teilnehmer } - searchFields={["User.firstname", "User.lastname", "User.publicId"]} ref={appointmentsTableRef} prismaModel={"participant"} - filter={{ - eventId: event?.id, - }} + showSearch + getFilter={(searchTerm) => + ({ + OR: [ + { + User: { + OR: [ + { firstname: { contains: searchTerm, mode: "insensitive" } }, + { lastname: { contains: searchTerm, mode: "insensitive" } }, + { publicId: { contains: searchTerm, mode: "insensitive" } }, + ], + }, + }, + ], + }) as Prisma.ParticipantWhereInput + } include={{ User: true, }} diff --git a/apps/hub/app/(app)/admin/heliport/page.tsx b/apps/hub/app/(app)/admin/heliport/page.tsx index 4d805e6c..08717fe5 100644 --- a/apps/hub/app/(app)/admin/heliport/page.tsx +++ b/apps/hub/app/(app)/admin/heliport/page.tsx @@ -3,7 +3,7 @@ import { DatabaseBackupIcon } from "lucide-react"; import { PaginatedTable } from "../../../_components/PaginatedTable"; import Link from "next/link"; import { ColumnDef } from "@tanstack/react-table"; -import { Heliport } from "@repo/db"; +import { Heliport, Prisma } from "@repo/db"; const page = () => { return ( @@ -11,7 +11,17 @@ const page = () => { + ({ + OR: [ + { siteName: { contains: searchTerm, mode: "insensitive" } }, + { info: { contains: searchTerm, mode: "insensitive" } }, + { hospital: { contains: searchTerm, mode: "insensitive" } }, + { designator: { contains: searchTerm, mode: "insensitive" } }, + ], + }) as Prisma.HeliportWhereInput + } + showSearch columns={ [ { diff --git a/apps/hub/app/(app)/admin/keyword/page.tsx b/apps/hub/app/(app)/admin/keyword/page.tsx index 73a75e0c..8d673a21 100644 --- a/apps/hub/app/(app)/admin/keyword/page.tsx +++ b/apps/hub/app/(app)/admin/keyword/page.tsx @@ -3,7 +3,7 @@ import { DatabaseBackupIcon } from "lucide-react"; import { PaginatedTable } from "../../../_components/PaginatedTable"; import Link from "next/link"; import { ColumnDef } from "@tanstack/react-table"; -import { Keyword } from "@repo/db"; +import { Keyword, Prisma } from "@repo/db"; export default () => { return ( @@ -12,7 +12,16 @@ export default () => { stickyHeaders initialOrderBy={[{ id: "category", desc: true }]} prismaModel="keyword" - searchFields={["name", "abreviation", "description"]} + showSearch + getFilter={(searchTerm) => + ({ + OR: [ + { name: { contains: searchTerm, mode: "insensitive" } }, + { abreviation: { contains: searchTerm, mode: "insensitive" } }, + { category: { contains: searchTerm, mode: "insensitive" } }, + ], + }) as Prisma.KeywordWhereInput + } columns={ [ { @@ -41,11 +50,11 @@ export default () => { } leftOfSearch={ - Stichwörter + Stichwörter } rightOfSearch={ -

+

diff --git a/apps/hub/app/(app)/admin/report/_components/form.tsx b/apps/hub/app/(app)/admin/report/_components/form.tsx index 407c02e0..38e3ca81 100644 --- a/apps/hub/app/(app)/admin/report/_components/form.tsx +++ b/apps/hub/app/(app)/admin/report/_components/form.tsx @@ -2,7 +2,7 @@ import { penaltyColumns as penaltyColumns } from "(app)/admin/penalty/columns"; import { editReport } from "(app)/admin/report/actions"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Report as IReport, User } from "@repo/db"; +import { Report as IReport, Prisma, User } from "@repo/db"; import { ReportSchema, Report as IReportZod } from "@repo/db/zod"; import { PaginatedTable } from "_components/PaginatedTable"; import { Button } from "_components/ui/Button"; @@ -149,9 +149,11 @@ export const ReportPenalties = ({ CreatedUser: true, Report: true, }} - filter={{ - reportId: report.id, - }} + getFilter={() => + ({ + reportId: report.id, + }) as Prisma.PenaltyWhereInput + } columns={penaltyColumns} />

diff --git a/apps/hub/app/(app)/admin/station/_components/Form.tsx b/apps/hub/app/(app)/admin/station/_components/Form.tsx index 207fae6a..c9276919 100644 --- a/apps/hub/app/(app)/admin/station/_components/Form.tsx +++ b/apps/hub/app/(app)/admin/station/_components/Form.tsx @@ -2,7 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { StationOptionalDefaultsSchema } from "@repo/db/zod"; import { useForm } from "react-hook-form"; -import { BosUse, ConnectedAircraft, Country, Station, User } from "@repo/db"; +import { BosUse, ConnectedAircraft, Country, Prisma, Station, User } from "@repo/db"; import { FileText, LocateIcon, PlaneIcon, UserIcon } from "lucide-react"; import { Input } from "../../../../_components/ui/Input"; import { deleteStation, upsertStation } from "../action"; @@ -198,10 +198,17 @@ export const StationForm = ({ station }: { station?: Station }) => { Verbundene Piloten
} - filter={{ - stationId: station?.id, - }} - searchFields={["User.firstname", "User.lastname", "User.publicId"]} + getFilter={(searchField) => + ({ + stationId: station?.id, + OR: [ + { User: { firstname: { contains: searchField, mode: "insensitive" } } }, + { User: { lastname: { contains: searchField, mode: "insensitive" } } }, + { User: { publicId: { contains: searchField, mode: "insensitive" } } }, + ], + }) as Prisma.ConnectedAircraftWhereInput + } + showSearch prismaModel={"connectedAircraft"} include={{ Station: true, User: true }} columns={ diff --git a/apps/hub/app/(app)/admin/station/page.tsx b/apps/hub/app/(app)/admin/station/page.tsx index b8103b06..b4d8b3d9 100644 --- a/apps/hub/app/(app)/admin/station/page.tsx +++ b/apps/hub/app/(app)/admin/station/page.tsx @@ -3,14 +3,22 @@ import { DatabaseBackupIcon } from "lucide-react"; import { PaginatedTable } from "../../../_components/PaginatedTable"; import Link from "next/link"; import { ColumnDef } from "@tanstack/react-table"; -import { Station } from "@repo/db"; +import { Prisma, Station } from "@repo/db"; const page = () => { return ( <> + ({ + OR: [ + { bosCallsign: { contains: searchField, mode: "insensitive" } }, + { operator: { contains: searchField, mode: "insensitive" } }, + ], + }) as Prisma.StationWhereInput + } stickyHeaders columns={ [ @@ -44,11 +52,11 @@ const page = () => { } leftOfSearch={ - Stationen + Stationen } rightOfSearch={ -

+

diff --git a/apps/hub/app/(app)/admin/user/[id]/_components/forms.tsx b/apps/hub/app/(app)/admin/user/[id]/_components/forms.tsx index 6829b137..1b56101d 100644 --- a/apps/hub/app/(app)/admin/user/[id]/_components/forms.tsx +++ b/apps/hub/app/(app)/admin/user/[id]/_components/forms.tsx @@ -8,6 +8,7 @@ import { DiscordAccount, Penalty, PERMISSION, + Prisma, Station, User, } from "@repo/db"; @@ -281,9 +282,11 @@ export const ConnectionHistory: React.FC<{ user: User }> = ({ user }: { user: Us + ({ + userId: user.id, + }) as Prisma.ConnectedDispatcherWhereInput + } prismaModel={"connectedDispatcher"} initialOrderBy={[ { @@ -349,9 +352,11 @@ export const ConnectionHistory: React.FC<{ user: User }> = ({ user }: { user: Us + ({ + userId: user.id, + }) as Prisma.ConnectedAircraftWhereInput + } prismaModel={"connectedAircraft"} include={{ Station: true }} initialOrderBy={[ @@ -505,9 +510,7 @@ export const UserPenalties = ({ user }: { user: User }) => { CreatedUser: true, Report: true, }} - filter={{ - userId: user.id, - }} + getFilter={() => ({ userId: user.id }) as Prisma.PenaltyWhereInput} columns={penaltyColumns} /> @@ -529,9 +532,11 @@ export const UserReports = ({ user }: { user: User }) => { + ({ + reportedUserId: user.id, + }) as Prisma.ReportWhereInput + } initialOrderBy={[ { id: "timestamp", @@ -720,9 +725,7 @@ export const AdminForm = ({ )} -

- Achtung! Dieser Account ist als Duplikat markiert oder hat Duplikate! -

+

{user.duplicateReason || "Keine Grund angegeben"}

)} {(!!openBans.length || !!openTimebans.length) && ( diff --git a/apps/hub/app/(app)/admin/user/page.tsx b/apps/hub/app/(app)/admin/user/page.tsx index 91faa09d..cdeb8b28 100644 --- a/apps/hub/app/(app)/admin/user/page.tsx +++ b/apps/hub/app/(app)/admin/user/page.tsx @@ -3,7 +3,7 @@ import { User2 } from "lucide-react"; import { PaginatedTable } from "../../../_components/PaginatedTable"; import Link from "next/link"; import { ColumnDef } from "@tanstack/react-table"; -import { DiscordAccount, User } from "@repo/db"; +import { DiscordAccount, Prisma, User } from "@repo/db"; import { useSession } from "next-auth/react"; const AdminUserPage = () => { @@ -14,7 +14,21 @@ const AdminUserPage = () => { { + return { + OR: [ + { firstname: { contains: searchTerm, mode: "insensitive" } }, + { lastname: { contains: searchTerm, mode: "insensitive" } }, + { email: { contains: searchTerm, mode: "insensitive" } }, + { + discordAccounts: { + some: { username: { contains: searchTerm, mode: "insensitive" } }, + }, + }, + ], + } as Prisma.UserWhereInput; + }} include={{ discordAccounts: true, }} diff --git a/apps/hub/app/(app)/logbook/page.tsx b/apps/hub/app/(app)/logbook/page.tsx index 9418a114..fd4647f8 100644 --- a/apps/hub/app/(app)/logbook/page.tsx +++ b/apps/hub/app/(app)/logbook/page.tsx @@ -1,5 +1,5 @@ "use client"; -import { Mission, MissionAlertLog, MissionLog, Station } from "@repo/db"; +import { Mission, MissionAlertLog, MissionLog, Prisma, Station } from "@repo/db"; import { ColumnDef } from "@tanstack/react-table"; import { Error } from "_components/Error"; import { PaginatedTable } from "_components/PaginatedTable"; @@ -14,19 +14,21 @@ const Page = () => { return (
-

- Einsatzhistorie +

+ Einsatzhistorie

-
+
+ ({ + userId: session.data?.user?.id ?? "", + Mission: { + state: "finished", + }, + }) as Prisma.MissionOnStationUsersWhereInput + } include={{ Station: true, User: true, diff --git a/apps/hub/app/_components/PaginatedTable.tsx b/apps/hub/app/_components/PaginatedTable.tsx index 87bc9fff..af4aedc0 100644 --- a/apps/hub/app/_components/PaginatedTable.tsx +++ b/apps/hub/app/_components/PaginatedTable.tsx @@ -9,12 +9,13 @@ export interface PaginatedTableRef { refresh: () => void; } -interface PaginatedTableProps extends Omit, "data"> { +interface PaginatedTableProps + extends Omit, "data"> { prismaModel: keyof PrismaClient; stickyHeaders?: boolean; - filter?: Record; initialRowsPerPage?: number; - searchFields?: string[]; + showSearch?: boolean; + getFilter?: (searchTerm: string) => TWhere; include?: Record; strictQuery?: boolean; leftOfSearch?: React.ReactNode; @@ -24,11 +25,11 @@ interface PaginatedTableProps extends Omit, "da ref?: Ref; } -export function PaginatedTable({ +export function PaginatedTable({ prismaModel, initialRowsPerPage = 30, - searchFields = [], - filter, + getFilter, + showSearch = false, include, ref, strictQuery = false, @@ -38,7 +39,7 @@ export function PaginatedTable({ leftOfPagination, supressQuery, ...restProps -}: PaginatedTableProps) { +}: PaginatedTableProps) { const [data, setData] = useState([]); const [rowsPerPage, setRowsPerPage] = useState(initialRowsPerPage); const [page, setPage] = useState(0); @@ -63,16 +64,14 @@ export function PaginatedTable({ return; } setLoading(true); - getData( - prismaModel, - rowsPerPage, - page * rowsPerPage, - searchTerm, - searchFields, - filter, + getData({ + model: prismaModel, + limit: rowsPerPage, + offset: page * rowsPerPage, + where: getFilter ? getFilter(searchTerm) : undefined, include, orderBy, - strictQuery + select: strictQuery ? restProps.columns .filter( (col): col is { accessorKey: string } => @@ -84,7 +83,7 @@ export function PaginatedTable({ return acc; }, {}) : undefined, - ) + }) .then((result) => { if (result) { setData(result.data); @@ -100,8 +99,7 @@ export function PaginatedTable({ rowsPerPage, page, searchTerm, - searchFields, - filter, + getFilter, include, orderBy, strictQuery, @@ -119,19 +117,19 @@ export function PaginatedTable({ if (supressQuery) return; setLoading(true); - }, [searchTerm, page, rowsPerPage, orderBy, filter, setLoading, supressQuery]); + }, [searchTerm, page, rowsPerPage, orderBy, getFilter, setLoading, supressQuery]); useDebounce( () => { refreshTableData(); }, 500, - [searchTerm, page, rowsPerPage, orderBy, filter], + [searchTerm, page, rowsPerPage, orderBy, getFilter], ); return (
- {(rightOfSearch || leftOfSearch || searchFields.length > 0) && ( + {(rightOfSearch || leftOfSearch || showSearch) && (
({
{leftOfSearch}
{loading && }
- {searchFields.length > 0 && ( + {showSearch && ( , - include?: Record, - orderBy?: Record, - select?: Record, -) { - if (!model || !prisma[model]) { +export async function getData({ + model, + limit, + offset, + where, + include, + orderBy, + select, +}: { + model: keyof PrismaClient; + limit: number; + offset: number; + where: Twhere; + include?: Record; + orderBy?: Record; + select?: Record; +}) { + if (!model || !(prisma as any)[model]) { return { data: [], total: 0 }; } - const formattedId = searchTerm.match(/^VAR(\d+)$/)?.[1]; + const delegate = (prisma as any)[model]; - const where = searchTerm - ? { - OR: [ - formattedId ? { id: formattedId } : undefined, - ...searchFields.map((field) => { - if (field.includes(".")) { - const parts: string[] = field.split("."); - - // Helper function to build nested object - const buildNestedFilter = (parts: string[], index = 0): any => { - if (index === parts.length - 1) { - // Reached the last part - add the contains filter - return { [parts[index] as string]: { contains: searchTerm } }; - } - - // For intermediate levels, nest the next level - return { [parts[index] as string]: buildNestedFilter(parts, index + 1) }; - }; - - return buildNestedFilter(parts); - } - - return { [field]: { contains: searchTerm } }; - }), - ].filter(Boolean), - ...filter, - } - : { ...filter }; - - if (!prisma[model]) { - return { data: [], total: 0 }; - } - const data = await (prisma[model] as any).findMany({ + const data = await delegate.findMany({ where, orderBy, take: limit, @@ -60,7 +34,7 @@ export async function getData( select, }); - const total = await (prisma[model] as any).count({ where }); + const total = await delegate.count({ where }); return { data, total }; } diff --git a/apps/hub/app/api/admin/user/search/route.ts b/apps/hub/app/api/admin/user/search/route.ts deleted file mode 100644 index 73571057..00000000 --- a/apps/hub/app/api/admin/user/search/route.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { NextResponse } from "next/server"; -import { prisma } from "@repo/db"; - -export async function GET(req: Request) { - const { searchParams } = new URL(req.url); - const publicId = searchParams.get("publicId")?.trim(); - const email = searchParams.get("email")?.trim()?.toLowerCase(); - - if (!publicId && !email) { - return NextResponse.json({ error: "Missing query" }, { status: 400 }); - } - - try { - const user = await prisma.user.findFirst({ - where: { - OR: [publicId ? { publicId } : undefined, email ? { email } : undefined].filter( - Boolean, - ) as any, - }, - select: { - id: true, - publicId: true, - firstname: true, - lastname: true, - isBanned: true, - }, - }); - if (!user) return NextResponse.json({ user: null }, { status: 200 }); - return NextResponse.json({ user }, { status: 200 }); - } catch (e) { - return NextResponse.json({ error: "Server error" }, { status: 500 }); - } -} diff --git a/apps/hub/app/vatsim/page.tsx b/apps/hub/app/vatsim/page.tsx index 83d885ca..1f6edb18 100644 --- a/apps/hub/app/vatsim/page.tsx +++ b/apps/hub/app/vatsim/page.tsx @@ -1,5 +1,5 @@ "use client"; -import { User } from "@repo/db"; +import { Prisma, User } from "@repo/db"; import { ColumnDef } from "@tanstack/react-table"; import { PaginatedTable } from "_components/PaginatedTable"; @@ -7,13 +7,24 @@ export default function () { return ( + ({ + AND: [ + { + vatsimCid: { + not: "", + }, + OR: [ + { firstname: { contains: searchTerm, mode: "insensitive" } }, + { lastname: { contains: searchTerm, mode: "insensitive" } }, + { vatsimCid: { contains: searchTerm, mode: "insensitive" } }, + ], + }, + ], + }) as Prisma.UserWhereInput + } leftOfSearch={

Vatsim-Nutzer

} columns={ [