diff --git a/frontend/prisma/schema.prisma b/frontend/prisma/schema.prisma index 4d28982..c0ec11d 100644 --- a/frontend/prisma/schema.prisma +++ b/frontend/prisma/schema.prisma @@ -17,8 +17,8 @@ model User { role UserRole @default(USER) nummer Int? isActive Boolean @default(true) - kontigent Kontigent @default(UBETALT) username String? @unique + kontigent Kontigent @default(UBETALT) accounts Account[] paameldinger ArrangementPaamelding[] bookings Booking[] @@ -99,11 +99,11 @@ model Arrangement { bilde String? kapasitet Int? beskrivelse String - arrangorID String + arrangorID String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt trinn Int[] @default([]) - arrangor komite @relation(fields: [arrangorID], references: [id]) + arrangor komite? @relation(fields: [arrangorID], references: [id]) paameldinger ArrangementPaamelding[] } @@ -171,9 +171,10 @@ model aeresEmiler { navn String aar Int } -model Kontigentpris{ - id String @id @default(cuid()) - aar String + +model Kontigentpris { + id String @id @default(cuid()) + aar String pris Int } @@ -200,8 +201,8 @@ enum UserRole { SUPER_USER } -enum Kontigent{ +enum Kontigent { BETALT UBETALT AVVENTER_BEKREFTELSE -} \ No newline at end of file +} diff --git a/frontend/src/app/api/arrangementer/[id]/paamelding/route.ts b/frontend/src/app/api/arrangementer/[id]/paamelding/route.ts index 30499d0..750551a 100644 --- a/frontend/src/app/api/arrangementer/[id]/paamelding/route.ts +++ b/frontend/src/app/api/arrangementer/[id]/paamelding/route.ts @@ -1,10 +1,5 @@ import { getUserById } from "@/data/user"; import { db } from "@/lib/db"; -import { - ArrangementPaamelding, - ArrangementPaameldingSchema, -} from "@/schemas/arrangement"; -import { id } from "date-fns/locale"; import { NextRequest, NextResponse } from "next/server"; export const revalidate = 0; @@ -20,57 +15,52 @@ export async function GET( }, }); - return NextResponse.json({data: arrangementPaameldinger }, { status: 200 }); + return NextResponse.json( + { data: arrangementPaameldinger }, + { status: 200 }, + ); } catch (error) { return NextResponse.json({ error: error }, { status: 500 }); } } -// export async function DELETE( -// req: NextRequest, -// { params }: { params: { id: string } }, -// ) { -// try { -// const { searchParams } = new URL(req.url); -// const userID = searchParams.get("userID"); +export async function DELETE( + req: NextRequest, + { params }: { params: { id: string } }, +) { + try { + const { id: eventId } = params; + const { userId } = await req.json(); -// if (!userID) { -// return NextResponse.json( -// { error: "UserID is required" }, -// { status: 400 }, -// ); -// } + if (!userId || !eventId) { + return NextResponse.json( + { error: "Missing eventId or userId" }, + { status: 400 }, + ); + } -// const arrangementPaamelding = await db.arrangementPaamelding.findFirst({ -// where: { -// arrangementID: params.id, -// userID: userID, -// }, -// include: { -// arrangement: true, -// }, -// }); + const deletedSignup = await db.arrangementPaamelding.delete({ + where: { + userID_arrangementID: { + userID: userId, + arrangementID: eventId, + }, + }, + }); -// if (arrangementPaamelding) { -// await db.arrangementPaamelding.delete({ -// where: { -// id: arrangementPaamelding.id, -// }, -// }); + if (!deletedSignup) { + return NextResponse.json({ error: "Signup not found" }, { status: 404 }); + } -// return NextResponse.json( -// { -// success: `Du har meldt deg av: ${arrangementPaamelding.arrangement.navn}`, -// }, -// { status: 200 }, -// ); -// } else { -// return NextResponse.json( -// { error: "ArrangementPaamelding not found." }, -// { status: 404 }, -// ); -// } -// } catch (error) { -// return NextResponse.json({ error: error }, { status: 500 }); -// } -// } + return NextResponse.json( + { message: "Signup deleted successfully" }, + { status: 200 }, + ); + } catch (error) { + console.error("Error deleting signup:", error); + return NextResponse.json( + { error: "An error occurred while deleting the signup" }, + { status: 500 }, + ); + } +} diff --git a/frontend/src/app/api/arrangementer/[id]/route.ts b/frontend/src/app/api/arrangementer/[id]/route.ts index 867a00c..b5c1818 100644 --- a/frontend/src/app/api/arrangementer/[id]/route.ts +++ b/frontend/src/app/api/arrangementer/[id]/route.ts @@ -3,7 +3,9 @@ import { NextRequest, NextResponse } from "next/server"; import { updateArrangementSchema, deleteArrangementSchema, + createArrangementSchema, } from "@/schemas/arrangement"; +import { z } from "zod"; export const revalidate = 0; @@ -37,11 +39,50 @@ export async function DELETE( ) { try { const parsedData = deleteArrangementSchema.parse({ id: params.id }); - await db.booking.delete({ + await db.arrangement.delete({ where: { id: parsedData.id }, }); - return NextResponse.json({ message: "Booking deleted" }, { status: 200 }); + return NextResponse.json( + { message: "Arrangement deleted" }, + { status: 200 }, + ); } catch (error) { return NextResponse.json({ error: error }, { status: 400 }); } } + +export async function PUT( + req: NextRequest, + { params }: { params: { id: string } }, +) { + try { + const { id } = params; + const body = await req.json(); + // console.log(body); + // const validatedData = createArrangementSchema.parse({ + // ...body, + // }); + const updatedEvent = await db.arrangement.update({ + where: { id }, + data: body, + }); + + if (!updatedEvent) { + return NextResponse.json({ error: "Event not found" }, { status: 404 }); + } + + return NextResponse.json(updatedEvent, { status: 200 }); + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: "Something went wrong" }, + { status: 400 }, + ); + } + console.error("Error updating event:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 }, + ); + } +} diff --git a/frontend/src/app/api/arrangementer/[id]/users/route.ts b/frontend/src/app/api/arrangementer/[id]/users/route.ts new file mode 100644 index 0000000..4d47b6e --- /dev/null +++ b/frontend/src/app/api/arrangementer/[id]/users/route.ts @@ -0,0 +1,32 @@ +import { getUserById } from "@/data/user"; +import { db } from "@/lib/db"; +import { NextRequest, NextResponse } from "next/server"; + +export async function GET( + req: NextRequest, + { params }: { params: { id: string } }, +) { + try { + const paameldinger = await db.arrangementPaamelding.findMany({ + where: { + arrangementID: params.id, + }, + }); + + if (!paameldinger || paameldinger.length === 0) { + return NextResponse.json( + { error: "Ingen paameldinger funnet" }, + { status: 404 }, + ); + } + const userPromises = paameldinger.map((p) => getUserById(p.userID)); + const users = await Promise.all(userPromises); + + return NextResponse.json(users); + } catch (error) { + return NextResponse.json( + { error: "En feil oppstod ved henting av brukere" }, + { status: 500 }, + ); + } +} diff --git a/frontend/src/app/api/arrangementer/route.ts b/frontend/src/app/api/arrangementer/route.ts index a87b15b..9a43d77 100644 --- a/frontend/src/app/api/arrangementer/route.ts +++ b/frontend/src/app/api/arrangementer/route.ts @@ -1,5 +1,7 @@ import { db } from "@/lib/db"; +import { createArrangementSchema } from "@/schemas/arrangement"; import { NextRequest, NextResponse } from "next/server"; +import { z, ZodError } from "zod"; export const revalidate = 0; @@ -24,3 +26,55 @@ export async function GET(req: NextRequest) { return NextResponse.json({ error: error }, { status: 400 }); } } + +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + console.log("Received body:", body); + + const parsedBody = { + ...body, + dato: body.dato ? new Date(body.dato) : undefined, + }; + + const validatedData = createArrangementSchema.parse(parsedBody); + console.log("Validated data:", validatedData); + + const newEvent = await db.arrangement.create({ + data: { + ...validatedData, + paameldinger: { + create: [], + }, + }, + }); + + console.log("New event created:", newEvent); + + return NextResponse.json( + { message: "Arrangement created successfully", data: newEvent }, + { status: 201 }, + ); + } catch (error) { + console.error("Error in POST /api/arrangementer:", error); + + if (error instanceof ZodError) { + return NextResponse.json( + { error: "Validation error", details: error.errors }, + { status: 400 }, + ); + } + + if (error instanceof Error) { + return NextResponse.json( + { error: "Database error", message: error.message }, + { status: 500 }, + ); + } + + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); + } +} diff --git a/frontend/src/components/admin/admin-dashboard.tsx b/frontend/src/components/admin/admin-dashboard.tsx index 814a39c..3d74860 100644 --- a/frontend/src/components/admin/admin-dashboard.tsx +++ b/frontend/src/components/admin/admin-dashboard.tsx @@ -28,6 +28,8 @@ import BookingComponent from "./booking"; import KomiteComponent from "./komiteComponent"; import HovedstyreComponent from "./hovedstyret"; import LavterskelarrangementComponent from "./lavterskelarrangement"; +import ArrangementComponent from "./arrangement"; +import ArrangementComponentNew from "./arrangementForm"; export function Dashboard() { return ( @@ -67,8 +69,9 @@ export function Dashboard() { - -
Arrangementer
+ + {/* */} + diff --git a/frontend/src/components/admin/arrangement.tsx b/frontend/src/components/admin/arrangement.tsx index 02f500d..abd2422 100644 --- a/frontend/src/components/admin/arrangement.tsx +++ b/frontend/src/components/admin/arrangement.tsx @@ -1,57 +1,421 @@ "use client"; -import useFetch from "@/hooks/use-fetch"; -import { lavTerskelArrangement } from "@/schemas/lavterskelArrangement"; import { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; +import { BookUser, Edit, Trash2, Upload, X } from "lucide-react"; +import { formatDate } from "date-fns"; +import { nb } from "date-fns/locale"; +import useFetch from "@/hooks/use-fetch"; +import { Arrangement } from "@/schemas/arrangement"; +import { formatTrinn } from "@/utils/arrangement/trinn"; +import { UserPrisma } from "@/schemas/user"; +import { uploadArrangementImage } from "@/utils/firebase/upload_arrangementImage"; +export default function EventDashboard() { + const [events, setEvents] = useState([]); + const [selectedEvent, setSelectedEvent] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isEditMode, setIsEditMode] = useState(false); + const [currentEventId, setCurrentEventId] = useState(null); + const [selectedTrinn, setSelectedTrinn] = useState([]); + const [imageFile, setImageFile] = useState(null); + const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); + const [eventToDelete, setEventToDelete] = useState(null); + const [participants, setParticipants] = useState([]); + const { data, loading, error } = useFetch<{ data: Arrangement[] }>( + "/api/arrangementer", + ); + useEffect(() => { + if (data && data.data) { + setEvents(data.data); + } + }, [data]); + const handleCreateEvent = () => { + setSelectedEvent(null); + setIsEditMode(false); + setSelectedTrinn([]); + setImageFile(null); + setIsModalOpen(true); + }; -const ArrangementComponent = () => { - const [openForm, setOpenForm] = useState(false); - const { data, loading, error } = useFetch<{data: lavTerskelArrangement[]} | null>("/api/lavterskelarrangement"); + const handleEditEvent = (event: Arrangement) => { + setSelectedEvent(event); + setIsEditMode(true); + setSelectedTrinn(event.trinn); + setImageFile(null); + setIsModalOpen(true); + }; - - const [LTData, setLTData] = useState([]); + const handleDeleteEvent = async (id: string) => { + try { + const response = await fetch(`/api/arrangementer/${id}`, { + method: "DELETE", + }); + if (response.ok) { + setEvents(events.filter((event) => event.id !== id)); + } else { + console.error("Failed to delete event"); + } + } catch (error) { + console.error("Error deleting event:", error); + } + setDeleteConfirmOpen(false); + setEventToDelete(null); + }; - useEffect(() => { - if (data && data.data) { - setLTData(data.data); + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + + let imageUrl = selectedEvent?.bilde || ""; + + if (imageFile) { + try { + imageUrl = await uploadArrangementImage(imageFile); + } catch (error) { + console.error("Error uploading image:", error); + return; + } } - }, [data]); + const eventData = { + navn: formData.get("navn") as string, + dato: new Date(formData.get("dato") as string), + sted: formData.get("sted") as string, + beskrivelse: formData.get("beskrivelse") as string, + bilde: imageUrl || undefined, + trinn: selectedTrinn, + kapasitet: formData.get("kapasitet") + ? Number(formData.get("kapasitet")) + : undefined, + arrangorID: selectedEvent?.arrangorID || "default-arranger-id", // Replace with actual default or logged-in user ID + }; + try { + const url = isEditMode + ? `/api/arrangementer/${selectedEvent?.id}` + : "/api/arrangementer"; + const method = isEditMode ? "PUT" : "POST"; + const response = await fetch(url, { + method, + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(eventData), + }); - + if (response.ok) { + const updatedEvent = await response.json(); + if (isEditMode) { + setEvents( + events.map((event) => + event.id === updatedEvent.id ? updatedEvent : event, + ), + ); + } else { + setEvents([...events, updatedEvent]); + } + setIsModalOpen(false); + setImageFile(null); + } else { + console.error("Failed to save event"); + // Optionally, you can show an error message to the user here + } + } catch (error) { + console.error("Error saving event:", error); + // Optionally, you can show an error message to the user here + } + }; + + const toggleParticipants = async (id: string) => { + if (currentEventId === id) { + setCurrentEventId(null); + setParticipants([]); + } else { + setCurrentEventId(id); + try { + const response = await fetch(`/api/arrangementer/${id}/users`); + if (response.ok) { + const data = await response.json(); + setParticipants(data); + } else { + console.error("Failed to fetch participants"); + } + } catch (error) { + console.error("Error fetching participants:", error); + } + } + }; + + const handleTrinnChange = (trinn: number) => { + setSelectedTrinn((prev) => + prev.includes(trinn) + ? prev.filter((t) => t !== trinn) + : [...prev, trinn].sort(), + ); + }; + + const handleImageChange = (e: React.ChangeEvent) => { + if (e.target.files && e.target.files[0]) { + setImageFile(e.target.files[0]); + } + }; + + const handleDeleteSignup = async (eventId: string, userId: string) => { + try { + const response = await fetch(`/api/arrangementer/${eventId}/paamelding`, { + method: "DELETE", + body: JSON.stringify({ + userId, + }), + }); + if (response.ok) { + setParticipants(participants.filter((p) => p.id !== userId)); + } else { + console.error("Failed to delete signup"); + } + } catch (error) { + console.error("Error deleting signup:", error); + } + }; + + if (loading) return
Loading...
; + if (error) return
Error: {error}
; return ( -
-
-

- Komiteer -

+
+
+

Arrangementer

+
- -
-
-
Rolle
-
Tekst
-
Bilde
- + +
+
+
Navn
+
Dato
+
Sted
+
Beskrivelse
+
Bilde
+
Trinn
+
Handlinger
- - {LTData.map((item) => ( -
-
{item.navn}
-
{item.sted}
- - - + + {events.map((event) => ( +
+
{event.navn}
+
+ {formatDate(new Date(event.dato), "PPP", { locale: nb })} +
+
{event.sted}
+
{event.beskrivelse}
+
{event.bilde}
+
{formatTrinn(event.trinn)}
+
+ + + +
))}
+ + setCurrentEventId(null)} + > + + + Påmeldte deltakere + +
+ + + + + + + + + + + {participants.map((p) => ( + + + + + + + ))} + +
NavnKontigentRolleHandling
{p.name}{p.kontigent}{p.role} + +
+
+ + + +
+
+ + + + + + {isEditMode ? "Rediger arrangement" : "Lag nytt arrangement"} + + +
+ + + +