Redesigned Search, removed Unused Admin Route

This commit is contained in:
PxlLoewe
2025-12-27 15:33:00 +01:00
parent e9a4c50a12
commit b16b719c74
16 changed files with 209 additions and 178 deletions

View File

@@ -1,5 +1,5 @@
"use client"; "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 { ColumnDef } from "@tanstack/react-table";
import { PaginatedTable } from "_components/PaginatedTable"; import { PaginatedTable } from "_components/PaginatedTable";
import { ArrowRight, NotebookText } from "lucide-react"; import { ArrowRight, NotebookText } from "lucide-react";
@@ -12,20 +12,22 @@ export const RecentFlights = () => {
<div className="card-body"> <div className="card-body">
<h2 className="card-title justify-between"> <h2 className="card-title justify-between">
<span className="card-title"> <span className="card-title">
<NotebookText className="w-4 h-4" /> Logbook <NotebookText className="h-4 w-4" /> Logbook
</span> </span>
<Link className="badge badge-sm badge-info badge-outline" href="/logbook"> <Link className="badge badge-sm badge-info badge-outline" href="/logbook">
Zum vollständigen Logbook <ArrowRight className="w-4 h-4" /> Zum vollständigen Logbook <ArrowRight className="h-4 w-4" />
</Link> </Link>
</h2> </h2>
<PaginatedTable <PaginatedTable
prismaModel={"missionOnStationUsers"} prismaModel={"missionOnStationUsers"}
filter={{ getFilter={() =>
userId: session.data?.user?.id ?? "", ({
Mission: { User: { id: session.data?.user.id },
state: "finished", Mission: {
}, state: { in: ["finished", "archived"] },
}} },
}) as Prisma.MissionOnStationUsersWhereInput
}
include={{ include={{
Station: true, Station: true,
User: true, User: true,

View File

@@ -3,7 +3,7 @@ import { DatabaseBackupIcon } from "lucide-react";
import { PaginatedTable } from "../../../_components/PaginatedTable"; import { PaginatedTable } from "../../../_components/PaginatedTable";
import Link from "next/link"; import Link from "next/link";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import { Keyword } from "@repo/db"; import { Keyword, Prisma } from "@repo/db";
export default () => { export default () => {
return ( return (
@@ -12,7 +12,15 @@ export default () => {
stickyHeaders stickyHeaders
initialOrderBy={[{ id: "title", desc: true }]} initialOrderBy={[{ id: "title", desc: true }]}
prismaModel="changelog" prismaModel="changelog"
searchFields={["title"]} showSearch
getFilter={(search) =>
({
OR: [
{ title: { contains: search, mode: "insensitive" } },
{ description: { contains: search, mode: "insensitive" } },
],
}) as Prisma.ChangelogWhereInput
}
columns={ columns={
[ [
{ {

View File

@@ -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 { EventAppointmentOptionalDefaults, InputJsonValueType } from "@repo/db/zod";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
@@ -167,9 +167,11 @@ export const AppointmentModal = ({
] as ColumnDef<Participant>[] ] as ColumnDef<Participant>[]
} }
prismaModel={"participant"} prismaModel={"participant"}
filter={{ getFilter={() =>
eventAppointmentId: appointmentForm.watch("id"), ({
}} eventAppointmentId: appointmentForm.watch("id")!,
}) as Prisma.ParticipantWhereInput
}
include={{ User: true }} include={{ User: true }}
leftOfPagination={ leftOfPagination={
<div className="flex gap-2"> <div className="flex gap-2">

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { zodResolver } from "@hookform/resolvers/zod"; 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 { import {
EventAppointmentOptionalDefaults, EventAppointmentOptionalDefaults,
EventAppointmentOptionalDefaultsSchema, EventAppointmentOptionalDefaultsSchema,
@@ -159,9 +159,11 @@ export const Form = ({ event }: { event?: Event }) => {
<PaginatedTable <PaginatedTable
ref={appointmentsTableRef} ref={appointmentsTableRef}
prismaModel={"eventAppointment"} prismaModel={"eventAppointment"}
filter={{ getFilter={() =>
eventId: event?.id, ({
}} eventId: event?.id,
}) as Prisma.EventAppointmentWhereInput
}
include={{ include={{
Presenter: true, Presenter: true,
Participants: true, Participants: true,
@@ -257,12 +259,24 @@ export const Form = ({ event }: { event?: Event }) => {
<UserIcon className="h-5 w-5" /> Teilnehmer <UserIcon className="h-5 w-5" /> Teilnehmer
</h2> </h2>
} }
searchFields={["User.firstname", "User.lastname", "User.publicId"]}
ref={appointmentsTableRef} ref={appointmentsTableRef}
prismaModel={"participant"} prismaModel={"participant"}
filter={{ showSearch
eventId: event?.id, getFilter={(searchTerm) =>
}} ({
OR: [
{
User: {
OR: [
{ firstname: { contains: searchTerm, mode: "insensitive" } },
{ lastname: { contains: searchTerm, mode: "insensitive" } },
{ publicId: { contains: searchTerm, mode: "insensitive" } },
],
},
},
],
}) as Prisma.ParticipantWhereInput
}
include={{ include={{
User: true, User: true,
}} }}

View File

@@ -3,7 +3,7 @@ import { DatabaseBackupIcon } from "lucide-react";
import { PaginatedTable } from "../../../_components/PaginatedTable"; import { PaginatedTable } from "../../../_components/PaginatedTable";
import Link from "next/link"; import Link from "next/link";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import { Heliport } from "@repo/db"; import { Heliport, Prisma } from "@repo/db";
const page = () => { const page = () => {
return ( return (
@@ -11,7 +11,17 @@ const page = () => {
<PaginatedTable <PaginatedTable
stickyHeaders stickyHeaders
prismaModel="heliport" prismaModel="heliport"
searchFields={["siteName", "info", "hospital", "designator"]} getFilter={(searchTerm) =>
({
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={ columns={
[ [
{ {

View File

@@ -3,7 +3,7 @@ import { DatabaseBackupIcon } from "lucide-react";
import { PaginatedTable } from "../../../_components/PaginatedTable"; import { PaginatedTable } from "../../../_components/PaginatedTable";
import Link from "next/link"; import Link from "next/link";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import { Keyword } from "@repo/db"; import { Keyword, Prisma } from "@repo/db";
export default () => { export default () => {
return ( return (
@@ -12,7 +12,16 @@ export default () => {
stickyHeaders stickyHeaders
initialOrderBy={[{ id: "category", desc: true }]} initialOrderBy={[{ id: "category", desc: true }]}
prismaModel="keyword" 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={ columns={
[ [
{ {
@@ -41,11 +50,11 @@ export default () => {
} }
leftOfSearch={ leftOfSearch={
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<DatabaseBackupIcon className="w-5 h-5" /> Stichwörter <DatabaseBackupIcon className="h-5 w-5" /> Stichwörter
</span> </span>
} }
rightOfSearch={ rightOfSearch={
<p className="text-2xl font-semibold text-left flex items-center gap-2 justify-between"> <p className="flex items-center justify-between gap-2 text-left text-2xl font-semibold">
<Link href={"/admin/keyword/new"}> <Link href={"/admin/keyword/new"}>
<button className="btn btn-sm btn-outline btn-primary">Erstellen</button> <button className="btn btn-sm btn-outline btn-primary">Erstellen</button>
</Link> </Link>

View File

@@ -2,7 +2,7 @@
import { penaltyColumns as penaltyColumns } from "(app)/admin/penalty/columns"; import { penaltyColumns as penaltyColumns } from "(app)/admin/penalty/columns";
import { editReport } from "(app)/admin/report/actions"; import { editReport } from "(app)/admin/report/actions";
import { zodResolver } from "@hookform/resolvers/zod"; 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 { ReportSchema, Report as IReportZod } from "@repo/db/zod";
import { PaginatedTable } from "_components/PaginatedTable"; import { PaginatedTable } from "_components/PaginatedTable";
import { Button } from "_components/ui/Button"; import { Button } from "_components/ui/Button";
@@ -149,9 +149,11 @@ export const ReportPenalties = ({
CreatedUser: true, CreatedUser: true,
Report: true, Report: true,
}} }}
filter={{ getFilter={() =>
reportId: report.id, ({
}} reportId: report.id,
}) as Prisma.PenaltyWhereInput
}
columns={penaltyColumns} columns={penaltyColumns}
/> />
</div> </div>

View File

@@ -2,7 +2,7 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { StationOptionalDefaultsSchema } from "@repo/db/zod"; import { StationOptionalDefaultsSchema } from "@repo/db/zod";
import { useForm } from "react-hook-form"; 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 { FileText, LocateIcon, PlaneIcon, UserIcon } from "lucide-react";
import { Input } from "../../../../_components/ui/Input"; import { Input } from "../../../../_components/ui/Input";
import { deleteStation, upsertStation } from "../action"; import { deleteStation, upsertStation } from "../action";
@@ -198,10 +198,17 @@ export const StationForm = ({ station }: { station?: Station }) => {
Verbundene Piloten Verbundene Piloten
</div> </div>
} }
filter={{ getFilter={(searchField) =>
stationId: station?.id, ({
}} stationId: station?.id,
searchFields={["User.firstname", "User.lastname", "User.publicId"]} 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"} prismaModel={"connectedAircraft"}
include={{ Station: true, User: true }} include={{ Station: true, User: true }}
columns={ columns={

View File

@@ -3,14 +3,22 @@ import { DatabaseBackupIcon } from "lucide-react";
import { PaginatedTable } from "../../../_components/PaginatedTable"; import { PaginatedTable } from "../../../_components/PaginatedTable";
import Link from "next/link"; import Link from "next/link";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import { Station } from "@repo/db"; import { Prisma, Station } from "@repo/db";
const page = () => { const page = () => {
return ( return (
<> <>
<PaginatedTable <PaginatedTable
prismaModel="station" prismaModel="station"
searchFields={["bosCallsign", "operator"]} showSearch
getFilter={(searchField) =>
({
OR: [
{ bosCallsign: { contains: searchField, mode: "insensitive" } },
{ operator: { contains: searchField, mode: "insensitive" } },
],
}) as Prisma.StationWhereInput
}
stickyHeaders stickyHeaders
columns={ columns={
[ [
@@ -44,11 +52,11 @@ const page = () => {
} }
leftOfSearch={ leftOfSearch={
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<DatabaseBackupIcon className="w-5 h-5" /> Stationen <DatabaseBackupIcon className="h-5 w-5" /> Stationen
</span> </span>
} }
rightOfSearch={ rightOfSearch={
<p className="text-2xl font-semibold text-left flex items-center gap-2 justify-between"> <p className="flex items-center justify-between gap-2 text-left text-2xl font-semibold">
<Link href={"/admin/station/new"}> <Link href={"/admin/station/new"}>
<button className="btn btn-sm btn-outline btn-primary">Erstellen</button> <button className="btn btn-sm btn-outline btn-primary">Erstellen</button>
</Link> </Link>

View File

@@ -8,6 +8,7 @@ import {
DiscordAccount, DiscordAccount,
Penalty, Penalty,
PERMISSION, PERMISSION,
Prisma,
Station, Station,
User, User,
} from "@repo/db"; } from "@repo/db";
@@ -281,9 +282,11 @@ export const ConnectionHistory: React.FC<{ user: User }> = ({ user }: { user: Us
</h2> </h2>
<PaginatedTable <PaginatedTable
ref={dispoTableRef} ref={dispoTableRef}
filter={{ getFilter={() =>
userId: user.id, ({
}} userId: user.id,
}) as Prisma.ConnectedDispatcherWhereInput
}
prismaModel={"connectedDispatcher"} prismaModel={"connectedDispatcher"}
initialOrderBy={[ initialOrderBy={[
{ {
@@ -349,9 +352,11 @@ export const ConnectionHistory: React.FC<{ user: User }> = ({ user }: { user: Us
</h2> </h2>
<PaginatedTable <PaginatedTable
ref={pilotTableRef} ref={pilotTableRef}
filter={{ getFilter={() =>
userId: user.id, ({
}} userId: user.id,
}) as Prisma.ConnectedAircraftWhereInput
}
prismaModel={"connectedAircraft"} prismaModel={"connectedAircraft"}
include={{ Station: true }} include={{ Station: true }}
initialOrderBy={[ initialOrderBy={[
@@ -505,9 +510,7 @@ export const UserPenalties = ({ user }: { user: User }) => {
CreatedUser: true, CreatedUser: true,
Report: true, Report: true,
}} }}
filter={{ getFilter={() => ({ userId: user.id }) as Prisma.PenaltyWhereInput}
userId: user.id,
}}
columns={penaltyColumns} columns={penaltyColumns}
/> />
</div> </div>
@@ -529,9 +532,11 @@ export const UserReports = ({ user }: { user: User }) => {
</div> </div>
<PaginatedTable <PaginatedTable
prismaModel="report" prismaModel="report"
filter={{ getFilter={() =>
reportedUserId: user.id, ({
}} reportedUserId: user.id,
}) as Prisma.ReportWhereInput
}
initialOrderBy={[ initialOrderBy={[
{ {
id: "timestamp", id: "timestamp",
@@ -720,9 +725,7 @@ export const AdminForm = ({
)} )}
</div> </div>
</div> </div>
<p className="text-sm text-gray-400"> <p className="text-sm text-gray-400">{user.duplicateReason || "Keine Grund angegeben"}</p>
Achtung! Dieser Account ist als Duplikat markiert oder hat Duplikate!
</p>
</div> </div>
)} )}
{(!!openBans.length || !!openTimebans.length) && ( {(!!openBans.length || !!openTimebans.length) && (

View File

@@ -3,7 +3,7 @@ import { User2 } from "lucide-react";
import { PaginatedTable } from "../../../_components/PaginatedTable"; import { PaginatedTable } from "../../../_components/PaginatedTable";
import Link from "next/link"; import Link from "next/link";
import { ColumnDef } from "@tanstack/react-table"; 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"; import { useSession } from "next-auth/react";
const AdminUserPage = () => { const AdminUserPage = () => {
@@ -14,7 +14,21 @@ const AdminUserPage = () => {
<PaginatedTable <PaginatedTable
stickyHeaders stickyHeaders
prismaModel="user" prismaModel="user"
searchFields={["publicId", "firstname", "lastname", "email"]} showSearch
getFilter={(searchTerm) => {
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={{ include={{
discordAccounts: true, discordAccounts: true,
}} }}

View File

@@ -1,5 +1,5 @@
"use client"; "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 { ColumnDef } from "@tanstack/react-table";
import { Error } from "_components/Error"; import { Error } from "_components/Error";
import { PaginatedTable } from "_components/PaginatedTable"; import { PaginatedTable } from "_components/PaginatedTable";
@@ -14,19 +14,21 @@ const Page = () => {
return ( return (
<div className="grid grid-cols-6 gap-4"> <div className="grid grid-cols-6 gap-4">
<div className="col-span-full"> <div className="col-span-full">
<p className="text-2xl font-semibold text-left flex items-center gap-2"> <p className="flex items-center gap-2 text-left text-2xl font-semibold">
<NotebookText className="w-5 h-5" /> Einsatzhistorie <NotebookText className="h-5 w-5" /> Einsatzhistorie
</p> </p>
</div> </div>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6"> <div className="card bg-base-200 col-span-6 mb-4 shadow-xl">
<PaginatedTable <PaginatedTable
prismaModel={"missionOnStationUsers"} prismaModel={"missionOnStationUsers"}
filter={{ getFilter={() =>
userId: session.data?.user?.id ?? "", ({
Mission: { userId: session.data?.user?.id ?? "",
state: "finished", Mission: {
}, state: "finished",
}} },
}) as Prisma.MissionOnStationUsersWhereInput
}
include={{ include={{
Station: true, Station: true,
User: true, User: true,

View File

@@ -9,12 +9,13 @@ export interface PaginatedTableRef {
refresh: () => void; refresh: () => void;
} }
interface PaginatedTableProps<TData> extends Omit<SortableTableProps<TData>, "data"> { interface PaginatedTableProps<TData, TWhere extends object>
extends Omit<SortableTableProps<TData>, "data"> {
prismaModel: keyof PrismaClient; prismaModel: keyof PrismaClient;
stickyHeaders?: boolean; stickyHeaders?: boolean;
filter?: Record<string, unknown>;
initialRowsPerPage?: number; initialRowsPerPage?: number;
searchFields?: string[]; showSearch?: boolean;
getFilter?: (searchTerm: string) => TWhere;
include?: Record<string, boolean>; include?: Record<string, boolean>;
strictQuery?: boolean; strictQuery?: boolean;
leftOfSearch?: React.ReactNode; leftOfSearch?: React.ReactNode;
@@ -24,11 +25,11 @@ interface PaginatedTableProps<TData> extends Omit<SortableTableProps<TData>, "da
ref?: Ref<PaginatedTableRef>; ref?: Ref<PaginatedTableRef>;
} }
export function PaginatedTable<TData>({ export function PaginatedTable<TData, TWhere extends object>({
prismaModel, prismaModel,
initialRowsPerPage = 30, initialRowsPerPage = 30,
searchFields = [], getFilter,
filter, showSearch = false,
include, include,
ref, ref,
strictQuery = false, strictQuery = false,
@@ -38,7 +39,7 @@ export function PaginatedTable<TData>({
leftOfPagination, leftOfPagination,
supressQuery, supressQuery,
...restProps ...restProps
}: PaginatedTableProps<TData>) { }: PaginatedTableProps<TData, TWhere>) {
const [data, setData] = useState<TData[]>([]); const [data, setData] = useState<TData[]>([]);
const [rowsPerPage, setRowsPerPage] = useState(initialRowsPerPage); const [rowsPerPage, setRowsPerPage] = useState(initialRowsPerPage);
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
@@ -63,16 +64,14 @@ export function PaginatedTable<TData>({
return; return;
} }
setLoading(true); setLoading(true);
getData( getData({
prismaModel, model: prismaModel,
rowsPerPage, limit: rowsPerPage,
page * rowsPerPage, offset: page * rowsPerPage,
searchTerm, where: getFilter ? getFilter(searchTerm) : undefined,
searchFields,
filter,
include, include,
orderBy, orderBy,
strictQuery select: strictQuery
? restProps.columns ? restProps.columns
.filter( .filter(
(col): col is { accessorKey: string } => (col): col is { accessorKey: string } =>
@@ -84,7 +83,7 @@ export function PaginatedTable<TData>({
return acc; return acc;
}, {}) }, {})
: undefined, : undefined,
) })
.then((result) => { .then((result) => {
if (result) { if (result) {
setData(result.data); setData(result.data);
@@ -100,8 +99,7 @@ export function PaginatedTable<TData>({
rowsPerPage, rowsPerPage,
page, page,
searchTerm, searchTerm,
searchFields, getFilter,
filter,
include, include,
orderBy, orderBy,
strictQuery, strictQuery,
@@ -119,19 +117,19 @@ export function PaginatedTable<TData>({
if (supressQuery) return; if (supressQuery) return;
setLoading(true); setLoading(true);
}, [searchTerm, page, rowsPerPage, orderBy, filter, setLoading, supressQuery]); }, [searchTerm, page, rowsPerPage, orderBy, getFilter, setLoading, supressQuery]);
useDebounce( useDebounce(
() => { () => {
refreshTableData(); refreshTableData();
}, },
500, 500,
[searchTerm, page, rowsPerPage, orderBy, filter], [searchTerm, page, rowsPerPage, orderBy, getFilter],
); );
return ( return (
<div className="m-4 space-y-4"> <div className="m-4 space-y-4">
{(rightOfSearch || leftOfSearch || searchFields.length > 0) && ( {(rightOfSearch || leftOfSearch || showSearch) && (
<div <div
className={cn( className={cn(
"sticky z-20 flex items-center gap-2 py-2", "sticky z-20 flex items-center gap-2 py-2",
@@ -142,7 +140,7 @@ export function PaginatedTable<TData>({
<div>{leftOfSearch}</div> <div>{leftOfSearch}</div>
<div>{loading && <span className="loading loading-dots loading-md" />}</div> <div>{loading && <span className="loading loading-dots loading-md" />}</div>
</div> </div>
{searchFields.length > 0 && ( {showSearch && (
<input <input
type="text" type="text"
placeholder="Suchen..." placeholder="Suchen..."

View File

@@ -2,56 +2,30 @@
"use server"; "use server";
import { prisma, PrismaClient } from "@repo/db"; import { prisma, PrismaClient } from "@repo/db";
export async function getData( export async function getData<Twhere>({
model: keyof PrismaClient, model,
limit: number, limit,
offset: number, offset,
searchTerm: string, where,
searchFields: string[], include,
filter?: Record<string, any>, orderBy,
include?: Record<string, boolean>, select,
orderBy?: Record<string, "asc" | "desc">, }: {
select?: Record<string, any>, model: keyof PrismaClient;
) { limit: number;
if (!model || !prisma[model]) { offset: number;
where: Twhere;
include?: Record<string, boolean>;
orderBy?: Record<string, "asc" | "desc">;
select?: Record<string, any>;
}) {
if (!model || !(prisma as any)[model]) {
return { data: [], total: 0 }; return { data: [], total: 0 };
} }
const formattedId = searchTerm.match(/^VAR(\d+)$/)?.[1]; const delegate = (prisma as any)[model];
const where = searchTerm const data = await delegate.findMany({
? {
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({
where, where,
orderBy, orderBy,
take: limit, take: limit,
@@ -60,7 +34,7 @@ export async function getData(
select, select,
}); });
const total = await (prisma[model] as any).count({ where }); const total = await delegate.count({ where });
return { data, total }; return { data, total };
} }

View File

@@ -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 });
}
}

View File

@@ -1,5 +1,5 @@
"use client"; "use client";
import { User } from "@repo/db"; import { Prisma, User } from "@repo/db";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import { PaginatedTable } from "_components/PaginatedTable"; import { PaginatedTable } from "_components/PaginatedTable";
@@ -7,13 +7,24 @@ export default function () {
return ( return (
<PaginatedTable <PaginatedTable
strictQuery strictQuery
searchFields={["firstname", "lastname", "vatsimCid"]} showSearch
prismaModel={"user"} prismaModel={"user"}
filter={{ getFilter={(searchTerm) =>
vatsimCid: { ({
not: "", AND: [
}, {
}} vatsimCid: {
not: "",
},
OR: [
{ firstname: { contains: searchTerm, mode: "insensitive" } },
{ lastname: { contains: searchTerm, mode: "insensitive" } },
{ vatsimCid: { contains: searchTerm, mode: "insensitive" } },
],
},
],
}) as Prisma.UserWhereInput
}
leftOfSearch={<h1 className="text-2xl font-bold">Vatsim-Nutzer</h1>} leftOfSearch={<h1 className="text-2xl font-bold">Vatsim-Nutzer</h1>}
columns={ columns={
[ [