Skip to content

Commit

Permalink
admin page work: facility creation/deletion
Browse files Browse the repository at this point in the history
  • Loading branch information
c0repwn3r committed Feb 28, 2024
1 parent 41db6fd commit 89a635a
Show file tree
Hide file tree
Showing 8 changed files with 365 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/routes/admin/+layout.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ export const load: LayoutServerLoad = async ({ cookies }) => {
if (!user.isSiteAdmin) {
redirect(307, "/");
}

return { user };
};
4 changes: 4 additions & 0 deletions src/routes/admin/+layout.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div
class="flex flex-col h-full overflow-auto w-full bg-background shadow-lg space-y-4 p-8 pt-6 rounded-md">
<slot />
</div>
19 changes: 19 additions & 0 deletions src/routes/admin/+page.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { PageServerLoad } from "./$types";
import prisma from "$lib/prisma";

export const load: PageServerLoad = async () => {
return {
totalUsers: await prisma.user.count(),
totalFacilities: await prisma.facility.count(),
divisionStaffAuthorizations: await prisma.userFacilityAssignment.count({
where: {
assignmentType: "DivisionalStaff"
}
}),
siteAdministrators: await prisma.user.count({
where: {
isSiteAdmin: true
}
})
}
}
67 changes: 67 additions & 0 deletions src/routes/admin/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<script lang="ts">
import * as Card from "$lib/components/ui/card";
import * as Table from "$lib/components/ui/table";
import {SquareUserIcon, TowerControlIcon, UserIcon} from "lucide-svelte";
import type {PageData} from "./$types";
export let data: PageData;
</script>

<div class="flex items-center justify-between space-y-2">
<h2 class="text-3xl font-bold tracking-tight">Site Administration</h2>
</div>

<div class="grid-cols-4 grid gap-4">
<Card.Root>
<Card.Header
class="flex flex-row items-center justify-between space-y-0 pb-2"
>
<Card.Title class="text-sm font-medium">Users</Card.Title>
<UserIcon class="h-4 w-4 text-muted-foreground" />
</Card.Header>
<Card.Content>
<div class="text-2xl font-bold">{data.totalUsers}</div>
<a href="/admin/users" class="text-sm text-foreground/80 underline underline-offset-4">Manage Users &rarr;</a>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header
class="flex flex-row items-center justify-between space-y-0 pb-2"
>
<Card.Title class="text-sm font-medium">Facilities</Card.Title>
<TowerControlIcon class="h-4 w-4 text-muted-foreground" />
</Card.Header>
<Card.Content>
<div class="text-2xl font-bold">{data.totalFacilities}</div>
<a href="/admin/facilities" class="text-sm text-foreground/80 underline underline-offset-4">Manage Facilities &rarr;</a>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header
class="flex flex-row items-center justify-between space-y-0 pb-2"
>
<Card.Title class="text-sm font-medium">Staff Authorizations</Card.Title>
<SquareUserIcon class="h-4 w-4 text-muted-foreground" />
</Card.Header>
<Card.Content>
<div class="text-2xl font-bold">{data.divisionStaffAuthorizations}</div>
<a href="/admin/division_staff" class="text-sm text-foreground/80 underline underline-offset-4">Manage Staff Authorizations &rarr;</a>
</Card.Content>
</Card.Root>
<Card.Root>
<Card.Header
class="flex flex-row items-center justify-between space-y-0 pb-2"
>
<Card.Title class="text-sm font-medium">Site Administrators</Card.Title>
<UserIcon class="h-4 w-4 text-muted-foreground" />
</Card.Header>
<Card.Content>
<div class="text-2xl font-bold">{data.siteAdministrators}</div>
<a href="/admin/admins" class="text-sm text-foreground/80 underline underline-offset-4">Manage Site Administrators &rarr;</a>
</Card.Content>
</Card.Root>
</div>

<div class="grid-cols-3 grid gap-4">

