diff --git a/scripts/db-availability-migration.mjs b/scripts/db-availability-migration.mjs new file mode 100644 index 0000000..ce0bb23 --- /dev/null +++ b/scripts/db-availability-migration.mjs @@ -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) +}) diff --git a/src/app/admin/availability/page.tsx b/src/app/admin/availability/page.tsx new file mode 100644 index 0000000..d70e8bc --- /dev/null +++ b/src/app/admin/availability/page.tsx @@ -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([]) + const [blocked, setBlocked] = useState([]) + 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 ( +
+
+

Tilgængelighed & blokeringer

+ + {loading ?

Indlæser...

: ( +
+
+

Ugentligt arbejdsskema

+
+ +
+ + +
+ setNote(e.target.value)} placeholder="Noter (valgfri)" className="border px-2 py-1 rounded" /> + +
+ +

Eksisterende skemaer

+
    + {weekly.map(w => ( +
  • +
    {['Søndag','Mandag','Tirsdag','Onsdag','Torsdag','Fredag','Lørdag'][w.weekday]} {w.start_time}–{w.end_time} {w.note ? `· ${w.note}` : ''}
    + +
  • + ))} +
+
+ +
+

Blokerede dage

+
+ setBlockDate(e.target.value)} className="border px-2 py-1 rounded" /> + setBlockNote(e.target.value)} placeholder="Noter (fx ferie)" className="border px-2 py-1 rounded" /> + +
+ +

Eksisterende blokeringer

+
    + {blocked.map(b => ( +
  • +
    {b.block_date} {b.note ? `· ${b.note}` : ''}
    + +
  • + ))} +
+
+
+ )} +
+
+ ) +} diff --git a/src/app/api/availability/blocked/route.ts b/src/app/api/availability/blocked/route.ts new file mode 100644 index 0000000..9345bfe --- /dev/null +++ b/src/app/api/availability/blocked/route.ts @@ -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 }) + } +} diff --git a/src/app/api/availability/weekly/route.ts b/src/app/api/availability/weekly/route.ts new file mode 100644 index 0000000..ce4033f --- /dev/null +++ b/src/app/api/availability/weekly/route.ts @@ -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 }) + } +}