feat(admin): availability management - weekly schedule & blocked dates (DB + API + admin UI)

This commit is contained in:
Alex Polo
2025-10-29 10:52:31 +01:00
parent 5b9bbace8b
commit 8afd6a36f3
4 changed files with 259 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
import mysql from 'mysql2/promise'
import dotenv from 'dotenv'
dotenv.config()
async function run() {
const pool = mysql.createPool({
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'warme',
password: process.env.DB_PASSWORD || 'warme123',
database: process.env.DB_NAME || 'warme',
})
const conn = await pool.getConnection()
try {
// weekly availability (recurring weekly schedule)
await conn.query(`
CREATE TABLE IF NOT EXISTS weekly_availability (
id VARCHAR(36) PRIMARY KEY,
weekday TINYINT NOT NULL,
start_time TIME NOT NULL,
end_time TIME NOT NULL,
note TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
`)
// blocked specific dates
await conn.query(`
CREATE TABLE IF NOT EXISTS blocked_dates (
id VARCHAR(36) PRIMARY KEY,
block_date DATE NOT NULL,
note TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
`)
console.log('Availability tables created (if not existed)')
} finally {
conn.release()
await pool.end()
}
}
run().then(() => process.exit(0)).catch(err => {
console.error(err)
process.exit(1)
})

View File

