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:
83
web/src/Login.tsx
Normal file
83
web/src/Login.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
const VALID_USER = 'tkb'
|
||||
const VALID_PASS = 'sluzby'
|
||||
const AUTH_KEY = 'tkb_auth'
|
||||
|
||||
export function isAuthenticated(): boolean {
|
||||
return sessionStorage.getItem(AUTH_KEY) === 'true'
|
||||
}
|
||||
|
||||
export function setAuthenticated(): void {
|
||||
sessionStorage.setItem(AUTH_KEY, 'true')
|
||||
}
|
||||
|
||||
interface LoginProps {
|
||||
onLogin: () => void
|
||||
}
|
||||
|
||||
export function Login({ onLogin }: LoginProps) {
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState(false)
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (username === VALID_USER && password === VALID_PASS) {
|
||||
setAuthenticated()
|
||||
onLogin()
|
||||
} else {
|
||||
setError(true)
|
||||
setTimeout(() => setError(false), 3000)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-900 flex items-center justify-center">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="bg-slate-800 border border-slate-700 rounded-lg p-8 w-80 shadow-xl"
|
||||
>
|
||||
<h1 className="text-lg font-semibold text-slate-100 mb-1 text-center">
|
||||
TKB Plan sluzeb
|
||||
</h1>
|
||||
<p className="text-sm text-slate-400 mb-6 text-center">
|
||||
Planovani smen a pohotovosti
|
||||
</p>
|
||||
|
||||
<label className="block text-xs text-slate-400 mb-1">Uzivatel</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
className="w-full mb-4 px-3 py-2 rounded bg-slate-900 border border-slate-600 text-slate-100
|
||||
text-sm focus:outline-none focus:border-blue-500"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<label className="block text-xs text-slate-400 mb-1">Heslo</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
className="w-full mb-6 px-3 py-2 rounded bg-slate-900 border border-slate-600 text-slate-100
|
||||
text-sm focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 text-xs text-red-400 text-center">
|
||||
Nespravne prihlasovaci udaje
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full py-2 rounded bg-blue-600 hover:bg-blue-500 text-white text-sm
|
||||
font-medium transition-colors cursor-pointer"
|
||||
>
|
||||
Prihlasit
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user