</div>
57 changes: 57 additions & 0 deletions src/routes/admin/facilities/+page.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { PageServerLoad, Actions } from "./$types";
import prisma from "$lib/prisma";
import {superValidate} from "sveltekit-superforms/server";
import {formSchema} from "./schema";
import {fail} from "@sveltejs/kit";
import {loadUserData} from "$lib/auth";

export const load: PageServerLoad = async ({ parent }) => {
let { user } = await parent();
if (!user.isSiteAdmin) { return {}; }

return {
facilities: await prisma.facility.findMany()!,
form: await superValidate(formSchema)
}
}

export const actions: Actions = {
create: async (event) => {
let form = await superValidate(event, formSchema);

if (!form.valid) {
return fail(400, { form });
}

let { user } = await loadUserData(event.cookies, null);
if (!user.isSiteAdmin) {
return fail(400, { form });
}

await prisma.facility.create({
data: {
id: form.data.id.toString(),
name: form.data.name.toString(),
dotnetId: form.data.dotnetId.toString(),
dotnetType: form.data.dotnetType.toString() === 'Division' ? 'Division' : 'Subdivision',
contactEmail: "todo@vatsim.me",
website: "https://vatsim.me"
}
});

return {
form
}
},
delete: async (event) => {
let { user } = await loadUserData(event.cookies, null);
if (!user.isSiteAdmin) {
return fail(400, {});
}
await prisma.facility.delete({
where: {
id: (await event.request.formData()).get("id")!.toString()
}
});
}
}
167 changes: 167 additions & 0 deletions src/routes/admin/facilities/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<script lang="ts">
import type { Facility } from "@prisma/client";
import { writable } from "svelte/store";
import {
createRender,
createTable,
Render,
Subscribe,
} from "svelte-headless-table";
import * as Table from "$lib/components/ui/table";
import * as Dialog from "$lib/components/ui/dialog";
import * as Form from "$lib/components/ui/form";
import { Plus } from "lucide-svelte";
import { Button } from "$lib/components/ui/button";
import { formSchema, type FormSchema } from "./schema";
import type { SuperValidated } from "sveltekit-superforms";
import type { PageData } from "./$types";
import {toast} from "svelte-sonner";
import ActionButtons from "./actions.svelte";
export let data: PageData;
let facilities_store = writable(data.facilities!);
$: $facilities_store = data.facilities!;
const table = createTable(facilities_store);
const columns = table.createColumns([
table.column({
accessor: "id",
header: "ID"
}),
table.column({
accessor: "name",
header: "Name",
}),
table.column({
accessor: "dotnetId",
header: ".NET ID"
}),
table.column({
accessor: "dotnetType",
header: ".NET Type"
}),
table.column({
accessor: ({id}) => id,
header: "",
cell: ({value}) => createRender(ActionButtons, { id: value })
})
]);
const { headerRows, pageRows, tableAttrs, tableBodyAttrs } =
table.createViewModel(columns);
let createDialogOpen = false;
export let form: SuperValidated<FormSchema>;
let options = {
onUpdated: ({ form }) => {
if (form.valid) {
createDialogOpen = false;
toast.success("Facility created!");
}
},
};
</script>

<div class="flex items-center justify-between">
<h2 class="text-3xl font-bold tracking-tight">Manage Facilities</h2>
<Button
on:click={() => {
createDialogOpen = true;
}}>
<Plus class="mr-2 w-4 h-4" />
Create
</Button>
</div>

