Fix blocked dates red styling - add modifiersClassNames
- Add modifiersClassNames to both DayPicker modes (single and multiple) - Blocked dates now show with rdp-day_blocked class (red background) - NonWorking and pending modifiers also properly styled - Improve layout: single column with centered calendar - Better spacing and hover effects on buttons
This commit is contained in:
@@ -13,3 +13,4 @@
|
||||
2025-10-30T19:58:29: > Ready on http://localhost:3000
|
||||
2025-10-31T19:17:21: > Ready on http://localhost:3000
|
||||
2025-10-31T20:05:46: > Ready on http://localhost:3000
|
||||
2025-10-31T20:42:18: > Ready on http://localhost:3000
|
||||
|
||||
@@ -262,64 +262,64 @@ function AvailabilityAdminContent() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-primary-50 py-8">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="container mx-auto px-4 max-w-6xl">
|
||||
<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">
|
||||
<div className="space-y-6">
|
||||
{/* Main calendar section */}
|
||||
<section className="bg-white rounded-lg p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Kalender — klik for at skifte tilgængelighed</h2>
|
||||
<p className="text-sm text-primary-600 mb-3">Klik på en dato for at markere den som ikke-tilgængelig (klik igen for at åbne).</p>
|
||||
<div>
|
||||
<div className="flex items-center gap-4 mb-3">
|
||||
<label className="inline-flex items-center gap-2">
|
||||
<input type="checkbox" checked={showOnlyBlocked} onChange={e => setShowOnlyBlocked(e.target.checked)} />
|
||||
<span className="text-sm">Vis kun blokerede</span>
|
||||
</label>
|
||||
<label className="inline-flex items-center gap-2">
|
||||
<input type="checkbox" checked={multiSelectMode} onChange={e => {
|
||||
const on = e.target.checked
|
||||
setMultiSelectMode(on)
|
||||
if (on) {
|
||||
// initialize selectedDates with existing blocked dates
|
||||
setSelectedDates(blocked.map(b => new Date(b.block_date)))
|
||||
} else {
|
||||
setSelectedDates(undefined)
|
||||
<h2 className="text-xl font-semibold mb-2">Kalender — klik for at skifte tilgængelighed</h2>
|
||||
<p className="text-sm text-gray-600 mb-4">Klik på en dato for at blokere/åbne den.</p>
|
||||
|
||||
<div className="flex flex-wrap gap-3 mb-4">
|
||||
<label className="inline-flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" checked={showOnlyBlocked} onChange={e => setShowOnlyBlocked(e.target.checked)} className="rounded" />
|
||||
<span className="text-sm">Vis kun blokerede</span>
|
||||
</label>
|
||||
<label className="inline-flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" checked={multiSelectMode} onChange={e => {
|
||||
const on = e.target.checked
|
||||
setMultiSelectMode(on)
|
||||
if (on) {
|
||||
setSelectedDates(blocked.map(b => new Date(b.block_date)))
|
||||
} else {
|
||||
setSelectedDates(undefined)
|
||||
}
|
||||
}} className="rounded" />
|
||||
<span className="text-sm">Vælg flere til sletning</span>
|
||||
</label>
|
||||
{multiSelectMode && (
|
||||
<button onClick={deleteSelectedBlocked} className="ml-auto px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 disabled:opacity-50 transition-colors" disabled={!selectedDates || selectedDates.length===0 || saving}>
|
||||
Slet valgte ({selectedDates ? selectedDates.length : 0})
|
||||
</button>
|
||||
)}
|
||||
{!multiSelectMode && pendingDates.length > 0 && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
setSaving(true)
|
||||
const isos = pendingDates.map(pd => isoFromDate(pd))
|
||||
setBlocked(prev => [...prev, ...isos.map(i => ({ id: `tmp-${i}`, block_date: i }))])
|
||||
try {
|
||||
await Promise.all(pendingDates.map(pd => fetch('/api/availability/blocked', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ block_date: isoFromDate(pd), note: '' }) })))
|
||||
addToast(`${pendingDates.length} dage blokeret`, 'success')
|
||||
setPendingDates([])
|
||||
} catch (err) {
|
||||
addToast('Kunne ikke oprette alle blokeringer', 'error')
|
||||
} finally {
|
||||
fetchAll()
|
||||
setSaving(false)
|
||||
}
|
||||
}} />
|
||||
<span className="text-sm">Vælg flere blokerede datoer</span>
|
||||
</label>
|
||||
{multiSelectMode && (
|
||||
<button onClick={deleteSelectedBlocked} className="ml-auto px-3 py-1 bg-red-500 text-white rounded" disabled={!selectedDates || selectedDates.length===0 || saving}>
|
||||
Slet valgte ({selectedDates ? selectedDates.length : 0})
|
||||
</button>
|
||||
)}
|
||||
{!multiSelectMode && pendingDates.length > 0 && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
setSaving(true)
|
||||
const isos = pendingDates.map(pd => isoFromDate(pd))
|
||||
setBlocked(prev => [...prev, ...isos.map(i => ({ id: `tmp-${i}`, block_date: i }))])
|
||||
try {
|
||||
await Promise.all(pendingDates.map(pd => fetch('/api/availability/blocked', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ block_date: isoFromDate(pd), note: '' }) })))
|
||||
addToast(`${pendingDates.length} dage blokeret`, 'success')
|
||||
setPendingDates([])
|
||||
} catch (err) {
|
||||
addToast('Kunne ikke oprette alle blokeringer', 'error')
|
||||
} finally {
|
||||
fetchAll()
|
||||
setSaving(false)
|
||||
}
|
||||
}}
|
||||
className="ml-auto px-3 py-1 bg-blue-500 text-white rounded"
|
||||
disabled={saving}
|
||||
>
|
||||
Bloker valgte ({pendingDates.length})
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
}}
|
||||
className="ml-auto px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50 transition-colors"
|
||||
disabled={saving}
|
||||
>
|
||||
Bloker valgte ({pendingDates.length})
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{multiSelectMode ? (
|
||||
<div className="flex justify-center">{multiSelectMode ? (
|
||||
<DayPicker
|
||||
className="admin-calendar"
|
||||
mode="multiple"
|
||||
@@ -330,6 +330,10 @@ function AvailabilityAdminContent() {
|
||||
blocked: blocked.map(b => new Date(b.block_date)),
|
||||
nonWorking: (day: Date) => !weekly.some(w => w.weekday === day.getDay())
|
||||
}}
|
||||
modifiersClassNames={{
|
||||
blocked: 'rdp-day_blocked',
|
||||
nonWorking: 'rdp-day_nonWorking',
|
||||
}}
|
||||
disabled={showOnlyBlocked ? ((day: Date) => !blocked.some(b => sameDay(new Date(b.block_date), day))) : undefined}
|
||||
/>
|
||||
) : (
|
||||
@@ -393,7 +397,9 @@ function AvailabilityAdminContent() {
|
||||
pending: pendingDates,
|
||||
}}
|
||||
modifiersClassNames={{
|
||||
blocked: 'rdp-day_blocked',
|
||||
pending: 'rdp-day_pending',
|
||||
nonWorking: 'rdp-day_nonWorking',
|
||||
}}
|
||||
disabled={showOnlyBlocked ? ((day: Date) => !blocked.some(b => sameDay(new Date(b.block_date), day))) : undefined}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,492 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { DayPicker } from 'react-day-picker'
|
||||
import { da } from 'date-fns/locale'
|
||||
import { ToastProvider, useToast } from '@/components/ui/toast'
|
||||
|
||||
type Weekly = { id: string; weekday: number; start_time: string; end_time: string; note?: string }
|
||||
type Blocked = { id: string; block_date: string; note?: string }
|
||||
|
||||
function BlockedList({ blocked, onDelete }: { blocked: Blocked[]; onDelete: (id: string) => void }) {
|
||||
if (!blocked || blocked.length === 0) return <p>Ingen blokeringer</p>
|
||||
const sorted = blocked.slice().sort((a, b) => a.block_date.localeCompare(b.block_date))
|
||||
const fmt = (d: string) => {
|
||||
try {
|
||||
const dt = new Date(d)
|
||||
return dt.toLocaleDateString('da-DK', { weekday: 'short', day: '2-digit', month: 'short', year: 'numeric' })
|
||||
} catch (e) {
|
||||
return d
|
||||
}
|
||||
}
|
||||
return (
|
||||
<ul className="space-y-2">
|
||||
{sorted.map(b => (
|
||||
<li key={b.id} className="flex justify-between items-start gap-4 border p-3 rounded">
|
||||
<div>
|
||||
<div className="font-medium">{fmt(b.block_date)}</div>
|
||||
{b.note && <div className="text-sm text-gray-600">{b.note}</div>}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={() => onDelete(b.id)} className="text-red-600">Slet</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default function AvailabilityAdmin() {
|
||||
return (
|
||||
<ToastProvider>
|
||||
<AvailabilityAdminContent />
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function AvailabilityAdminContent() {
|
||||
const { addToast } = useToast()
|
||||
const [weekly, setWeekly] = useState<Weekly[]>([])
|
||||
const [blocked, setBlocked] = useState<Blocked[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
// form state
|
||||
const [weekday, setWeekday] = useState(1)
|
||||
const [startTime, setStartTime] = useState('09:00')
|
||||
const [endTime, setEndTime] = useState('17:00')
|
||||
const [note, setNote] = useState('')
|
||||
const [editingId, setEditingId] = useState<string | null>(null)
|
||||
|
||||
const [blockDate, setBlockDate] = useState('')
|
||||
const [blockNote, setBlockNote] = useState('')
|
||||
const [multiSelectMode, setMultiSelectMode] = useState(false)
|
||||
const [selectedDates, setSelectedDates] = useState<Date[] | undefined>(undefined)
|
||||
const [showOnlyBlocked, setShowOnlyBlocked] = useState(false)
|
||||
const [lastClickedDate, setLastClickedDate] = useState<Date | null>(null)
|
||||
const [pendingDates, setPendingDates] = useState<Date[]>([]) // for Ctrl/Cmd+click accumulation
|
||||
|
||||
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()
|
||||
e.currentTarget && (e.currentTarget as HTMLFormElement).querySelectorAll('button')
|
||||
setSaving(true)
|
||||
try {
|
||||
if (editingId) {
|
||||
// send PUT to update
|
||||
await fetch('/api/availability/weekly', { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ id: editingId, weekday, start_time: startTime, end_time: endTime, note }) })
|
||||
addToast('Skema opdateret', 'success')
|
||||
setEditingId(null)
|
||||
} else {
|
||||
await fetch('/api/availability/weekly', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ weekday, start_time: startTime, end_time: endTime, note }) })
|
||||
addToast('Skema tilføjet', 'success')
|
||||
}
|
||||
setNote('')
|
||||
fetchAll()
|
||||
} catch (err) {
|
||||
addToast('Kunne ikke gemme skema', 'error')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteWeekly(id: string) {
|
||||
if (!confirm('Slet dette skema?')) return
|
||||
setSaving(true)
|
||||
try {
|
||||
await fetch('/api/availability/weekly', { method: 'DELETE', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ id }) })
|
||||
addToast('Skema slettet', 'success')
|
||||
fetchAll()
|
||||
} catch (err) {
|
||||
addToast('Kunne ikke slette skema', 'error')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
function startEdit(w: Weekly) {
|
||||
setEditingId(w.id)
|
||||
setWeekday(w.weekday)
|
||||
setStartTime(w.start_time)
|
||||
setEndTime(w.end_time)
|
||||
setNote(w.note || '')
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
async function addBlocked(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
if (!blockDate) {
|
||||
addToast('Vælg en dato', 'warning')
|
||||
return
|
||||
}
|
||||
setSaving(true)
|
||||
try {
|
||||
await fetch('/api/availability/blocked', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ block_date: blockDate, note: blockNote }) })
|
||||
setBlockDate('')
|
||||
setBlockNote('')
|
||||
addToast('Dato blokeret', 'success')
|
||||
fetchAll()
|
||||
} catch (err) {
|
||||
addToast('Kunne ikke blokere dato', 'error')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle a single date's blocked state when clicked in the calendar
|
||||
async function toggleBlockedDate(d: Date) {
|
||||
const iso = isoFromDate(d)
|
||||
|
||||
const found = blocked.find(b => b.block_date === iso)
|
||||
const weeklyForDay = weekly.find(w => w.weekday === d.getDay())
|
||||
// If this date falls on a working day, confirm user intention to block full day
|
||||
if (!found && weeklyForDay) {
|
||||
const proceed = confirm(`Der er allerede et arbejdsskema for denne ugedag (${['Søndag','Mandag','Tirsdag','Onsdag','Torsdag','Fredag','Lørdag'][weeklyForDay.weekday]} ${weeklyForDay.start_time}–${weeklyForDay.end_time}).\n\nVil du blokere hele dagen alligevel?`)
|
||||
if (!proceed) return
|
||||
}
|
||||
if (found) {
|
||||
// optimistic UI: remove locally first
|
||||
setBlocked(prev => prev.filter(b => b.block_date !== iso))
|
||||
try {
|
||||
await fetch('/api/availability/blocked', { method: 'DELETE', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ id: found.id }) })
|
||||
addToast('Blokering fjernet', 'success')
|
||||
} catch (err) {
|
||||
// revert on failure
|
||||
fetchAll()
|
||||
addToast('Kunne ikke fjerne blokering — prøv igen', 'error')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// optimistic UI: add temporary entry so calendar turns red immediately
|
||||
setBlocked(prev => [...prev, { id: `tmp-${iso}`, block_date: iso }])
|
||||
try {
|
||||
await fetch('/api/availability/blocked', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ block_date: iso, note: '' }) })
|
||||
addToast('Dato blokeret', 'success')
|
||||
// refresh to pick up real id
|
||||
fetchAll()
|
||||
return
|
||||
} catch (err) {
|
||||
// revert on failure
|
||||
setBlocked(prev => prev.filter(b => b.block_date !== iso))
|
||||
addToast('Kunne ikke oprette blokering — prøv igen', 'error')
|
||||
return
|
||||
}
|
||||
}
|
||||
fetchAll()
|
||||
}
|
||||
|
||||
// Bulk delete of selected blocked dates (multi-select mode)
|
||||
async function deleteSelectedBlocked() {
|
||||
if (!selectedDates || selectedDates.length === 0) return
|
||||
if (!confirm(`Slet ${selectedDates.length} valgte blokering(er)?`)) return
|
||||
setSaving(true)
|
||||
const isos = selectedDates.map(d => {
|
||||
const y = d.getFullYear()
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const dd = String(d.getDate()).padStart(2, '0')
|
||||
return `${y}-${m}-${dd}`
|
||||
})
|
||||
// map isos to ids
|
||||
const ids = blocked.filter(b => isos.includes(b.block_date)).map(b => b.id)
|
||||
if (ids.length === 0) {
|
||||
addToast('Ingen matcher blandt eksisterende blokeringer', 'warning')
|
||||
setSaving(false)
|
||||
return
|
||||
}
|
||||
// optimistic
|
||||
setBlocked(prev => prev.filter(b => !isos.includes(b.block_date)))
|
||||
try {
|
||||
await Promise.all(ids.map(id => fetch('/api/availability/blocked', { method: 'DELETE', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ id }) })))
|
||||
setSelectedDates(undefined)
|
||||
setMultiSelectMode(false)
|
||||
addToast(`${ids.length} blokering(er) slettet`, 'success')
|
||||
} catch (err) {
|
||||
addToast('Noget gik galt ved sletning — opdaterer listen', 'error')
|
||||
fetchAll()
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteBlocked(id: string) {
|
||||
if (!confirm('Slet denne blokering?')) return
|
||||
setSaving(true)
|
||||
try {
|
||||
await fetch('/api/availability/blocked', { method: 'DELETE', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ id }) })
|
||||
addToast('Blokering slettet', 'success')
|
||||
fetchAll()
|
||||
} catch (err) {
|
||||
addToast('Kunne ikke slette blokering', 'error')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
function sameDay(a: Date, b: Date) {
|
||||
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
|
||||
}
|
||||
|
||||
function isoFromDate(d: Date) {
|
||||
const y = d.getFullYear()
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const dd = String(d.getDate()).padStart(2, '0')
|
||||
return `${y}-${m}-${dd}`
|
||||
}
|
||||
|
||||
// Generate inclusive date range between a and b
|
||||
function datesBetween(a: Date, b: Date) {
|
||||
const start = new Date(a.getFullYear(), a.getMonth(), a.getDate())
|
||||
const end = new Date(b.getFullYear(), b.getMonth(), b.getDate())
|
||||
const out: Date[] = []
|
||||
const step = start <= end ? 1 : -1
|
||||
let cur = new Date(start)
|
||||
while (true) {
|
||||
out.push(new Date(cur))
|
||||
if (cur.getFullYear() === end.getFullYear() && cur.getMonth() === end.getMonth() && cur.getDate() === end.getDate()) break
|
||||
cur.setDate(cur.getDate() + step)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-primary-50 py-8">
|
||||
<div className="container mx-auto px-4 max-w-6xl">
|
||||
<h1 className="text-3xl font-bold mb-6">Tilgængelighed & blokeringer</h1>
|
||||
|
||||
{loading ? <p>Indlæser...</p> : (
|
||||
<div className="space-y-6">
|
||||
{/* Main calendar section */}
|
||||
<section className="bg-white rounded-lg p-6">
|
||||
<h2 className="text-xl font-semibold mb-2">Kalender — klik for at skifte tilgængelighed</h2>
|
||||
<p className="text-sm text-gray-600 mb-4">Klik på en dato for at blokere/åbne den.</p>
|
||||
|
||||
<div className="flex flex-wrap gap-3 mb-4">
|
||||
<label className="inline-flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" checked={showOnlyBlocked} onChange={e => setShowOnlyBlocked(e.target.checked)} className="rounded" />
|
||||
<span className="text-sm">Vis kun blokerede</span>
|
||||
</label>
|
||||
<label className="inline-flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" checked={multiSelectMode} onChange={e => {
|
||||
const on = e.target.checked
|
||||
setMultiSelectMode(on)
|
||||
if (on) {
|
||||
setSelectedDates(blocked.map(b => new Date(b.block_date)))
|
||||
} else {
|
||||
setSelectedDates(undefined)
|
||||
}
|
||||
}} className="rounded" />
|
||||
<span className="text-sm">Vælg flere til sletning</span>
|
||||
</label>
|
||||
{multiSelectMode && (
|
||||
<button onClick={deleteSelectedBlocked} className="ml-auto px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 disabled:opacity-50 transition-colors" disabled={!selectedDates || selectedDates.length===0 || saving}>
|
||||
Slet valgte ({selectedDates ? selectedDates.length : 0})
|
||||
</button>
|
||||
)}
|
||||
{!multiSelectMode && pendingDates.length > 0 && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
setSaving(true)
|
||||
const isos = pendingDates.map(pd => isoFromDate(pd))
|
||||
setBlocked(prev => [...prev, ...isos.map(i => ({ id: `tmp-${i}`, block_date: i }))])
|
||||
try {
|
||||
await Promise.all(pendingDates.map(pd => fetch('/api/availability/blocked', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ block_date: isoFromDate(pd), note: '' }) })))
|
||||
addToast(`${pendingDates.length} dage blokeret`, 'success')
|
||||
setPendingDates([])
|
||||
} catch (err) {
|
||||
addToast('Kunne ikke oprette alle blokeringer', 'error')
|
||||
} finally {
|
||||
fetchAll()
|
||||
setSaving(false)
|
||||
}
|
||||
}}
|
||||
className="ml-auto px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50 transition-colors"
|
||||
disabled={saving}
|
||||
>
|
||||
Bloker valgte ({pendingDates.length})
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">{multiSelectMode ? (
|
||||
<DayPicker
|
||||
className="admin-calendar"
|
||||
mode="multiple"
|
||||
selected={selectedDates}
|
||||
onSelect={(d: any) => setSelectedDates(d as Date[] | undefined)}
|
||||
locale={da}
|
||||
modifiers={{
|
||||
blocked: blocked.map(b => new Date(b.block_date)),
|
||||
nonWorking: (day: Date) => !weekly.some(w => w.weekday === day.getDay())
|
||||
}}
|
||||
modifiersClassNames={{
|
||||
blocked: 'rdp-day_blocked',
|
||||
nonWorking: 'rdp-day_nonWorking',
|
||||
}}
|
||||
disabled={showOnlyBlocked ? ((day: Date) => !blocked.some(b => sameDay(new Date(b.block_date), day))) : undefined}
|
||||
/>
|
||||
) : (
|
||||
<DayPicker
|
||||
className="admin-calendar"
|
||||
mode="single"
|
||||
onDayClick={async (d: Date, modifiers: any, e: any) => {
|
||||
if (!d) return
|
||||
|
||||
// Ctrl/Cmd+click: accumulate dates for batch blocking
|
||||
if (e && (e.ctrlKey || e.metaKey)) {
|
||||
const iso = isoFromDate(d)
|
||||
const alreadyPending = pendingDates.some(pd => isoFromDate(pd) === iso)
|
||||
if (alreadyPending) {
|
||||
setPendingDates(prev => prev.filter(pd => isoFromDate(pd) !== iso))
|
||||
} else {
|
||||
setPendingDates(prev => [...prev, d])
|
||||
}
|
||||
setLastClickedDate(d)
|
||||
return
|
||||
}
|
||||
|
||||
// Shift+click range selection
|
||||
if (e && e.shiftKey && lastClickedDate) {
|
||||
setSaving(true)
|
||||
const range = datesBetween(lastClickedDate, d)
|
||||
// confirm if any date in range falls on a working weekly schedule
|
||||
const hasWorking = range.some(rd => weekly.some(w => w.weekday === rd.getDay()))
|
||||
if (hasWorking) {
|
||||
const proceed = confirm('Nogle dage i det valgte interval har et arbejdsskema. Vil du blokere alle dagene alligevel?')
|
||||
if (!proceed) {
|
||||
setLastClickedDate(d)
|
||||
setSaving(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
// optimistic add
|
||||
const isos = range.map(r => isoFromDate(r))
|
||||
setBlocked(prev => [...prev, ...isos.map(i => ({ id: `tmp-${i}`, block_date: i }))])
|
||||
try {
|
||||
await Promise.all(range.map(r => fetch('/api/availability/blocked', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ block_date: isoFromDate(r), note: '' }) })))
|
||||
addToast(`${range.length} dage blokeret`, 'success')
|
||||
} catch (err) {
|
||||
addToast('Kunne ikke oprette alle blokeringer — opdaterer listen', 'error')
|
||||
} finally {
|
||||
fetchAll()
|
||||
setSaving(false)
|
||||
}
|
||||
setLastClickedDate(d)
|
||||
return
|
||||
}
|
||||
|
||||
// normal single toggle
|
||||
await toggleBlockedDate(d)
|
||||
setLastClickedDate(d)
|
||||
}}
|
||||
locale={da}
|
||||
modifiers={{
|
||||
blocked: blocked.map(b => new Date(b.block_date)),
|
||||
nonWorking: (day: Date) => !weekly.some(w => w.weekday === day.getDay()),
|
||||
pending: pendingDates,
|
||||
}}
|
||||
modifiersClassNames={{
|
||||
blocked: 'rdp-day_blocked',
|
||||
pending: 'rdp-day_pending',
|
||||
nonWorking: 'rdp-day_nonWorking',
|
||||
}}
|
||||
disabled={showOnlyBlocked ? ((day: Date) => !blocked.some(b => sameDay(new Date(b.block_date), day))) : undefined}
|
||||
/>
|
||||
)}
|
||||
<div className="mt-3 text-sm flex items-center gap-4">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<span className="w-3 h-3 bg-green-400 rounded-sm inline-block" />
|
||||
<span>Tilgængelig</span>
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<span className="w-3 h-3 bg-red-500 rounded-sm inline-block" />
|
||||
<span>Blokeret</span>
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<span className="w-3 h-3 bg-amber-500 rounded-sm inline-block" />
|
||||
<span>Valgt</span>
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<span className="w-3 h-3 bg-blue-400 rounded-sm inline-block" />
|
||||
<span>Venter (Ctrl+klik)</span>
|
||||
</span>
|
||||
</div>
|
||||
{!multiSelectMode && (
|
||||
<div className="mt-2 text-xs text-gray-600">
|
||||
<p>Tips: Hold Shift og klik for at vælge interval. Hold Ctrl/⌘ og klik for at vælge flere enkeltdage.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</section>
|
||||
<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 disabled:opacity-50" disabled={saving}>
|
||||
{saving ? 'Gemmer...' : (editingId ? 'Opdater' : '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>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => startEdit(w)} className="text-primary-600">Rediger</button>
|
||||
<button onClick={() => deleteWeekly(w.id)} className="text-red-600">Slet</button>
|
||||
</div>
|
||||
</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 disabled:opacity-50" disabled={saving}>
|
||||
{saving ? 'Gemmer...' : 'Bloker dato'}
|
||||
</button>
|
||||
</form>
|
||||
<h3 className="font-semibold mb-2">Eksisterende blokeringer</h3>
|
||||
<BlockedList blocked={blocked} onDelete={deleteBlocked} />
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user