@@ -0,0 +1,130 @@
"use client"
import { useEffect, useState } from 'react'
type Weekly = { id: string; weekday: number; start_time: string; end_time: string; note?: string }
type Blocked = { id: string; block_date: string; note?: string }
export default function AvailabilityAdmin() {
const [weekly, setWeekly] = useState<Weekly[]>([])
const [blocked, setBlocked] = useState<Blocked[]>([])
const [loading, setLoading] = useState(true)
// form state
const [weekday, setWeekday] = useState(1)
const [startTime, setStartTime] = useState('09:00')
const [endTime, setEndTime] = useState('17:00')
const [note, setNote] = useState('')
const [blockDate, setBlockDate] = useState('')
const [blockNote, setBlockNote] = useState('')
useEffect(() => { fetchAll() }, [])
async function fetchAll() {
setLoading(true)
const [wRes, bRes] = await Promise.all([
fetch('/api/availability/weekly').then(r => r.json()),
fetch('/api/availability/blocked').then(r => r.json())
])
setWeekly(wRes || [])
setBlocked(bRes || [])
setLoading(false)
}
async function addWeekly(e: React.FormEvent) {
e.preventDefault()
await fetch('/api/availability/weekly', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ weekday, start_time: startTime, end_time: endTime, note }) })
setNote('')
fetchAll()
}
async function deleteWeekly(id: string) {
if (!confirm('Slet dette skema?')) return
await fetch('/api/availability/weekly', { method: 'DELETE', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ id }) })
fetchAll()
}
async function addBlocked(e: React.FormEvent) {
e.preventDefault()
if (!blockDate) return alert('Vælg en dato')
await fetch('/api/availability/blocked', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ block_date: blockDate, note: blockNote }) })
setBlockDate('')
setBlockNote('')
fetchAll()
}
async function deleteBlocked(id: string) {
if (!confirm('Slet denne blokering?')) return
await fetch('/api/availability/blocked', { method: 'DELETE', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ id }) })
fetchAll()
}
return (
<div className="min-h-screen bg-primary-50 py-8">
<div className="container mx-auto px-4">
<h1 className="text-3xl font-bold mb-6">Tilgængelighed & blokeringer</h1>
{loading ? <p>Indlæser...</p> : (
<div className="grid md:grid-cols-2 gap-8">
<section className="bg-white rounded-lg p-6">
<h2 className="text-xl font-semibold mb-4">Ugentligt arbejdsskema</h2>
<form onSubmit={addWeekly} className="grid gap-3 mb-4">
<label>Ugedag
<select value={weekday} onChange={e => setWeekday(parseInt(e.target.value))} className="ml-2">
<option value={1}>Mandag</option>
<option value={2}>Tirsdag</option>
<option value={3}>Onsdag</option>
<option value={4}>Torsdag</option>
<option value={5}>Fredag</option>
<option value={6}>Lørdag</option>
<option value={0}>Søndag</option>
</select>
</label>
<div className="flex gap-2">
<label>Start
<input type="time" value={startTime} onChange={e => setStartTime(e.target.value)} className="ml-2" />
</label>
<label>Slut
<input type="time" value={endTime} onChange={e => setEndTime(e.target.value)} className="ml-2" />
</label>
</div>
<input value={note} onChange={e => setNote(e.target.value)} placeholder="Noter (valgfri)" className="border px-2 py-1 rounded" />
<button className="px-4 py-2 bg-accent text-white rounded">Tilføj</button>
</form>
<h3 className="font-semibold mb-2">Eksisterende skemaer</h3>
<ul className="space-y-2">
{weekly.map(w => (
<li key={w.id} className="flex justify-between items-center border p-2 rounded">
<div>{['Søndag','Mandag','Tirsdag','Onsdag','Torsdag','Fredag','Lørdag'][w.weekday]} {w.start_time}{w.end_time} {w.note ? `· ${w.note}` : ''}</div>
<button onClick={() => deleteWeekly(w.id)} className="text-red-600">Slet</button>
</li>
))}
</ul>
</section>
<section className="bg-white rounded-lg p-6">
<h2 className="text-xl font-semibold mb-4">Blokerede dage</h2>
<form onSubmit={addBlocked} className="grid gap-3 mb-4">
<input type="date" value={blockDate} onChange={e => setBlockDate(e.target.value)} className="border px-2 py-1 rounded" />
<input value={blockNote} onChange={e => setBlockNote(e.target.value)} placeholder="Noter (fx ferie)" className="border px-2 py-1 rounded" />
<button className="px-4 py-2 bg-accent text-white rounded">Bloker dato</button>
</form>
<h3 className="font-semibold mb-2">Eksisterende blokeringer</h3>
<ul className="space-y-2">
{blocked.map(b => (
<li key={b.id} className="flex justify-between items-center border p-2 rounded">
<div>{b.block_date} {b.note ? `· ${b.note}` : ''}</div>
<button onClick={() => deleteBlocked(b.id)} className="text-red-600">Slet</button>
</li>
))}
</ul>
</section>
</div>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,39 @@
import { NextResponse } from 'next/server'
import { pool } from '@/lib/db'
import crypto from 'crypto'
export async function GET() {
try {
const [rows] = await pool.query('SELECT * FROM blocked_dates ORDER BY block_date')
return NextResponse.json(rows)
} catch (error) {
console.error('Error fetching blocked dates', error)
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 })
}
}
export async function POST(req: Request) {
try {
const body = await req.json()
const { block_date, note } = body
if (!block_date) return NextResponse.json({ error: 'Missing block_date' }, { status: 400 })
const id = crypto.randomUUID()
await pool.query('INSERT INTO blocked_dates SET ?', [{ id, block_date, note }])
return NextResponse.json({ ok: true, id })
} catch (error) {
console.error('Error creating blocked date', error)
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 })
}
}
export async function DELETE(req: Request) {
try {
const { id } = await req.json()
if (!id) return NextResponse.json({ error: 'Missing id' }, { status: 400 })
await pool.query('DELETE FROM blocked_dates WHERE id = ?', [id])
return NextResponse.json({ ok: true })
} catch (error) {
console.error('Error deleting blocked date', error)
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 })
}
}

View File

@@ -0,0 +1,42 @@
import { NextResponse } from 'next/server'
import { pool } from '@/lib/db'
import crypto from 'crypto'
export async function GET() {
try {
const [rows] = await pool.query('SELECT * FROM weekly_availability ORDER BY weekday, start_time')
return NextResponse.json(rows)
} catch (error) {
console.error('Error fetching weekly availability', error)
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 })
}
}
export async function POST(req: Request) {
try {
const body = await req.json()
const { weekday, start_time, end_time, note } = body
if (weekday === undefined || !start_time || !end_time) {
return NextResponse.json({ error: 'Missing fields' }, { status: 400 })
}
const id = crypto.randomUUID()
await pool.query('INSERT INTO weekly_availability SET ?', [{ id, weekday, start_time, end_time, note }])
return NextResponse.json({ ok: true, id })
} catch (error) {
console.error('Error creating weekly availability', error)
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 })
}
}
export async function DELETE(req: Request) {
try {
const { id } = await req.json()
if (!id) return NextResponse.json({ error: 'Missing id' }, { status: 400 })
await pool.query('DELETE FROM weekly_availability WHERE id = ?', [id])
return NextResponse.json({ ok: true })
} catch (error) {
console.error('Error deleting weekly availability', error)
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 })
}
}