Files
TKB_plan/web/src/ProposalModal.tsx
Docker Config Backup b4158d687f feat: TKB shift scheduler — personnel shift planning web app
Full rewrite of METRO HMG for TKB tunnel department:
- People-based grid (18 TKB + 5 IT), year-long calendar
- Color-coded shift values (4/6/8/12/A/B/D/N/U/O)
- Drag-and-drop cells, multi-cell selection (click/ctrl/shift/drag)
- Right-click context menu with color palette
- Tunnel closure + Metro + D8 info rows (toggleable)
- Czech holidays highlighted with names
- PDF export (2-page A4 landscape, DejaVu font for Czech chars)
- Improvement proposals system
- Sticky headers (vertical + horizontal scroll)
- Cell value filter toggles in legend

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 09:48:38 +02:00

112 lines
3.8 KiB
TypeScript

import { useState, useEffect, useRef } from 'react'
interface ProposalModalProps {
onClose: () => void
}
export function ProposalModal({ onClose }: ProposalModalProps) {
const [text, setText] = useState('')
const [author, setAuthor] = useState('')
const [proposals, setProposals] = useState('')
const [submitting, setSubmitting] = useState(false)
const [submitted, setSubmitted] = useState(false)
const overlayRef = useRef<HTMLDivElement>(null)
const textareaRef = useRef<HTMLTextAreaElement>(null)
useEffect(() => {
fetch('/api/proposals')
.then(r => r.json())
.then(d => setProposals(d.content || ''))
.catch(() => {})
}, [submitted])
useEffect(() => {
textareaRef.current?.focus()
}, [])
useEffect(() => {
const handleKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
}
document.addEventListener('keydown', handleKey)
return () => document.removeEventListener('keydown', handleKey)
}, [onClose])
const handleSubmit = async () => {
if (!text.trim()) return
setSubmitting(true)
try {
const res = await fetch('/api/proposals', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: text.trim(), author: author.trim() || undefined }),
})
if (res.ok) {
setText('')
setSubmitted(s => !s)
}
} catch {
} finally {
setSubmitting(false)
}
}
return (
<div
ref={overlayRef}
className="fixed inset-0 z-[300] bg-black/50 flex items-center justify-center"
onClick={(e) => { if (e.target === overlayRef.current) onClose() }}
>
<div className="bg-slate-800 border border-slate-600 rounded-xl shadow-2xl w-full max-w-xl max-h-[80vh] flex flex-col">
<div className="flex items-center justify-between px-5 py-3 border-b border-slate-700">
<h2 className="text-sm font-semibold text-slate-200">Navrhy na vylepseni</h2>
<button
onClick={onClose}
className="text-slate-500 hover:text-slate-300 cursor-pointer text-lg leading-none"
>
&times;
</button>
</div>
<div className="px-5 py-3 border-b border-slate-700">
<input
type="text"
value={author}
onChange={e => setAuthor(e.target.value)}
placeholder="Vase jmeno (nepovinne)"
className="w-full bg-slate-900 border border-slate-600 rounded px-3 py-1.5 text-xs text-slate-200
placeholder-slate-500 outline-none focus:border-blue-500 transition-colors mb-2"
/>
<textarea
ref={textareaRef}
value={text}
onChange={e => setText(e.target.value)}
placeholder="Popiste svuj navrh nebo problem..."
rows={3}
className="w-full bg-slate-900 border border-slate-600 rounded px-3 py-2 text-xs text-slate-200
placeholder-slate-500 outline-none focus:border-blue-500 transition-colors resize-none"
/>
<div className="flex justify-end mt-2">
<button
onClick={handleSubmit}
disabled={!text.trim() || submitting}
className="px-4 py-1.5 rounded text-xs bg-blue-600 text-white hover:bg-blue-500
disabled:opacity-30 disabled:cursor-not-allowed cursor-pointer transition-colors"
>
{submitting ? 'Odesilam...' : 'Odeslat'}
</button>
</div>
</div>
<div className="flex-1 overflow-y-auto px-5 py-3">
{proposals ? (
<pre className="text-xs text-slate-300 whitespace-pre-wrap font-sans">{proposals}</pre>
) : (
<p className="text-xs text-slate-500">Zatim zadne navrhy.</p>
)}
</div>
</div>
</div>
)
}