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>
This commit is contained in:
111
web/src/ProposalModal.tsx
Normal file
111
web/src/ProposalModal.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
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"
|
||||
>
|
||||
×
|
||||
</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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user