feat(admin): availability management - weekly schedule & blocked dates (DB + API + admin UI)
This commit is contained in:
48
scripts/db-availability-migration.mjs
Normal file
48
scripts/db-availability-migration.mjs
Normal 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)
|
||||
})
|
||||
130
src/app/admin/availability/page.tsx
Normal file
130
src/app/admin/availability/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
39
src/app/api/availability/blocked/route.ts
Normal file
39
src/app/api/availability/blocked/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
42
src/app/api/availability/weekly/route.ts
Normal file
42
src/app/api/availability/weekly/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user