Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/app/admin/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -69,6 +70,13 @@ export default function AdminPanel() {
</section>
</>
)}

{activeTab === 'bookings' && (
<section className="admin-page__section">
<AdminBookingPanel />
</section>
)}

{activeTab === 'import' && (
<>
<AdminMovieForm onSubmitMovie={handleAddMovie} movieError={movieError} setMovieError={setMovieError} />
Expand Down
46 changes: 46 additions & 0 deletions src/app/api/bookings/[id]/route.js
Original file line number Diff line number Diff line change
@@ -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 })
}
}
16 changes: 16 additions & 0 deletions src/app/api/bookings/route.js
Original file line number Diff line number Diff line change
@@ -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 })
}
}
21 changes: 21 additions & 0 deletions src/app/api/screenings/[id]/details/route.js
Original file line number Diff line number Diff line change
@@ -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 })
}
}
66 changes: 66 additions & 0 deletions src/components/admin/AdminBookingFilters.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use client'

export default function AdminBookingFilters({ filters, setFilters, movieOptions, roomOptions, resetFilters }) {
return (
<div className="admin-bookings__topbar">
<div className="admin-bookings__search-wrapper">
<input
type="text"
placeholder="Sök efter användare eller film..."
value={filters.search}
onChange={(e) => setFilters((prev) => ({ ...prev, search: e.target.value }))}
/>
</div>

<div className="admin-bookings__filters-row">
<div className="admin-bookings__filter-group">
<label>Bokningsdatum</label>
<input
type="date"
value={filters.bookingDate}
onChange={(e) => setFilters((prev) => ({ ...prev, bookingDate: e.target.value }))}
/>
</div>

<div className="admin-bookings__filter-group">
<label>Visningsdatum</label>
<input
type="date"
value={filters.screeningDate}
onChange={(e) => setFilters((prev) => ({ ...prev, screeningDate: e.target.value }))}
/>
</div>

<div className="admin-bookings__filter-group">
<label>Film</label>
<select
value={filters.selectedMovie}
onChange={(e) => setFilters((prev) => ({ ...prev, selectedMovie: e.target.value }))}
>
<option value="">Alla filmer</option>
{movieOptions.map((title) => (
<option key={title}>{title}</option>
))}
</select>
</div>

<div className="admin-bookings__filter-group">
<label>Salong</label>
<select
value={filters.selectedRoom}
onChange={(e) => setFilters((prev) => ({ ...prev, selectedRoom: e.target.value }))}
>
<option value="">Alla salonger</option>
{roomOptions.map((room) => (
<option key={room}>{room}</option>
))}
</select>
</div>

<button className="admin-bookings__clear" onClick={resetFilters}>
Rensa filter
</button>
</div>
</div>
)
}
33 changes: 33 additions & 0 deletions src/components/admin/AdminBookingList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use client'

import { formatDateTime } from '@/lib/utils/formatDateTime'

export default function AdminBookingList({ bookings, onShow, onDelete, loading }) {
if (loading) {
return <p className="admin-page__loading">Laddar bokningar...</p>
}

if (bookings.length === 0) {
return <p className="admin-bookings__no-results">Inga bokningar hittades.</p>
}

return (
<ul className="admin-bookings__list">
{bookings.map((booking) => (
<li key={booking._id} className="admin-bookings__item">
<strong>{booking.name || 'Okänd användare'}</strong> – {booking.movieTitle} (
{formatDateTime(booking.screeningTime)})
<div className="admin-bookings__created">Bokning skapades: {formatDateTime(booking.bookedAt)}</div>
<div className="admin-bookings__buttons">
<button onClick={() => onShow(booking)} className="admin-bookings__show-seats">
Visa platser
</button>
<button onClick={() => onDelete(booking)} className="admin-bookings__delete">
Ta bort
</button>
</div>
</li>
))}
</ul>
)
}
73 changes: 73 additions & 0 deletions src/components/admin/AdminBookingModal.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="admin-room__modal-backdrop">
<div className="admin-room__modal">
<button onClick={onClose} className="admin-room__modal-close">
×
</button>

<h3 className="admin-room__movie-modal-title">{booking.movieTitle}</h3>
<h3 className="admin-room__main-modal-title">Salong {booking.room.name}</h3>

<div className="admin-room__modal-selected-seats">
Bokade platser för: {booking.name}
<ul style={{ listStyle: 'none', padding: 0 }}>
{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]) => (
<div key={row}>
Rad {row}: Plats {seatNumbers.sort((a, b) => a - b).join(', ')}
</div>
))}
</ul>
</div>

<button onClick={toggleEditMode} className="admin-bookings__edit">
{isEditing ? 'Avbryt redigering' : 'Redigera platser'}
</button>

{isEditing && (
<button onClick={onSave} className="admin-bookings__save">
Spara ändringar
</button>
)}

{errorMessage && <ErrorMessage message={errorMessage} onClose={() => setErrorMessage('')} />}

<SeatMap
screening={booking}
selectedSeats={booking.seats}
onSelect={
isEditing
? (newSelectedSeats) =>
setSelectedBooking((prev) => ({
...prev,
seats: newSelectedSeats,
}))
: () => setErrorMessage('Du måste klicka på "Redigera platser" innan du kan göra ändringar.')
}
/>
</div>
</div>
)
}
73 changes: 73 additions & 0 deletions src/components/admin/AdminBookingPanel.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="admin-bookings">
<h2 className="admin-page__section-title">Alla bokningar</h2>

<AdminBookingFilters
filters={filters}
setFilters={setFilters}
movieOptions={movieOptions}
roomOptions={roomOptions}
resetFilters={resetFilters}
/>

{errorMessage && <ErrorMessage message={errorMessage} onClose={() => setErrorMessage('')} />}

<AdminBookingList bookings={filtered} onShow={showSeatMap} onDelete={confirmDeleteBooking} loading={loading} />

{selectedBooking && (
<AdminBookingModal
booking={selectedBooking}
isEditing={isEditing}
toggleEditMode={toggleEditMode}
setSelectedBooking={setSelectedBooking}
onSave={handleSaveBooking}
onClose={() => {
setSelectedBooking(null)
setIsEditing(false)
}}
errorMessage={errorMessage}
setErrorMessage={setErrorMessage}
/>
)}

{modal && <ConfirmModal message={modal.message} onConfirm={modal.onConfirm} onCancel={() => setModal(null)} />}

{successMessage && <SuccessModal message={successMessage} onClose={() => setSuccessMessage(null)} />}
</div>
)
}
3 changes: 3 additions & 0 deletions src/components/admin/AdminTabNav.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export default function AdminTabNav({ activeTab, setActiveTab }) {
<button onClick={() => setActiveTab('list')} className={activeTab === 'list' ? 'active' : ''}>
Översikt
</button>
<button onClick={() => setActiveTab('bookings')} className={activeTab === 'bookings' ? 'active' : ''}>
Hantera bokningar
</button>
<button onClick={() => setActiveTab('import')} className={activeTab === 'import' ? 'active' : ''}>
Importera film
</button>
Expand Down
12 changes: 9 additions & 3 deletions src/components/booking/SeatMap.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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(
Expand Down
Loading