<Table.Root {...$tableAttrs}>
<Table.Header>
{#each $headerRows as headerRow}
<Subscribe rowAttrs={headerRow.attrs()}>
<Table.Row>
{#each headerRow.cells as cell (cell.id)}
<Subscribe attrs={cell.attrs()} let:attrs props={cell.props()}>
<Table.Head {...attrs}>
<Render of={cell.render()} />
</Table.Head>
</Subscribe>
{/each}
</Table.Row>
</Subscribe>
{/each}
</Table.Header>
<Table.Body {...$tableBodyAttrs}>
{#each $pageRows as row (row.id)}
<Subscribe rowAttrs={row.attrs()} let:rowAttrs>
<Table.Row {...rowAttrs}>
{#each row.cells as cell (cell.id)}
<Subscribe attrs={cell.attrs()} let:attrs>
<Table.Cell {...attrs}>
<Render of={cell.render()} />
</Table.Cell>
</Subscribe>
{/each}
</Table.Row>
</Subscribe>
{/each}
</Table.Body>
</Table.Root>

<Dialog.Root bind:open={createDialogOpen}>
<Dialog.Content class="sm:max-w-[425px]">
<Dialog.Header>
<Dialog.Title>New Facility</Dialog.Title>
</Dialog.Header>

<div class="space-y-2">
<Form.Root
action="?/create"
method="POST"
{form}
schema={formSchema}
{options}
let:config>
<Form.Field {config} name="id">
<Form.Item>
<Form.Label>Facility ID</Form.Label>
<Form.Input />
<Form.Validation />
</Form.Item>
</Form.Field>
<Form.Field {config} name="name">
<Form.Item>
<Form.Label>Facility Name</Form.Label>
<Form.Input />
<Form.Validation />
</Form.Item>
</Form.Field>
<Form.Field {config} name="dotnetId">
<Form.Item>
<Form.Label>.NET ID</Form.Label>
<Form.Input />
<Form.Validation />
</Form.Item>
</Form.Field>
<Form.Item>
<Form.Field {config} name="dotnetType">
<Form.Label>.NET Type</Form.Label>
<Form.Select>
<Form.SelectTrigger placeholder=".NET Type" />
<Form.SelectContent>
<Form.SelectItem value="Division">Division</Form.SelectItem>
<Form.SelectItem value="Subdivision">Subdivision</Form.SelectItem>
</Form.SelectContent>
</Form.Select>
<Form.Validation />
</Form.Field>
</Form.Item>
<Form.Button class="mt-2 w-100">
<Plus class="mr-2 w-4 h-4" />
Create
</Form.Button>
</Form.Root>
</div>
</Dialog.Content>
</Dialog.Root>
39 changes: 39 additions & 0 deletions src/routes/admin/facilities/actions.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script lang="ts">
import {Button, buttonVariants} from "$lib/components/ui/button";
import {toast} from "svelte-sonner";
import * as Dialog from "$lib/components/ui/dialog";
import {invalidateAll} from "$app/navigation";
export let id: string;
let deleteOpen = false;
async function removeIt() {
await fetch("?/delete", {
method: 'POST',
body: `id=${id}`,
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
});
await invalidateAll();
toast.success("Facility removed successfully!");
}
</script>

<Button href="/{id}/" variant="outline">Open HQ</Button>
<Dialog.Root bind:open={deleteOpen}>
<Dialog.Trigger class={buttonVariants({ variant: "outline" })}>Remove</Dialog.Trigger>
<Dialog.Content class="sm:max-w-[425px]">
<Dialog.Header>
<Dialog.Title>Are you sure?</Dialog.Title>
<Dialog.Description>
This is an <b>incredibly destructive operation</b>. All data associated with this facility and it's membership information will be permanently obliterated!
</Dialog.Description>
</Dialog.Header>
<Dialog.Footer>
<Button type="submit" on:click={() => {deleteOpen = false;}}>Nevermind</Button>
<Button type="submit" on:click={() => {removeIt(); deleteOpen = false;}} class="bg-red-600 text-red-950 hover:bg-red-700 hover:text-red-950">I'm sure</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
10 changes: 10 additions & 0 deletions src/routes/admin/facilities/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { z } from "zod";

export const formSchema = z.object({
id: z.string(),
name: z.string(),
dotnetId: z.string(),
dotnetType: z.enum(["Subdivision", "Division"]),
});

export type FormSchema = typeof formSchema;

0 comments on commit 89a635a

Please sign in to comment.