diff --git a/src/app/admin/page.jsx b/src/app/admin/page.jsx
index 4a61174..42549f6 100644
--- a/src/app/admin/page.jsx
+++ b/src/app/admin/page.jsx
@@ -11,6 +11,7 @@ import SuccessModal from '@/components/SuccessModal'
import { useAdminData } from '@/hooks/useAdminData'
import AdminTabNav from '@/components/admin/AdminTabNav'
import AdminRoomList from '@/components/admin/AdminRoomList'
+import AdminBookingPanel from '@/components/admin/AdminBookingPanel'
export default function AdminPanel() {
const [activeTab, setActiveTab] = useState('list')
@@ -69,6 +70,13 @@ export default function AdminPanel() {
>
)}
+
+ {activeTab === 'bookings' && (
+
+ )}
+
{activeTab === 'import' && (
<>
diff --git a/src/app/api/bookings/[id]/route.js b/src/app/api/bookings/[id]/route.js
new file mode 100644
index 0000000..e8ffa7e
--- /dev/null
+++ b/src/app/api/bookings/[id]/route.js
@@ -0,0 +1,46 @@
+import { NextResponse } from 'next/server'
+import { updateBookingSeats, deleteBookingById } from '@/lib/db/bookingDbService'
+import { requireAdminAccess } from '@/lib/auth/requireAdminAccess'
+
+export async function PATCH(req, context) {
+ if (requireAdminAccess()) {
+ return NextResponse.json({ error: 'Endast tillgängligt för administratörer' }, { status: 403 })
+ }
+ try {
+ const { id } = await context.params
+ const { seats } = await req.json()
+
+ if (!Array.isArray(seats) || seats.length === 0) {
+ return NextResponse.json({ error: 'Du måste skicka minst en plats.' }, { status: 400 })
+ }
+
+ const isValidSeat = (s) => typeof s.row === 'number' && typeof s.seat === 'number'
+ if (!seats.every(isValidSeat)) {
+ return NextResponse.json(
+ { error: 'Ogiltiga platser – varje plats måste ha rad och platsnummer.' },
+ { status: 400 }
+ )
+ }
+
+ const updated = await updateBookingSeats(id, seats)
+ return NextResponse.json(updated)
+ } catch (err) {
+ console.error('❌ PATCH /api/bookings/:id failed:', err)
+ return NextResponse.json({ error: err.message }, { status: 500 })
+ }
+}
+
+export async function DELETE(req, context) {
+ if (requireAdminAccess()) {
+ return NextResponse.json({ error: 'Endast tillgängligt för administratörer' }, { status: 403 })
+ }
+
+ try {
+ const { id } = await context.params
+ await deleteBookingById(id)
+ return NextResponse.json({ message: 'Bokningen har tagits bort' })
+ } catch (err) {
+ console.error('❌ DELETE /api/bookings/:id failed:', err)
+ return NextResponse.json({ error: err.message }, { status: 500 })
+ }
+}
diff --git a/src/app/api/bookings/route.js b/src/app/api/bookings/route.js
index e69de29..a346ec8 100644
--- a/src/app/api/bookings/route.js
+++ b/src/app/api/bookings/route.js
@@ -0,0 +1,16 @@
+import { NextResponse } from 'next/server'
+import { getAllBookings } from '@/lib/db/bookingDbService'
+import { requireAdminAccess } from '@/lib/auth/requireAdminAccess'
+
+export async function GET() {
+ if (requireAdminAccess()) {
+ return NextResponse.json({ error: 'Endast tillgängligt för administratörer' }, { status: 403 })
+ }
+
+ try {
+ const bookings = await getAllBookings()
+ return NextResponse.json(bookings)
+ } catch (err) {
+ return NextResponse.json({ error: err.message }, { status: 500 })
+ }
+}
diff --git a/src/app/api/screenings/[id]/details/route.js b/src/app/api/screenings/[id]/details/route.js
new file mode 100644
index 0000000..eee1cf6
--- /dev/null
+++ b/src/app/api/screenings/[id]/details/route.js
@@ -0,0 +1,21 @@
+import { NextResponse } from 'next/server'
+import connectDB from '@/lib/db/connectDB'
+import { getScreeningWithDetails } from '@/lib/db/screeningDbService'
+
+export async function GET(req, context) {
+ const { id } = await context.params
+
+ try {
+ await connectDB()
+ const screening = await getScreeningWithDetails(id)
+
+ if (!screening) {
+ return NextResponse.json({ error: 'Visning hittades inte' }, { status: 404 })
+ }
+
+ return NextResponse.json({ screening })
+ } catch (err) {
+ console.error('❌ API error in /screenings/[id]/details:', err)
+ return NextResponse.json({ error: 'Något gick fel' }, { status: 500 })
+ }
+}
diff --git a/src/components/admin/AdminBookingFilters.jsx b/src/components/admin/AdminBookingFilters.jsx
new file mode 100644
index 0000000..ec0da5a
--- /dev/null
+++ b/src/components/admin/AdminBookingFilters.jsx
@@ -0,0 +1,66 @@
+'use client'
+
+export default function AdminBookingFilters({ filters, setFilters, movieOptions, roomOptions, resetFilters }) {
+ return (
+
+
+ setFilters((prev) => ({ ...prev, search: e.target.value }))}
+ />
+
+
+
+
+
+ setFilters((prev) => ({ ...prev, bookingDate: e.target.value }))}
+ />
+
+
+
+
+ setFilters((prev) => ({ ...prev, screeningDate: e.target.value }))}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/admin/AdminBookingList.jsx b/src/components/admin/AdminBookingList.jsx
new file mode 100644
index 0000000..5cf08ca
--- /dev/null
+++ b/src/components/admin/AdminBookingList.jsx
@@ -0,0 +1,33 @@
+'use client'
+
+import { formatDateTime } from '@/lib/utils/formatDateTime'
+
+export default function AdminBookingList({ bookings, onShow, onDelete, loading }) {
+ if (loading) {
+ return Laddar bokningar...
+ }
+
+ if (bookings.length === 0) {
+ return Inga bokningar hittades.
+ }
+
+ return (
+
+ )
+}
diff --git a/src/components/admin/AdminBookingModal.jsx b/src/components/admin/AdminBookingModal.jsx
new file mode 100644
index 0000000..c639fbf
--- /dev/null
+++ b/src/components/admin/AdminBookingModal.jsx
@@ -0,0 +1,73 @@
+'use client'
+
+import SeatMap from '@/components/booking/SeatMap'
+import ErrorMessage from '@/components/ErrorMessage'
+
+export default function AdminBookingModal({
+ booking,
+ isEditing,
+ toggleEditMode,
+ setSelectedBooking,
+ onSave,
+ onClose,
+ errorMessage,
+ setErrorMessage,
+}) {
+ return (
+
+
+
+
+
{booking.movieTitle}
+
Salong {booking.room.name}
+
+
+ Bokade platser för: {booking.name}
+
+ {Object.entries(
+ booking.seats.reduce((acc, seat) => {
+ const row = seat.row
+ const seatNum = seat.seat
+ if (!acc[row]) acc[row] = []
+ acc[row].push(seatNum)
+ return acc
+ }, {})
+ ).map(([row, seatNumbers]) => (
+
+ Rad {row}: Plats {seatNumbers.sort((a, b) => a - b).join(', ')}
+
+ ))}
+
+
+
+
+
+ {isEditing && (
+
+ )}
+
+ {errorMessage &&
setErrorMessage('')} />}
+
+
+ setSelectedBooking((prev) => ({
+ ...prev,
+ seats: newSelectedSeats,
+ }))
+ : () => setErrorMessage('Du måste klicka på "Redigera platser" innan du kan göra ändringar.')
+ }
+ />
+
+
+ )
+}
diff --git a/src/components/admin/AdminBookingPanel.jsx b/src/components/admin/AdminBookingPanel.jsx
new file mode 100644
index 0000000..2703fd8
--- /dev/null
+++ b/src/components/admin/AdminBookingPanel.jsx
@@ -0,0 +1,73 @@
+'use client'
+
+import { useAdminBookings } from '@/hooks/useAdminBookings'
+import AdminBookingFilters from './AdminBookingFilters'
+import AdminBookingList from './AdminBookingList'
+import AdminBookingModal from './AdminBookingModal'
+import ConfirmModal from '@/components/ConfirmModal'
+import SuccessModal from '@/components/SuccessModal'
+import ErrorMessage from '@/components/ErrorMessage'
+
+export default function AdminBookingPanel() {
+ const {
+ filtered,
+ selectedBooking,
+ isEditing,
+ modal,
+ successMessage,
+ errorMessage,
+ filters,
+ setFilters,
+ movieOptions,
+ roomOptions,
+ resetFilters,
+ confirmDeleteBooking,
+ setModal,
+ setSuccessMessage,
+ setErrorMessage,
+ showSeatMap,
+ setSelectedBooking,
+ toggleEditMode,
+ handleSaveBooking,
+ setIsEditing,
+ loading,
+ } = useAdminBookings()
+
+ return (
+
+
Alla bokningar
+
+
+
+ {errorMessage &&
setErrorMessage('')} />}
+
+
+
+ {selectedBooking && (
+ {
+ setSelectedBooking(null)
+ setIsEditing(false)
+ }}
+ errorMessage={errorMessage}
+ setErrorMessage={setErrorMessage}
+ />
+ )}
+
+ {modal && setModal(null)} />}
+
+ {successMessage && setSuccessMessage(null)} />}
+
+ )
+}
diff --git a/src/components/admin/AdminTabNav.jsx b/src/components/admin/AdminTabNav.jsx
index 85382f3..3650d05 100644
--- a/src/components/admin/AdminTabNav.jsx
+++ b/src/components/admin/AdminTabNav.jsx
@@ -6,6 +6,9 @@ export default function AdminTabNav({ activeTab, setActiveTab }) {
+
diff --git a/src/components/booking/SeatMap.jsx b/src/components/booking/SeatMap.jsx
index ef57306..8203289 100644
--- a/src/components/booking/SeatMap.jsx
+++ b/src/components/booking/SeatMap.jsx
@@ -10,6 +10,10 @@ export default function SeatMap({ screening, selectedSeats, onSelect, nrOfTicket
const toggleSeatSelection = (rowNumber, seatNumber) => {
if (isSeatBooked(rowNumber, seatNumber)) return
+ if (typeof nrOfTickets === 'number' && nrOfTickets === 0) {
+ return
+ }
+
const isAlreadySelected = selectedSeats.some((seat) => seat.row === rowNumber && seat.seat === seatNumber)
if (isAlreadySelected) {
@@ -18,10 +22,12 @@ export default function SeatMap({ screening, selectedSeats, onSelect, nrOfTicket
return
}
- if (selectedSeats.length < nrOfTickets) {
- const updatedSelection = [...selectedSeats, { row: rowNumber, seat: seatNumber }]
- onSelect?.(updatedSelection)
+ if (typeof nrOfTickets === 'number' && nrOfTickets > 0 && selectedSeats.length >= nrOfTickets) {
+ return
}
+
+ const updated = [...selectedSeats, { row: rowNumber, seat: seatNumber }]
+ onSelect?.(updated)
}
const groupedSeats = Object.entries(
diff --git a/src/hooks/useAdminBookings.js b/src/hooks/useAdminBookings.js
new file mode 100644
index 0000000..d9d7553
--- /dev/null
+++ b/src/hooks/useAdminBookings.js
@@ -0,0 +1,165 @@
+import { useEffect, useState } from 'react'
+import { fetchBookings, deleteBookingById, updateBookingSeats } from '@/lib/services/bookingApiService'
+import { fetchScreeningDetails } from '@/lib/services/screeningApiService'
+
+export function useAdminBookings() {
+ const [bookings, setBookings] = useState([])
+ const [filtered, setFiltered] = useState([])
+ const [selectedBooking, setSelectedBooking] = useState(null)
+ const [isEditing, setIsEditing] = useState(false)
+ const [modal, setModal] = useState(null)
+ const [successMessage, setSuccessMessage] = useState(null)
+ const [errorMessage, setErrorMessage] = useState('')
+ const [loading, setLoading] = useState(true)
+
+ const [filters, setFilters] = useState({
+ search: '',
+ bookingDate: '',
+ screeningDate: '',
+ selectedMovie: '',
+ selectedRoom: '',
+ })
+
+ const [movieOptions, setMovieOptions] = useState([])
+ const [roomOptions, setRoomOptions] = useState([])
+
+ useEffect(() => {
+ const load = async () => {
+ try {
+ const data = await fetchBookings()
+ setBookings(data)
+ setFiltered(data)
+ setMovieOptions([...new Set(data.map((b) => b.movieTitle))].sort())
+ setRoomOptions([...new Set(data.map((b) => b.roomName))].sort())
+ } catch (err) {
+ setErrorMessage('Kunde inte hämta bokningar. Kontrollera servern eller din internetanslutning.')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ load()
+ }, [])
+
+ useEffect(() => {
+ const query = filters.search.toLowerCase()
+ const formatDate = (str) => new Date(str).toLocaleDateString('sv-SE')
+
+ setFiltered(
+ bookings.filter((booking) => {
+ const nameMatch = booking.name?.toLowerCase().includes(query)
+ const titleMatch = booking.movieTitle?.toLowerCase().includes(query)
+
+ const bookingMatch = !filters.bookingDate || formatDate(booking.bookedAt) === filters.bookingDate
+ const screeningMatch = !filters.screeningDate || formatDate(booking.screeningTime) === filters.screeningDate
+ const movieMatch = !filters.selectedMovie || booking.movieTitle === filters.selectedMovie
+ const roomMatch = !filters.selectedRoom || booking.roomName === filters.selectedRoom
+
+ return (nameMatch || titleMatch) && bookingMatch && screeningMatch && movieMatch && roomMatch
+ })
+ )
+ }, [filters, bookings])
+
+ const resetFilters = () => {
+ setFilters({
+ search: '',
+ bookingDate: '',
+ screeningDate: '',
+ selectedMovie: '',
+ selectedRoom: '',
+ })
+ }
+
+ const confirmDeleteBooking = (booking) => {
+ setModal({
+ message: `Vill du ta bort bokningen för ${booking.name}?`,
+ onConfirm: async () => {
+ await deleteBookingById(booking._id)
+ setBookings((prev) => prev.filter((booking) => booking._id !== booking._id))
+ setSuccessMessage('Bokningen har tagits bort!')
+ setModal(null)
+ },
+ })
+ }
+
+ const showSeatMap = async (booking) => {
+ const data = await fetchScreeningDetails(booking.screening)
+ const screening = data.screening
+
+ if (!screening || !screening.room) {
+ setErrorMessage('Kunde inte hämta information om visningen.')
+ return
+ }
+
+ if (!screening?.room) return
+ setSelectedBooking({
+ ...booking,
+ room: screening.room,
+ bookedSeats: screening.bookedSeats,
+ })
+ }
+
+ const toggleEditMode = () => {
+ setIsEditing((prev) => {
+ if (prev) {
+ setSelectedBooking((prevBooking) => ({
+ ...prevBooking,
+ seats: prevBooking.bookedSeats.filter((seat) => String(seat._id) === String(prevBooking._id)),
+ }))
+ }
+ return !prev
+ })
+ setErrorMessage('')
+ }
+
+ const handleSaveBooking = async () => {
+ if (!selectedBooking || selectedBooking.seats.length === 0) {
+ setErrorMessage('Du måste välja minst en plats.')
+ return
+ }
+ try {
+ const updated = await updateBookingSeats(selectedBooking._id, selectedBooking.seats)
+
+ const data = await fetchScreeningDetails(updated.screening)
+ const screening = data.screening
+
+ setSelectedBooking({
+ ...updated,
+ room: screening.room,
+ bookedSeats: screening.bookedSeats,
+ })
+
+ setBookings((prev) => prev.map((booking) => (booking._id === updated._id ? updated : booking)))
+ setIsEditing(false)
+ setErrorMessage('')
+ setSuccessMessage('Bokningen har uppdaterats!')
+ } catch (err) {
+ console.error(err)
+ setErrorMessage('Kunde inte spara bokningen. Försök igen.')
+ }
+ }
+
+ return {
+ filtered,
+ selectedBooking,
+ isEditing,
+ modal,
+ successMessage,
+ errorMessage,
+ filters,
+ setFilters,
+ movieOptions,
+ roomOptions,
+ resetFilters,
+ confirmDeleteBooking,
+ setModal,
+ setSuccessMessage,
+ setErrorMessage,
+ showSeatMap,
+ setSelectedBooking,
+ toggleEditMode,
+ handleSaveBooking,
+ setIsEditing,
+ loading,
+ }
+}
diff --git a/src/lib/db/bookingDbService.js b/src/lib/db/bookingDbService.js
index abb15e1..74635b2 100644
--- a/src/lib/db/bookingDbService.js
+++ b/src/lib/db/bookingDbService.js
@@ -1,5 +1,54 @@
import Booking from '@/lib/db/models/Booking'
+import connectDB from '@/lib/db/connectDB'
+import Screening from '@/lib/db/models/Screening'
export async function deleteBookingsByScreeningId(screeningId) {
return await Booking.deleteMany({ screening: screeningId })
}
+
+export async function getAllBookings() {
+ await connectDB()
+ return await Booking.find().sort({ bookedAt: -1 }).lean()
+}
+
+export async function deleteBookingById(id) {
+ await connectDB()
+
+ const booking = await Booking.findById(id)
+ if (!booking) throw new Error('Booking not found')
+
+ await Screening.updateOne(
+ { _id: booking.screening },
+ {
+ $pull: {
+ bookedSeats: {
+ $or: booking.seats.map((s) => ({ row: s.row, seat: s.seat })),
+ },
+ },
+ }
+ )
+
+ await Booking.findByIdAndDelete(id)
+}
+
+export async function updateBookingSeats(id, seats) {
+ await connectDB()
+
+ const booking = await Booking.findById(id)
+ if (!booking) throw new Error('Booking not found')
+
+ const screening = await Screening.findById(booking.screening)
+ if (!screening) throw new Error('Screening not found')
+
+ screening.bookedSeats = screening.bookedSeats.filter(
+ (s) => !booking.seats.some((bs) => bs.row === s.row && bs.seat === s.seat)
+ )
+
+ screening.bookedSeats.push(...seats.map((s) => ({ ...s, _id: booking._id })))
+ await screening.save()
+
+ booking.seats = seats
+ await booking.save()
+
+ return booking.toObject()
+}
diff --git a/src/lib/db/screeningDbService.js b/src/lib/db/screeningDbService.js
index 5d3bff2..dec0e0a 100644
--- a/src/lib/db/screeningDbService.js
+++ b/src/lib/db/screeningDbService.js
@@ -80,3 +80,21 @@ export async function createScreening({ movieId, date, room }) {
return await Screening.findById(screening._id).populate('room', 'name').populate('movie', 'title runtime')
}
+
+export async function getScreeningWithDetails(id) {
+ const screening = await Screening.findById(id)
+ .populate('room', 'name rows wheelchairSeats')
+ .populate('movie', 'title runtime')
+ .lean()
+
+ if (!screening) return null
+
+ if (Array.isArray(screening.bookedSeats)) {
+ screening.bookedSeats.sort((a, b) => {
+ if (a.row !== b.row) return a.row - b.row
+ return a.seat - b.seat
+ })
+ }
+
+ return screening
+}
diff --git a/src/lib/services/bookingApiService.js b/src/lib/services/bookingApiService.js
new file mode 100644
index 0000000..dd68c90
--- /dev/null
+++ b/src/lib/services/bookingApiService.js
@@ -0,0 +1,20 @@
+export async function fetchBookings() {
+ const res = await fetch('/api/bookings')
+ if (!res.ok) throw new Error('Kunde inte hämta bokningar.')
+ return await res.json()
+}
+
+export async function deleteBookingById(id) {
+ const res = await fetch(`/api/bookings/${id}`, { method: 'DELETE' })
+ return { success: res.ok }
+}
+
+export async function updateBookingSeats(id, seats) {
+ const res = await fetch(`/api/bookings/${id}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ seats }),
+ })
+ if (!res.ok) throw new Error('Kunde inte uppdatera bokningen.')
+ return await res.json()
+}
diff --git a/src/lib/services/screeningApiService.js b/src/lib/services/screeningApiService.js
index 974add8..30f3a1d 100644
--- a/src/lib/services/screeningApiService.js
+++ b/src/lib/services/screeningApiService.js
@@ -28,3 +28,9 @@ export async function deleteScreening(id) {
const res = await fetch(`/api/screenings/${id}`, { method: 'DELETE' })
if (!res.ok) throw new Error('Kunde inte ta bort visningen.')
}
+
+export async function fetchScreeningDetails(id) {
+ const res = await fetch(`/api/screenings/${id}/details`)
+ if (!res.ok) throw new Error('Kunde inte hämta screening info.')
+ return res.json()
+}
diff --git a/src/styles/admin/AdminBookings.scss b/src/styles/admin/AdminBookings.scss
new file mode 100644
index 0000000..ecb5efd
--- /dev/null
+++ b/src/styles/admin/AdminBookings.scss
@@ -0,0 +1,26 @@
+@use './AdminBookingsFilters';
+@use './AdminBookingsList';
+@use './AdminBookingsModal';
+
+.admin-bookings {
+ &__search {
+ padding: 0.5rem;
+ width: 100%;
+ margin-bottom: 1rem;
+ }
+
+ &__created {
+ font-size: 0.85rem;
+ color: #666;
+ margin-top: 0.3rem;
+ }
+
+ &__no-results {
+ text-align: center;
+ display: block;
+ margin: 3rem auto 2rem;
+ color: #1f4163;
+ font-size: 1.5rem;
+ font-weight: bold;
+ }
+}
diff --git a/src/styles/admin/AdminBookingsFilters.scss b/src/styles/admin/AdminBookingsFilters.scss
new file mode 100644
index 0000000..a295e4a
--- /dev/null
+++ b/src/styles/admin/AdminBookingsFilters.scss
@@ -0,0 +1,66 @@
+.admin-bookings__topbar {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ margin-bottom: 1.5rem;
+}
+
+.admin-bookings__search-wrapper {
+ width: 100%;
+ max-width: 600px;
+ margin-bottom: 1rem;
+
+ input {
+ width: 100%;
+ padding: 0.6rem;
+ font-size: 1rem;
+ border-radius: 6px;
+ border: 1px solid #ccc;
+ }
+}
+
+.admin-bookings__filters-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+ align-items: flex-end;
+ width: 100%;
+}
+
+.admin-bookings__filter-group {
+ display: flex;
+ flex-direction: column;
+ font-size: 0.85rem;
+
+ label {
+ margin-bottom: 0.2rem;
+ font-weight: 600;
+ color: #1f4163;
+ }
+
+ input,
+ select {
+ padding: 0.5rem;
+ border-radius: 4px;
+ border: 1px solid #ccc;
+ font-size: 0.9rem;
+ min-width: 160px;
+ }
+}
+
+.admin-bookings__clear {
+ margin-left: auto;
+ background-color: #03658c;
+ color: white;
+ border: none;
+ padding: 0.5rem 1rem;
+ border-radius: 6px;
+ font-size: 0.9rem;
+ cursor: pointer;
+ font-weight: bold;
+
+ &:hover {
+ background-color: #52b3d9;
+ color: black;
+ }
+}
diff --git a/src/styles/admin/AdminBookingsList.scss b/src/styles/admin/AdminBookingsList.scss
new file mode 100644
index 0000000..6cc2bce
--- /dev/null
+++ b/src/styles/admin/AdminBookingsList.scss
@@ -0,0 +1,47 @@
+.admin-bookings__list {
+ list-style: none;
+ padding: 0;
+}
+
+.admin-bookings__item {
+ background: #f7f7f7;
+ padding: 1rem;
+ margin-bottom: 0.5rem;
+ border-radius: 6px;
+ position: relative;
+}
+
+.admin-bookings__delete {
+ position: absolute;
+ right: 1rem;
+ top: 1rem;
+ background: none;
+ border: 1px solid red;
+ color: red;
+ border-radius: 4px;
+ padding: 0.25rem 0.5rem;
+ font-size: 0.9rem;
+ cursor: pointer;
+
+ &:hover {
+ background: rgba(255, 0, 0, 0.1);
+ }
+}
+
+.admin-bookings__show-seats {
+ margin-top: 0.5rem;
+ background: #1f4163;
+ color: white;
+ padding: 0.4rem 0.8rem;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 0.9rem;
+ font-weight: 500;
+ transition: background 0.2s;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+
+ &:hover {
+ background: #16324e;
+ }
+}
diff --git a/src/styles/admin/AdminBookingsModal.scss b/src/styles/admin/AdminBookingsModal.scss
new file mode 100644
index 0000000..16f8115
--- /dev/null
+++ b/src/styles/admin/AdminBookingsModal.scss
@@ -0,0 +1,64 @@
+.admin-bookings__edit {
+ display: block;
+ background-color: #f0ad4e;
+ color: white;
+ padding: 0.6rem 1rem;
+ border: none;
+ border-radius: 4px;
+ font-size: 0.9rem;
+ cursor: pointer;
+ transition: background 0.2s ease;
+ margin: 0 auto 0.5rem;
+ font-weight: bold;
+
+ &:hover {
+ background-color: #ec971f;
+ }
+}
+
+.admin-bookings__save {
+ display: block;
+ background-color: #2c6;
+ color: white;
+ padding: 0.6rem 1.3rem;
+ margin: 0 auto 1rem;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-weight: bold;
+ font-size: 0.9rem;
+
+ &:hover {
+ background-color: #279b55;
+ }
+}
+
+.admin-room__main-modal-title {
+ color: #1f4163;
+ text-align: center;
+ margin: 0 auto 0.5rem;
+}
+
+.admin-room__modal-selected-seats {
+ text-align: center;
+ font-weight: bold;
+ padding: 0.5rem;
+ border-radius: 6px;
+ background: #dceeff;
+ color: #1f4163;
+ border: 1px solid #a8c4e3;
+ width: 50%;
+ margin: 0 auto 1rem;
+}
+
+.admin-room__modal-body {
+ max-height: calc(100vh - 6rem);
+ overflow-x: auto;
+}
+
+.admin-room__movie-modal-title {
+ color: #1f4163;
+ margin: 0 auto 1rem;
+ text-align: center;
+ font-size: 1.5rem;
+}
diff --git a/src/styles/admin/AdminIndex.scss b/src/styles/admin/AdminIndex.scss
index 51e292c..e6010e9 100644
--- a/src/styles/admin/AdminIndex.scss
+++ b/src/styles/admin/AdminIndex.scss
@@ -3,3 +3,4 @@
@use './AdminNav';
@use './AdminForm';
@use './AdminRoomForm';
+@use './AdminBookings';
diff --git a/src/styles/admin/AdminNav.scss b/src/styles/admin/AdminNav.scss
index 04bea1d..62cd9c5 100644
--- a/src/styles/admin/AdminNav.scss
+++ b/src/styles/admin/AdminNav.scss
@@ -1,23 +1,24 @@
.admin-panel__nav {
display: flex;
- justify-content: center;
+ justify-content: space-between;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
button {
- flex: 1 1 160px;
- text-align: center;
+ flex: 1;
+ min-width: 120px;
+ max-width: 180px;
+ padding: 0.7rem 1rem;
background-color: #ffffff;
color: #1f4163;
border: 2px solid #1f4163;
- padding: 0.7rem 1rem;
border-radius: 6px;
font-weight: bold;
font-size: 0.9rem;
cursor: pointer;
+ text-align: center;
transition: all 0.2s ease-in-out;
- min-width: 160px;
&:hover {
background-color: #1f4163;
diff --git a/src/tests/api/bookings/bookings.test.js b/src/tests/api/bookings/bookings.test.js
new file mode 100644
index 0000000..87d6673
--- /dev/null
+++ b/src/tests/api/bookings/bookings.test.js
@@ -0,0 +1,134 @@
+import { expect, jest, test, describe, beforeAll, beforeEach } from '@jest/globals'
+
+jest.unstable_mockModule('@/lib/db/connectDB', () => ({
+ __esModule: true,
+ default: jest.fn(),
+}))
+
+jest.unstable_mockModule('@/lib/db/bookingDbService', () => ({
+ __esModule: true,
+ getAllBookings: jest.fn(),
+ updateBookingSeats: jest.fn(),
+ deleteBookingById: jest.fn(),
+}))
+
+jest.unstable_mockModule('@/lib/auth/requireAdminAccess', () => ({
+ __esModule: true,
+ requireAdminAccess: jest.fn(() => false),
+}))
+
+let GET, PATCH, DELETE
+let bookingService
+
+beforeAll(async () => {
+ bookingService = await import('@/lib/db/bookingDbService')
+ const routeModule = await import('@/app/api/bookings/route')
+ const detailModule = await import('@/app/api/bookings/[id]/route')
+ GET = routeModule.GET
+ PATCH = detailModule.PATCH
+ DELETE = detailModule.DELETE
+})
+
+beforeEach(() => {
+ jest.clearAllMocks()
+ process.env.NODE_ENV = 'development'
+})
+
+describe('GET /api/bookings (mocked)', () => {
+ test('returns all bookings', async () => {
+ const mockBookings = [{ _id: '1', name: 'Alex' }]
+ bookingService.getAllBookings.mockResolvedValue(mockBookings)
+
+ const res = await GET()
+ const data = await res.json()
+
+ expect(res.status).toBe(200)
+ expect(data).toEqual(mockBookings)
+ })
+
+ test('returns 500 if DB throws error', async () => {
+ bookingService.getAllBookings.mockRejectedValue(new Error('Databasfel'))
+
+ const res = await GET()
+ const data = await res.json()
+
+ expect(res.status).toBe(500)
+ expect(data.error).toMatch(/databasfel/i)
+ })
+})
+
+describe('PATCH /api/bookings/:id (mocked)', () => {
+ test('updates booking seats', async () => {
+ const updated = { _id: '1', seats: [{ row: 1, seat: 2 }] }
+ bookingService.updateBookingSeats.mockResolvedValue(updated)
+
+ const req = {
+ json: async () => ({ seats: [{ row: 1, seat: 2 }] }),
+ }
+
+ const res = await PATCH(req, { params: { id: '1' } })
+ const data = await res.json()
+
+ expect(res.status).toBe(200)
+ expect(data).toEqual(updated)
+ })
+
+ test('returns 400 if seats array is missing or empty', async () => {
+ const req = { json: async () => ({ seats: [] }) }
+ const res = await PATCH(req, { params: { id: '1' } })
+ const data = await res.json()
+
+ expect(res.status).toBe(400)
+ expect(data.error).toMatch(/minst en plats/i)
+ })
+
+ test('returns 400 if seats are invalid', async () => {
+ const req = {
+ json: async () => ({
+ seats: [{ row: 'a', seat: 2 }, { seat: 3 }],
+ }),
+ }
+
+ const res = await PATCH(req, { params: { id: '1' } })
+ const data = await res.json()
+
+ expect(res.status).toBe(400)
+ expect(data.error).toMatch(/ogiltiga platser/i)
+ })
+
+ test('returns 500 if updateBookingSeats throws', async () => {
+ bookingService.updateBookingSeats.mockRejectedValue(new Error('Kunde inte uppdatera bokning'))
+
+ const req = {
+ json: async () => ({ seats: [{ row: 1, seat: 2 }] }),
+ }
+
+ const res = await PATCH(req, { params: { id: '1' } })
+ const data = await res.json()
+
+ expect(res.status).toBe(500)
+ expect(data.error).toMatch(/uppdatera bokning/i)
+ })
+})
+
+describe('DELETE /api/bookings/:id (mocked)', () => {
+ test('deletes a booking successfully', async () => {
+ bookingService.deleteBookingById.mockResolvedValue()
+
+ const res = await DELETE({}, { params: { id: '1' } })
+ const data = await res.json()
+
+ expect(res.status).toBe(200)
+ expect(data.message).toMatch(/tagits bort/i)
+ })
+
+ test('returns 500 if deleteBookingById throws', async () => {
+ bookingService.deleteBookingById.mockRejectedValue(new Error('Något gick fel'))
+
+ const res = await DELETE({}, { params: { id: '1' } })
+ const data = await res.json()
+
+ expect(res.status).toBe(500)
+ expect(data.error).toMatch(/något gick fel/i)
+ })
+})
diff --git a/src/tests/api/movies/movies.test.js b/src/tests/api/movies/movies.test.js
index 2d3b2ea..e306b56 100644
--- a/src/tests/api/movies/movies.test.js
+++ b/src/tests/api/movies/movies.test.js
@@ -1,7 +1,5 @@
-// src/tests/admin/admin.test.js
import { FormData } from 'formdata-node'
import { expect, jest, test, describe, beforeEach, beforeAll } from '@jest/globals'
-import { findMoviesByTitle } from '@/lib/db/movieDbService'
// Mock database and services
jest.unstable_mockModule('@/lib/db/connectDB', () => ({
diff --git a/src/tests/api/screenings/screenings.test.js b/src/tests/api/screenings/screenings.test.js
index bbd644e..870deb3 100644
--- a/src/tests/api/screenings/screenings.test.js
+++ b/src/tests/api/screenings/screenings.test.js
@@ -1,4 +1,3 @@
-// src/tests/screenings/screenings.test.js
import { FormData } from 'formdata-node'
import { expect, jest, test, describe, beforeEach, beforeAll } from '@jest/globals'