keyword admin page
This commit is contained in:
@@ -7,6 +7,7 @@ export const BaseMaps = () => {
|
|||||||
<TileLayer
|
<TileLayer
|
||||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
className="invert-100"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
13
apps/hub/app/(app)/admin/keyword/[id]/page.tsx
Normal file
13
apps/hub/app/(app)/admin/keyword/[id]/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { prisma } from '@repo/db';
|
||||||
|
import { StationForm } from '../_components/Form';
|
||||||
|
|
||||||
|
export default async ({ params }: { params: Promise<{ id: string }> }) => {
|
||||||
|
const { id } = await params;
|
||||||
|
const station = await prisma.station.findUnique({
|
||||||
|
where: {
|
||||||
|
id: parseInt(id),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!station) return <div>Station not found</div>;
|
||||||
|
return <StationForm station={station} />;
|
||||||
|
};
|
||||||
245
apps/hub/app/(app)/admin/keyword/_components/Form.tsx
Normal file
245
apps/hub/app/(app)/admin/keyword/_components/Form.tsx
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
"use client";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { StationOptionalDefaultsSchema } from "@repo/db/zod";
|
||||||
|
import { set, useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { BosUse, Country, Station } from "@repo/db";
|
||||||
|
import { FileText, LocateIcon, PlaneIcon } from "lucide-react";
|
||||||
|
import { Input } from "../../../../_components/ui/Input";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { deleteStation, upsertStation } from "../action";
|
||||||
|
import { Button } from "../../../../_components/ui/Button";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
export const StationForm = ({ station }: { station?: Station }) => {
|
||||||
|
const form = useForm<z.infer<typeof StationOptionalDefaultsSchema>>({
|
||||||
|
resolver: zodResolver(StationOptionalDefaultsSchema),
|
||||||
|
defaultValues: station,
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(async (values) => {
|
||||||
|
setLoading(true);
|
||||||
|
const createdStation = await upsertStation(values, station?.id);
|
||||||
|
setLoading(false);
|
||||||
|
if (!station) redirect(`/admin/station`);
|
||||||
|
})}
|
||||||
|
className="grid grid-cols-6 gap-3"
|
||||||
|
>
|
||||||
|
<div className="card bg-base-200 shadow-xl col-span-2 max-xl:col-span-6">
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title">
|
||||||
|
<FileText className="w-5 h-5" /> Allgemeines
|
||||||
|
</h2>
|
||||||
|
<Input
|
||||||
|
form={form}
|
||||||
|
label="BOS Rufname"
|
||||||
|
name="bosCallsign"
|
||||||
|
className="input-sm"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
form={form}
|
||||||
|
label="BOS Rufname (kurz)"
|
||||||
|
name="bosCallsignShort"
|
||||||
|
className="input-sm"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
form={form}
|
||||||
|
label="Betreiber"
|
||||||
|
name="operator"
|
||||||
|
className="input-sm"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
form={form}
|
||||||
|
label="ATC Rufname"
|
||||||
|
name="atcCallsign"
|
||||||
|
className="input-sm"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
form={form}
|
||||||
|
label="FIR (Flight Information Region)"
|
||||||
|
name="fir"
|
||||||
|
className="input-sm"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
form={form}
|
||||||
|
label="Leitstelle Rufname"
|
||||||
|
name="bosRadioArea"
|
||||||
|
className="input-sm"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label className="form-control w-full">
|
||||||
|
<span className="label-text text-lg flex items-center gap-2">
|
||||||
|
BOS Nutzung
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
className="input-sm select select-bordered select-sm"
|
||||||
|
{...form.register("bosUse")}
|
||||||
|
>
|
||||||
|
{Object.keys(BosUse).map((use) => (
|
||||||
|
<option key={use} value={use}>
|
||||||
|
{use}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card bg-base-200 shadow-xl col-span-2 max-xl:col-span-6">
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title">
|
||||||
|
<LocateIcon className="w-5 h-5" /> Standort + Ausrüstung
|
||||||
|
</h2>
|
||||||
|
<label className="form-control w-full">
|
||||||
|
<span className="label-text text-lg flex items-center gap-2">
|
||||||
|
Land
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
className="input-sm select select-bordered select-sm"
|
||||||
|
{...form.register("country", {})}
|
||||||
|
>
|
||||||
|
{Object.keys(Country).map((use) => (
|
||||||
|
<option key={use} value={use}>
|
||||||
|
{use}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
form={form}
|
||||||
|
label="Bundesland"
|
||||||
|
name="locationState"
|
||||||
|
className="input-sm"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
form={form}
|
||||||
|
label="Bundesland (kurz)"
|
||||||
|
name="locationStateShort"
|
||||||
|
className="input-sm"
|
||||||
|
/>
|
||||||
|
<span className="label-text text-lg flex items-center gap-2">
|
||||||
|
Ausgerüstet mit:
|
||||||
|
</span>
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label cursor-pointer">
|
||||||
|
<span>Winde</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="toggle"
|
||||||
|
{...form.register("hasWinch")}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="label cursor-pointer">
|
||||||
|
<span>Nachtsicht Gerät</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="toggle"
|
||||||
|
{...form.register("hasNvg")}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="label cursor-pointer">
|
||||||
|
<span>24-Stunden Einsatzfähig</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="toggle"
|
||||||
|
{...form.register("is24h")}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="label cursor-pointer">
|
||||||
|
<span>Bergetau</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="toggle"
|
||||||
|
{...form.register("hasRope")}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
form={form}
|
||||||
|
label="Breitengrad"
|
||||||
|
name="latitude"
|
||||||
|
className="input-sm"
|
||||||
|
formOptions={{ valueAsNumber: true }}
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
form={form}
|
||||||
|
label="Längengrad"
|
||||||
|
name="longitude"
|
||||||
|
className="input-sm"
|
||||||
|
formOptions={{ valueAsNumber: true }}
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
/>
|
||||||
|
<label className="label cursor-pointer">
|
||||||
|
<span className="text-lg">Reichweiten ausblenden</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="toggle"
|
||||||
|
{...form.register("hideRangeRings")}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card bg-base-200 shadow-xl col-span-2 max-xl:col-span-6">
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title">
|
||||||
|
<PlaneIcon className="w-5 h-5" /> Hubschrauber
|
||||||
|
</h2>
|
||||||
|
<Input
|
||||||
|
form={form}
|
||||||
|
label="Hubschrauber Typ"
|
||||||
|
name="aircraft"
|
||||||
|
className="input-sm"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
form={form}
|
||||||
|
formOptions={{ valueAsNumber: true }}
|
||||||
|
type="number"
|
||||||
|
label="Hubschrauber Geschwindigkeit"
|
||||||
|
className="input-sm"
|
||||||
|
name="aircraftSpeed"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
form={form}
|
||||||
|
label="Hubschrauber Registrierung"
|
||||||
|
name="aircraftRegistration"
|
||||||
|
className="input-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card bg-base-200 shadow-xl col-span-6">
|
||||||
|
<div className="card-body ">
|
||||||
|
<div className="flex w-full gap-4">
|
||||||
|
<Button
|
||||||
|
isLoading={loading}
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-primary flex-1"
|
||||||
|
>
|
||||||
|
Speichern
|
||||||
|
</Button>
|
||||||
|
{station && (
|
||||||
|
<Button
|
||||||
|
isLoading={deleteLoading}
|
||||||
|
onClick={async () => {
|
||||||
|
setDeleteLoading(true);
|
||||||
|
await deleteStation(station.id);
|
||||||
|
redirect("/admin/station");
|
||||||
|
}}
|
||||||
|
className="btn btn-error"
|
||||||
|
>
|
||||||
|
Löschen
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
20
apps/hub/app/(app)/admin/keyword/action.ts
Normal file
20
apps/hub/app/(app)/admin/keyword/action.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { prisma, Prisma, Station } from "@repo/db";
|
||||||
|
|
||||||
|
export const upsertKeyword = async (
|
||||||
|
station: Prisma.StationCreateInput,
|
||||||
|
id?: Station["id"],
|
||||||
|
) => {
|
||||||
|
const newStation = id
|
||||||
|
? await prisma.station.update({
|
||||||
|
where: { id: id },
|
||||||
|
data: station,
|
||||||
|
})
|
||||||
|
: await prisma.station.create({ data: station });
|
||||||
|
return newStation;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteStation = async (id: Station["id"]) => {
|
||||||
|
await prisma.station.delete({ where: { id: id } });
|
||||||
|
};
|
||||||
20
apps/hub/app/(app)/admin/keyword/layout.tsx
Normal file
20
apps/hub/app/(app)/admin/keyword/layout.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { prisma } from "@repo/db";
|
||||||
|
import { Error } from "_components/Error";
|
||||||
|
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
||||||
|
|
||||||
|
export default async ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const session = await getServerSession();
|
||||||
|
|
||||||
|
if (!session) return <Error title="Nicht eingeloggt" statusCode={401} />;
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: session.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user?.permissions.includes("ADMIN_STATION"))
|
||||||
|
return <Error title="Keine Berechtigung" statusCode={403} />;
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
5
apps/hub/app/(app)/admin/keyword/new/page.tsx
Normal file
5
apps/hub/app/(app)/admin/keyword/new/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { StationForm } from '../_components/Form';
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return <StationForm />;
|
||||||
|
};
|
||||||
47
apps/hub/app/(app)/admin/keyword/page.tsx
Normal file
47
apps/hub/app/(app)/admin/keyword/page.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { DatabaseBackupIcon } from 'lucide-react';
|
||||||
|
import { PaginatedTable } from '../../../_components/PaginatedTable';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PaginatedTable
|
||||||
|
showEditButton
|
||||||
|
prismaModel="station"
|
||||||
|
searchFields={['bosCallsign', 'bosUse', 'country', 'operator']}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
header: 'BOS Name',
|
||||||
|
accessorKey: 'bosCallsign',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Bos Use',
|
||||||
|
accessorKey: 'bosUse',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Country',
|
||||||
|
accessorKey: 'country',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'operator',
|
||||||
|
accessorKey: 'operator',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
leftOfSearch={
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<DatabaseBackupIcon className="w-5 h-5" /> Stationen
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
rightOfSearch={
|
||||||
|
<p className="text-2xl font-semibold text-left flex items-center gap-2 justify-between">
|
||||||
|
<Link href={'/admin/station/new'}>
|
||||||
|
<button className="btn btn-sm btn-outline btn-primary">
|
||||||
|
Erstellen
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
Binary file not shown.
10
packages/database/prisma/schema/keyword.prisma
Normal file
10
packages/database/prisma/schema/keyword.prisma
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
enum KEYWORD_CATEGORY {
|
||||||
|
AB_ATMUNG: "AB_ATMUNG",
|
||||||
|
}
|
||||||
|
|
||||||
|
model Keyword {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
name String
|
||||||
|
description String
|
||||||
|
// relations:
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user