added Station Add, Delete Update
This commit is contained in:
13
apps/hub/app/(app)/admin/station/[id]/page.tsx
Normal file
13
apps/hub/app/(app)/admin/station/[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} />;
|
||||
};
|
||||
246
apps/hub/app/(app)/admin/station/_components/Form.tsx
Normal file
246
apps/hub/app/(app)/admin/station/_components/Form.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
'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);
|
||||
console.log(form.formState.errors);
|
||||
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/station/action.ts
Normal file
20
apps/hub/app/(app)/admin/station/action.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
'use server';
|
||||
|
||||
import { prisma, Prisma, Station } from '@repo/db';
|
||||
|
||||
export const upsertStation = 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 } });
|
||||
};
|
||||
5
apps/hub/app/(app)/admin/station/new/page.tsx
Normal file
5
apps/hub/app/(app)/admin/station/new/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { StationForm } from '../_components/Form';
|
||||
|
||||
export default () => {
|
||||
return <StationForm />;
|
||||
};
|
||||
42
apps/hub/app/(app)/admin/station/page.tsx
Normal file
42
apps/hub/app/(app)/admin/station/page.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { DatabaseBackupIcon } from 'lucide-react';
|
||||
import { PaginatedTable } from '../../../_components/PaginatedTable';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<>
|
||||
<p className="text-2xl font-semibold text-left flex items-center gap-2 justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<DatabaseBackupIcon className="w-5 h-5" /> Stationen
|
||||
</span>
|
||||
<Link href={'/admin/station/new'}>
|
||||
<button className="btn btn-sm btn-outline btn-primary">
|
||||
Erstellen
|
||||
</button>
|
||||
</Link>
|
||||
</p>
|
||||
<PaginatedTable
|
||||
showEditButton
|
||||
prismaModel="station"
|
||||
columns={[
|
||||
{
|
||||
header: 'BOS Name',
|
||||
accessorKey: 'bosCallsign',
|
||||
},
|
||||
{
|
||||
header: 'Bos Use',
|
||||
accessorKey: 'bosUse',
|
||||
},
|
||||
{
|
||||
header: 'Country',
|
||||
accessorKey: 'country',
|
||||
},
|
||||
{
|
||||
header: 'operator',
|
||||
accessorKey: 'operator',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
5
apps/hub/app/(app)/admin/user/loading.tsx
Normal file
5
apps/hub/app/(app)/admin/user/loading.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Loading } from '../../../_components/ui/Loading';
|
||||
|
||||
export default () => {
|
||||
<Loading />;
|
||||
};
|
||||
@@ -1,29 +1,34 @@
|
||||
import Link from 'next/link';
|
||||
import { User2 } from 'lucide-react';
|
||||
import { PaginatedTable } from '../../../_components/PaginatedTable';
|
||||
|
||||
export default async () => {
|
||||
return (
|
||||
<PaginatedTable
|
||||
showEditButton
|
||||
prismaModel="user"
|
||||
columns={[
|
||||
{
|
||||
header: 'ID',
|
||||
accessorKey: 'publicId',
|
||||
},
|
||||
{
|
||||
header: 'Vorname',
|
||||
accessorKey: 'firstname',
|
||||
},
|
||||
{
|
||||
header: 'Nachname',
|
||||
accessorKey: 'lastname',
|
||||
},
|
||||
{
|
||||
header: 'Email',
|
||||
accessorKey: 'email',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<>
|
||||
<p className="text-2xl font-semibold text-left flex items-center gap-2">
|
||||
<User2 className="w-5 h-5" /> Benutzer
|
||||
</p>
|
||||
<PaginatedTable
|
||||
showEditButton
|
||||
prismaModel="user"
|
||||
columns={[
|
||||
{
|
||||
header: 'ID',
|
||||
accessorKey: 'publicId',
|
||||
},
|
||||
{
|
||||
header: 'Vorname',
|
||||
accessorKey: 'firstname',
|
||||
},
|
||||
{
|
||||
header: 'Nachname',
|
||||
accessorKey: 'lastname',
|
||||
},
|
||||
{
|
||||
header: 'Email',
|
||||
accessorKey: 'email',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Link from 'next/link';
|
||||
import { PaginatedTable } from '../_components/PaginatedTable';
|
||||
import { Header } from '../_components/ui/Header';
|
||||
import { PrismaClient } from '@repo/db';
|
||||
|
||||
export default async function Home() {
|
||||
return (
|
||||
|
||||
@@ -227,6 +227,7 @@ export const SocialForm = ({
|
||||
type="number"
|
||||
className="input input-bordered w-full"
|
||||
placeholder="1445241"
|
||||
defaultValue={user.vatsimCid as number | undefined}
|
||||
{...form.register('vatsimCid', {
|
||||
valueAsNumber: true,
|
||||
})}
|
||||
|
||||
@@ -39,7 +39,7 @@ export default function SortableTable<TData>({
|
||||
<Link
|
||||
href={`/admin/${prismaModel as string}/${(row.original as any).id}`}
|
||||
>
|
||||
<button className="btn">Edit</button>
|
||||
<button className="btn btn-sm">Edit</button>
|
||||
</Link>
|
||||
</div>
|
||||
),
|
||||
|
||||
52
apps/hub/app/_components/ui/Input.tsx
Normal file
52
apps/hub/app/_components/ui/Input.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
FieldValues,
|
||||
Path,
|
||||
RegisterOptions,
|
||||
UseFormReturn,
|
||||
} from 'react-hook-form';
|
||||
import { cn } from '../../../helper/cn';
|
||||
|
||||
interface InputProps<T extends FieldValues>
|
||||
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'form'> {
|
||||
name: Path<T>;
|
||||
form: UseFormReturn<T>;
|
||||
formOptions?: RegisterOptions<T>;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const Input = <T extends FieldValues>({
|
||||
name,
|
||||
label = name,
|
||||
placeholder = label,
|
||||
form,
|
||||
formOptions,
|
||||
className,
|
||||
...inputProps
|
||||
}: InputProps<T>) => {
|
||||
return (
|
||||
<label className="form-control w-full">
|
||||
<div className="label">
|
||||
<span className="label-text text-lg flex items-center gap-2">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
{...form.register(name, formOptions)}
|
||||
type="text"
|
||||
className={cn(
|
||||
'input input-bordered w-full placeholder:text-neutral-600',
|
||||
className
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
{...inputProps}
|
||||
/>
|
||||
{form.formState.errors[name] && (
|
||||
<p className="text-error">
|
||||
{form.formState.errors[name].message as string}
|
||||
</p>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
8
apps/hub/app/_components/ui/Loading.tsx
Normal file
8
apps/hub/app/_components/ui/Loading.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
export const Loading = () => {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="spinner"></div>
|
||||
<p>LAAAADEN</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -29,7 +29,10 @@ export const VerticalNav = () => {
|
||||
</summary>
|
||||
<ul>
|
||||
<li>
|
||||
<Link href="/admin/user">User</Link>
|
||||
<Link href="/admin/user">Benutzer</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/admin/station">Stationen</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
Reference in New Issue
Block a user