feat(docs, ui): restructure frontend documentation & introduce Vision directories for modularity
- Moved existing FIGMA-related files into `Vison_01` and `Vision_02` folders to better support versioning and collaboration. - Added PostCSS configuration for extending plugins in Tailwind CSS. - Introduced new style guidelines, theme configurations, and modular imports for `Vision_02`. - Documented detailed ÖTO tournament structures and parameters for CSN/CDN inclusions. - Enhanced routing and UI files for future scalability, including new `theme.tsx` and `routes.tsx`. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"name": "@figma/my-make-file",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "11.14.0",
|
||||
"@emotion/styled": "11.14.1",
|
||||
"@mui/icons-material": "7.3.5",
|
||||
"@mui/material": "7.3.5",
|
||||
"@mui/x-date-pickers": "^8.27.2",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@radix-ui/react-accordion": "1.2.3",
|
||||
"@radix-ui/react-alert-dialog": "1.1.6",
|
||||
"@radix-ui/react-aspect-ratio": "1.1.2",
|
||||
"@radix-ui/react-avatar": "1.1.3",
|
||||
"@radix-ui/react-checkbox": "1.1.4",
|
||||
"@radix-ui/react-collapsible": "1.1.3",
|
||||
"@radix-ui/react-context-menu": "2.2.6",
|
||||
"@radix-ui/react-dialog": "1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.6",
|
||||
"@radix-ui/react-hover-card": "1.1.6",
|
||||
"@radix-ui/react-label": "2.1.2",
|
||||
"@radix-ui/react-menubar": "1.1.6",
|
||||
"@radix-ui/react-navigation-menu": "1.2.5",
|
||||
"@radix-ui/react-popover": "1.1.6",
|
||||
"@radix-ui/react-progress": "1.1.2",
|
||||
"@radix-ui/react-radio-group": "1.2.3",
|
||||
"@radix-ui/react-scroll-area": "1.2.3",
|
||||
"@radix-ui/react-select": "2.1.6",
|
||||
"@radix-ui/react-separator": "1.1.2",
|
||||
"@radix-ui/react-slider": "1.2.3",
|
||||
"@radix-ui/react-slot": "1.1.2",
|
||||
"@radix-ui/react-switch": "1.1.3",
|
||||
"@radix-ui/react-tabs": "1.1.3",
|
||||
"@radix-ui/react-toggle": "1.1.2",
|
||||
"@radix-ui/react-toggle-group": "1.1.2",
|
||||
"@radix-ui/react-tooltip": "1.1.8",
|
||||
"canvas-confetti": "1.9.4",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
"date-fns": "3.6.0",
|
||||
"embla-carousel-react": "8.6.0",
|
||||
"input-otp": "1.4.2",
|
||||
"lucide-react": "0.487.0",
|
||||
"motion": "12.23.24",
|
||||
"next-themes": "0.4.6",
|
||||
"react-day-picker": "8.10.1",
|
||||
"react-dnd": "16.0.1",
|
||||
"react-dnd-html5-backend": "16.0.1",
|
||||
"react-hook-form": "7.55.0",
|
||||
"react-popper": "2.3.0",
|
||||
"react-resizable-panels": "2.1.7",
|
||||
"react-responsive-masonry": "2.7.1",
|
||||
"react-router": "7.13.0",
|
||||
"react-slick": "0.31.0",
|
||||
"recharts": "2.15.2",
|
||||
"sonner": "2.0.3",
|
||||
"tailwind-merge": "3.2.0",
|
||||
"tw-animate-css": "1.3.8",
|
||||
"vaul": "1.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "4.1.12",
|
||||
"@vitejs/plugin-react": "4.7.0",
|
||||
"tailwindcss": "4.1.12",
|
||||
"vite": "6.3.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"vite": "6.3.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,16 @@
|
||||
import {ThemeProvider} from '@mui/material/styles';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import {RouterProvider} from 'react-router';
|
||||
import {theme} from './theme';
|
||||
import {router} from './routes';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline/>
|
||||
<RouterProvider router={router}/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,445 @@
|
||||
import {useState} from 'react';
|
||||
import {useNavigate} from 'react-router';
|
||||
import Box from '@mui/material/Box';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Button from '@mui/material/Button';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import CalendarTodayIcon from '@mui/icons-material/CalendarToday';
|
||||
import LocationOnIcon from '@mui/icons-material/LocationOn';
|
||||
import EmojiEventsIcon from '@mui/icons-material/EmojiEvents';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import PlayCircleIcon from '@mui/icons-material/PlayCircle';
|
||||
|
||||
// Mock-Daten für Veranstaltungen
|
||||
export const veranstaltungenData = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Union Reit- und Fahrverein Neumarkt Frühjahrsturnier 2026',
|
||||
ort: 'Reitanlage Stroblmair, Neumarkt/M., OÖ',
|
||||
datum: '25.-26. April 2026',
|
||||
datumVon: new Date('2026-04-25'),
|
||||
datumBis: new Date('2026-04-26'),
|
||||
status: 'vorbereitung' as const,
|
||||
turniere: [
|
||||
{
|
||||
nr: '26128',
|
||||
name: 'CSN-C NEU CSNP-C NEU',
|
||||
datum: '25.04.2026',
|
||||
kategorie: 'C',
|
||||
disziplin: 'Springen',
|
||||
bewerbeAnzahl: 14,
|
||||
znsStatus: 'geladen'
|
||||
},
|
||||
{
|
||||
nr: '26129',
|
||||
name: 'CDN-C NEU CDNP-C NEU',
|
||||
datum: '26.04.2026',
|
||||
kategorie: 'C',
|
||||
disziplin: 'Dressur',
|
||||
bewerbeAnzahl: 12,
|
||||
znsStatus: 'geladen'
|
||||
}
|
||||
],
|
||||
nennungen: 87,
|
||||
letzteAktivitaet: '22.03.2026 14:30'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'AWÖ-Cup Stadl-Paura 2025',
|
||||
ort: 'Bundesgestüt Piber, Stadl-Paura',
|
||||
datum: '15.-17. Mai 2025',
|
||||
datumVon: new Date('2025-05-15'),
|
||||
datumBis: new Date('2025-05-17'),
|
||||
status: 'abgeschlossen' as const,
|
||||
turniere: [
|
||||
{
|
||||
nr: '25001',
|
||||
name: 'CSN-A',
|
||||
datum: '15.05.2025',
|
||||
kategorie: 'A',
|
||||
disziplin: 'Springen',
|
||||
bewerbeAnzahl: 18,
|
||||
znsStatus: 'geladen'
|
||||
},
|
||||
{
|
||||
nr: '25002',
|
||||
name: 'CDN-A',
|
||||
datum: '16.05.2025',
|
||||
kategorie: 'A',
|
||||
disziplin: 'Dressur',
|
||||
bewerbeAnzahl: 15,
|
||||
znsStatus: 'geladen'
|
||||
}
|
||||
],
|
||||
nennungen: 142,
|
||||
letzteAktivitaet: '17.05.2025 18:45'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Linzer Pferdetage 2026',
|
||||
ort: 'Reitsportzentrum Linz-Ebelsberg',
|
||||
datum: '12.-14. Juni 2026',
|
||||
datumVon: new Date('2026-06-12'),
|
||||
datumBis: new Date('2026-06-14'),
|
||||
status: 'vorbereitung' as const,
|
||||
turniere: [
|
||||
{
|
||||
nr: '26201',
|
||||
name: 'CSN-B',
|
||||
datum: '12.06.2026',
|
||||
kategorie: 'B',
|
||||
disziplin: 'Springen',
|
||||
bewerbeAnzahl: 16,
|
||||
znsStatus: 'ausstehend'
|
||||
},
|
||||
{
|
||||
nr: '26202',
|
||||
name: 'CDN-B',
|
||||
datum: '13.06.2026',
|
||||
kategorie: 'B',
|
||||
disziplin: 'Dressur',
|
||||
bewerbeAnzahl: 14,
|
||||
znsStatus: 'ausstehend'
|
||||
}
|
||||
],
|
||||
nennungen: 23,
|
||||
letzteAktivitaet: '20.03.2026 09:15'
|
||||
}
|
||||
];
|
||||
|
||||
export function AdminVerwaltung() {
|
||||
const navigate = useNavigate();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState<'alle' | 'vorbereitung' | 'live' | 'abgeschlossen'>('alle');
|
||||
|
||||
// Statistiken berechnen
|
||||
const stats = {
|
||||
gesamt: veranstaltungenData.length,
|
||||
vorbereitung: veranstaltungenData.filter(v => v.status === 'vorbereitung').length,
|
||||
live: veranstaltungenData.filter(v => v.status === 'live').length,
|
||||
abgeschlossen: veranstaltungenData.filter(v => v.status === 'abgeschlossen').length,
|
||||
};
|
||||
|
||||
// Filter
|
||||
const filteredVeranstaltungen = veranstaltungenData.filter(v => {
|
||||
const matchesSearch = v.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
v.ort.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
v.turniere.some(t => t.nr.includes(searchTerm));
|
||||
const matchesStatus = statusFilter === 'alle' || v.status === statusFilter;
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'vorbereitung':
|
||||
return 'info';
|
||||
case 'live':
|
||||
return 'success';
|
||||
case 'abgeschlossen':
|
||||
return 'default';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'vorbereitung':
|
||||
return <CalendarTodayIcon sx={{fontSize: 14}}/>;
|
||||
case 'live':
|
||||
return <PlayCircleIcon sx={{fontSize: 14}}/>;
|
||||
case 'abgeschlossen':
|
||||
return <CheckCircleIcon sx={{fontSize: 14}}/>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
switch (status) {
|
||||
case 'vorbereitung':
|
||||
return 'Vorbereitung';
|
||||
case 'live':
|
||||
return 'Live';
|
||||
case 'abgeschlossen':
|
||||
return 'Abgeschlossen';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
};
|
||||
|
||||
const handleVeranstaltungOeffnen = (id: number) => {
|
||||
navigate(`/veranstaltung/${id}`);
|
||||
};
|
||||
|
||||
const handleTurnierOeffnen = (veranstaltungId: number, turnierNr: string) => {
|
||||
navigate(`/veranstaltung/${veranstaltungId}/turnier/${turnierNr}`);
|
||||
};
|
||||
|
||||
const handleNeueVeranstaltung = () => {
|
||||
navigate('/veranstaltung/neu');
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{height: '100vh', bgcolor: 'background.default', display: 'flex', flexDirection: 'column'}}>
|
||||
{/* Header */}
|
||||
<Box sx={{bgcolor: 'primary.main', color: 'white', py: 2, px: 3}}>
|
||||
<Typography variant="h5" sx={{fontSize: '16px', fontWeight: 600}}>
|
||||
Admin - Verwaltung
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Content */}
|
||||
<Box sx={{flex: 1, overflow: 'auto', p: 3}}>
|
||||
{/* Statistik-Cards - dezent und auf gesamte Breite */}
|
||||
<Grid container spacing={1.5} sx={{mb: 3}}>
|
||||
<Grid size={{xs: 12, sm: 3}}>
|
||||
<Paper sx={{p: 1.5, bgcolor: 'success.50', borderLeft: 3, borderColor: 'success.main'}}>
|
||||
<Typography variant="body2"
|
||||
sx={{fontSize: '9px', color: 'text.secondary', textTransform: 'uppercase', mb: 0.5}}>
|
||||
Live / Aktiv
|
||||
</Typography>
|
||||
<Typography variant="h5" sx={{fontSize: '20px', fontWeight: 600, color: 'success.main'}}>
|
||||
{stats.live}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid size={{xs: 12, sm: 3}}>
|
||||
<Paper sx={{p: 1.5, bgcolor: 'info.50', borderLeft: 3, borderColor: 'info.main'}}>
|
||||
<Typography variant="body2"
|
||||
sx={{fontSize: '9px', color: 'text.secondary', textTransform: 'uppercase', mb: 0.5}}>
|
||||
In Vorbereitung
|
||||
</Typography>
|
||||
<Typography variant="h5" sx={{fontSize: '20px', fontWeight: 600, color: 'info.main'}}>
|
||||
{stats.vorbereitung}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid size={{xs: 12, sm: 3}}>
|
||||
<Paper sx={{p: 1.5, bgcolor: 'grey.50', borderLeft: 3, borderColor: 'primary.main'}}>
|
||||
<Typography variant="body2"
|
||||
sx={{fontSize: '9px', color: 'text.secondary', textTransform: 'uppercase', mb: 0.5}}>
|
||||
Gesamt
|
||||
</Typography>
|
||||
<Typography variant="h5" sx={{fontSize: '20px', fontWeight: 600, color: 'primary.main'}}>
|
||||
{stats.gesamt}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid size={{xs: 12, sm: 3}}>
|
||||
<Paper sx={{p: 1.5, bgcolor: 'grey.50', borderLeft: 3, borderColor: 'grey.500'}}>
|
||||
<Typography variant="body2"
|
||||
sx={{fontSize: '9px', color: 'text.secondary', textTransform: 'uppercase', mb: 0.5}}>
|
||||
Archiv
|
||||
</Typography>
|
||||
<Typography variant="h5" sx={{fontSize: '20px', fontWeight: 600, color: 'grey.600'}}>
|
||||
{stats.abgeschlossen}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Toolbar - neue Reihenfolge */}
|
||||
<Box sx={{display: 'flex', gap: 2, mb: 3, alignItems: 'center'}}>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon/>}
|
||||
onClick={handleNeueVeranstaltung}
|
||||
sx={{fontSize: '11px'}}
|
||||
>
|
||||
Neue Veranstaltung
|
||||
</Button>
|
||||
|
||||
<TextField
|
||||
size="small"
|
||||
placeholder="Suche nach Name, Ort oder Turnier-Nr..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
sx={{
|
||||
flex: 1,
|
||||
maxWidth: 400,
|
||||
'& .MuiInputBase-input': {fontSize: '11px'}
|
||||
}}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon sx={{fontSize: 18}}/>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box sx={{display: 'flex', gap: 1}}>
|
||||
<Chip
|
||||
label="Alle"
|
||||
onClick={() => setStatusFilter('alle')}
|
||||
color={statusFilter === 'alle' ? 'primary' : 'default'}
|
||||
size="small"
|
||||
sx={{fontSize: '10px'}}
|
||||
/>
|
||||
<Chip
|
||||
label="Vorbereitung"
|
||||
onClick={() => setStatusFilter('vorbereitung')}
|
||||
color={statusFilter === 'vorbereitung' ? 'primary' : 'default'}
|
||||
size="small"
|
||||
sx={{fontSize: '10px'}}
|
||||
/>
|
||||
<Chip
|
||||
label="Live"
|
||||
onClick={() => setStatusFilter('live')}
|
||||
color={statusFilter === 'live' ? 'primary' : 'default'}
|
||||
size="small"
|
||||
sx={{fontSize: '10px'}}
|
||||
/>
|
||||
<Chip
|
||||
label="Abgeschlossen"
|
||||
onClick={() => setStatusFilter('abgeschlossen')}
|
||||
color={statusFilter === 'abgeschlossen' ? 'primary' : 'default'}
|
||||
size="small"
|
||||
sx={{fontSize: '10px'}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Veranstaltungs-Liste - volle Breite */}
|
||||
<Box sx={{display: 'flex', flexDirection: 'column', gap: 2}}>
|
||||
{filteredVeranstaltungen.map((v) => (
|
||||
<Card key={v.id} sx={{position: 'relative'}}>
|
||||
<CardContent>
|
||||
{/* Header mit Status */}
|
||||
<Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 1.5}}>
|
||||
<Typography variant="h6" sx={{fontSize: '13px', fontWeight: 600, flex: 1}}>
|
||||
{v.name}
|
||||
</Typography>
|
||||
<Chip
|
||||
icon={getStatusIcon(v.status)}
|
||||
label={getStatusLabel(v.status)}
|
||||
color={getStatusColor(v.status)}
|
||||
size="small"
|
||||
sx={{fontSize: '9px'}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Ort und Datum */}
|
||||
<Box sx={{display: 'flex', gap: 2, mb: 2}}>
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 0.5}}>
|
||||
<LocationOnIcon sx={{fontSize: 14, color: 'text.secondary'}}/>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'text.secondary'}}>
|
||||
{v.ort}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 0.5}}>
|
||||
<CalendarTodayIcon sx={{fontSize: 14, color: 'text.secondary'}}/>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'text.secondary'}}>
|
||||
{v.datum}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Turniere */}
|
||||
<Box sx={{mb: 2}}>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', fontWeight: 600, mb: 1}}>
|
||||
<EmojiEventsIcon sx={{fontSize: 12, verticalAlign: 'middle', mr: 0.5}}/>
|
||||
Turniere ({v.turniere.length}):
|
||||
</Typography>
|
||||
<Box sx={{display: 'flex', flexDirection: 'column', gap: 1}}>
|
||||
{v.turniere.map((t) => (
|
||||
<Box
|
||||
key={t.nr}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
p: 1,
|
||||
bgcolor: 'background.default',
|
||||
borderRadius: 1
|
||||
}}
|
||||
>
|
||||
<Chip
|
||||
label={t.nr}
|
||||
size="small"
|
||||
sx={{fontSize: '9px', fontWeight: 600, minWidth: 60}}
|
||||
/>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', flex: 1}}>
|
||||
{t.name} ({t.bewerbeAnzahl} Bewerbe)
|
||||
</Typography>
|
||||
<Chip
|
||||
label={t.disziplin}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{fontSize: '8px'}}
|
||||
/>
|
||||
{t.kategorie === 'B' || t.kategorie === 'A' ? (
|
||||
t.znsStatus === 'geladen' ? (
|
||||
<CheckCircleIcon sx={{fontSize: 14, color: 'success.main'}}
|
||||
titleAccess="ZNS N2-Daten geladen"/>
|
||||
) : (
|
||||
<WarningIcon sx={{fontSize: 14, color: 'warning.main'}}
|
||||
titleAccess="ZNS N2-Daten ausstehend"/>
|
||||
)
|
||||
) : null}
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => handleTurnierOeffnen(v.id, t.nr)}
|
||||
sx={{fontSize: '9px', py: 0.5, px: 1.5, minWidth: 0}}
|
||||
>
|
||||
Öffnen
|
||||
</Button>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Statistik */}
|
||||
<Box sx={{display: 'flex', gap: 2, mb: 2}}>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'text.secondary'}}>
|
||||
Nennungen: <strong>{v.nennungen}</strong>
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'text.secondary'}}>
|
||||
Letzte Aktivität: {v.letzteAktivitaet}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Actions */}
|
||||
<Box sx={{display: 'flex', gap: 1, justifyContent: 'flex-end'}}>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => handleVeranstaltungOeffnen(v.id)}
|
||||
sx={{fontSize: '10px'}}
|
||||
>
|
||||
Öffnen
|
||||
</Button>
|
||||
<IconButton size="small" sx={{fontSize: '10px', color: 'error.main'}}>
|
||||
<DeleteIcon sx={{fontSize: 16}}/>
|
||||
</IconButton>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{filteredVeranstaltungen.length === 0 && (
|
||||
<Paper sx={{p: 4, textAlign: 'center'}}>
|
||||
<Typography variant="body2" sx={{fontSize: '11px', color: 'text.secondary'}}>
|
||||
Keine Veranstaltungen gefunden
|
||||
</Typography>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import {useState, useEffect} from 'react';
|
||||
import {useNavigate} from 'react-router';
|
||||
import Box from '@mui/material/Box';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Button from '@mui/material/Button';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Visibility from '@mui/icons-material/Visibility';
|
||||
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
||||
import WifiIcon from '@mui/icons-material/Wifi';
|
||||
import WifiOffIcon from '@mui/icons-material/WifiOff';
|
||||
|
||||
export function Login() {
|
||||
const navigate = useNavigate();
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isOnline, setIsOnline] = useState(navigator.onLine);
|
||||
|
||||
// Internet-Verbindung überwachen
|
||||
useEffect(() => {
|
||||
const handleOnline = () => setIsOnline(true);
|
||||
const handleOffline = () => setIsOnline(false);
|
||||
|
||||
window.addEventListener('online', handleOnline);
|
||||
window.addEventListener('offline', handleOffline);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('online', handleOnline);
|
||||
window.removeEventListener('offline', handleOffline);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
// Simulated login delay
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
|
||||
// Hardcoded credentials für Phase 1
|
||||
if (username === 'admin' && password === 'Admin#1234') {
|
||||
// Login erfolgreich
|
||||
localStorage.setItem('isAuthenticated', 'true');
|
||||
localStorage.setItem('userRole', 'admin');
|
||||
localStorage.setItem('username', username);
|
||||
navigate('/admin');
|
||||
} else {
|
||||
setError('Ungültige Anmeldedaten. Bitte überprüfen Sie Benutzername und Passwort.');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
bgcolor: 'grey.100',
|
||||
backgroundImage: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
{/* Internet-Status Anzeige */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 16,
|
||||
right: 16,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
bgcolor: isOnline ? 'success.main' : 'error.main',
|
||||
color: 'white',
|
||||
px: 2,
|
||||
py: 1,
|
||||
borderRadius: 2,
|
||||
fontSize: '11px',
|
||||
fontWeight: 600
|
||||
}}
|
||||
>
|
||||
{isOnline ? (
|
||||
<>
|
||||
<WifiIcon sx={{fontSize: 18}}/>
|
||||
Online
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<WifiOffIcon sx={{fontSize: 18}}/>
|
||||
Offline
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Paper
|
||||
elevation={8}
|
||||
sx={{
|
||||
width: '100%',
|
||||
maxWidth: 420,
|
||||
p: 4,
|
||||
mx: 2,
|
||||
borderRadius: 3
|
||||
}}
|
||||
>
|
||||
{/* Logo & Titel */}
|
||||
<Box sx={{textAlign: 'center', mb: 4}}>
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
backgroundClip: 'text',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
mb: 1
|
||||
}}
|
||||
>
|
||||
Turnierverwaltung
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{fontSize: '11px', color: 'text.secondary'}}>
|
||||
Österreichischer Pferdesportverband
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Fehler-Anzeige */}
|
||||
{error && (
|
||||
<Alert severity="error" sx={{mb: 3, fontSize: '11px'}}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Login-Formular */}
|
||||
<form onSubmit={handleLogin}>
|
||||
<Box sx={{display: 'flex', flexDirection: 'column', gap: 2.5}}>
|
||||
<TextField
|
||||
label="Benutzername"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
fullWidth
|
||||
autoFocus
|
||||
disabled={loading}
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '12px'}}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Passwort"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
fullWidth
|
||||
disabled={loading}
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '12px'}}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
edge="end"
|
||||
size="small"
|
||||
>
|
||||
{showPassword ? <VisibilityOff/> : <Visibility/>}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
fullWidth
|
||||
disabled={loading || !username || !password}
|
||||
sx={{
|
||||
py: 1.5,
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
'&:hover': {
|
||||
background: 'linear-gradient(135deg, #5568d3 0%, #6a4193 100%)',
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<CircularProgress size={20} sx={{color: 'white'}}/>
|
||||
) : (
|
||||
'Anmelden'
|
||||
)}
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
|
||||
{/* Hinweis */}
|
||||
<Box
|
||||
sx={{
|
||||
mt: 4,
|
||||
p: 2,
|
||||
bgcolor: 'grey.50',
|
||||
borderRadius: 2,
|
||||
border: 1,
|
||||
borderColor: 'grey.200'
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'text.secondary', mb: 1}}>
|
||||
<strong>Demo-Zugang (Phase 1):</strong>
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'text.secondary', fontFamily: 'monospace'}}>
|
||||
Benutzer: <strong>admin</strong><br/>
|
||||
Passwort: <strong>Admin#1234</strong>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
import {useState, useEffect, useRef} from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Button from '@mui/material/Button';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Badge from '@mui/material/Badge';
|
||||
|
||||
// Mock-Daten für Pferde
|
||||
const mockPferde = [
|
||||
{
|
||||
id: 1,
|
||||
kopfnr: 'A123',
|
||||
name: "Obora's Donna",
|
||||
rasse: 'Hannoveraner',
|
||||
farbe: 'Brauner',
|
||||
besitzer: 'Franz Huber',
|
||||
stall: 'Box 12'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
kopfnr: 'H597',
|
||||
name: 'Weltmeyer',
|
||||
rasse: 'Trakehner',
|
||||
farbe: 'Schimmel',
|
||||
besitzer: 'Maria Gruber',
|
||||
stall: 'Box 8'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
kopfnr: '9939',
|
||||
name: 'Rubinstein',
|
||||
rasse: 'Westfale',
|
||||
farbe: 'Fuchs',
|
||||
besitzer: 'Johann Maier',
|
||||
stall: 'Box 15'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
kopfnr: 'D456',
|
||||
name: "Obora's Danilo",
|
||||
rasse: 'Oldenburger',
|
||||
farbe: 'Rappe',
|
||||
besitzer: 'Anna Schmidt',
|
||||
stall: 'Box 3'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
kopfnr: '4568',
|
||||
name: 'Domino',
|
||||
rasse: 'Holsteiner',
|
||||
farbe: 'Brauner',
|
||||
besitzer: 'Thomas Bauer',
|
||||
stall: 'Box 5'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
kopfnr: 'B789',
|
||||
name: "Obora's Dream",
|
||||
rasse: 'Hannoveraner',
|
||||
farbe: 'Fuchs',
|
||||
besitzer: 'Franz Huber',
|
||||
stall: 'Box 14'
|
||||
},
|
||||
];
|
||||
|
||||
// Mock-Daten für Reiter
|
||||
const mockReiter = [
|
||||
{
|
||||
id: 1,
|
||||
kopfnr: '201',
|
||||
vorname: 'Anna',
|
||||
nachname: 'Schneider',
|
||||
verein: 'RV Wien',
|
||||
lizenz: 'LNR-2024-4587',
|
||||
lizenzGueltig: true,
|
||||
kontoSaldo: 0,
|
||||
geburtsjahr: 1995
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
kopfnr: '202',
|
||||
vorname: 'Thomas',
|
||||
nachname: 'Bauer',
|
||||
verein: 'RC Graz',
|
||||
lizenz: 'LNR-2023-1234',
|
||||
lizenzGueltig: false,
|
||||
kontoSaldo: -125.50,
|
||||
geburtsjahr: 1998
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
kopfnr: '203',
|
||||
vorname: 'Sophie',
|
||||
nachname: 'Wagner',
|
||||
verein: 'RFV Salzburg',
|
||||
lizenz: 'LNR-2024-9876',
|
||||
lizenzGueltig: true,
|
||||
kontoSaldo: 50.00,
|
||||
geburtsjahr: 1992
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
kopfnr: '204',
|
||||
vorname: 'Michael',
|
||||
nachname: 'Müller',
|
||||
verein: 'RC Innsbruck',
|
||||
lizenz: 'LNR-2024-5555',
|
||||
lizenzGueltig: true,
|
||||
kontoSaldo: 0,
|
||||
geburtsjahr: 2001
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
kopfnr: '205',
|
||||
vorname: 'Franz',
|
||||
nachname: 'Huber',
|
||||
verein: 'RV Linz',
|
||||
lizenz: 'LNR-2024-7777',
|
||||
lizenzGueltig: true,
|
||||
kontoSaldo: 0,
|
||||
geburtsjahr: 2002
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
kopfnr: '206',
|
||||
vorname: 'Franz',
|
||||
nachname: 'Huber',
|
||||
verein: 'RC Wien',
|
||||
lizenz: 'LNR-2024-8888',
|
||||
lizenzGueltig: true,
|
||||
kontoSaldo: 0,
|
||||
geburtsjahr: 1998
|
||||
},
|
||||
];
|
||||
|
||||
// Mock-Daten für bereits getätigte Nennungen (IMS = Im System)
|
||||
const turnieNennungen = [
|
||||
{reiterId: 2, pferdId: 5, bewerbNr: 3}, // Thomas Bauer mit Domino in Bewerb 3
|
||||
{reiterId: 1, pferdId: 1, bewerbNr: 2}, // Anna Schneider mit Obora's Donna in Bewerb 2
|
||||
{reiterId: 1, pferdId: 2, bewerbNr: 5}, // Anna Schneider mit Weltmeyer in Bewerb 5
|
||||
];
|
||||
|
||||
interface Props {
|
||||
selectedPferd: any;
|
||||
setSelectedPferd: (pferd: any) => void;
|
||||
selectedReiter: any;
|
||||
setSelectedReiter: (reiter: any) => void;
|
||||
}
|
||||
|
||||
export function PferdReiterEingabe({selectedPferd, setSelectedPferd, selectedReiter, setSelectedReiter}: Props) {
|
||||
const [pferdSuche, setPferdSuche] = useState('');
|
||||
const [reiterSuche, setReiterSuche] = useState('');
|
||||
const [pferdErgebnisse, setPferdErgebnisse] = useState<any[]>([]);
|
||||
const [reiterErgebnisse, setReiterErgebnisse] = useState<any[]>([]);
|
||||
const [selectedPferdIndex, setSelectedPferdIndex] = useState(0);
|
||||
const [selectedReiterIndex, setSelectedReiterIndex] = useState(0);
|
||||
|
||||
const pferdInputRef = useRef<HTMLInputElement>(null);
|
||||
const reiterInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Autofokus auf Pferd-Suchfeld beim Laden
|
||||
useEffect(() => {
|
||||
pferdInputRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
// Pferd-Suche
|
||||
useEffect(() => {
|
||||
if (pferdSuche.length > 0) {
|
||||
// Normale Suche nach Eingabe
|
||||
const results = mockPferde.filter(p =>
|
||||
p.kopfnr.toLowerCase().includes(pferdSuche.toLowerCase()) ||
|
||||
p.name.toLowerCase().includes(pferdSuche.toLowerCase())
|
||||
);
|
||||
setPferdErgebnisse(results);
|
||||
setSelectedPferdIndex(0);
|
||||
} else if (selectedReiter && !pferdSuche) {
|
||||
// Cross-Reference: Zeige Pferde des ausgewählten Reiters
|
||||
const reiterPferde = turnieNennungen
|
||||
.filter(n => n.reiterId === selectedReiter.id)
|
||||
.map(n => mockPferde.find(p => p.id === n.pferdId))
|
||||
.filter(Boolean);
|
||||
setPferdErgebnisse(reiterPferde);
|
||||
} else {
|
||||
setPferdErgebnisse([]);
|
||||
}
|
||||
}, [pferdSuche, selectedReiter]);
|
||||
|
||||
// Reiter-Suche
|
||||
useEffect(() => {
|
||||
if (reiterSuche.length > 0) {
|
||||
// Normale Suche nach Eingabe
|
||||
const results = mockReiter.filter(r =>
|
||||
r.vorname.toLowerCase().includes(reiterSuche.toLowerCase()) ||
|
||||
r.nachname.toLowerCase().includes(reiterSuche.toLowerCase()) ||
|
||||
`${r.vorname} ${r.nachname}`.toLowerCase().includes(reiterSuche.toLowerCase())
|
||||
);
|
||||
setReiterErgebnisse(results);
|
||||
setSelectedReiterIndex(0);
|
||||
} else if (selectedPferd && !reiterSuche) {
|
||||
// Cross-Reference: Zeige Reiter des ausgewählten Pferdes
|
||||
const pferdReiter = turnieNennungen
|
||||
.filter(n => n.pferdId === selectedPferd.id)
|
||||
.map(n => mockReiter.find(r => r.id === n.reiterId))
|
||||
.filter(Boolean);
|
||||
setReiterErgebnisse(pferdReiter);
|
||||
} else {
|
||||
setReiterErgebnisse([]);
|
||||
}
|
||||
}, [reiterSuche, selectedPferd]);
|
||||
|
||||
// Hilfsfunktion: Prüft ob Pferd im System ist (IMS)
|
||||
const isPferdIMS = (pferdId: number) => {
|
||||
return turnieNennungen.some(n => n.pferdId === pferdId);
|
||||
};
|
||||
|
||||
// Hilfsfunktion: Prüft ob Reiter im System ist (IMS)
|
||||
const isReiterIMS = (reiterId: number) => {
|
||||
return turnieNennungen.some(n => n.reiterId === reiterId);
|
||||
};
|
||||
|
||||
// Pferd auswählen
|
||||
const handlePferdAuswahl = (pferd: any) => {
|
||||
setSelectedPferd(pferd);
|
||||
|
||||
// Cross-Reference: Zeige Reiter dieses Pferdes
|
||||
const pferdReiter = turnieNennungen
|
||||
.filter(n => n.pferdId === pferd.id)
|
||||
.map(n => mockReiter.find(r => r.id === n.reiterId))
|
||||
.filter(Boolean);
|
||||
|
||||
if (pferdReiter.length > 0) {
|
||||
setReiterErgebnisse(pferdReiter);
|
||||
}
|
||||
|
||||
reiterInputRef.current?.focus();
|
||||
};
|
||||
|
||||
// Reiter auswählen
|
||||
const handleReiterAuswahl = (reiter: any) => {
|
||||
setSelectedReiter(reiter);
|
||||
|
||||
// Cross-Reference: Zeige Pferde dieses Reiters
|
||||
const reiterPferde = turnieNennungen
|
||||
.filter(n => n.reiterId === reiter.id)
|
||||
.map(n => mockPferde.find(p => p.id === n.pferdId))
|
||||
.filter(Boolean);
|
||||
|
||||
if (reiterPferde.length > 0) {
|
||||
setPferdErgebnisse(reiterPferde);
|
||||
}
|
||||
};
|
||||
|
||||
// Keyboard Navigation für Pferd
|
||||
const handlePferdKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (pferdErgebnisse.length === 0) return;
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
setSelectedPferdIndex(prev => Math.min(prev + 1, pferdErgebnisse.length - 1));
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
setSelectedPferdIndex(prev => Math.max(prev - 1, 0));
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (pferdErgebnisse[selectedPferdIndex]) {
|
||||
handlePferdAuswahl(pferdErgebnisse[selectedPferdIndex]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Keyboard Navigation für Reiter
|
||||
const handleReiterKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (reiterErgebnisse.length === 0) return;
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
setSelectedReiterIndex(prev => Math.min(prev + 1, reiterErgebnisse.length - 1));
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
setSelectedReiterIndex(prev => Math.max(prev - 1, 0));
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (reiterErgebnisse[selectedReiterIndex]) {
|
||||
handleReiterAuswahl(reiterErgebnisse[selectedReiterIndex]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePferdLeeren = () => {
|
||||
setPferdSuche('');
|
||||
setSelectedPferd(null);
|
||||
setPferdErgebnisse([]);
|
||||
pferdInputRef.current?.focus();
|
||||
};
|
||||
|
||||
const handleReiterLeeren = () => {
|
||||
setReiterSuche('');
|
||||
setSelectedReiter(null);
|
||||
setReiterErgebnisse([]);
|
||||
reiterInputRef.current?.focus();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{display: 'flex', height: '100%'}}>
|
||||
{/* Linke Hälfte: Pferd */}
|
||||
<Box sx={{
|
||||
flex: 1,
|
||||
borderRight: 1,
|
||||
borderColor: 'divider',
|
||||
p: 1.5,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 1
|
||||
}}>
|
||||
{/* Eingabefeld */}
|
||||
<Box sx={{display: 'flex', gap: 1, alignItems: 'center'}}>
|
||||
<Typography variant="caption" sx={{fontWeight: 600, minWidth: 50, fontSize: '11px'}}>
|
||||
Pferd:
|
||||
</Typography>
|
||||
<TextField
|
||||
inputRef={pferdInputRef}
|
||||
fullWidth
|
||||
size="small"
|
||||
placeholder="Kopfnummer oder Name"
|
||||
value={pferdSuche}
|
||||
onChange={(e) => setPferdSuche(e.target.value)}
|
||||
onKeyDown={handlePferdKeyDown}
|
||||
sx={{
|
||||
flex: 1,
|
||||
'& .MuiInputBase-input': {fontSize: '11px', py: 0.75},
|
||||
}}
|
||||
/>
|
||||
<Button variant="outlined" size="small" sx={{minWidth: 30, px: 0.5, fontSize: '10px'}}>
|
||||
...
|
||||
</Button>
|
||||
<Button variant="outlined" size="small" onClick={handlePferdLeeren} sx={{fontSize: '10px', px: 1}}>
|
||||
Leeren
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Suchergebnisse - bleiben immer sichtbar */}
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{
|
||||
height: selectedPferd ? '25%' : '50%',
|
||||
overflow: 'auto',
|
||||
transition: 'height 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<List dense disablePadding>
|
||||
{pferdErgebnisse.length > 0 ? (
|
||||
(pferdSuche ? pferdErgebnisse : pferdErgebnisse.slice(0, 4)).map((pferd, idx) => {
|
||||
const istIMS = isPferdIMS(pferd.id);
|
||||
return (
|
||||
<ListItem key={pferd.id} disablePadding>
|
||||
<ListItemButton
|
||||
selected={idx === selectedPferdIndex}
|
||||
onDoubleClick={() => handlePferdAuswahl(pferd)}
|
||||
sx={{py: 0.25, display: 'flex', gap: 1}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={`${pferd.kopfnr} - ${pferd.name}`}
|
||||
primaryTypographyProps={{fontSize: '11px'}}
|
||||
/>
|
||||
{istIMS && (
|
||||
<Chip
|
||||
label="IMS"
|
||||
size="small"
|
||||
color="primary"
|
||||
sx={{height: 16, fontSize: '8px', fontWeight: 600}}
|
||||
/>
|
||||
)}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="Keine Ergebnisse"
|
||||
primaryTypographyProps={{fontSize: '11px', color: 'text.secondary', textAlign: 'center'}}
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
</Paper>
|
||||
|
||||
{/* Pferd Details - erscheint nach Auswahl */}
|
||||
{selectedPferd && (
|
||||
<Paper variant="outlined" sx={{p: 1.5, bgcolor: 'primary.50', flex: 1}}>
|
||||
<Typography variant="caption" sx={{fontWeight: 600, mb: 0.5, display: 'block', fontSize: '10px'}}>
|
||||
Pferd Details
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{fontSize: '10px', mb: 0.25, display: 'block'}}>
|
||||
<strong>Kopfnummer:</strong> {selectedPferd.kopfnr}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{fontSize: '10px', mb: 0.25, display: 'block'}}>
|
||||
<strong>Name:</strong> {selectedPferd.name}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{fontSize: '10px', mb: 0.25, display: 'block'}}>
|
||||
<strong>Rasse:</strong> {selectedPferd.rasse}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{fontSize: '10px', mb: 0.25, display: 'block'}}>
|
||||
<strong>Farbe:</strong> {selectedPferd.farbe}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{fontSize: '10px', mb: 0.25, display: 'block'}}>
|
||||
<strong>Besitzer:</strong> {selectedPferd.besitzer}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{fontSize: '10px', display: 'block'}}>
|
||||
<strong>Stall:</strong> {selectedPferd.stall}
|
||||
</Typography>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* Buttons */}
|
||||
<Box sx={{display: 'flex', gap: 0.5}}>
|
||||
<Button variant="outlined" size="small" fullWidth sx={{fontSize: '10px', py: 0.5}}>
|
||||
Neu
|
||||
</Button>
|
||||
<Button variant="outlined" size="small" fullWidth disabled={!selectedPferd} sx={{fontSize: '10px', py: 0.5}}>
|
||||
Bearbeiten
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Rechte Hälfte: Reiter */}
|
||||
<Box sx={{flex: 1, p: 1.5, display: 'flex', flexDirection: 'column', gap: 1}}>
|
||||
{/* Eingabefeld */}
|
||||
<Box sx={{display: 'flex', gap: 1, alignItems: 'center'}}>
|
||||
<Typography variant="caption" sx={{fontWeight: 600, minWidth: 50, fontSize: '11px'}}>
|
||||
Reiter:
|
||||
</Typography>
|
||||
<TextField
|
||||
inputRef={reiterInputRef}
|
||||
fullWidth
|
||||
size="small"
|
||||
placeholder="Vorname und/oder Nachname"
|
||||
value={reiterSuche}
|
||||
onChange={(e) => setReiterSuche(e.target.value)}
|
||||
onKeyDown={handleReiterKeyDown}
|
||||
sx={{
|
||||
flex: 1,
|
||||
'& .MuiInputBase-input': {fontSize: '11px', py: 0.75},
|
||||
}}
|
||||
/>
|
||||
<Button variant="outlined" size="small" sx={{minWidth: 30, px: 0.5, fontSize: '10px'}}>
|
||||
...
|
||||
</Button>
|
||||
<Button variant="outlined" size="small" onClick={handleReiterLeeren} sx={{fontSize: '10px', px: 1}}>
|
||||
Leeren
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Suchergebnisse - bleiben immer sichtbar */}
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{
|
||||
height: selectedReiter ? '25%' : '50%',
|
||||
overflow: 'auto',
|
||||
transition: 'height 0.2s ease',
|
||||
}}
|
||||
>
|
||||
<List dense disablePadding>
|
||||
{reiterErgebnisse.length > 0 ? (
|
||||
(reiterSuche ? reiterErgebnisse : reiterErgebnisse.slice(0, 4)).map((reiter, idx) => {
|
||||
const istIMS = isReiterIMS(reiter.id);
|
||||
return (
|
||||
<ListItem key={reiter.id} disablePadding>
|
||||
<ListItemButton
|
||||
selected={idx === selectedReiterIndex}
|
||||
onDoubleClick={() => handleReiterAuswahl(reiter)}
|
||||
sx={{py: 0.25, display: 'flex', gap: 1}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={`${reiter.vorname} ${reiter.nachname}`}
|
||||
secondary={reiter.geburtsjahr ? `*${reiter.geburtsjahr}` : undefined}
|
||||
primaryTypographyProps={{fontSize: '11px'}}
|
||||
secondaryTypographyProps={{fontSize: '9px'}}
|
||||
/>
|
||||
{istIMS && (
|
||||
<Chip
|
||||
label="IMS"
|
||||
size="small"
|
||||
color="primary"
|
||||
sx={{height: 16, fontSize: '8px', fontWeight: 600}}
|
||||
/>
|
||||
)}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="Keine Ergebnisse"
|
||||
primaryTypographyProps={{fontSize: '11px', color: 'text.secondary', textAlign: 'center'}}
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
</Paper>
|
||||
|
||||
{/* Reiter Details - erscheint nach Auswahl */}
|
||||
{selectedReiter && (
|
||||
<Paper variant="outlined" sx={{p: 1.5, bgcolor: 'primary.50', flex: 1}}>
|
||||
<Typography variant="caption" sx={{fontWeight: 600, mb: 0.5, display: 'block', fontSize: '10px'}}>
|
||||
Reiter Details
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{fontSize: '10px', mb: 0.25, display: 'block'}}>
|
||||
<strong>Name:</strong> {selectedReiter.vorname} {selectedReiter.nachname}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{fontSize: '10px', mb: 0.25, display: 'block'}}>
|
||||
<strong>Verein:</strong> {selectedReiter.verein}
|
||||
</Typography>
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 1, mb: 0.25}}>
|
||||
<Typography variant="caption" sx={{fontSize: '10px'}}>
|
||||
<strong>Lizenz:</strong> {selectedReiter.lizenz}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={selectedReiter.lizenzGueltig ? 'Gültig' : 'Abgelaufen'}
|
||||
size="small"
|
||||
color={selectedReiter.lizenzGueltig ? 'success' : 'error'}
|
||||
sx={{height: 16, fontSize: '9px'}}
|
||||
/>
|
||||
</Box>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
fontSize: '10px',
|
||||
color: selectedReiter.kontoSaldo < 0 ? 'error.main' : 'text.primary',
|
||||
fontWeight: selectedReiter.kontoSaldo < 0 ? 600 : 400,
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
<strong>Konto-Saldo:</strong> €{selectedReiter.kontoSaldo.toFixed(2)}
|
||||
</Typography>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* Buttons */}
|
||||
<Box sx={{display: 'flex', gap: 0.5}}>
|
||||
<Button variant="outlined" size="small" fullWidth sx={{fontSize: '10px', py: 0.5}}>
|
||||
Neu
|
||||
</Button>
|
||||
<Button variant="outlined" size="small" fullWidth disabled={!selectedReiter} sx={{fontSize: '10px', py: 0.5}}>
|
||||
Bearbeiten
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import {useState} from 'react';
|
||||
import {useParams, useNavigate} from 'react-router';
|
||||
import Box from '@mui/material/Box';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Breadcrumbs from '@mui/material/Breadcrumbs';
|
||||
import Link from '@mui/material/Link';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import HomeIcon from '@mui/icons-material/Home';
|
||||
import {StammdatenTab} from './turnier/StammdatenTab';
|
||||
import {OrganisationTab} from './turnier/OrganisationTab';
|
||||
import {BewerbeTab} from './turnier/BewerbeTab';
|
||||
import {PreislisteTab} from './turnier/PreislisteTab';
|
||||
import {veranstaltungenData} from './Dashboard';
|
||||
|
||||
export function TurnierAnsicht() {
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const veranstaltungId = params.veranstaltungId;
|
||||
const turnierNr = params.nr;
|
||||
|
||||
// Bei neu: Direkt zu Stammdaten (Tab 0), sonst Stammdaten (Tab 0)
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
// Veranstaltung laden
|
||||
const veranstaltung = veranstaltungId !== 'neu'
|
||||
? veranstaltungenData.find(v => v.id === parseInt(veranstaltungId || '0'))
|
||||
: null;
|
||||
|
||||
// Turnier laden (wenn nicht neu)
|
||||
const turnier = turnierNr !== 'neu' && veranstaltung
|
||||
? veranstaltung.turniere.find(t => t.nr === turnierNr)
|
||||
: null;
|
||||
|
||||
const handleZurueck = () => {
|
||||
navigate(`/veranstaltung/${veranstaltungId}`);
|
||||
};
|
||||
|
||||
const handleToAdmin = () => {
|
||||
navigate('/admin');
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{display: 'flex', flexDirection: 'column', height: '100vh', bgcolor: 'background.default'}}>
|
||||
{/* Header mit Navigation */}
|
||||
<AppBar position="static" elevation={1}>
|
||||
<Toolbar variant="dense" sx={{gap: 2}}>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
onClick={handleZurueck}
|
||||
sx={{mr: 1}}
|
||||
>
|
||||
<ArrowBackIcon/>
|
||||
</IconButton>
|
||||
|
||||
<Breadcrumbs
|
||||
aria-label="breadcrumb"
|
||||
sx={{
|
||||
color: 'white',
|
||||
'& .MuiBreadcrumbs-separator': {color: 'rgba(255,255,255,0.7)'}
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
underline="hover"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
cursor: 'pointer',
|
||||
fontSize: '11px'
|
||||
}}
|
||||
onClick={handleToAdmin}
|
||||
>
|
||||
<HomeIcon sx={{mr: 0.5, fontSize: 16}}/>
|
||||
Admin - Verwaltung
|
||||
</Link>
|
||||
<Link
|
||||
underline="hover"
|
||||
sx={{
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
cursor: 'pointer',
|
||||
fontSize: '11px'
|
||||
}}
|
||||
onClick={handleZurueck}
|
||||
>
|
||||
{veranstaltung?.name || 'Veranstaltung'}
|
||||
</Link>
|
||||
<Typography sx={{color: 'white', fontSize: '11px', fontWeight: 600}}>
|
||||
{turnier ? `Turnier ${turnier.nr}` : 'Neues Turnier'}
|
||||
</Typography>
|
||||
</Breadcrumbs>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(_, v) => setActiveTab(v)}
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider',
|
||||
bgcolor: 'background.paper',
|
||||
'& .MuiTab-root': {
|
||||
fontSize: '11px',
|
||||
minHeight: 36,
|
||||
py: 1,
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tab label="Stammdaten"/>
|
||||
<Tab label="Organisation"/>
|
||||
<Tab label="Bewerbe"/>
|
||||
<Tab label="Preisliste"/>
|
||||
</Tabs>
|
||||
|
||||
{/* Tab Content */}
|
||||
<Box sx={{flex: 1, overflow: 'auto'}}>
|
||||
{activeTab === 0 && <StammdatenTab turnierId={turnierNr}/>}
|
||||
{activeTab === 1 && <OrganisationTab/>}
|
||||
{activeTab === 2 && <BewerbeTab/>}
|
||||
{activeTab === 3 && <PreislisteTab/>}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import {useState} from 'react';
|
||||
import {useParams, useNavigate} from 'react-router';
|
||||
import Box from '@mui/material/Box';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Breadcrumbs from '@mui/material/Breadcrumbs';
|
||||
import Link from '@mui/material/Link';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import HomeIcon from '@mui/icons-material/Home';
|
||||
import {VeranstaltungUebersicht} from './turnier/VeranstaltungUebersicht';
|
||||
import {veranstaltungenData} from './Dashboard';
|
||||
import {StammdatenTab} from './turnier/StammdatenTab';
|
||||
import {OrganisationTab} from './turnier/OrganisationTab';
|
||||
import {BewerbeTab} from './turnier/BewerbeTab';
|
||||
import {PreislisteTab} from './turnier/PreislisteTab';
|
||||
|
||||
export function TurnierErstellen() {
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const id = params.id;
|
||||
|
||||
// Bei neu: Direkt zu Stammdaten (Tab 1), sonst Veranstaltung - Übersicht (Tab 0)
|
||||
const [activeTab, setActiveTab] = useState(id === 'neu' ? 1 : 0);
|
||||
|
||||
// Veranstaltung laden
|
||||
const veranstaltung = id !== 'neu'
|
||||
? veranstaltungenData.find(v => v.id === parseInt(id || '0'))
|
||||
: null;
|
||||
|
||||
const handleZurueck = () => {
|
||||
navigate('/admin');
|
||||
};
|
||||
|
||||
// Für bestehende Veranstaltungen: Nur "Veranstaltung - Übersicht" Tab
|
||||
// Für neue Veranstaltungen: Alle Tabs anzeigen
|
||||
const istNeueVeranstaltung = id === 'neu';
|
||||
const istBestehendeVeranstaltung = !istNeueVeranstaltung && veranstaltung;
|
||||
|
||||
return (
|
||||
<Box sx={{display: 'flex', flexDirection: 'column', height: '100vh', bgcolor: 'background.default'}}>
|
||||
{/* Header mit Navigation */}
|
||||
<AppBar position="static" elevation={1}>
|
||||
<Toolbar variant="dense" sx={{gap: 2}}>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
onClick={handleZurueck}
|
||||
sx={{mr: 1}}
|
||||
>
|
||||
<ArrowBackIcon/>
|
||||
</IconButton>
|
||||
|
||||
<Breadcrumbs
|
||||
aria-label="breadcrumb"
|
||||
sx={{
|
||||
color: 'white',
|
||||
'& .MuiBreadcrumbs-separator': {color: 'rgba(255,255,255,0.7)'}
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
underline="hover"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
cursor: 'pointer',
|
||||
fontSize: '11px'
|
||||
}}
|
||||
onClick={handleZurueck}
|
||||
>
|
||||
<HomeIcon sx={{mr: 0.5, fontSize: 16}}/>
|
||||
Admin - Verwaltung
|
||||
</Link>
|
||||
<Typography sx={{color: 'white', fontSize: '11px', fontWeight: 600}}>
|
||||
{veranstaltung?.name || 'Neue Veranstaltung'}
|
||||
</Typography>
|
||||
</Breadcrumbs>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
{istBestehendeVeranstaltung ? (
|
||||
// Nur "Veranstaltung - Übersicht" für bestehende Veranstaltungen
|
||||
<Tabs
|
||||
value={0}
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider',
|
||||
bgcolor: 'background.paper',
|
||||
'& .MuiTab-root': {
|
||||
fontSize: '11px',
|
||||
minHeight: 36,
|
||||
py: 1,
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tab label="Veranstaltung - Übersicht"/>
|
||||
</Tabs>
|
||||
) : (
|
||||
// Alle Tabs für neue Veranstaltungen
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(_, v) => setActiveTab(v)}
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider',
|
||||
bgcolor: 'background.paper',
|
||||
'& .MuiTab-root': {
|
||||
fontSize: '11px',
|
||||
minHeight: 36,
|
||||
py: 1,
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tab label="Veranstaltung - Übersicht"/>
|
||||
<Tab label="Stammdaten"/>
|
||||
<Tab label="Organisation"/>
|
||||
<Tab label="Bewerbe"/>
|
||||
<Tab label="Preisliste"/>
|
||||
</Tabs>
|
||||
)}
|
||||
|
||||
{/* Tab Content */}
|
||||
<Box sx={{flex: 1, overflow: 'auto'}}>
|
||||
{istBestehendeVeranstaltung ? (
|
||||
// Nur Veranstaltung - Übersicht für bestehende Veranstaltungen
|
||||
<VeranstaltungUebersicht/>
|
||||
) : (
|
||||
// Alle Tabs für neue Veranstaltungen
|
||||
<>
|
||||
{activeTab === 0 && <VeranstaltungUebersicht/>}
|
||||
{activeTab === 1 && <StammdatenTab turnierId={id}/>}
|
||||
{activeTab === 2 && <OrganisationTab/>}
|
||||
{activeTab === 3 && <BewerbeTab/>}
|
||||
{activeTab === 4 && <PreislisteTab/>}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,398 @@
|
||||
import {useState} from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Button from '@mui/material/Button';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Select from '@mui/material/Select';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
|
||||
interface Richter {
|
||||
id: number;
|
||||
name: string;
|
||||
qualifikation: string;
|
||||
funktion: string;
|
||||
}
|
||||
|
||||
// Mock-Qualifikationen basierend auf OEPS-System
|
||||
const qualifikationen = [
|
||||
'D-E', 'D-A', 'D-L', 'D-M', 'D-S', 'D-GP', // Dressur
|
||||
'S-E', 'S-A', 'S-L', 'S-M', 'S-S', // Springen
|
||||
'V-E', 'V-A', 'V-L', 'V-M', 'V-S', // Vielseitigkeit
|
||||
'FEI Level 1', 'FEI Level 2', 'FEI Level 3' // International
|
||||
];
|
||||
|
||||
const richterfunktionen = [
|
||||
'Hauptrichter',
|
||||
'Beisitzer',
|
||||
'Richter bei C',
|
||||
'Richter bei H',
|
||||
'Richter bei M',
|
||||
'Richter bei B',
|
||||
'Richter bei E'
|
||||
];
|
||||
|
||||
export function FunktionaereTab() {
|
||||
// Einzelne Funktionäre
|
||||
const [turnierleiter, setTurnierleiter] = useState('');
|
||||
const [turnierbeauftragter, setTurnierbeauftragter] = useState('');
|
||||
const [technischerDelegierter, setTechnischerDelegierter] = useState('');
|
||||
const [parcourschef, setParcourschef] = useState('');
|
||||
const [tierarzt, setTierarzt] = useState('');
|
||||
const [schmied, setSchmied] = useState('');
|
||||
const [steward, setSteward] = useState('');
|
||||
|
||||
// Richterkollegium (dynamische Liste)
|
||||
const [richter, setRichter] = useState<Richter[]>([
|
||||
{id: 1, name: 'Alexandra Schuster', qualifikation: 'D-GP', funktion: 'Hauptrichter'},
|
||||
{id: 2, name: 'Ulrike Knasmüller-Prinz', qualifikation: 'D-M', funktion: 'Beisitzer'},
|
||||
]);
|
||||
|
||||
const handleRichterHinzufuegen = () => {
|
||||
const newId = Math.max(0, ...richter.map(r => r.id)) + 1;
|
||||
setRichter([
|
||||
...richter,
|
||||
{id: newId, name: '', qualifikation: 'D-E', funktion: 'Beisitzer'}
|
||||
]);
|
||||
};
|
||||
|
||||
const handleRichterLoeschen = (id: number) => {
|
||||
setRichter(richter.filter(r => r.id !== id));
|
||||
};
|
||||
|
||||
const handleRichterAendern = (id: number, field: keyof Richter, value: string) => {
|
||||
setRichter(richter.map(r =>
|
||||
r.id === id ? {...r, [field]: value} : r
|
||||
));
|
||||
};
|
||||
|
||||
const handleSpeichern = () => {
|
||||
console.log('Funktionäre speichern:', {
|
||||
turnierleiter,
|
||||
turnierbeauftragter,
|
||||
technischerDelegierter,
|
||||
parcourschef,
|
||||
tierarzt,
|
||||
schmied,
|
||||
steward,
|
||||
richter,
|
||||
});
|
||||
// TODO: Backend Integration (C-Satz)
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{p: 3, bgcolor: 'background.default', height: '100%', overflowY: 'auto'}}>
|
||||
<Box sx={{maxWidth: 1000}}>
|
||||
<Typography variant="h6" sx={{fontSize: '13px', fontWeight: 600, mb: 3}}>
|
||||
Funktionäre & Offizielle (C-Satz)
|
||||
</Typography>
|
||||
|
||||
{/* Turnier-Organisation */}
|
||||
<Paper variant="outlined" sx={{p: 2.5, mb: 3}}>
|
||||
<Typography variant="body2" sx={{fontSize: '11px', fontWeight: 600, mb: 2}}>
|
||||
Turnier-Organisation
|
||||
</Typography>
|
||||
|
||||
<Box sx={{display: 'flex', flexDirection: 'column', gap: 2}}>
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 200, fontSize: '11px'}}>
|
||||
Turnierleiter:
|
||||
</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
value={turnierleiter}
|
||||
onChange={(e) => setTurnierleiter(e.target.value)}
|
||||
placeholder="z.B. Ursula Stroblmair"
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<IconButton size="small" sx={{mr: -1}}>
|
||||
<SearchIcon sx={{fontSize: 16}}/>
|
||||
</IconButton>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 200, fontSize: '11px'}}>
|
||||
Turnierbeauftragte/r:
|
||||
</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
value={turnierbeauftragter}
|
||||
onChange={(e) => setTurnierbeauftragter(e.target.value)}
|
||||
placeholder="z.B. Rudi Kreupl"
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<IconButton size="small" sx={{mr: -1}}>
|
||||
<SearchIcon sx={{fontSize: 16}}/>
|
||||
</IconButton>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 200, fontSize: '11px'}}>
|
||||
Technischer Delegierter (TD):
|
||||
</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
value={technischerDelegierter}
|
||||
onChange={(e) => setTechnischerDelegierter(e.target.value)}
|
||||
placeholder="Optional (hauptsächlich Vielseitigkeit)"
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<IconButton size="small" sx={{mr: -1}}>
|
||||
<SearchIcon sx={{fontSize: 16}}/>
|
||||
</IconButton>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 200, fontSize: '11px'}}>
|
||||
Steward:
|
||||
</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
value={steward}
|
||||
onChange={(e) => setSteward(e.target.value)}
|
||||
placeholder="z.B. Barbara Hruschka"
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<IconButton size="small" sx={{mr: -1}}>
|
||||
<SearchIcon sx={{fontSize: 16}}/>
|
||||
</IconButton>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Parcours & Technik */}
|
||||
<Paper variant="outlined" sx={{p: 2.5, mb: 3}}>
|
||||
<Typography variant="body2" sx={{fontSize: '11px', fontWeight: 600, mb: 2}}>
|
||||
Parcours & Technik
|
||||
</Typography>
|
||||
|
||||
<Box sx={{display: 'flex', flexDirection: 'column', gap: 2}}>
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 200, fontSize: '11px'}}>
|
||||
Parcourschef:
|
||||
</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
value={parcourschef}
|
||||
onChange={(e) => setParcourschef(e.target.value)}
|
||||
placeholder="z.B. Kurt Reitetschläger"
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<IconButton size="small" sx={{mr: -1}}>
|
||||
<SearchIcon sx={{fontSize: 16}}/>
|
||||
</IconButton>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Medizinische Versorgung */}
|
||||
<Paper variant="outlined" sx={{p: 2.5, mb: 3}}>
|
||||
<Typography variant="body2" sx={{fontSize: '11px', fontWeight: 600, mb: 2}}>
|
||||
Medizinische Versorgung
|
||||
</Typography>
|
||||
|
||||
<Box sx={{display: 'flex', flexDirection: 'column', gap: 2}}>
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 200, fontSize: '11px'}}>
|
||||
Turniertierarzt:
|
||||
</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
value={tierarzt}
|
||||
onChange={(e) => setTierarzt(e.target.value)}
|
||||
placeholder="z.B. Dr. Sabine Ötschmaier"
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<IconButton size="small" sx={{mr: -1}}>
|
||||
<SearchIcon sx={{fontSize: 16}}/>
|
||||
</IconButton>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 200, fontSize: '11px'}}>
|
||||
Schmied:
|
||||
</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
value={schmied}
|
||||
onChange={(e) => setSchmied(e.target.value)}
|
||||
placeholder="Name des Turnierschmieds"
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<IconButton size="small" sx={{mr: -1}}>
|
||||
<SearchIcon sx={{fontSize: 16}}/>
|
||||
</IconButton>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Richterkollegium */}
|
||||
<Paper variant="outlined" sx={{p: 2.5, mb: 3}}>
|
||||
<Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2}}>
|
||||
<Typography variant="body2" sx={{fontSize: '11px', fontWeight: 600}}>
|
||||
Richterkollegium
|
||||
</Typography>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<AddIcon/>}
|
||||
onClick={handleRichterHinzufuegen}
|
||||
sx={{fontSize: '10px'}}
|
||||
>
|
||||
Richter hinzufügen
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600}}>Name</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, width: 180}}>Qualifikation</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, width: 180}}>Funktion</TableCell>
|
||||
<TableCell sx={{width: 50}}></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{richter.map((r) => (
|
||||
<TableRow key={r.id}>
|
||||
<TableCell sx={{py: 1}}>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
value={r.name}
|
||||
onChange={(e) => handleRichterAendern(r.id, 'name', e.target.value)}
|
||||
placeholder="Name des Richters"
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.5}}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<IconButton size="small" sx={{mr: -1}}>
|
||||
<SearchIcon sx={{fontSize: 14}}/>
|
||||
</IconButton>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell sx={{py: 1}}>
|
||||
<Select
|
||||
size="small"
|
||||
fullWidth
|
||||
value={r.qualifikation}
|
||||
onChange={(e) => handleRichterAendern(r.id, 'qualifikation', e.target.value)}
|
||||
sx={{fontSize: '11px'}}
|
||||
>
|
||||
{qualifikationen.map((q) => (
|
||||
<MenuItem key={q} value={q} sx={{fontSize: '11px'}}>
|
||||
{q}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableCell sx={{py: 1}}>
|
||||
<Select
|
||||
size="small"
|
||||
fullWidth
|
||||
value={r.funktion}
|
||||
onChange={(e) => handleRichterAendern(r.id, 'funktion', e.target.value)}
|
||||
sx={{fontSize: '11px'}}
|
||||
>
|
||||
{richterfunktionen.map((f) => (
|
||||
<MenuItem key={f} value={f} sx={{fontSize: '11px'}}>
|
||||
{f}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableCell sx={{py: 1}}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleRichterLoeschen(r.id)}
|
||||
>
|
||||
<DeleteIcon sx={{fontSize: 16}}/>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{richter.length === 0 && (
|
||||
<Box sx={{textAlign: 'center', py: 3}}>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'text.secondary'}}>
|
||||
Keine Richter definiert
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{/* Hinweis */}
|
||||
<Paper variant="outlined" sx={{p: 2, mb: 3, bgcolor: '#f5f5f5'}}>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', mb: 1, fontWeight: 600}}>
|
||||
ℹ️ Hinweis zu Funktionären
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', lineHeight: 1.6}}>
|
||||
Die Funktionäre werden im <strong>C-Satz</strong> der ZNS-Schnittstelle übermittelt.
|
||||
Richter müssen entsprechende Qualifikationen für die jeweiligen Klassen besitzen (z.B. D-GP für Grand Prix
|
||||
Dressur).
|
||||
Bei internationalen Turnieren sind FEI-Lizenzen erforderlich.
|
||||
</Typography>
|
||||
</Paper>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Box sx={{display: 'flex', gap: 2, justifyContent: 'flex-end'}}>
|
||||
<Button variant="outlined" size="small" sx={{fontSize: '11px', px: 3}}>
|
||||
Zurücksetzen
|
||||
</Button>
|
||||
<Button variant="contained" size="small" onClick={handleSpeichern} sx={{fontSize: '11px', px: 3}}>
|
||||
Speichern
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
import {useState} from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Button from '@mui/material/Button';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Select from '@mui/material/Select';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import Divider from '@mui/material/Divider';
|
||||
|
||||
interface Richter {
|
||||
id: number;
|
||||
name: string;
|
||||
qualifikation: string;
|
||||
funktion: string;
|
||||
}
|
||||
|
||||
interface Platz {
|
||||
id: number;
|
||||
sparte: string;
|
||||
groesse: string;
|
||||
bezeichnung: string;
|
||||
}
|
||||
|
||||
// Mock-Qualifikationen basierend auf OEPS-System
|
||||
const qualifikationen = [
|
||||
'D-E', 'D-A', 'D-L', 'D-M', 'D-S', 'D-GP', // Dressur
|
||||
'S-E', 'S-A', 'S-L', 'S-M', 'S-S', // Springen
|
||||
'V-E', 'V-A', 'V-L', 'V-M', 'V-S', // Vielseitigkeit
|
||||
'FEI Level 1', 'FEI Level 2', 'FEI Level 3' // International
|
||||
];
|
||||
|
||||
const richterfunktionen = [
|
||||
'Hauptrichter',
|
||||
'Beisitzer',
|
||||
'Richter bei C',
|
||||
'Richter bei H',
|
||||
'Richter bei M',
|
||||
'Richter bei B',
|
||||
'Richter bei E'
|
||||
];
|
||||
|
||||
const sparten = ['Dressur', 'Springen', 'Vielseitigkeit'];
|
||||
|
||||
const platzgroessen = [
|
||||
'20 x 40 m',
|
||||
'20 x 60 m',
|
||||
'25 x 60 m',
|
||||
'30 x 60 m',
|
||||
'Springplatz'
|
||||
];
|
||||
|
||||
export function OrganisationTab() {
|
||||
// Einzelne Funktionäre
|
||||
const [turnierleiter, setTurnierleiter] = useState('');
|
||||
const [turnierbeauftragter, setTurnierbeauftragter] = useState('');
|
||||
const [technischerDelegierter, setTechnischerDelegierter] = useState('');
|
||||
const [parcourschef, setParcourschef] = useState('');
|
||||
const [tierarzt, setTierarzt] = useState('');
|
||||
const [schmied, setSchmied] = useState('');
|
||||
const [steward, setSteward] = useState('');
|
||||
|
||||
// Richterkollegium (dynamische Liste)
|
||||
const [richter, setRichter] = useState<Richter[]>([
|
||||
{id: 1, name: 'Alexandra Schuster', qualifikation: 'D-GP', funktion: 'Hauptrichter'},
|
||||
{id: 2, name: 'Ulrike Knasmüller-Prinz', qualifikation: 'D-M', funktion: 'Beisitzer'},
|
||||
]);
|
||||
|
||||
// Plätze (dynamische Liste)
|
||||
const [plaetze, setPlaetze] = useState<Platz[]>([
|
||||
{id: 1, sparte: 'Dressur', groesse: '20 x 60 m', bezeichnung: 'Hauptplatz'},
|
||||
{id: 2, sparte: 'Dressur', groesse: '20 x 40 m', bezeichnung: 'Abreiteplatz 1'},
|
||||
]);
|
||||
|
||||
const handleRichterHinzufuegen = () => {
|
||||
const newId = Math.max(0, ...richter.map(r => r.id)) + 1;
|
||||
setRichter([
|
||||
...richter,
|
||||
{id: newId, name: '', qualifikation: 'D-E', funktion: 'Beisitzer'}
|
||||
]);
|
||||
};
|
||||
|
||||
const handleRichterLoeschen = (id: number) => {
|
||||
setRichter(richter.filter(r => r.id !== id));
|
||||
};
|
||||
|
||||
const handleRichterAendern = (id: number, field: keyof Richter, value: string) => {
|
||||
setRichter(richter.map(r =>
|
||||
r.id === id ? {...r, [field]: value} : r
|
||||
));
|
||||
};
|
||||
|
||||
const handlePlatzHinzufuegen = () => {
|
||||
const newId = Math.max(0, ...plaetze.map(p => p.id)) + 1;
|
||||
setPlaetze([
|
||||
...plaetze,
|
||||
{id: newId, sparte: 'Dressur', groesse: '20 x 60 m', bezeichnung: ''}
|
||||
]);
|
||||
};
|
||||
|
||||
const handlePlatzLoeschen = (id: number) => {
|
||||
setPlaetze(plaetze.filter(p => p.id !== id));
|
||||
};
|
||||
|
||||
const handlePlatzAendern = (id: number, field: keyof Platz, value: string) => {
|
||||
setPlaetze(plaetze.map(p =>
|
||||
p.id === id ? {...p, [field]: value} : p
|
||||
));
|
||||
};
|
||||
|
||||
const handleSpeichern = () => {
|
||||
console.log('Organisation speichern:', {
|
||||
turnierleiter,
|
||||
turnierbeauftragter,
|
||||
technischerDelegierter,
|
||||
parcourschef,
|
||||
tierarzt,
|
||||
schmied,
|
||||
steward,
|
||||
richter,
|
||||
plaetze,
|
||||
});
|
||||
// TODO: Backend Integration (C-Satz)
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{p: 3, bgcolor: 'background.default', height: '100%', overflowY: 'auto'}}>
|
||||
<Box sx={{maxWidth: 1200}}>
|
||||
{/* === FUNKTIONÄRE === */}
|
||||
<Typography variant="h6" sx={{fontSize: '13px', fontWeight: 600, mb: 3}}>
|
||||
Funktionäre & Offizielle (C-Satz)
|
||||
</Typography>
|
||||
|
||||
{/* Turnier-Organisation */}
|
||||
<Paper variant="outlined" sx={{p: 2.5, mb: 3}}>
|
||||
<Typography variant="subtitle2" sx={{fontSize: '11px', fontWeight: 600, mb: 2}}>
|
||||
Turnier-Organisation
|
||||
</Typography>
|
||||
|
||||
<Box sx={{display: 'grid', gridTemplateColumns: '180px 1fr', gap: 2, alignItems: 'center'}}>
|
||||
<Typography variant="body2" sx={{fontSize: '11px'}}>Turnierleiter:</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
value={turnierleiter}
|
||||
onChange={(e) => setTurnierleiter(e.target.value)}
|
||||
placeholder="Name suchen..."
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}}
|
||||
/>
|
||||
|
||||
<Typography variant="body2" sx={{fontSize: '11px'}}>Turnierbeauftragter:</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
value={turnierbeauftragter}
|
||||
onChange={(e) => setTurnierbeauftragter(e.target.value)}
|
||||
placeholder="Name suchen..."
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}}
|
||||
/>
|
||||
|
||||
<Typography variant="body2" sx={{fontSize: '11px'}}>Technischer Delegierter:</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
value={technischerDelegierter}
|
||||
onChange={(e) => setTechnischerDelegierter(e.target.value)}
|
||||
placeholder="Name suchen..."
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}}
|
||||
/>
|
||||
|
||||
<Typography variant="body2" sx={{fontSize: '11px'}}>Parcourschef:</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
value={parcourschef}
|
||||
onChange={(e) => setParcourschef(e.target.value)}
|
||||
placeholder="Name suchen..."
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Support-Team */}
|
||||
<Paper variant="outlined" sx={{p: 2.5, mb: 3}}>
|
||||
<Typography variant="subtitle2" sx={{fontSize: '11px', fontWeight: 600, mb: 2}}>
|
||||
Support-Team
|
||||
</Typography>
|
||||
|
||||
<Box sx={{display: 'grid', gridTemplateColumns: '180px 1fr', gap: 2, alignItems: 'center'}}>
|
||||
<Typography variant="body2" sx={{fontSize: '11px'}}>Tierarzt:</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
value={tierarzt}
|
||||
onChange={(e) => setTierarzt(e.target.value)}
|
||||
placeholder="Name suchen..."
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}}
|
||||
/>
|
||||
|
||||
<Typography variant="body2" sx={{fontSize: '11px'}}>Schmied:</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
value={schmied}
|
||||
onChange={(e) => setSchmied(e.target.value)}
|
||||
placeholder="Name suchen..."
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}}
|
||||
/>
|
||||
|
||||
<Typography variant="body2" sx={{fontSize: '11px'}}>Steward:</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
value={steward}
|
||||
onChange={(e) => setSteward(e.target.value)}
|
||||
placeholder="Name suchen..."
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Richterkollegium */}
|
||||
<Paper variant="outlined" sx={{p: 2.5, mb: 4}}>
|
||||
<Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2}}>
|
||||
<Typography variant="subtitle2" sx={{fontSize: '11px', fontWeight: 600}}>
|
||||
Richterkollegium
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<AddIcon/>}
|
||||
onClick={handleRichterHinzufuegen}
|
||||
sx={{fontSize: '10px', px: 2}}
|
||||
>
|
||||
Richter hinzufügen
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, bgcolor: 'grey.50'}}>Name</TableCell>
|
||||
<TableCell
|
||||
sx={{fontSize: '10px', fontWeight: 600, bgcolor: 'grey.50', width: 180}}>Qualifikation</TableCell>
|
||||
<TableCell
|
||||
sx={{fontSize: '10px', fontWeight: 600, bgcolor: 'grey.50', width: 180}}>Funktion</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, bgcolor: 'grey.50', width: 60}}
|
||||
align="center">Aktion</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{richter.map((r) => (
|
||||
<TableRow key={r.id} hover>
|
||||
<TableCell sx={{py: 1}}>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
value={r.name}
|
||||
onChange={(e) => handleRichterAendern(r.id, 'name', e.target.value)}
|
||||
placeholder="Name suchen..."
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.5}}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell sx={{py: 1}}>
|
||||
<Select
|
||||
size="small"
|
||||
fullWidth
|
||||
value={r.qualifikation}
|
||||
onChange={(e) => handleRichterAendern(r.id, 'qualifikation', e.target.value)}
|
||||
sx={{fontSize: '11px'}}
|
||||
>
|
||||
{qualifikationen.map((q) => (
|
||||
<MenuItem key={q} value={q} sx={{fontSize: '11px'}}>{q}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableCell sx={{py: 1}}>
|
||||
<Select
|
||||
size="small"
|
||||
fullWidth
|
||||
value={r.funktion}
|
||||
onChange={(e) => handleRichterAendern(r.id, 'funktion', e.target.value)}
|
||||
sx={{fontSize: '11px'}}
|
||||
>
|
||||
{richterfunktionen.map((f) => (
|
||||
<MenuItem key={f} value={f} sx={{fontSize: '11px'}}>{f}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableCell align="center" sx={{py: 1}}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleRichterLoeschen(r.id)}
|
||||
sx={{color: 'error.main'}}
|
||||
>
|
||||
<DeleteIcon sx={{fontSize: 16}}/>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Paper>
|
||||
|
||||
<Divider sx={{my: 4}}/>
|
||||
|
||||
{/* === PLÄTZE === */}
|
||||
<Typography variant="h6" sx={{fontSize: '13px', fontWeight: 600, mb: 3}}>
|
||||
Austragungsplätze
|
||||
</Typography>
|
||||
|
||||
<Paper variant="outlined" sx={{p: 2.5, mb: 3}}>
|
||||
<Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2}}>
|
||||
<Typography variant="subtitle2" sx={{fontSize: '11px', fontWeight: 600}}>
|
||||
Plätze & Anlagen
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<AddIcon/>}
|
||||
onClick={handlePlatzHinzufuegen}
|
||||
sx={{fontSize: '10px', px: 2}}
|
||||
>
|
||||
Platz hinzufügen
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, bgcolor: 'grey.50', width: 150}}>Sparte</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, bgcolor: 'grey.50', width: 150}}>Größe</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, bgcolor: 'grey.50'}}>Bezeichnung</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, bgcolor: 'grey.50', width: 60}}
|
||||
align="center">Aktion</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{plaetze.map((p) => (
|
||||
<TableRow key={p.id} hover>
|
||||
<TableCell sx={{py: 1}}>
|
||||
<Select
|
||||
size="small"
|
||||
fullWidth
|
||||
value={p.sparte}
|
||||
onChange={(e) => handlePlatzAendern(p.id, 'sparte', e.target.value)}
|
||||
sx={{fontSize: '11px'}}
|
||||
>
|
||||
{sparten.map((s) => (
|
||||
<MenuItem key={s} value={s} sx={{fontSize: '11px'}}>{s}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableCell sx={{py: 1}}>
|
||||
<Select
|
||||
size="small"
|
||||
fullWidth
|
||||
value={p.groesse}
|
||||
onChange={(e) => handlePlatzAendern(p.id, 'groesse', e.target.value)}
|
||||
sx={{fontSize: '11px'}}
|
||||
>
|
||||
{platzgroessen.map((g) => (
|
||||
<MenuItem key={g} value={g} sx={{fontSize: '11px'}}>{g}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableCell sx={{py: 1}}>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
value={p.bezeichnung}
|
||||
onChange={(e) => handlePlatzAendern(p.id, 'bezeichnung', e.target.value)}
|
||||
placeholder="z.B. Hauptplatz, Abreiteplatz 1..."
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.5}}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center" sx={{py: 1}}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handlePlatzLoeschen(p.id)}
|
||||
sx={{color: 'error.main'}}
|
||||
>
|
||||
<DeleteIcon sx={{fontSize: 16}}/>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Paper>
|
||||
|
||||
{/* Speichern Button */}
|
||||
<Box sx={{display: 'flex', justifyContent: 'flex-end', gap: 2, mt: 4}}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleSpeichern}
|
||||
sx={{fontSize: '11px', px: 4, py: 0.75}}
|
||||
>
|
||||
Speichern
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
import {useState} from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Button from '@mui/material/Button';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import Divider from '@mui/material/Divider';
|
||||
|
||||
interface Gebuehr {
|
||||
id: number;
|
||||
bezeichnung: string;
|
||||
betrag: string;
|
||||
pflicht: boolean;
|
||||
}
|
||||
|
||||
export function PreislisteTab() {
|
||||
// Nennungs- und Startgebühren
|
||||
const [nenngebuehrProPferd, setNenngebuehrProPferd] = useState('0.00');
|
||||
const [startgebuehrProBewerb, setStartgebuehrProBewerb] = useState('15.00');
|
||||
const [sporteuro, setSporteuro] = useState('0.00');
|
||||
const [nachnennungsgebuehr, setNachnennungsgebuehr] = useState('0.00');
|
||||
const [nennungstauschgebuehr, setNennungstauschgebuehr] = useState('0.00');
|
||||
|
||||
// Stallungen & Boxen
|
||||
const [boxenProTag, setBoxenProTag] = useState('0.00');
|
||||
const [einstreuErst, setEinstreuErst] = useState('0.00');
|
||||
const [einstreuNach, setEinstreuNach] = useState('0.00');
|
||||
const [paddockProTag, setPaddockProTag] = useState('0.00');
|
||||
|
||||
// Zusatzgebühren (dynamisch)
|
||||
const [zusatzgebuehren, setZusatzgebuehren] = useState<Gebuehr[]>([
|
||||
{id: 1, bezeichnung: 'Stromanschluss pro Tag', betrag: '5.00', pflicht: false},
|
||||
{id: 2, bezeichnung: 'Camping pro Nacht', betrag: '10.00', pflicht: false},
|
||||
]);
|
||||
|
||||
const handleZusatzgebuehrHinzufuegen = () => {
|
||||
const newId = Math.max(0, ...zusatzgebuehren.map(g => g.id)) + 1;
|
||||
setZusatzgebuehren([
|
||||
...zusatzgebuehren,
|
||||
{id: newId, bezeichnung: '', betrag: '0.00', pflicht: false}
|
||||
]);
|
||||
};
|
||||
|
||||
const handleZusatzgebuehrLoeschen = (id: number) => {
|
||||
setZusatzgebuehren(zusatzgebuehren.filter(g => g.id !== id));
|
||||
};
|
||||
|
||||
const handleZusatzgebuehrAendern = (id: number, field: keyof Gebuehr, value: string | boolean) => {
|
||||
setZusatzgebuehren(zusatzgebuehren.map(g =>
|
||||
g.id === id ? {...g, [field]: value} : g
|
||||
));
|
||||
};
|
||||
|
||||
const handleSpeichern = () => {
|
||||
console.log('Preisliste speichern:', {
|
||||
nenngebuehrProPferd,
|
||||
startgebuehrProBewerb,
|
||||
sporteuro,
|
||||
nachnennungsgebuehr,
|
||||
nennungstauschgebuehr,
|
||||
boxenProTag,
|
||||
einstreuErst,
|
||||
einstreuNach,
|
||||
paddockProTag,
|
||||
zusatzgebuehren,
|
||||
});
|
||||
// TODO: Backend Integration
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{p: 3, bgcolor: 'background.default', height: '100%', overflowY: 'auto'}}>
|
||||
<Box sx={{maxWidth: 900}}>
|
||||
<Typography variant="h6" sx={{fontSize: '13px', fontWeight: 600, mb: 3}}>
|
||||
Nennungen & Gebühren
|
||||
</Typography>
|
||||
|
||||
{/* Nennungs- und Startgebühren */}
|
||||
<Paper variant="outlined" sx={{p: 2.5, mb: 3}}>
|
||||
<Typography variant="body2" sx={{fontSize: '11px', fontWeight: 600, mb: 2}}>
|
||||
Nennungs- und Startgebühren
|
||||
</Typography>
|
||||
|
||||
<Box sx={{display: 'flex', flexDirection: 'column', gap: 2}}>
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 240, fontSize: '11px'}}>
|
||||
Nenngebühr pro Pferd/Reiter:
|
||||
</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
value={nenngebuehrProPferd}
|
||||
onChange={(e) => setNenngebuehrProPferd(e.target.value)}
|
||||
sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}}
|
||||
InputProps={{endAdornment: '€'}}
|
||||
/>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'text.secondary', fontStyle: 'italic'}}>
|
||||
(Grundgebühr unabhängig von Anzahl Bewerben)
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 240, fontSize: '11px'}}>
|
||||
Startgebühr pro Bewerb:
|
||||
</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
value={startgebuehrProBewerb}
|
||||
onChange={(e) => setStartgebuehrProBewerb(e.target.value)}
|
||||
sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}}
|
||||
InputProps={{endAdornment: '€'}}
|
||||
/>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'text.secondary', fontStyle: 'italic'}}>
|
||||
(Pro einzelner Prüfung)
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 240, fontSize: '11px'}}>
|
||||
Sporteuro (Beitrag OEPS):
|
||||
</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
value={sporteuro}
|
||||
onChange={(e) => setSporteuro(e.target.value)}
|
||||
sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}}
|
||||
InputProps={{endAdornment: '€'}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{my: 1}}/>
|
||||
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 240, fontSize: '11px'}}>
|
||||
Nachnennungsgebühr:
|
||||
</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
value={nachnennungsgebuehr}
|
||||
onChange={(e) => setNachnennungsgebuehr(e.target.value)}
|
||||
sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}}
|
||||
InputProps={{endAdornment: '€'}}
|
||||
/>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'text.secondary', fontStyle: 'italic'}}>
|
||||
(Nach Nennschluss)
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 240, fontSize: '11px'}}>
|
||||
Nennungstausch-Gebühr:
|
||||
</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
value={nennungstauschgebuehr}
|
||||
onChange={(e) => setNennungstauschgebuehr(e.target.value)}
|
||||
sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}}
|
||||
InputProps={{endAdornment: '€'}}
|
||||
/>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'text.secondary', fontStyle: 'italic'}}>
|
||||
(Pferd- oder Reiter-Wechsel)
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Stallungen & Boxen */}
|
||||
<Paper variant="outlined" sx={{p: 2.5, mb: 3}}>
|
||||
<Typography variant="body2" sx={{fontSize: '11px', fontWeight: 600, mb: 2}}>
|
||||
Stallungen & Boxen
|
||||
</Typography>
|
||||
|
||||
<Box sx={{display: 'flex', flexDirection: 'column', gap: 2}}>
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 240, fontSize: '11px'}}>
|
||||
Box pro Tag:
|
||||
</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
value={boxenProTag}
|
||||
onChange={(e) => setBoxenProTag(e.target.value)}
|
||||
sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}}
|
||||
InputProps={{endAdornment: '€'}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 240, fontSize: '11px'}}>
|
||||
Einstreu (Erst-Einstreu):
|
||||
</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
value={einstreuErst}
|
||||
onChange={(e) => setEinstreuErst(e.target.value)}
|
||||
sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}}
|
||||
InputProps={{endAdornment: '€'}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 240, fontSize: '11px'}}>
|
||||
Einstreu (Nachlegen):
|
||||
</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
value={einstreuNach}
|
||||
onChange={(e) => setEinstreuNach(e.target.value)}
|
||||
sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}}
|
||||
InputProps={{endAdornment: '€'}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 240, fontSize: '11px'}}>
|
||||
Paddock pro Tag:
|
||||
</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
value={paddockProTag}
|
||||
onChange={(e) => setPaddockProTag(e.target.value)}
|
||||
sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}}
|
||||
InputProps={{endAdornment: '€'}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Zusatzgebühren */}
|
||||
<Paper variant="outlined" sx={{p: 2.5, mb: 3}}>
|
||||
<Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2}}>
|
||||
<Typography variant="body2" sx={{fontSize: '11px', fontWeight: 600}}>
|
||||
Zusatzgebühren
|
||||
</Typography>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<AddIcon/>}
|
||||
onClick={handleZusatzgebuehrHinzufuegen}
|
||||
sx={{fontSize: '10px'}}
|
||||
>
|
||||
Hinzufügen
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600}}>Bezeichnung</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, width: 140}}>Betrag</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, width: 100}}>Pflicht</TableCell>
|
||||
<TableCell sx={{width: 50}}></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{zusatzgebuehren.map((gebuehr) => (
|
||||
<TableRow key={gebuehr.id}>
|
||||
<TableCell sx={{py: 1}}>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
value={gebuehr.bezeichnung}
|
||||
onChange={(e) => handleZusatzgebuehrAendern(gebuehr.id, 'bezeichnung', e.target.value)}
|
||||
placeholder="z.B. Stromanschluss"
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.5}}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell sx={{py: 1}}>
|
||||
<TextField
|
||||
size="small"
|
||||
value={gebuehr.betrag}
|
||||
onChange={(e) => handleZusatzgebuehrAendern(gebuehr.id, 'betrag', e.target.value)}
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.5, textAlign: 'right'}}}
|
||||
InputProps={{endAdornment: '€'}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell sx={{py: 1}}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={gebuehr.pflicht}
|
||||
onChange={(e) => handleZusatzgebuehrAendern(gebuehr.id, 'pflicht', e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label={<Typography sx={{fontSize: '10px'}}>Pflicht</Typography>}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell sx={{py: 1}}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleZusatzgebuehrLoeschen(gebuehr.id)}
|
||||
>
|
||||
<DeleteIcon sx={{fontSize: 16}}/>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{zusatzgebuehren.length === 0 && (
|
||||
<Box sx={{textAlign: 'center', py: 3}}>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'text.secondary'}}>
|
||||
Keine Zusatzgebühren definiert
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{/* Hinweis */}
|
||||
<Paper variant="outlined" sx={{p: 2, mb: 3, bgcolor: '#f5f5f5'}}>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', mb: 1, fontWeight: 600}}>
|
||||
ℹ️ Hinweis zur Preisliste
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', lineHeight: 1.6}}>
|
||||
Die Gebührenstruktur wird in der offiziellen Ausschreibung veröffentlicht und ist für alle Teilnehmer
|
||||
verbindlich. Bei nationalen Turnieren der Kategorie C-Neu sind oft reduzierte Gebühren oder
|
||||
Gebührenbefreiungen
|
||||
üblich (z.B. kein Nenngeld, kein Sporteuro).
|
||||
</Typography>
|
||||
</Paper>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Box sx={{display: 'flex', gap: 2, justifyContent: 'flex-end'}}>
|
||||
<Button variant="outlined" size="small" sx={{fontSize: '11px', px: 3}}>
|
||||
Zurücksetzen
|
||||
</Button>
|
||||
<Button variant="contained" size="small" onClick={handleSpeichern} sx={{fontSize: '11px', px: 3}}>
|
||||
Speichern
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,831 @@
|
||||
import {useState} from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Button from '@mui/material/Button';
|
||||
import Radio from '@mui/material/Radio';
|
||||
import RadioGroup from '@mui/material/RadioGroup';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Select from '@mui/material/Select';
|
||||
import Autocomplete from '@mui/material/Autocomplete';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import {DatePicker} from '@mui/x-date-pickers/DatePicker';
|
||||
import {LocalizationProvider} from '@mui/x-date-pickers/LocalizationProvider';
|
||||
import {AdapterDateFns} from '@mui/x-date-pickers/AdapterDateFns';
|
||||
import {de} from 'date-fns/locale';
|
||||
import FolderOpenIcon from '@mui/icons-material/FolderOpen';
|
||||
|
||||
// Kategorien basierend auf Screenshot
|
||||
const kategorienDressur = [
|
||||
'CDN-A', 'CDN-A*', 'CDN-B', 'CDN-B*', 'CDN-C', 'CDN-C-Neu', 'CDNP-B', 'CDNP-C', 'CDNP-C-Neu'
|
||||
];
|
||||
|
||||
const kategorienSpringen = [
|
||||
'CSN-A', 'CSN-A*', 'CSN-B', 'CSN-B*', 'CSN-C', 'CSN-C-Neu', 'CSNP-A', 'CSNP-B', 'CSNP-C', 'CSNP-C-Neu'
|
||||
];
|
||||
|
||||
// Mock-Daten für Vereine (später vom Backend)
|
||||
const mockVereine = [
|
||||
'RFV Neumarkt/Hausruck',
|
||||
'Reitclub Wien',
|
||||
'Reitverein Salzburg',
|
||||
'Pferdesportverband OÖ',
|
||||
'RC Linz',
|
||||
];
|
||||
|
||||
interface StammdatenTabProps {
|
||||
turnierId?: string;
|
||||
}
|
||||
|
||||
export function StammdatenTab({turnierId}: StammdatenTabProps) {
|
||||
const [turniernummer, setTurniernummer] = useState('');
|
||||
const [turniernummerError, setTurniernummerError] = useState('');
|
||||
const [turnierTitel, setTurnierTitel] = useState('');
|
||||
const [kommentar, setKommentar] = useState('');
|
||||
const [typ, setTyp] = useState('national');
|
||||
const [sprache, setSprache] = useState('deutsch');
|
||||
|
||||
// Sparten (kombinierbar)
|
||||
const [sparteDressur, setSparteDressur] = useState(false);
|
||||
const [sparteSpringen, setSparteSpringen] = useState(false);
|
||||
|
||||
// Klassen (kombinierbar!)
|
||||
const [klasseC, setKlasseC] = useState(false);
|
||||
const [klasseB, setKlasseB] = useState(false);
|
||||
const [klasseA, setKlasseA] = useState(false);
|
||||
|
||||
// Kategorien (Mehrfachauswahl!)
|
||||
const [selectedKategorien, setSelectedKategorien] = useState<string[]>([]);
|
||||
|
||||
const [datumVon, setDatumVon] = useState<Date | null>(null);
|
||||
const [datumBis, setDatumBis] = useState<Date | null>(null);
|
||||
const [verein, setVerein] = useState<string | null>(null);
|
||||
const [logo, setLogo] = useState('');
|
||||
|
||||
// Vorschau nach Speichern
|
||||
const [showVorschau, setShowVorschau] = useState(false);
|
||||
|
||||
// Bestätigungsdialog für Initialisierung
|
||||
const [showInitDialog, setShowInitDialog] = useState(false);
|
||||
|
||||
// Initialisierungs-Status: Turnier ist initialisiert, wenn eine Turnier-Nr. vorhanden ist
|
||||
const istNeu = turnierId === 'neu';
|
||||
const [turniernummerBestaetigt, setTurniernummerBestaetigt] = useState(false);
|
||||
const istInitialisiert = !istNeu || turniernummerBestaetigt;
|
||||
|
||||
// Turniernummer validieren
|
||||
const validateTurniernummer = (value: string) => {
|
||||
if (value.length === 0) {
|
||||
setTurniernummerError('');
|
||||
return;
|
||||
}
|
||||
if (!/^\d+$/.test(value)) {
|
||||
setTurniernummerError('Nur Zahlen erlaubt');
|
||||
return;
|
||||
}
|
||||
if (value.length !== 5) {
|
||||
setTurniernummerError('Muss 5-stellig sein');
|
||||
return;
|
||||
}
|
||||
setTurniernummerError('');
|
||||
};
|
||||
|
||||
// Verfügbare Kategorien basierend auf Sparte UND Klasse
|
||||
const verfuegbareKategorien = (() => {
|
||||
const kategorien: string[] = [];
|
||||
|
||||
// Sparte bestimmt die Basis-Kategorien
|
||||
const basisKategorien: string[] = [];
|
||||
if (sparteDressur) basisKategorien.push(...kategorienDressur);
|
||||
if (sparteSpringen) basisKategorien.push(...kategorienSpringen);
|
||||
|
||||
// Filter nach Klassen (C, B, A)
|
||||
const selectedKlassen: string[] = [];
|
||||
if (klasseC) selectedKlassen.push('C', 'C-Neu');
|
||||
if (klasseB) selectedKlassen.push('B', 'B*');
|
||||
if (klasseA) selectedKlassen.push('A', 'A*');
|
||||
|
||||
if (selectedKlassen.length > 0 && basisKategorien.length > 0) {
|
||||
return basisKategorien.filter(kat => {
|
||||
// Extrahiere die Klasse aus der Kategorie (z.B. "CSN-C-Neu" -> "C-Neu")
|
||||
const match = kat.match(/-(C-Neu|C|B\*|B|A\*|A)$/i);
|
||||
if (match) {
|
||||
const katKlasse = match[1].toUpperCase();
|
||||
return selectedKlassen.some(k => k.toUpperCase() === katKlasse);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
})();
|
||||
|
||||
const handleKategorieToggle = (kategorie: string) => {
|
||||
if (!istInitialisiert) return;
|
||||
setSelectedKategorien(prev =>
|
||||
prev.includes(kategorie)
|
||||
? prev.filter(k => k !== kategorie)
|
||||
: [...prev, kategorie]
|
||||
);
|
||||
};
|
||||
|
||||
const handleInitialisieren = () => {
|
||||
if (turniernummer.trim().length !== 5 || turniernummerError) return;
|
||||
console.log('Turnier initialisieren mit Nr.:', turniernummer);
|
||||
// TODO: Backend-Call zur Datenbank-Initialisierung
|
||||
setTurniernummerBestaetigt(true);
|
||||
};
|
||||
|
||||
const handleZuruecksetzen = () => {
|
||||
setTurniernummer('');
|
||||
setTurniernummerError('');
|
||||
setTurnierTitel('');
|
||||
setKommentar('');
|
||||
setTyp('national');
|
||||
setSprache('deutsch');
|
||||
setSparteDressur(false);
|
||||
setSparteSpringen(false);
|
||||
setKlasseC(false);
|
||||
setKlasseB(false);
|
||||
setKlasseA(false);
|
||||
setSelectedKategorien([]);
|
||||
setDatumVon(null);
|
||||
setDatumBis(null);
|
||||
setVerein(null);
|
||||
setLogo('');
|
||||
setShowVorschau(false);
|
||||
};
|
||||
|
||||
const handleSpeichern = () => {
|
||||
console.log('Turnier speichern:', {
|
||||
turniernummer,
|
||||
turnierTitel,
|
||||
kommentar,
|
||||
typ,
|
||||
sprache,
|
||||
sparteDressur,
|
||||
sparteSpringen,
|
||||
klasseC,
|
||||
klasseB,
|
||||
klasseA,
|
||||
selectedKategorien,
|
||||
datumVon,
|
||||
datumBis,
|
||||
verein,
|
||||
logo,
|
||||
});
|
||||
// TODO: Backend Integration
|
||||
setShowVorschau(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={de}>
|
||||
<Box sx={{height: '100%', bgcolor: 'background.default', p: 3, overflowY: 'auto'}}>
|
||||
{/* Vorschau nach Speichern (oben zentral) */}
|
||||
{showVorschau && (
|
||||
<Paper elevation={3} sx={{p: 3, mb: 3, maxWidth: 800, mx: 'auto', borderRadius: 2}}>
|
||||
<Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2}}>
|
||||
<Typography variant="h6" sx={{fontSize: '13px', fontWeight: 600}}>
|
||||
✅ Turnier gespeichert
|
||||
</Typography>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => setShowVorschau(false)}
|
||||
sx={{fontSize: '10px', minWidth: 'auto', px: 1}}
|
||||
>
|
||||
Schließen
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Paper elevation={2} sx={{p: 2.5, borderRadius: 2, bgcolor: 'grey.50'}}>
|
||||
{/* Turniernummer Badge */}
|
||||
<Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2}}>
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: 'primary.main',
|
||||
color: 'white',
|
||||
px: 1.5,
|
||||
py: 0.5,
|
||||
borderRadius: 1,
|
||||
fontSize: '11px',
|
||||
fontWeight: 600
|
||||
}}
|
||||
>
|
||||
{turniernummer}
|
||||
</Box>
|
||||
<Box sx={{display: 'flex', gap: 0.5, flexWrap: 'wrap', justifyContent: 'flex-end'}}>
|
||||
{selectedKategorien.slice(0, 5).map((kat, idx) => (
|
||||
<Box
|
||||
key={idx}
|
||||
sx={{
|
||||
bgcolor: 'grey.200',
|
||||
px: 1,
|
||||
py: 0.25,
|
||||
borderRadius: 0.5,
|
||||
fontSize: '9px'
|
||||
}}
|
||||
>
|
||||
{kat}
|
||||
</Box>
|
||||
))}
|
||||
{selectedKategorien.length > 5 && (
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: 'grey.300',
|
||||
px: 1,
|
||||
py: 0.25,
|
||||
borderRadius: 0.5,
|
||||
fontSize: '9px'
|
||||
}}
|
||||
>
|
||||
+{selectedKategorien.length - 5}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Turnier-Titel */}
|
||||
<Typography variant="h6" sx={{fontSize: '14px', fontWeight: 600, mb: 1.5}}>
|
||||
{turnierTitel || 'Frühjahrs-Turnier 2026'}
|
||||
</Typography>
|
||||
|
||||
{/* Sparten & Klassen */}
|
||||
<Box sx={{display: 'flex', gap: 1, flexWrap: 'wrap', mb: 2}}>
|
||||
{sparteDressur && (
|
||||
<Box
|
||||
sx={{bgcolor: 'info.100', color: 'info.dark', px: 1.5, py: 0.5, borderRadius: 1, fontSize: '10px'}}>
|
||||
🏇 Dressur
|
||||
</Box>
|
||||
)}
|
||||
{sparteSpringen && (
|
||||
<Box sx={{
|
||||
bgcolor: 'success.100',
|
||||
color: 'success.dark',
|
||||
px: 1.5,
|
||||
py: 0.5,
|
||||
borderRadius: 1,
|
||||
fontSize: '10px'
|
||||
}}>
|
||||
🐴 Springen
|
||||
</Box>
|
||||
)}
|
||||
{klasseC && (
|
||||
<Box sx={{
|
||||
bgcolor: 'warning.100',
|
||||
color: 'warning.dark',
|
||||
px: 1.5,
|
||||
py: 0.5,
|
||||
borderRadius: 1,
|
||||
fontSize: '10px'
|
||||
}}>
|
||||
Klasse C
|
||||
</Box>
|
||||
)}
|
||||
{klasseB && (
|
||||
<Box sx={{
|
||||
bgcolor: 'warning.100',
|
||||
color: 'warning.dark',
|
||||
px: 1.5,
|
||||
py: 0.5,
|
||||
borderRadius: 1,
|
||||
fontSize: '10px'
|
||||
}}>
|
||||
Klasse B
|
||||
</Box>
|
||||
)}
|
||||
{klasseA && (
|
||||
<Box sx={{
|
||||
bgcolor: 'warning.100',
|
||||
color: 'warning.dark',
|
||||
px: 1.5,
|
||||
py: 0.5,
|
||||
borderRadius: 1,
|
||||
fontSize: '10px'
|
||||
}}>
|
||||
Klasse A
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Details */}
|
||||
<Box sx={{display: 'flex', gap: 3, flexWrap: 'wrap'}}>
|
||||
{(datumVon || datumBis) && (
|
||||
<Typography variant="body2" sx={{fontSize: '11px', color: 'text.secondary'}}>
|
||||
📅 {datumVon?.toLocaleDateString('de-DE') || '...'} - {datumBis?.toLocaleDateString('de-DE') || '...'}
|
||||
</Typography>
|
||||
)}
|
||||
{verein && (
|
||||
<Typography variant="body2" sx={{fontSize: '11px', color: 'text.secondary'}}>
|
||||
🏛️ {verein}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Kommentar */}
|
||||
{kommentar && (
|
||||
<Typography variant="body2" sx={{
|
||||
fontSize: '10px',
|
||||
color: 'text.secondary',
|
||||
mt: 2,
|
||||
pt: 2,
|
||||
borderTop: 1,
|
||||
borderColor: 'divider',
|
||||
fontStyle: 'italic'
|
||||
}}>
|
||||
{kommentar}
|
||||
</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* Formular (volle Breite) */}
|
||||
<Box sx={{maxWidth: 800, mx: 'auto', display: 'flex', flexDirection: 'column', gap: 2.5}}>
|
||||
{/* Hinweis für neue Veranstaltung */}
|
||||
{istNeu && !istInitialisiert && (
|
||||
<Paper sx={{p: 2, mb: 2, bgcolor: 'info.50', borderLeft: 3, borderColor: 'info.main'}}>
|
||||
<Typography variant="body2" sx={{fontSize: '11px', fontWeight: 600, mb: 0.5}}>
|
||||
🔑 Turnier-Nummer erforderlich
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', lineHeight: 1.6}}>
|
||||
Bitte geben Sie zuerst eine <strong>5-stellige Turnier-Nummer</strong> ein und klicken Sie auf
|
||||
"Initialisieren".
|
||||
Diese eindeutige Nummer wird vom ÖPSS vergeben und dient als Schlüssel für die
|
||||
Datenbank-Initialisierung.
|
||||
</Typography>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* Turniernummer mit Initialisieren-Button */}
|
||||
<Box sx={{display: 'flex', alignItems: 'flex-start', gap: 2}}>
|
||||
<Typography variant="body2" sx={{
|
||||
minWidth: 180,
|
||||
fontSize: '11px',
|
||||
fontWeight: istNeu && !istInitialisiert ? 600 : 400,
|
||||
mt: 1
|
||||
}}>
|
||||
Turnier-Nr.: {istNeu && !istInitialisiert && <span style={{color: 'red'}}>*</span>}
|
||||
</Typography>
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2, flex: 1}}>
|
||||
<TextField
|
||||
size="small"
|
||||
value={turniernummer}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
// Nur Zahlen erlauben, maximal 5 Stellen
|
||||
if (value === '' || (/^\d+$/.test(value) && value.length <= 5)) {
|
||||
setTurniernummer(value);
|
||||
validateTurniernummer(value);
|
||||
}
|
||||
}}
|
||||
placeholder="z.B. 26128"
|
||||
autoFocus={istNeu}
|
||||
disabled={istInitialisiert && istNeu}
|
||||
error={!!turniernummerError}
|
||||
helperText={turniernummerError}
|
||||
sx={{
|
||||
width: 150,
|
||||
'& .MuiInputBase-input': {
|
||||
fontSize: '11px',
|
||||
py: 0.75,
|
||||
fontWeight: istNeu && !istInitialisiert ? 600 : 400,
|
||||
},
|
||||
'& .MuiOutlinedInput-root': {
|
||||
bgcolor: istNeu && !istInitialisiert ? 'info.50' : 'background.paper'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{istNeu && !istInitialisiert && (
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => setShowInitDialog(true)}
|
||||
disabled={turniernummer.trim().length !== 5 || !!turniernummerError}
|
||||
sx={{fontSize: '11px', px: 2.5, py: 0.75}}
|
||||
>
|
||||
Initialisieren
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Typ */}
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 180, fontSize: '11px'}}>
|
||||
Typ:
|
||||
</Typography>
|
||||
<FormControl disabled={!istInitialisiert}>
|
||||
<RadioGroup
|
||||
row
|
||||
value={typ}
|
||||
onChange={(e) => setTyp(e.target.value)}
|
||||
>
|
||||
<FormControlLabel
|
||||
value="national"
|
||||
control={<Radio size="small"/>}
|
||||
label={<Typography sx={{fontSize: '11px'}}>National</Typography>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="international"
|
||||
control={<Radio size="small"/>}
|
||||
label={<Typography sx={{fontSize: '11px', color: 'text.disabled'}}>International</Typography>}
|
||||
disabled
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<Typography variant="body2" sx={{fontSize: '9px', color: 'text.secondary', fontStyle: 'italic'}}>
|
||||
(kommt später)
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Sprache */}
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 180, fontSize: '11px'}}>
|
||||
Sprache:
|
||||
</Typography>
|
||||
<FormControl disabled={!istInitialisiert}>
|
||||
<RadioGroup
|
||||
row
|
||||
value={sprache}
|
||||
onChange={(e) => setSprache(e.target.value)}
|
||||
>
|
||||
<FormControlLabel
|
||||
value="deutsch"
|
||||
control={<Radio size="small"/>}
|
||||
label={<Typography sx={{fontSize: '11px'}}>Deutsch</Typography>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="english"
|
||||
control={<Radio size="small"/>}
|
||||
label={<Typography sx={{fontSize: '11px', color: 'text.disabled'}}>English</Typography>}
|
||||
disabled
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<Typography variant="body2" sx={{fontSize: '9px', color: 'text.secondary', fontStyle: 'italic'}}>
|
||||
(kommt später)
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Sparten */}
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 180, fontSize: '11px'}}>
|
||||
Sparten:
|
||||
</Typography>
|
||||
<Box sx={{display: 'flex', gap: 2}}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={sparteDressur}
|
||||
onChange={(e) => {
|
||||
setSparteDressur(e.target.checked);
|
||||
setSelectedKategorien([]);
|
||||
}}
|
||||
disabled={!istInitialisiert}
|
||||
/>
|
||||
}
|
||||
label={<Typography sx={{fontSize: '11px'}}>Dressur</Typography>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={sparteSpringen}
|
||||
onChange={(e) => {
|
||||
setSparteSpringen(e.target.checked);
|
||||
setSelectedKategorien([]);
|
||||
}}
|
||||
disabled={!istInitialisiert}
|
||||
/>
|
||||
}
|
||||
label={<Typography sx={{fontSize: '11px'}}>Springen</Typography>}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Klassen */}
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 180, fontSize: '11px'}}>
|
||||
Klassen:
|
||||
</Typography>
|
||||
<Box sx={{display: 'flex', gap: 2}}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={klasseC}
|
||||
onChange={(e) => {
|
||||
setKlasseC(e.target.checked);
|
||||
setSelectedKategorien([]);
|
||||
}}
|
||||
disabled={!istInitialisiert}
|
||||
/>
|
||||
}
|
||||
label={<Typography sx={{fontSize: '11px'}}>C</Typography>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={klasseB}
|
||||
onChange={(e) => {
|
||||
setKlasseB(e.target.checked);
|
||||
setSelectedKategorien([]);
|
||||
}}
|
||||
disabled={!istInitialisiert}
|
||||
/>
|
||||
}
|
||||
label={<Typography sx={{fontSize: '11px'}}>B</Typography>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={klasseA}
|
||||
onChange={(e) => {
|
||||
setKlasseA(e.target.checked);
|
||||
setSelectedKategorien([]);
|
||||
}}
|
||||
disabled={!istInitialisiert}
|
||||
/>
|
||||
}
|
||||
label={<Typography sx={{fontSize: '11px'}}>A</Typography>}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Kategorien (Mehrfachauswahl) */}
|
||||
<Box sx={{display: 'flex', alignItems: 'flex-start', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 180, fontSize: '11px', mt: 1}}>
|
||||
Kategorien:
|
||||
</Typography>
|
||||
<Paper variant="outlined" sx={{
|
||||
p: 1.5,
|
||||
flex: 1,
|
||||
maxHeight: 200,
|
||||
overflow: 'auto',
|
||||
bgcolor: !istInitialisiert || verfuegbareKategorien.length === 0 ? 'action.disabledBackground' : 'background.paper'
|
||||
}}>
|
||||
{verfuegbareKategorien.length > 0 ? (
|
||||
<FormGroup>
|
||||
{verfuegbareKategorien.map((kategorie) => (
|
||||
<FormControlLabel
|
||||
key={kategorie}
|
||||
control={
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={selectedKategorien.includes(kategorie)}
|
||||
onChange={() => handleKategorieToggle(kategorie)}
|
||||
disabled={!istInitialisiert}
|
||||
/>
|
||||
}
|
||||
label={<Typography sx={{fontSize: '11px'}}>{kategorie}</Typography>}
|
||||
sx={{mb: 0.25}}
|
||||
/>
|
||||
))}
|
||||
</FormGroup>
|
||||
) : (
|
||||
<Typography variant="body2" sx={{
|
||||
fontSize: '10px',
|
||||
color: 'text.secondary',
|
||||
fontStyle: 'italic',
|
||||
textAlign: 'center',
|
||||
py: 2
|
||||
}}>
|
||||
{!sparteDressur && !sparteSpringen
|
||||
? 'Bitte Sparte(n) auswählen'
|
||||
: !klasseC && !klasseB && !klasseA
|
||||
? 'Bitte Klasse(n) auswählen'
|
||||
: 'Keine Kategorien verfügbar'}
|
||||
</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
{/* Datum von und bis in einer Zeile */}
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 180, fontSize: '11px'}}>
|
||||
Datum:
|
||||
</Typography>
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 1.5}}>
|
||||
<DatePicker
|
||||
value={datumVon}
|
||||
onChange={(newValue) => setDatumVon(newValue)}
|
||||
disabled={!istInitialisiert}
|
||||
slotProps={{
|
||||
textField: {
|
||||
size: 'small',
|
||||
placeholder: 'von',
|
||||
sx: {width: 160, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Typography variant="body2" sx={{fontSize: '11px'}}>bis</Typography>
|
||||
<DatePicker
|
||||
value={datumBis}
|
||||
onChange={(newValue) => setDatumBis(newValue)}
|
||||
disabled={!istInitialisiert}
|
||||
minDate={datumVon || undefined}
|
||||
slotProps={{
|
||||
textField: {
|
||||
size: 'small',
|
||||
placeholder: 'bis',
|
||||
sx: {width: 160, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Verein */}
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 180, fontSize: '11px'}}>
|
||||
Verein:
|
||||
</Typography>
|
||||
<Autocomplete
|
||||
size="small"
|
||||
fullWidth
|
||||
value={verein}
|
||||
onChange={(e, newValue) => setVerein(newValue)}
|
||||
disabled={!istInitialisiert}
|
||||
options={mockVereine}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
placeholder="Suche Verein..."
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Logo */}
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 180, fontSize: '11px'}}>
|
||||
Logo:
|
||||
</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
value={logo}
|
||||
onChange={(e) => setLogo(e.target.value)}
|
||||
disabled={!istInitialisiert}
|
||||
placeholder="Logo-Datei auswählen..."
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
disabled={!istInitialisiert}
|
||||
sx={{minWidth: 40, px: 1, fontSize: '10px'}}
|
||||
startIcon={<FolderOpenIcon sx={{fontSize: 14}}/>}
|
||||
>
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Turnier-Titel */}
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 180, fontSize: '11px'}}>
|
||||
Turnier-Titel:
|
||||
</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
value={turnierTitel}
|
||||
onChange={(e) => setTurnierTitel(e.target.value)}
|
||||
disabled={!istInitialisiert}
|
||||
placeholder="Frühjahrs-Turnier 2026"
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Kommentar */}
|
||||
<Box sx={{display: 'flex', alignItems: 'flex-start', gap: 2}}>
|
||||
<Typography variant="body2" sx={{minWidth: 180, fontSize: '11px', mt: 1}}>
|
||||
Kommentar:
|
||||
</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
value={kommentar}
|
||||
onChange={(e) => setKommentar(e.target.value)}
|
||||
disabled={!istInitialisiert}
|
||||
placeholder="z.B. KIDS CUP • PONY EINSTEIGER CUP OÖ"
|
||||
sx={{'& .MuiInputBase-input': {fontSize: '11px'}}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
gap: 2,
|
||||
justifyContent: 'flex-end',
|
||||
mt: 3,
|
||||
pt: 3,
|
||||
borderTop: 1,
|
||||
borderColor: 'divider'
|
||||
}}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={handleZuruecksetzen}
|
||||
sx={{fontSize: '11px', px: 3, py: 0.75}}
|
||||
>
|
||||
Zurücksetzen
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={handleSpeichern}
|
||||
disabled={!istInitialisiert}
|
||||
sx={{fontSize: '11px', px: 3, py: 0.75}}
|
||||
>
|
||||
Speichern
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Bestätigungsdialog für Initialisierung */}
|
||||
<Dialog
|
||||
open={showInitDialog}
|
||||
onClose={() => setShowInitDialog(false)}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle sx={{fontSize: '13px', fontWeight: 600}}>
|
||||
Turnier-Nummer bestätigen
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Paper sx={{p: 2, mb: 2, bgcolor: 'warning.50', borderLeft: 3, borderColor: 'warning.main'}}>
|
||||
<Typography variant="body2" sx={{fontSize: '11px', fontWeight: 600, mb: 1}}>
|
||||
⚠️ Wichtig
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', lineHeight: 1.6, mb: 1}}>
|
||||
Die Turnier-Nummer kann nach der Initialisierung <strong>nicht mehr geändert</strong> werden.
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', lineHeight: 1.6}}>
|
||||
Bitte überprüfen Sie die eingegebene Nummer sorgfältig.
|
||||
</Typography>
|
||||
</Paper>
|
||||
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 2, mt: 3}}>
|
||||
<Typography variant="body2" sx={{fontSize: '11px', fontWeight: 600}}>
|
||||
Turnier-Nr.:
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: 'primary.main',
|
||||
color: 'white',
|
||||
px: 2,
|
||||
py: 1,
|
||||
borderRadius: 1,
|
||||
fontSize: '16px',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.1em'
|
||||
}}
|
||||
>
|
||||
{turniernummer}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" sx={{fontSize: '11px', mt: 3, color: 'text.secondary'}}>
|
||||
Ist diese Turnier-Nummer korrekt?
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{p: 2, gap: 1}}>
|
||||
<Button
|
||||
onClick={() => setShowInitDialog(false)}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{fontSize: '11px', px: 3}}
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleInitialisieren();
|
||||
setShowInitDialog(false);
|
||||
}}
|
||||
variant="contained"
|
||||
size="small"
|
||||
sx={{fontSize: '11px', px: 3}}
|
||||
>
|
||||
Ja, initialisieren
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
</LocalizationProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
import {useState} from 'react';
|
||||
import {useParams} from 'react-router';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import FolderOpenIcon from '@mui/icons-material/FolderOpen';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import UploadIcon from '@mui/icons-material/Upload';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import UsbIcon from '@mui/icons-material/Usb';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
|
||||
import CloudDownloadIcon from '@mui/icons-material/CloudDownload';
|
||||
import EmojiEventsIcon from '@mui/icons-material/EmojiEvents';
|
||||
import CalendarTodayIcon from '@mui/icons-material/CalendarToday';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import {veranstaltungenData} from '../Dashboard';
|
||||
|
||||
export function TransferTab() {
|
||||
const {id} = useParams();
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedTurnierId, setSelectedTurnierId] = useState<string | null>(null);
|
||||
|
||||
// Veranstaltung laden
|
||||
const veranstaltung = id !== 'neu'
|
||||
? veranstaltungenData.find(v => v.id === parseInt(id || '0'))
|
||||
: null;
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, turnierId: string) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
setSelectedTurnierId(turnierId);
|
||||
};
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null);
|
||||
setSelectedTurnierId(null);
|
||||
};
|
||||
|
||||
const handleNeuesTurnier = () => {
|
||||
console.log('Neues Turnier erstellen für Veranstaltung:', id);
|
||||
// TODO: Dialog öffnen
|
||||
};
|
||||
|
||||
const handleImportZNS = (turnierId: string) => {
|
||||
console.log('Import ZNS N2-Daten für Turnier:', turnierId);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleExportZNS = (turnierId: string) => {
|
||||
console.log('Export ZNS für Turnier:', turnierId);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleImportUSB = (turnierId: string) => {
|
||||
console.log('Import von USB für Turnier:', turnierId);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleExportUSB = (turnierId: string) => {
|
||||
console.log('Export auf USB für Turnier:', turnierId);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleImportLokal = (turnierId: string) => {
|
||||
console.log('Import von lokaler Datei für Turnier:', turnierId);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleExportLokal = (turnierId: string) => {
|
||||
console.log('Export als lokale Datei für Turnier:', turnierId);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
if (!veranstaltung) {
|
||||
return (
|
||||
<Box sx={{p: 3}}>
|
||||
<Typography variant="body2" sx={{fontSize: '11px'}}>
|
||||
Veranstaltung nicht gefunden
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{p: 3, bgcolor: 'background.default', height: '100%', overflowY: 'auto'}}>
|
||||
<Box sx={{maxWidth: 1200}}>
|
||||
{/* Veranstaltungs-Info oben */}
|
||||
<Paper variant="outlined" sx={{p: 2.5, mb: 3, bgcolor: 'primary.50'}}>
|
||||
<Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start'}}>
|
||||
<Box sx={{flex: 1}}>
|
||||
<Typography variant="h6" sx={{fontSize: '13px', fontWeight: 600, mb: 1}}>
|
||||
{veranstaltung.name}
|
||||
</Typography>
|
||||
<Box sx={{display: 'flex', gap: 2}}>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'text.secondary'}}>
|
||||
📍 {veranstaltung.ort}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'text.secondary'}}>
|
||||
📅 {veranstaltung.datum}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'text.secondary'}}>
|
||||
🏆 {veranstaltung.turniere.length} Turniere
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Chip
|
||||
label={veranstaltung.status === 'vorbereitung' ? 'In Vorbereitung' : veranstaltung.status}
|
||||
color="info"
|
||||
size="small"
|
||||
sx={{fontSize: '9px'}}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Button: Neues Turnier */}
|
||||
<Box sx={{mb: 3, display: 'flex', justifyContent: 'center'}}>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
startIcon={<AddIcon/>}
|
||||
onClick={handleNeuesTurnier}
|
||||
sx={{fontSize: '11px', px: 4, py: 1.5}}
|
||||
>
|
||||
Neues Turnier
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Turniere dieser Veranstaltung */}
|
||||
<Typography variant="h6" sx={{fontSize: '12px', fontWeight: 600, mb: 2}}>
|
||||
Turniere dieser Veranstaltung
|
||||
</Typography>
|
||||
|
||||
<Box sx={{display: 'flex', flexDirection: 'column', gap: 2}}>
|
||||
{veranstaltung.turniere.map((turnier) => (
|
||||
<Card key={turnier.nr}>
|
||||
<CardContent>
|
||||
<Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2}}>
|
||||
<Box sx={{flex: 1}}>
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 1.5, mb: 1}}>
|
||||
<Chip
|
||||
label={turnier.nr}
|
||||
size="small"
|
||||
sx={{fontSize: '10px', fontWeight: 600, bgcolor: 'primary.main', color: 'white'}}
|
||||
/>
|
||||
<Typography variant="h6" sx={{fontSize: '13px', fontWeight: 600}}>
|
||||
{turnier.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{display: 'flex', gap: 2, mb: 1.5}}>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'text.secondary'}}>
|
||||
<CalendarTodayIcon sx={{fontSize: 11, verticalAlign: 'middle', mr: 0.5}}/>
|
||||
{turnier.datum}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'text.secondary'}}>
|
||||
<EmojiEventsIcon sx={{fontSize: 11, verticalAlign: 'middle', mr: 0.5}}/>
|
||||
{turnier.disziplin}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'text.secondary'}}>
|
||||
{turnier.bewerbeAnzahl} Bewerbe
|
||||
</Typography>
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 0.5}}>
|
||||
{(turnier.kategorie === 'B' || turnier.kategorie === 'A') && (
|
||||
turnier.znsStatus === 'geladen' ? (
|
||||
<>
|
||||
<CheckCircleIcon sx={{fontSize: 11, color: 'success.main'}}/>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'success.main'}}>
|
||||
ZNS N2-Daten geladen
|
||||
</Typography>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<WarningIcon sx={{fontSize: 11, color: 'warning.main'}}/>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'warning.main'}}>
|
||||
ZNS N2-Daten ausstehend
|
||||
</Typography>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => handleMenuOpen(e, turnier.nr)}
|
||||
>
|
||||
<MoreVertIcon sx={{fontSize: 18}}/>
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
{/* Actions für dieses Turnier */}
|
||||
<Box sx={{display: 'flex', gap: 1, flexWrap: 'wrap'}}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<FolderOpenIcon sx={{fontSize: 14}}/>}
|
||||
sx={{fontSize: '10px'}}
|
||||
>
|
||||
Öffnen
|
||||
</Button>
|
||||
|
||||
{(turnier.kategorie === 'B' || turnier.kategorie === 'A') && (
|
||||
<>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<CloudDownloadIcon sx={{fontSize: 14}}/>}
|
||||
onClick={() => handleImportZNS(turnier.nr)}
|
||||
sx={{fontSize: '10px'}}
|
||||
>
|
||||
ZNS N2 Import
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<CloudUploadIcon sx={{fontSize: 14}}/>}
|
||||
onClick={() => handleExportZNS(turnier.nr)}
|
||||
sx={{fontSize: '10px'}}
|
||||
>
|
||||
ZNS Export
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<UploadIcon sx={{fontSize: 14}}/>}
|
||||
onClick={() => handleImportLokal(turnier.nr)}
|
||||
sx={{fontSize: '10px'}}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<DownloadIcon sx={{fontSize: 14}}/>}
|
||||
onClick={() => handleExportLokal(turnier.nr)}
|
||||
sx={{fontSize: '10px'}}
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<UsbIcon sx={{fontSize: 14}}/>}
|
||||
onClick={() => handleImportUSB(turnier.nr)}
|
||||
sx={{fontSize: '10px'}}
|
||||
>
|
||||
USB
|
||||
</Button>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{veranstaltung.turniere.length === 0 && (
|
||||
<Paper variant="outlined" sx={{p: 4, textAlign: 'center'}}>
|
||||
<Typography variant="body2" sx={{fontSize: '11px', color: 'text.secondary', mb: 2}}>
|
||||
Noch keine Turniere für diese Veranstaltung angelegt
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon/>}
|
||||
onClick={handleNeuesTurnier}
|
||||
sx={{fontSize: '10px'}}
|
||||
>
|
||||
Erstes Turnier erstellen
|
||||
</Button>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* Context Menu */}
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
>
|
||||
<MenuItem onClick={() => selectedTurnierId && handleImportLokal(selectedTurnierId)} sx={{fontSize: '10px'}}>
|
||||
<UploadIcon sx={{fontSize: 14, mr: 1}}/>
|
||||
Import von lokaler Datei
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => selectedTurnierId && handleExportLokal(selectedTurnierId)} sx={{fontSize: '10px'}}>
|
||||
<DownloadIcon sx={{fontSize: 14, mr: 1}}/>
|
||||
Export als lokale Datei
|
||||
</MenuItem>
|
||||
<Divider/>
|
||||
<MenuItem onClick={() => selectedTurnierId && handleImportUSB(selectedTurnierId)} sx={{fontSize: '10px'}}>
|
||||
<UsbIcon sx={{fontSize: 14, mr: 1}}/>
|
||||
Import von USB-Stick
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => selectedTurnierId && handleExportUSB(selectedTurnierId)} sx={{fontSize: '10px'}}>
|
||||
<UsbIcon sx={{fontSize: 14, mr: 1}}/>
|
||||
Export auf USB-Stick
|
||||
</MenuItem>
|
||||
{selectedTurnierId && veranstaltung.turniere.find(t => t.nr === selectedTurnierId)?.kategorie !== 'C' && (
|
||||
<>
|
||||
<Divider/>
|
||||
<MenuItem onClick={() => selectedTurnierId && handleImportZNS(selectedTurnierId)} sx={{fontSize: '10px'}}>
|
||||
<CloudDownloadIcon sx={{fontSize: 14, mr: 1}}/>
|
||||
ZNS N2-Daten importieren
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => selectedTurnierId && handleExportZNS(selectedTurnierId)} sx={{fontSize: '10px'}}>
|
||||
<CloudUploadIcon sx={{fontSize: 14, mr: 1}}/>
|
||||
ZNS Ergebnisse exportieren
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
+347
@@ -0,0 +1,347 @@
|
||||
import {useState} from 'react';
|
||||
import {useParams, useNavigate} from 'react-router';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import FolderOpenIcon from '@mui/icons-material/FolderOpen';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import UploadIcon from '@mui/icons-material/Upload';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import UsbIcon from '@mui/icons-material/Usb';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
|
||||
import CloudDownloadIcon from '@mui/icons-material/CloudDownload';
|
||||
import EmojiEventsIcon from '@mui/icons-material/EmojiEvents';
|
||||
import CalendarTodayIcon from '@mui/icons-material/CalendarToday';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import {veranstaltungenData} from '../Dashboard';
|
||||
|
||||
export function VeranstaltungUebersicht() {
|
||||
const params = useParams();
|
||||
const id = params.id;
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [selectedTurnierId, setSelectedTurnierId] = useState<string | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Veranstaltung laden
|
||||
const veranstaltung = id !== 'neu'
|
||||
? veranstaltungenData.find(v => v.id === parseInt(id || '0'))
|
||||
: null;
|
||||
|
||||
// Wenn neu, zeige eine leere Ansicht für neue Veranstaltung
|
||||
if (id === 'neu') {
|
||||
return (
|
||||
<Box sx={{p: 3, bgcolor: 'background.default', height: '100%', overflowY: 'auto'}}>
|
||||
<Box sx={{maxWidth: 1200}}>
|
||||
<Paper variant="outlined" sx={{p: 2.5, mb: 3, bgcolor: 'info.50'}}>
|
||||
<Typography variant="h6" sx={{fontSize: '13px', fontWeight: 600, mb: 1}}>
|
||||
🆕 Neue Veranstaltung erstellen
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{fontSize: '11px', color: 'text.secondary'}}>
|
||||
Bitte wechseln Sie zu den Tabs "Stammdaten", "Organisation", "Bewerbe" oder "Preisliste", um die
|
||||
Veranstaltung zu konfigurieren.
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (!veranstaltung) {
|
||||
return (
|
||||
<Box sx={{p: 3}}>
|
||||
<Typography variant="body2" sx={{fontSize: '11px'}}>
|
||||
Veranstaltung nicht gefunden
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>, turnierId: string) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
setSelectedTurnierId(turnierId);
|
||||
};
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null);
|
||||
setSelectedTurnierId(null);
|
||||
};
|
||||
|
||||
const handleNeuesTurnier = () => {
|
||||
console.log('Neues Turnier erstellen für Veranstaltung:', id);
|
||||
navigate(`/veranstaltung/${id}/turnier/neu`);
|
||||
};
|
||||
|
||||
const handleImportZNS = (turnierId: string) => {
|
||||
console.log('Import ZNS N2-Daten für Turnier:', turnierId);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleExportZNS = (turnierId: string) => {
|
||||
console.log('Export ZNS für Turnier:', turnierId);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleImportUSB = (turnierId: string) => {
|
||||
console.log('Import von USB für Turnier:', turnierId);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleExportUSB = (turnierId: string) => {
|
||||
console.log('Export auf USB für Turnier:', turnierId);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleImportLokal = (turnierId: string) => {
|
||||
console.log('Import von lokaler Datei für Turnier:', turnierId);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleExportLokal = (turnierId: string) => {
|
||||
console.log('Export als lokale Datei für Turnier:', turnierId);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{p: 3, bgcolor: 'background.default', height: '100%', overflowY: 'auto'}}>
|
||||
<Box sx={{maxWidth: 1200}}>
|
||||
{/* Veranstaltungs-Info oben */}
|
||||
<Paper variant="outlined" sx={{p: 2.5, mb: 3, bgcolor: 'primary.50'}}>
|
||||
<Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start'}}>
|
||||
<Box sx={{flex: 1}}>
|
||||
<Typography variant="h6" sx={{fontSize: '13px', fontWeight: 600, mb: 1}}>
|
||||
{veranstaltung.name}
|
||||
</Typography>
|
||||
<Box sx={{display: 'flex', gap: 2}}>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'text.secondary'}}>
|
||||
📍 {veranstaltung.ort}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'text.secondary'}}>
|
||||
📅 {veranstaltung.datum}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'text.secondary'}}>
|
||||
🏆 {veranstaltung.turniere.length} Turniere
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Chip
|
||||
label={veranstaltung.status === 'vorbereitung' ? 'In Vorbereitung' : veranstaltung.status}
|
||||
color="info"
|
||||
size="small"
|
||||
sx={{fontSize: '9px'}}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Button: Neues Turnier */}
|
||||
<Box sx={{mb: 3, display: 'flex', justifyContent: 'center'}}>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
startIcon={<AddIcon/>}
|
||||
onClick={handleNeuesTurnier}
|
||||
sx={{fontSize: '11px', px: 4, py: 1.5}}
|
||||
>
|
||||
Neues Turnier
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Turniere dieser Veranstaltung */}
|
||||
<Typography variant="h6" sx={{fontSize: '12px', fontWeight: 600, mb: 2}}>
|
||||
Turniere dieser Veranstaltung
|
||||
</Typography>
|
||||
|
||||
<Box sx={{display: 'flex', flexDirection: 'column', gap: 2}}>
|
||||
{veranstaltung.turniere.map((turnier) => (
|
||||
<Card key={turnier.nr}>
|
||||
<CardContent>
|
||||
<Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2}}>
|
||||
<Box sx={{flex: 1}}>
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 1.5, mb: 1}}>
|
||||
<Chip
|
||||
label={turnier.nr}
|
||||
size="small"
|
||||
sx={{fontSize: '10px', fontWeight: 600, bgcolor: 'primary.main', color: 'white'}}
|
||||
/>
|
||||
<Typography variant="h6" sx={{fontSize: '13px', fontWeight: 600}}>
|
||||
{turnier.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{display: 'flex', gap: 2, mb: 1.5}}>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'text.secondary'}}>
|
||||
<CalendarTodayIcon sx={{fontSize: 11, verticalAlign: 'middle', mr: 0.5}}/>
|
||||
{turnier.datum}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'text.secondary'}}>
|
||||
<EmojiEventsIcon sx={{fontSize: 11, verticalAlign: 'middle', mr: 0.5}}/>
|
||||
{turnier.disziplin}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'text.secondary'}}>
|
||||
{turnier.bewerbeAnzahl} Bewerbe
|
||||
</Typography>
|
||||
<Box sx={{display: 'flex', alignItems: 'center', gap: 0.5}}>
|
||||
{(turnier.kategorie === 'B' || turnier.kategorie === 'A') && (
|
||||
turnier.znsStatus === 'geladen' ? (
|
||||
<>
|
||||
<CheckCircleIcon sx={{fontSize: 11, color: 'success.main'}}/>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'success.main'}}>
|
||||
ZNS N2-Daten geladen
|
||||
</Typography>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<WarningIcon sx={{fontSize: 11, color: 'warning.main'}}/>
|
||||
<Typography variant="body2" sx={{fontSize: '10px', color: 'warning.main'}}>
|
||||
ZNS N2-Daten ausstehend
|
||||
</Typography>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => handleMenuOpen(e, turnier.nr)}
|
||||
>
|
||||
<MoreVertIcon sx={{fontSize: 18}}/>
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
{/* Actions für dieses Turnier */}
|
||||
<Box sx={{display: 'flex', gap: 1, flexWrap: 'wrap'}}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<FolderOpenIcon sx={{fontSize: 14}}/>}
|
||||
onClick={() => navigate(`/veranstaltung/${id}/turnier/${turnier.nr}`)}
|
||||
sx={{fontSize: '10px'}}
|
||||
>
|
||||
Öffnen
|
||||
</Button>
|
||||
|
||||
{(turnier.kategorie === 'B' || turnier.kategorie === 'A') && (
|
||||
<>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<CloudDownloadIcon sx={{fontSize: 14}}/>}
|
||||
onClick={() => handleImportZNS(turnier.nr)}
|
||||
sx={{fontSize: '10px'}}
|
||||
>
|
||||
ZNS N2 Import
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<CloudUploadIcon sx={{fontSize: 14}}/>}
|
||||
onClick={() => handleExportZNS(turnier.nr)}
|
||||
sx={{fontSize: '10px'}}
|
||||
>
|
||||
ZNS Export
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<UploadIcon sx={{fontSize: 14}}/>}
|
||||
onClick={() => handleImportLokal(turnier.nr)}
|
||||
sx={{fontSize: '10px'}}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<DownloadIcon sx={{fontSize: 14}}/>}
|
||||
onClick={() => handleExportLokal(turnier.nr)}
|
||||
sx={{fontSize: '10px'}}
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<UsbIcon sx={{fontSize: 14}}/>}
|
||||
onClick={() => handleImportUSB(turnier.nr)}
|
||||
sx={{fontSize: '10px'}}
|
||||
>
|
||||
USB
|
||||
</Button>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{veranstaltung.turniere.length === 0 && (
|
||||
<Paper variant="outlined" sx={{p: 4, textAlign: 'center'}}>
|
||||
<Typography variant="body2" sx={{fontSize: '11px', color: 'text.secondary', mb: 2}}>
|
||||
Noch keine Turniere für diese Veranstaltung angelegt
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon/>}
|
||||
onClick={handleNeuesTurnier}
|
||||
sx={{fontSize: '10px'}}
|
||||
>
|
||||
Erstes Turnier erstellen
|
||||
</Button>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* Context Menu */}
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
>
|
||||
<MenuItem onClick={() => selectedTurnierId && handleImportLokal(selectedTurnierId)} sx={{fontSize: '10px'}}>
|
||||
<UploadIcon sx={{fontSize: 14, mr: 1}}/>
|
||||
Import von lokaler Datei
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => selectedTurnierId && handleExportLokal(selectedTurnierId)} sx={{fontSize: '10px'}}>
|
||||
<DownloadIcon sx={{fontSize: 14, mr: 1}}/>
|
||||
Export als lokale Datei
|
||||
</MenuItem>
|
||||
<Divider/>
|
||||
<MenuItem onClick={() => selectedTurnierId && handleImportUSB(selectedTurnierId)} sx={{fontSize: '10px'}}>
|
||||
<UsbIcon sx={{fontSize: 14, mr: 1}}/>
|
||||
Import von USB-Stick
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => selectedTurnierId && handleExportUSB(selectedTurnierId)} sx={{fontSize: '10px'}}>
|
||||
<UsbIcon sx={{fontSize: 14, mr: 1}}/>
|
||||
Export auf USB-Stick
|
||||
</MenuItem>
|
||||
{selectedTurnierId && veranstaltung.turniere.find(t => t.nr === selectedTurnierId)?.kategorie !== 'C' && (
|
||||
<>
|
||||
<Divider/>
|
||||
<MenuItem onClick={() => selectedTurnierId && handleImportZNS(selectedTurnierId)} sx={{fontSize: '10px'}}>
|
||||
<CloudDownloadIcon sx={{fontSize: 14, mr: 1}}/>
|
||||
ZNS N2-Daten importieren
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => selectedTurnierId && handleExportZNS(selectedTurnierId)} sx={{fontSize: '10px'}}>
|
||||
<CloudUploadIcon sx={{fontSize: 14, mr: 1}}/>
|
||||
ZNS Ergebnisse exportieren
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {createBrowserRouter} from 'react-router';
|
||||
import {Login} from './components/Login';
|
||||
import {AdminVerwaltung} from './components/Dashboard';
|
||||
import {TurnierErstellen} from './components/TurnierErstellen';
|
||||
import {TurnierAnsicht} from './components/TurnierAnsicht';
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
Component: Login,
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
Component: AdminVerwaltung,
|
||||
},
|
||||
{
|
||||
path: '/veranstaltung/:id',
|
||||
Component: TurnierErstellen,
|
||||
},
|
||||
{
|
||||
path: '/veranstaltung/:veranstaltungId/turnier/neu',
|
||||
Component: TurnierAnsicht,
|
||||
},
|
||||
{
|
||||
path: '/veranstaltung/:veranstaltungId/turnier/:nr',
|
||||
Component: TurnierAnsicht,
|
||||
},
|
||||
]);
|
||||
@@ -0,0 +1,56 @@
|
||||
import {createTheme} from '@mui/material/styles';
|
||||
|
||||
export const theme = createTheme({
|
||||
palette: {
|
||||
primary: {
|
||||
main: '#3F51B5', // Indigo
|
||||
},
|
||||
secondary: {
|
||||
main: '#FF4081',
|
||||
},
|
||||
background: {
|
||||
default: '#fafafa',
|
||||
paper: '#ffffff',
|
||||
},
|
||||
},
|
||||
typography: {
|
||||
fontSize: 11,
|
||||
body1: {
|
||||
fontSize: '11px',
|
||||
},
|
||||
body2: {
|
||||
fontSize: '11px',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
textTransform: 'none',
|
||||
fontSize: '11px',
|
||||
},
|
||||
sizeSmall: {
|
||||
padding: '4px 12px',
|
||||
fontSize: '10px',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTextField: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
'& .MuiInputBase-input': {
|
||||
fontSize: '11px',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableCell: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
fontSize: '11px',
|
||||
padding: '6px 8px',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
# CSN-C NEU / CSNP-C NEU NEUMARKT/M.
|
||||
|
||||
**Turnier-Nr.: 26128** | [cite_start]**Datum: 25. April 2026** [cite: 1, 2]
|
||||
|
||||
## Allgemeine Informationen
|
||||
|
||||
* **Veranstalter:** Union Reit- u. Fahrverein Neumarkt/M. (6-009) [cite_start][cite: 3]
|
||||
* [cite_start]**Ort:** Reitanlage Stroblmair, 4212 Neumarkt [cite: 3]
|
||||
* [cite_start]**Kontakt:** Ursula Stroblmair, Brandstetterweg 2, 4212 Neumarkt [cite: 4]
|
||||
* **Tel.:** 0664 1832381
|
||||
* [cite_start]**E-Mail:** reit-stall@gmx.at [cite: 4]
|
||||
* [cite_start]**Nennungsschluss:** 24.04.2026, 19:00 Uhr [cite: 4]
|
||||
* [cite_start]**Online-Nennung:** Ab Mittwoch, 22.04.
|
||||
auf [www.ihremeldestelle.at](http://www.ihremeldestelle.at) [cite: 5]
|
||||
* [cite_start]**Meldestelle:** Geöffnet ab 24.04., 17:00 Uhr (Tel: +43 681 10769120) [cite: 8]
|
||||
|
||||
## Technische Details
|
||||
|
||||
* [cite_start]**Austragungsplatz:** 45 x 65 m (Sand/Vlies) [cite: 6]
|
||||
* [cite_start]**Vorbereitungsplatz:** 20 x 40 m Halle (Sand/Vlies) [cite: 6]
|
||||
* [cite_start]**Warmreiten:** Draußen (20 x 60 m Sand/Vlies) möglich [cite: 16]
|
||||
* [cite_start]**Boxen:** Keine Einstallung möglich [cite: 9]
|
||||
|
||||
## Funktionäre
|
||||
|
||||
* [cite_start]**Turnierleiter:** Ursula Stroblmair [cite: 6]
|
||||
* [cite_start]**Turnierbeauftragter:** Rudi Kreupl [cite: 7]
|
||||
* [cite_start]**Richter:** Rudi Kreupl, Helmut Riedler [cite: 7]
|
||||
* [cite_start]**Parcoursbauchef:** Kurt Reitetschlägerr [cite: 8]
|
||||
* [cite_start]**Tierarzt:** Dr. Sabine Ötschmaier [cite: 8]
|
||||
|
||||
---
|
||||
|
||||
## Besondere Bestimmungen
|
||||
|
||||
* **Kosten:** Startgeld € 15,- pro Bewerb. [cite_start]Kein Nenngeld, kein Sporteuro. [cite: 11]
|
||||
* **Teilnahmebedingungen:**
|
||||
* [cite_start]Für Springprüfungen bis 95 cm: Mitgliedschaft OEPS-Verein und Reiterpass erforderlich. [cite: 12]
|
||||
* [cite_start]Pferde bis 90 cm müssen **nicht** beim OEPS registriert sein. [cite: 14]
|
||||
* [cite_start]Pferdepass mit gültigem Impfschutz (§ 11 OTO) ist vorzulegen. [cite: 15]
|
||||
* [cite_start]Haftpflichtversicherung für jedes Pferd ist Pflicht. [cite: 21]
|
||||
* **Startregelung:**
|
||||
* [cite_start]Ein Pferd darf maximal 3x pro Tag starten. [cite: 14]
|
||||
* [cite_start]In Bewerben bis 95 cm darf ein Pferd mit zwei verschiedenen Reitern starten. [cite: 13]
|
||||
* [cite_start]**Hunde:** Am gesamten Gelände herrscht Leinenpflicht. [cite: 18]
|
||||
|
||||
---
|
||||
|
||||
## Bewerbe (Samstag, 25. April 2026 - Beginn 08:00 Uhr)
|
||||
|
||||
| Nr. | Bewerb | Höhe | Richtverfahren / Abteilungen |
|
||||
|:-------|:--------------------------------|:-------|:------------------------------------------------------------------------------------|
|
||||
| **1** | Pony Stilspringprüfung | 60 cm | [cite_start]RV: § 204/4 (CSNP-C) [cite: 27] |
|
||||
| **2** | Einlaufspringprüfung | 60 cm | [cite_start]RV: § 204/4 (1. Abt: lizenzfrei / 2. Abt: mit Lizenz) [cite: 27] |
|
||||
| **3** | Pony Stilspringprüfung | 70 cm | [cite_start]RV: § 204/4 (CSNP-C) [cite: 27] |
|
||||
| **4** | Einlaufspringprüfung | 70 cm | [cite_start]RV: § 218 (1. Abt: lizenzfrei / 2. Abt: mit Lizenz) [cite: 27] |
|
||||
| **5** | Pony Stilspringprüfung | 80 cm | [cite_start]RV: § 204/4 (CSNP-C) [cite: 27] |
|
||||
| **6** | Stilspringprüfung | 80 cm | [cite_start]RV: § 204/4 (1. Abt: lizenzfrei / 2. Abt: R1 & 5-6j. Pferde) [cite: 27] |
|
||||
| **7** | Pony Stilspringprüfung | 95 cm | [cite_start]RV: § 204/4 (CSNP-C) [cite: 27] |
|
||||
| **8** | Springreiterbewerb (lizenzfrei) | 95 cm | [cite_start]RV: § 204/4 (CSNP-C) [cite: 27] |
|
||||
| **9** | Standardspringprüfung | 95 cm | [cite_start]RV: A2 (1. Abt: R1 / 2. Abt: R2 und höher) [cite: 27] |
|
||||
| **10** | Springpferdeprüfung | 105 cm | [cite_start]RV: § 203/3 (1. Abt: 4-jährig / 2. Abt: 5-6-jährig) [cite: 27] |
|
||||
| **11** | Stilspringprüfung | 105 cm | [cite_start]RV: § 204/4 (1. Abt: R1) [cite: 27] |
|
||||
| **12** | Standardspringprüfung | 105 cm | [cite_start]RV: A2 (1. Abt: R1 / 2. Abt: R2/RS2 und höher) [cite: 27] |
|
||||
| **13** | Stilspringprüfung | 115 cm | [cite_start]RV: § 204/4 (1. Abt: R1) [cite: 28, 30, 31] |
|
||||
| **14** | Standardspringprüfung | 115 cm | [cite_start]RV: A2 (1. Abt: R1 / 2. Abt: R2/RS2 und höher) [cite: 32, 34, 36] |
|
||||
|
||||
---
|
||||
**Haftung:** Der Veranstalter übernimmt keine Haftung. [cite_start]Teilnehmer haften persönlich für Schäden gegenüber
|
||||
Dritten. [cite: 19, 20]
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# CDN-C NEU / CDNP-C NEU NEUMARKT/M., OÖ
|
||||
|
||||
**Turnier-Nr.: 26129** | [cite_start]**Datum: 26. April 2026** [cite: 37]
|
||||
|
||||
## Allgemeine Informationen
|
||||
|
||||
* **Veranstalter**: Union Reit- u. Fahrverein Neumarkt/M. (6-009) [cite_start][cite: 38].
|
||||
* [cite_start]**Ort**: Reitanlage Stroblmair, 4212 Neumarkt[cite: 38].
|
||||
* [cite_start]**Kontaktadresse**: Ursula Stroblmair, Brandstetterweg 2, 4212 Neumarkt[cite: 39].
|
||||
* [cite_start]**Telefon**: 0664 1832381[cite: 39].
|
||||
* [cite_start]**E-Mail**: reit-stall@gmx.at[cite: 39].
|
||||
* [cite_start]**Nennungsschluss**: 25.04.2026, 19:00 Uhr[cite: 39, 53].
|
||||
* [cite_start]**Online-Nennung**: Ab Mittwoch, 22.04. auf www.ihremeldestelle.at möglich[cite: 40].
|
||||
* [cite_start]**Meldestelle**: Geöffnet ab 25.04., 17:00 Uhr (Tel: +43 681 10769120)[cite: 43].
|
||||
* [cite_start]**Start- und Ergebnislisten**: Ab 20:30 Uhr auf www.ihremeldestelle.at verfügbar[cite: 44].
|
||||
|
||||
## Technische Details und Gebühren
|
||||
|
||||
* [cite_start]**Austragungsplatz**: 20 x 60 m Sand/Vlies[cite: 41].
|
||||
* [cite_start]**Vorbereitungsplatz**: 20 x 40 m Halle (Sand/Vlies) und 20 x 60 m (Sand/Vlies)[cite: 41].
|
||||
* [cite_start]**Boxen**: Keine Einstallung möglich[cite: 44].
|
||||
* [cite_start]**Kosten**: Startgeld € 15,- pro Bewerb; kein Nenngeld und kein Sporteuro[cite: 40, 47].
|
||||
|
||||
## Funktionäre
|
||||
|
||||
* [cite_start]**Turnierleiter**: Ursula Stroblmair[cite: 41].
|
||||
* [cite_start]**Turnierbeauftragte**: Alexandra Schuster[cite: 42].
|
||||
* [cite_start]**Richter**: Alexandra Schuster, Ulrike Knasmüller-Prinz, Karin Wallner[cite: 42].
|
||||
* [cite_start]**Steward**: Barbara Hruschka[cite: 42].
|
||||
* [cite_start]**Tierarzt**: Dr. Sabine Ötschmaier[cite: 42].
|
||||
|
||||
---
|
||||
|
||||
## Besondere Bestimmungen
|
||||
|
||||
* **Teilnahmevoraussetzungen**:
|
||||
* [cite_start]Für Reiterpass-/Reiternadel-Aufgaben ist die Mitgliedschaft bei einem OEPS-Verein und der Besitz des
|
||||
Reiterpasses erforderlich[cite: 48].
|
||||
* [cite_start]Pferde für Reiterpass-/Reiternadel-Aufgaben müssen nicht beim OEPS registriert sein[cite: 50].
|
||||
* **Pferde**:
|
||||
* [cite_start]Ein Pferd darf pro Tag maximal 3x starten[cite: 49].
|
||||
* [cite_start]Ein Pferd darf mit zwei verschiedenen Reitern an den Start gehen[cite: 49].
|
||||
* [cite_start]Vorlage des Pferdepasses mit gültigem Impfschutz gemäß § 11 OTO ist Pflicht[cite: 51].
|
||||
* [cite_start]Jedes teilnehmende Pferd muss haftpflichtversichert sein[cite: 57].
|
||||
* [cite_start]**Haftung**: Der Veranstalter übernimmt keine Haftung jeder Art und Ursache[cite: 55]. [cite_start]
|
||||
Teilnehmer und Besitzer haften persönlich für Schäden gegenüber Dritten[cite: 56].
|
||||
* [cite_start]**Sonstiges**: Es gilt Leinenpflicht für Hunde auf dem gesamten Gelände[cite: 54]. [cite_start]
|
||||
Ausländische Equiden unterliegen der TRACES-Pflicht[cite: 58].
|
||||
|
||||
---
|
||||
|
||||
## Bewerbe (Sonntag, 26. April 2026 - Beginn 08:00 Uhr)
|
||||
|
||||
| Nr. | Bewerb | Aufg. | Details / Abteilungen |
|
||||
|:-------|:---------------------------------|:---------------|:------------------------------------------------------------------------|
|
||||
| **1** | Dressurreiterprüfung Reiterpass | R1 | [cite_start]RV: A § 103/5 [cite: 63] |
|
||||
| **2** | Dressurreiterprüfung Reiternadel | R4 | [cite_start]RV: A § 103/5 [cite: 64] |
|
||||
| **3** | Dressurreiterprüfung lizenzfrei | LF1 | [cite_start]RV: A § 103/5 [cite: 68] |
|
||||
| **4** | Dressurreiterprüfung lizenzfrei | LF3 | [cite_start]RV: A § 103/5 [cite: 69] |
|
||||
| **5** | First Ridden | - [cite_start] | [cite: 71] |
|
||||
| **6** | Führzügelklasse | - [cite_start] | [cite: 73] |
|
||||
| **7** | Pony Dressurprüfung Kl. A | P1 | [cite_start]RV: A, § 901 [cite: 75, 76] |
|
||||
| **8** | Dressurreiterprüfung Kl. A | DRA1 | 1. Abt: R1/RD1; 2. [cite_start]Abt: R2/RD2 u. höher [cite: 78, 79, 81] |
|
||||
| **9** | Dressurprüfung Kl. A | A5 | 1. Abt: R1/RD1; 2. [cite_start]Abt: R2/RD2 u. höher [cite: 82, 83, 98] |
|
||||
| **13** | Dressurpferdeprüfung Kl. A | DPA1 | 1. Abt: 4-jähr. Pferde; 2. Abt: 5-6-jähr. [cite_start]Pferde [cite: 85] |
|
||||
| **14** | Dressurpferdprüfung Kl. L | DPL1 | Für 5-6-jähr. [cite_start]Pferde [cite: 87] |
|
||||
| **10** | Pony Dressurprüfung Kl. L | P6 | [cite_start]RV: A, § 901 [cite: 89, 90] |
|
||||
| **11** | Dressurreiterprüfung Kl. L | DRL1 | 1. Abt: R1/RD1; 2. [cite_start]Abt: R2/RD2 u. höher [cite: 89, 92, 97] |
|
||||
| **12** | Dressurprüfung Kl. L | L3 | 1. Abt: R1/RD1; 2. [cite_start]Abt: R2/RD2 u. höher [cite: 94, 96] |
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# Detaillierte Bewerbs-Parameter: Springen und Dressur
|
||||
|
||||
Dieses Dokument beschreibt die genauen Parameter, die in der Turnier-Ausschreibung für die einzelnen Bewerbe der Sparten
|
||||
Springen (CSN) und Dressur (CDN) definiert werden müssen, basierend auf der aktuellen ÖTO.
|
||||
|
||||
---
|
||||
|
||||
## 1. Verständnis eines Ausschreibungs-Beispiels
|
||||
|
||||
Ein typischer Bewerb im Ausschreibungs-Text sieht oft so aus:
|
||||
> **6 Stilspringprüfung 80 cm J RV: § 204/4 CSN-C Neu**
|
||||
> **1. Abt. lizenzfrei 2.Abt. R1 und Reiter mit 5 & 6 jährigen Pferden**
|
||||
|
||||
### Aufschlüsselung der Parameter:
|
||||
|
||||
* **`6`**: **Bewerbsnummer**. Eine fortlaufende Nummer zur eindeutigen Identifikation des Bewerbs (Prüfung Nr. 6 des
|
||||
Turniers).
|
||||
* **`Stilspringprüfung`**: **Art der Prüfung**. Es geht hier nicht rein nach Fehlern und Zeit, sondern der Reiter wird
|
||||
von Richtern mit einer Wertnote für seinen Sitz, seine Einwirkung und den Rhythmus beurteilt.
|
||||
* **`80 cm`**: **Klasse / Maximale Hindernishöhe**. In diesem Fall entspricht dies der Einsteigerklasse (E bzw. E0).
|
||||
* **`J`**: **Startbuchstabe**. Nach diesem Buchstaben wird die Startreihenfolge alphabetisch gelost (oft nach dem Namen
|
||||
des Pferdes). Ist der Buchstabe "J", startet das Pferd "Jolly Jumper" als erstes, danach "Karino" usw., bis am Ende
|
||||
das Pferd "Ikarus" startet.
|
||||
* **`RV: § 204/4`**: **Richtverfahren**. Der direkte Verweis auf den entsprechenden Paragraphen der ÖTO. Hier bedeutet
|
||||
dies: Stilspringprüfung mit Wertnoten von 0,0 bis 10,0.
|
||||
* **`CSN-C Neu`**: **Turnierkategorie**. Nationales Springturnier, Kategorie C-Neu. Das ist eine Einsteiger-Kategorie,
|
||||
bei der auch Reiter ohne Lizenz (nur mit Reiterpass) antreten dürfen. Es gibt hier kein Preisgeld, nur Sachpreise und
|
||||
Schleifen.
|
||||
* **`1. Abt. lizenzfrei`**: **Abteilung 1 (Unterteilung)**. Diese Abteilung (erste Siegerehrung/Platzierung) ist
|
||||
exklusiv für Reiter, die noch keine Turnierlizenz haben.
|
||||
* **`2. Abt. R1 und Reiter mit 5 & 6 jährigen Pferden`**: **Abteilung 2**. Die zweite Abteilung wertet alle Reiter mit
|
||||
der Einstiegslizenz (R1) sowie erfahrene Reiter, wenn sie junge Nachwuchspferde reiten. So wird Fairness garantiert.
|
||||
|
||||
---
|
||||
|
||||
## 2. Sparte Springen (CSN) im Detail
|
||||
|
||||
### 2.1 Die Klassen (Höhen für Großpferde)
|
||||
|
||||
Die Klassen geben die maximale Höhe der Hindernisse an:
|
||||
|
||||
* **Klasse E0 (Einsteiger):** 60 bis 90 cm
|
||||
* **Klasse A (Leicht):** 105 bis 110 cm
|
||||
* **Klasse L (Mittelleicht):** 115 bis 120 cm
|
||||
* **Klasse LM (Leicht-Mittelschwer):** 125 bis 130 cm
|
||||
* **Klasse M (Mittelschwer):** 135 cm
|
||||
* **Klasse S (Schwer):** 140 bis 160 cm
|
||||
*(Hinweis: Für Ponys sind die Höhen reduziert und die Abstände in Kombinationen verkürzt)*
|
||||
|
||||
### 2.2 Die Richtverfahren (RV) gemäß § 204 ÖTO
|
||||
|
||||
Das Richtverfahren definiert, wie Fehler und Zeiten gewertet werden.
|
||||
|
||||
#### Richtverfahren A (Standardspringprüfung)
|
||||
|
||||
Es gibt Strafpunkte (Fehler) für Abwürfe (4 Fehlerpunkte) und Verweigerungen/Ungehorsam (4 Fehlerpunkte beim ersten Mal,
|
||||
beim zweiten Mal ab 115 cm Ausschluss). Zeitüberschreitungen über die "Erlaubte Zeit" geben Zeitfehler (0,25 Punkte pro
|
||||
Sekunde).
|
||||
|
||||
* **A1:** Es gibt keine Zeitwertung. Alle fehlerfreien Ritte (gleiche Punktezahl) sind ex aequo auf Platz 1.
|
||||
* **A2:** Fehler und Zeit. Bei Punktegleichheit gewinnt die schnellere Umlaufzeit.
|
||||
* **A3 (Idealzeit):** Eine Idealzeit (meist Erlaubte Zeit minus 10%) wird definiert. Es gewinnt der Reiter, der am
|
||||
nächsten an der Idealzeit ist (darunter oder darüber). Schützt vor "Bolzen" in Anfängerprüfungen.
|
||||
* **AM3, AM4, AM5, AM6:** Standardspringen **mit Stechen**. Wer im Grundparcours fehlerfrei bleibt, reitet danach einen
|
||||
verkürzten Stechparcours auf Zeit.
|
||||
|
||||
#### Richtverfahren C (Zeitspringen)
|
||||
|
||||
Fehlerpunkte gibt es nicht. Für jeden Abwurf werden stattdessen **Strafsekunden** (meist 4 Sekunden) zur gerittenen Zeit
|
||||
addiert. Die schnellste Endzeit gewinnt.
|
||||
|
||||
### 2.3 Spezial-Richtverfahren / Prüfungsarten
|
||||
|
||||
* **Einlaufspringprüfung (§ 218):** Ein Trainingsbewerb (RV A1). Es gibt keine Platzierung. Jeder fehlerfreie Reiter
|
||||
bekommt eine braune "Clear-Round"-Schleife.
|
||||
* **Punktespringprüfung (§ 219):** Man sammelt Pluspunkte für fehlerfreie Sprünge. Am Ende steht oft ein schwieriger "
|
||||
Joker-Sprung", der doppelte Punkte oder bei einem Fehler doppelten Abzug bringt.
|
||||
* **2-Phasenspringprüfung (§ 220):** Der Parcours ist in zwei Teile geteilt. Wer Phase 1 fehlerfrei schafft, reitet ohne
|
||||
anzuhalten sofort in Phase 2 (die meist auf Zeit geht). Wer in Phase 1 patzt, wird abgeglockt.
|
||||
* **Stilspringprüfung:** Bewertung mit Wertnoten von 0 bis 10. Abzüge erfolgen für Ungehorsam (-0,5 oder -1,0) und
|
||||
Hindernisfehler (-0,5).
|
||||
|
||||
---
|
||||
|
||||
## 3. Sparte Dressur (CDN) im Detail
|
||||
|
||||
### 3.1 Die Klassen und Aufgaben
|
||||
|
||||
Die Dressur wird nach vorgegebenen "Aufgaben" geritten (z.B. "Aufgabe A2", "FEI Grand Prix"), die die zu reitenden
|
||||
Hufschlagfiguren exakt vorgeben.
|
||||
|
||||
* **Klasse A:** Grundlagen, Hufschlagfiguren, einfache Galoppwechsel über den Trab.
|
||||
* **Klasse L:** Beginnende Versammlung, Kurzkehrt, Außengalopp.
|
||||
* **Klasse LM:** Schulterherein, fliegende Wechsel können vorkommen. (Ab hier wahlweise Trense oder Kandare).
|
||||
* **Klasse M:** Traversalen, fliegende Galoppwechsel. (Kandarenpflicht).
|
||||
* **Klasse S:** Pirouetten, Serienwechsel (z.B. Wechsel alle 2 Sprünge), Piaffe, Passage.
|
||||
|
||||
### 3.2 Die Richtverfahren (RV) gemäß § 104 ÖTO
|
||||
|
||||
Das Richtverfahren definiert die Sitzverteilung und Art der Notengebung der Richter.
|
||||
|
||||
#### Richtverfahren A (Gemeinsames Richten)
|
||||
|
||||
Typisch für untere bis mittlere Klassen. Die Richtergruppe (die zusammen bei "C" sitzt) einigt sich auf **eine
|
||||
gemeinsame Wertnote** zwischen 0,0 und 10,0 (z.B. 7,4).
|
||||
|
||||
* *Verreiten:* Wird mit einem Abzug von der Gesamtnote bestraft (1. Mal: -0,2 / 2. Mal: -0,4).
|
||||
|
||||
#### Richtverfahren B (Getrenntes Richten)
|
||||
|
||||
Typisch ab Klasse M und bei Meisterschaften. Mindestens drei Richter sitzen an verschiedenen Stellen (C, H, M) und
|
||||
werten völlig unabhängig voneinander. Jede einzelne Lektion wird mit einer Note (0 bis 10) bewertet. Am Ende werden die
|
||||
Punkte addiert und in einen Prozentwert umgerechnet (z.B. 68,542 %).
|
||||
|
||||
* *Verreiten:* Führt zu Abzügen bei der Gesamtpunktezahl bei jedem Richter (1. Mal: -2 Pkt. pro Richter / 2. Mal: -4
|
||||
Pkt. pro Richter).
|
||||
|
||||
*Wichtig für alle Dressurprüfungen:* Ein **drittes Verreiten** führt unausweichlich zum Ausschluss (Abglocken) des
|
||||
Teilnehmers.
|
||||
|
||||
### 3.3 Sonderformen
|
||||
|
||||
* **Musikkür:** Wird immer nach RV B gerichtet. Hierbei werden zwei getrennte Notensets vergeben: Eine Note für die *
|
||||
*Technik** (Wurden alle geforderten Lektionen korrekt gezeigt?) und eine Note für die **Künstlerische Ausführung** (
|
||||
Choreographie, Musikinterpretation, Schwierigkeitsgrad).
|
||||
* **Dressurreiterprüfung / Dressurpferdeprüfung:** Ähnlich wie im Stilspringen zählt hier primär der Sitz und die
|
||||
Einwirkung des Reiters (Reiterprüfung) bzw. das Potenzial und die Grundgangarten des jungen Pferdes (Pferdeprüfung).
|
||||
Wird in der Regel nach RV A (Wertnoten) gerichtet.
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
# Struktur einer Turnier-Ausschreibung gemäß ÖTO
|
||||
|
||||
Diese Dokumentation beschreibt die notwendigen Felder und Sektionen einer offiziellen Ausschreibung für den
|
||||
österreichischen Pferdesport (OEPS).
|
||||
|
||||
## 1. Allgemeine Turnierdaten (Header)
|
||||
|
||||
Dies entspricht im Wesentlichen dem **A-Satz** der ZNS-Schnittstelle.
|
||||
|
||||
* **Turniernummer:** Eindeutige vom OEPS vergebene Kennung (z.B. 25123).
|
||||
* **Veranstalter:** Name des durchführenden Vereins und dessen Vereinsnummer.
|
||||
* **Austragungsort:** Genaue Adresse der Reitanlage.
|
||||
* **Datum:** Von-bis Datum des Turniers.
|
||||
* **Turnierkategorie:** Einstufung (z.B. CSN-B, CDN-A, CCN-C).
|
||||
* **Nennschluss:** Datum, bis zu dem reguläre Nennungen möglich sind.
|
||||
|
||||
## 2. Besondere Bestimmungen (Rechtlicher Rahmen)
|
||||
|
||||
* **Regelwerk:** Hinweis auf die gültige ÖTO (Österreichische Turnierordnung) und ggf. FEI-Regeln.
|
||||
* **Haftungsausschluss:** Verweis auf die Haftungsbestimmungen der ÖTO.
|
||||
* **Teilnahmeberechtigung:** Allgemeine Einschränkungen (z.B. nur für Mitglieder bestimmter Landesverbände oder geladene
|
||||
Gäste).
|
||||
|
||||
## 3. Funktionäre (Offizielle)
|
||||
|
||||
Wichtig für die Zuweisung im **C-Satz**.
|
||||
|
||||
* **Turnierleiter:** Verantwortliche Person des Veranstalters.
|
||||
* **Richterkollegium:** Liste der Richter inkl. deren Qualifikationen (z.B. "D-GP", "S").
|
||||
* **Technischer Delegierter (TD):** (Vor allem bei Vielseitigkeit).
|
||||
* **Parcourschef:** Verantwortlich für das Design der Hindernisse.
|
||||
* **Turniertierarzt & Schmied:** Notwendige medizinische Versorgung.
|
||||
|
||||
## 4. Beschaffenheit der Anlage
|
||||
|
||||
Informationen, die Einfluss auf das **DressurPruefungSpezifika** oder **SpringenPruefungSpezifika** haben.
|
||||
|
||||
* **Austragungsplatz:** Maße (z.B. 20x60m), Bodenbelag (Sand, Gras).
|
||||
* **Vorbereitungsplatz:** (Abreiteplatz) Maße und Bodenbelag.
|
||||
|
||||
## 5. Nennungen & Gebühren
|
||||
|
||||
Grundlage für den **Nennungs_Context**.
|
||||
|
||||
* **Nennweg:** Hinweis auf das ZNS (Zentrales Nennsystem).
|
||||
* **Nenngebühr:** Grundgebühr pro Pferd/Reiter-Paar.
|
||||
* **Startgebühr:** Gebühr pro einzelner Prüfung.
|
||||
* **Boxen/Einstreu:** Kosten für fixe oder mobile Boxen, inkl. Erst-Einstreu.
|
||||
* **Zusatzgebühren:** Stromanschluss, Camping, Nachnenngebühren.
|
||||
|
||||
## 6. Prüfungs-Programm (Bewerbe)
|
||||
|
||||
Dies ist das Herzstück und bildet den **B-Satz** ab. Jede Prüfung muss folgende Details aufweisen:
|
||||
|
||||
### Pflichtfelder pro Prüfung:
|
||||
|
||||
| Feld | Beschreibung | Beispiel |
|
||||
|:-------------------|:--------------------------------------|:---------------------------------|
|
||||
| **Bewerbsnummer** | Fortlaufende Nummer | 01, 02, ... |
|
||||
| **Bezeichnung** | Name der Prüfung | Standardspringprüfung |
|
||||
| **Klasse** | Schwierigkeitsgrad | E, A, L, LM, M, S |
|
||||
| **Abteilungen** | Unterteilung nach Lizenzen | Abt. 1: R1 / Abt. 2: R2 u. höher |
|
||||
| **Aufgabe** | (Nur Dressur) Spezifische ÖTO-Aufgabe | A2, L1, FEI Grand Prix |
|
||||
| **Anforderungen** | Erforderliche Lizenzen/Alter | R1 oder höher |
|
||||
| **Richtverfahren** | Verweis auf ÖTO-Paragraphen | § 218, § 204 |
|
||||
| **Dotierung** | Preisgeld-Aufstellung | EUR 500,- (150/100/80/...) |
|
||||
|
||||
## 7. Stallungen & Unterbringung
|
||||
|
||||
* Anreise- und Abreisezeiten.
|
||||
* Verfügbarkeit von Futter und Einstreu.
|
||||
* Veterinäramtliche Bestimmungen (Impfschutz-Kontrolle gemäß ÖTO).
|
||||
|
||||
## 8. Vorläufige Zeiteinteilung
|
||||
|
||||
* Grober Ablaufplan (welcher Tag, welche Prüfungen).
|
||||
* Hinweis auf die endgültige Zeiteinteilung (meist am Vorabend im Nennungs-System).
|
||||
@@ -0,0 +1,5 @@
|
||||
This Figma Make file includes components from [shadcn/ui](https://ui.shadcn.com/) used
|
||||
under [MIT license](https://github.com/shadcn-ui/ui/blob/main/LICENSE.md).
|
||||
|
||||
This Figma Make file includes photos from [Unsplash](https://unsplash.com) used
|
||||
under [license](https://unsplash.com/license).
|
||||
@@ -0,0 +1,61 @@
|
||||
**Add your own guidelines here**
|
||||
<!--
|
||||
|
||||
System Guidelines
|
||||
|
||||
Use this file to provide the AI with rules and guidelines you want it to follow.
|
||||
This template outlines a few examples of things you can add. You can add your own sections and format it to suit your needs
|
||||
|
||||
TIP: More context isn't always better. It can confuse the LLM. Try and add the most important rules you need
|
||||
|
||||
# General guidelines
|
||||
|
||||
Any general rules you want the AI to follow.
|
||||
For example:
|
||||
|
||||
* Only use absolute positioning when necessary. Opt for responsive and well structured layouts that use flexbox and grid by default
|
||||
* Refactor code as you go to keep code clean
|
||||
* Keep file sizes small and put helper functions and components in their own files.
|
||||
|
||||
--------------
|
||||
|
||||
# Design system guidelines
|
||||
Rules for how the AI should make generations look like your company's design system
|
||||
|
||||
Additionally, if you select a design system to use in the prompt box, you can reference
|
||||
your design system's components, tokens, variables and components.
|
||||
For example:
|
||||
|
||||
* Use a base font-size of 14px
|
||||
* Date formats should always be in the format “Jun 10”
|
||||
* The bottom toolbar should only ever have a maximum of 4 items
|
||||
* Never use the floating action button with the bottom toolbar
|
||||
* Chips should always come in sets of 3 or more
|
||||
* Don't use a dropdown if there are 2 or fewer options
|
||||
|
||||
You can also create sub sections and add more specific details
|
||||
For example:
|
||||
|
||||
|
||||
## Button
|
||||
The Button component is a fundamental interactive element in our design system, designed to trigger actions or navigate
|
||||
users through the application. It provides visual feedback and clear affordances to enhance user experience.
|
||||
|
||||
### Usage
|
||||
Buttons should be used for important actions that users need to take, such as form submissions, confirming choices,
|
||||
or initiating processes. They communicate interactivity and should have clear, action-oriented labels.
|
||||
|
||||
### Variants
|
||||
* Primary Button
|
||||
* Purpose : Used for the main action in a section or page
|
||||
* Visual Style : Bold, filled with the primary brand color
|
||||
* Usage : One primary button per section to guide users toward the most important action
|
||||
* Secondary Button
|
||||
* Purpose : Used for alternative or supporting actions
|
||||
* Visual Style : Outlined with the primary color, transparent background
|
||||
* Usage : Can appear alongside a primary button for less important actions
|
||||
* Tertiary Button
|
||||
* Purpose : Used for the least important actions
|
||||
* Visual Style : Text-only with no border, using primary color
|
||||
* Usage : For actions that should be available but not emphasized
|
||||
-->
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* PostCSS Configuration
|
||||
*
|
||||
* Tailwind CSS v4 (via @tailwindcss/vite) automatically sets up all required
|
||||
* PostCSS plugins — you do NOT need to include `tailwindcss` or `autoprefixer` here.
|
||||
*
|
||||
* This file only exists for adding additional PostCSS plugins, if needed.
|
||||
* For example:
|
||||
*
|
||||
* import postcssNested from 'postcss-nested'
|
||||
* export default { plugins: [postcssNested()] }
|
||||
*
|
||||
* Otherwise, you can leave this file empty.
|
||||
*/
|
||||
export default {}
|
||||
@@ -0,0 +1,139 @@
|
||||
import {useState} from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Button from '@mui/material/Button';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||
|
||||
// Mock-Daten für Bewerbe
|
||||
const mockBewerbe = [
|
||||
{tag: 'So', platz: 1, nr: '1', beginn: '08:00', nenn: 0, name: 'Dressurreiterprüfung Ratepass', klasse: 'A'},
|
||||
{tag: 'So', platz: 1, nr: '2', beginn: '08:20', nenn: 0, name: 'Dressurreiterprüfung Katecnadel', klasse: 'L'},
|
||||
{tag: 'So', platz: 1, nr: '3', beginn: '08:40', nenn: 0, name: 'Dressurreiterprüfung Idf. (Idf.)', klasse: 'M'},
|
||||
{tag: 'So', platz: 1, nr: '4', beginn: '09:00', nenn: 0, name: 'Dressurprüfung Idf. (Idf.)', klasse: 'L'},
|
||||
{tag: 'So', platz: 1, nr: '5', beginn: '09:20', nenn: 0, name: 'Führzügelklasse', klasse: 'E'},
|
||||
{tag: 'So', platz: 1, nr: '6', beginn: '09:40', nenn: 0, name: 'First Ridden', klasse: 'E'},
|
||||
{tag: 'So', platz: 1, nr: '7', beginn: '10:00', nenn: 0, name: 'Pony Dressurprüfung Kl. A', klasse: 'A'},
|
||||
{tag: 'So', platz: 1, nr: '8', beginn: '10:20', nenn: 0, name: 'Dressurreiterprüfung Kl. A', klasse: 'A'},
|
||||
{tag: 'So', platz: 1, nr: '9', beginn: '10:40', nenn: 0, name: 'Dressurprüfung Kl. A', klasse: 'A'},
|
||||
{tag: 'So', platz: 1, nr: '10', beginn: '11:00', nenn: 0, name: 'Pony Dressurprüfung Kl. A', klasse: 'A'},
|
||||
{tag: 'So', platz: 1, nr: '11', beginn: '11:20', nenn: 0, name: 'Dressurreiterprüfung Kl. L', klasse: 'L'},
|
||||
{tag: 'So', platz: 1, nr: '12', beginn: '11:40', nenn: 0, name: 'Dressurprüfung Kl. L', klasse: 'L'},
|
||||
];
|
||||
|
||||
interface Props {
|
||||
selectedPferd: any;
|
||||
selectedReiter: any;
|
||||
onNennung: (bewerb: any) => void;
|
||||
}
|
||||
|
||||
export function Bewerbsliste({selectedPferd, selectedReiter, onNennung}: Props) {
|
||||
const [selectedBewerb, setSelectedBewerb] = useState<string | null>(null);
|
||||
|
||||
const handleBewerbDoppelklick = (bewerb: any) => {
|
||||
if (selectedPferd && selectedReiter) {
|
||||
onNennung(bewerb);
|
||||
setSelectedBewerb(bewerb.nr);
|
||||
}
|
||||
};
|
||||
|
||||
const canNennen = selectedPferd && selectedReiter;
|
||||
|
||||
return (
|
||||
<Box sx={{display: 'flex', flexDirection: 'column', height: '100%', p: 1.5}}>
|
||||
<Typography variant="caption" sx={{mb: 1, fontWeight: 600, fontSize: '11px'}}>
|
||||
Bewerbsübersicht
|
||||
</Typography>
|
||||
|
||||
<Toolbar variant="dense" sx={{
|
||||
minHeight: 28,
|
||||
px: 1,
|
||||
gap: 1,
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 1,
|
||||
mb: 1,
|
||||
border: 1,
|
||||
borderColor: 'divider'
|
||||
}}>
|
||||
<IconButton size="small" sx={{width: 24, height: 24}}>
|
||||
<RefreshIcon sx={{fontSize: 16}}/>
|
||||
</IconButton>
|
||||
<Typography variant="caption" sx={{fontSize: '10px'}}>
|
||||
Aktualisieren
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{flex: 1, textAlign: 'center', fontWeight: 600, fontSize: '10px'}}>
|
||||
{mockBewerbe.length} Bewerbe
|
||||
</Typography>
|
||||
<Button size="small" variant="text" startIcon={<FilterListIcon sx={{fontSize: 14}}/>}
|
||||
sx={{fontSize: '10px', py: 0.25}}>
|
||||
Filtern
|
||||
</Button>
|
||||
<Typography variant="caption" color="text.secondary" sx={{fontSize: '10px'}}>
|
||||
0 gefiltert
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
|
||||
<TableContainer sx={{flex: 1, border: 1, borderColor: 'divider', borderRadius: 1}}>
|
||||
<Table size="small" stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5}}>Tag</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5}}>Pl.</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5}}>Bewerb</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5}}>Beginn</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5}} align="center">Nenn.</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5}}>Bewerbsname</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{mockBewerbe.map((bewerb, idx) => {
|
||||
const isSelected = selectedBewerb === bewerb.nr;
|
||||
const isClickable = canNennen;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={idx}
|
||||
hover={isClickable}
|
||||
selected={isSelected}
|
||||
onDoubleClick={() => handleBewerbDoppelklick(bewerb)}
|
||||
sx={{
|
||||
cursor: isClickable ? 'pointer' : 'default',
|
||||
'&:nth-of-type(odd)': {bgcolor: isSelected ? 'primary.100' : 'action.hover'},
|
||||
'&.Mui-selected': {
|
||||
bgcolor: 'primary.100',
|
||||
'&:hover': {
|
||||
bgcolor: 'primary.200',
|
||||
},
|
||||
},
|
||||
opacity: isClickable ? 1 : 0.5,
|
||||
}}
|
||||
>
|
||||
<TableCell sx={{fontSize: '10px', py: 0.5}}>{bewerb.tag}</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', py: 0.5}}>{bewerb.platz}</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5}}>{bewerb.nr}</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', py: 0.5}}>{bewerb.beginn}</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', py: 0.5}} align="center">{bewerb.nenn}</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', py: 0.5}}>{bewerb.name}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{!canNennen && (
|
||||
<Typography variant="caption" color="text.secondary" sx={{mt: 1, textAlign: 'center', fontSize: '10px'}}>
|
||||
Bitte wählen Sie zuerst ein Pferd und einen Reiter aus
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import {useState} from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
|
||||
interface Props {
|
||||
nennungen: any[];
|
||||
selectedPferd: any;
|
||||
selectedReiter: any;
|
||||
}
|
||||
|
||||
export function NennungenTabelle({nennungen, selectedPferd, selectedReiter}: Props) {
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
|
||||
// Filter basierend auf Tab
|
||||
const getFilteredNennungen = () => {
|
||||
if (!selectedPferd && !selectedReiter) return [];
|
||||
|
||||
switch (tabValue) {
|
||||
case 0: // Reiter
|
||||
return selectedReiter
|
||||
? nennungen.filter(n => n.reiter === selectedReiter.vorname + ' ' + selectedReiter.nachname)
|
||||
: [];
|
||||
case 1: // Pferd
|
||||
return selectedPferd
|
||||
? nennungen.filter(n => n.pferd === selectedPferd.name)
|
||||
: [];
|
||||
case 2: // Bewerbe
|
||||
return (selectedPferd && selectedReiter)
|
||||
? nennungen.filter(n =>
|
||||
n.pferd === selectedPferd.name &&
|
||||
n.reiter === selectedReiter.vorname + ' ' + selectedReiter.nachname
|
||||
)
|
||||
: [];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const filteredNennungen = getFilteredNennungen();
|
||||
|
||||
return (
|
||||
<Box sx={{display: 'flex', flexDirection: 'column', height: '100%'}}>
|
||||
<Tabs value={tabValue} onChange={(_, v) => setTabValue(v)}
|
||||
sx={{borderBottom: 1, borderColor: 'divider', minHeight: 32}}>
|
||||
<Tab label="Reiter" sx={{minHeight: 32, fontSize: '11px', py: 0.5}}/>
|
||||
<Tab label="Pferd" sx={{minHeight: 32, fontSize: '11px', py: 0.5}}/>
|
||||
<Tab label="Bewerbe" sx={{minHeight: 32, fontSize: '11px', py: 0.5}}/>
|
||||
</Tabs>
|
||||
|
||||
<Toolbar variant="dense" sx={{
|
||||
minHeight: 28,
|
||||
px: 1,
|
||||
gap: 1,
|
||||
bgcolor: 'background.paper',
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider'
|
||||
}}>
|
||||
<IconButton size="small" sx={{width: 24, height: 24}}>
|
||||
<RefreshIcon sx={{fontSize: 16}}/>
|
||||
</IconButton>
|
||||
<Typography variant="caption" sx={{fontSize: '10px'}}>
|
||||
Aktualisieren
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{flex: 1, textAlign: 'center', fontWeight: 600, fontSize: '10px'}}>
|
||||
{filteredNennungen.length} Nennungen
|
||||
</Typography>
|
||||
<Button size="small" variant="text" sx={{fontSize: '10px', py: 0.25}}>
|
||||
Positionieren
|
||||
</Button>
|
||||
<Button size="small" variant="text" color="error" sx={{fontSize: '10px', py: 0.25}}>
|
||||
Stornieren
|
||||
</Button>
|
||||
</Toolbar>
|
||||
|
||||
<TableContainer sx={{flex: 1}}>
|
||||
<Table size="small" stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5}}>Tag</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5}}>Pl.</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5}}>Bewerb</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5}}>Bewerbsname</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5}}>Bemerkung</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5}}>Pferd</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{filteredNennungen.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} sx={{textAlign: 'center', color: 'text.secondary', fontSize: '10px', py: 2}}>
|
||||
Keine Nennungen vorhanden
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredNennungen.map((nennung, idx) => (
|
||||
<TableRow
|
||||
key={idx}
|
||||
sx={{
|
||||
'&:nth-of-type(odd)': {bgcolor: 'action.hover'},
|
||||
bgcolor: nennung.startwunsch === 'Vorne' ? 'success.50' : nennung.startwunsch === 'Hinten' ? 'info.50' : undefined,
|
||||
}}
|
||||
>
|
||||
<TableCell sx={{fontSize: '10px', py: 0.5}}>{nennung.tag}</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', py: 0.5}}>{nennung.platz}</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5}}>{nennung.bewerbNr}</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', py: 0.5}}>{nennung.bewerbName}</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', py: 0.5}}>{nennung.startwunsch || '-'}</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', py: 0.5}}>{nennung.pferd}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import {useState} from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import {PferdReiterEingabe} from './PferdReiterEingabe';
|
||||
import {NennungenTabelle} from './NennungenTabelle';
|
||||
import {VerkaufBuchungen} from './VerkaufBuchungen';
|
||||
import {Bewerbsliste} from './Bewerbsliste';
|
||||
import ListIcon from '@mui/icons-material/List';
|
||||
import EmojiEventsIcon from '@mui/icons-material/EmojiEvents';
|
||||
import ReceiptIcon from '@mui/icons-material/Receipt';
|
||||
|
||||
export function NennungsMaske() {
|
||||
const [selectedPferd, setSelectedPferd] = useState<any>(null);
|
||||
const [selectedReiter, setSelectedReiter] = useState<any>(null);
|
||||
const [nennungen, setNennungen] = useState<any[]>([]);
|
||||
|
||||
const handleNennung = (bewerb: any) => {
|
||||
if (selectedPferd && selectedReiter) {
|
||||
const neueNennung = {
|
||||
tag: bewerb.tag,
|
||||
platz: bewerb.platz,
|
||||
bewerbNr: bewerb.nr,
|
||||
bewerbName: bewerb.name,
|
||||
beginn: bewerb.beginn,
|
||||
pferd: selectedPferd.name,
|
||||
reiter: `${selectedReiter.vorname} ${selectedReiter.nachname}`,
|
||||
startwunsch: null,
|
||||
};
|
||||
setNennungen([...nennungen, neueNennung]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{display: 'flex', flexDirection: 'column', height: '100vh', bgcolor: 'background.default'}}>
|
||||
{/* Zeile 1 (50% Höhe): Pferd/Reiter Suche + Verkauf/Buchungen */}
|
||||
<Box sx={{height: '50%', display: 'flex', borderBottom: 1, borderColor: 'divider'}}>
|
||||
{/* Links: Pferd & Reiter Eingabe (60%) */}
|
||||
<Box sx={{width: '60%', borderRight: 1, borderColor: 'divider'}}>
|
||||
<PferdReiterEingabe
|
||||
selectedPferd={selectedPferd}
|
||||
setSelectedPferd={setSelectedPferd}
|
||||
selectedReiter={selectedReiter}
|
||||
setSelectedReiter={setSelectedReiter}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Rechts: Verkauf/Buchungen (40%) */}
|
||||
<Box sx={{width: '40%'}}>
|
||||
<VerkaufBuchungen selectedReiter={selectedReiter}/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Zeile 2 (5% Höhe): Navigation Buttons */}
|
||||
<Box
|
||||
sx={{
|
||||
height: '5%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 2,
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider',
|
||||
bgcolor: 'background.paper',
|
||||
px: 2,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
startIcon={<ListIcon fontSize="small"/>}
|
||||
sx={{minWidth: 130, fontSize: '11px', py: 0.5}}
|
||||
>
|
||||
Startliste
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
startIcon={<EmojiEventsIcon fontSize="small"/>}
|
||||
sx={{minWidth: 130, fontSize: '11px', py: 0.5}}
|
||||
>
|
||||
Ergebnisse
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
startIcon={<ReceiptIcon fontSize="small"/>}
|
||||
sx={{minWidth: 130, fontSize: '11px', py: 0.5}}
|
||||
>
|
||||
Abrechnung
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Zeile 3 (45% Höhe): Nennungsübersicht + Bewerbsübersicht */}
|
||||
<Box sx={{flex: 1, display: 'flex', minHeight: 0}}>
|
||||
{/* Links: Nennungsübersicht (60%) */}
|
||||
<Box sx={{width: '60%', borderRight: 1, borderColor: 'divider'}}>
|
||||
<NennungenTabelle
|
||||
nennungen={nennungen}
|
||||
selectedPferd={selectedPferd}
|
||||
selectedReiter={selectedReiter}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Rechts: Bewerbsübersicht (40%) */}
|
||||
<Box sx={{width: '40%'}}>
|
||||
<Bewerbsliste
|
||||
selectedPferd={selectedPferd}
|
||||
selectedReiter={selectedReiter}
|
||||
onNennung={handleNennung}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import {useState} from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Button from '@mui/material/Button';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import RemoveIcon from '@mui/icons-material/Remove';
|
||||
|
||||
// Mock-Daten für Verkauf
|
||||
const mockVerkaufArtikel = [
|
||||
{knr: '', text: 'Belastung', einzelpreis: 0, menge: 0, gebucht: '0.00'},
|
||||
{knr: '', text: 'Gutschrift', einzelpreis: 0, menge: 0, gebucht: '0.00'},
|
||||
{knr: '', text: 'Boxenpauschale', einzelpreis: 115.00, menge: 0, gebucht: '0.00'},
|
||||
{knr: '', text: 'Ansage', einzelpreis: 2.00, menge: 0, gebucht: '0.00'},
|
||||
{knr: '', text: 'Füttern', einzelpreis: 3.00, menge: 0, gebucht: '0.00'},
|
||||
{knr: '', text: 'Heu', einzelpreis: 13.00, menge: 0, gebucht: '0.00'},
|
||||
{knr: '', text: 'Späne', einzelpreis: 15.00, menge: 0, gebucht: '0.00'},
|
||||
{knr: '', text: 'Stroh', einzelpreis: 5.00, menge: 0, gebucht: '0.00'},
|
||||
{knr: '', text: 'Strom', einzelpreis: 50.00, menge: 0, gebucht: '0.00'},
|
||||
{knr: '', text: 'Y-Nummer', einzelpreis: 35.00, menge: 0, gebucht: '0.00'},
|
||||
{knr: '', text: 'Z-Nummer', einzelpreis: 10.00, menge: 0, gebucht: '0.00'},
|
||||
];
|
||||
|
||||
interface Props {
|
||||
selectedReiter: any;
|
||||
}
|
||||
|
||||
export function VerkaufBuchungen({selectedReiter}: Props) {
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
const [verkaufMengen, setVerkaufMengen] = useState<{ [key: string]: number }>({});
|
||||
|
||||
const handleMengeChange = (text: string, delta: number) => {
|
||||
setVerkaufMengen(prev => ({
|
||||
...prev,
|
||||
[text]: Math.max(0, (prev[text] || 0) + delta),
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{display: 'flex', flexDirection: 'column', height: '100%'}}>
|
||||
<Tabs value={tabValue} onChange={(_, v) => setTabValue(v)}
|
||||
sx={{borderBottom: 1, borderColor: 'divider', minHeight: 32}}>
|
||||
<Tab label="Verkauf" sx={{minHeight: 32, fontSize: '11px', py: 0.5}}/>
|
||||
<Tab label="Buchungen" sx={{minHeight: 32, fontSize: '11px', py: 0.5}}/>
|
||||
</Tabs>
|
||||
|
||||
{tabValue === 0 && (
|
||||
<>
|
||||
<Toolbar variant="dense" sx={{
|
||||
minHeight: 28,
|
||||
px: 1,
|
||||
gap: 1,
|
||||
bgcolor: 'background.paper',
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider'
|
||||
}}>
|
||||
<IconButton size="small" sx={{width: 24, height: 24}}>
|
||||
<RefreshIcon sx={{fontSize: 16}}/>
|
||||
</IconButton>
|
||||
<Typography variant="caption" sx={{fontSize: '10px'}}>
|
||||
Aktualisieren
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{flex: 1, textAlign: 'center', fontWeight: 600, fontSize: '10px'}}>
|
||||
{mockVerkaufArtikel.length} Artikel
|
||||
</Typography>
|
||||
<Button size="small" variant="text" sx={{fontSize: '10px', py: 0.25}}>
|
||||
Rückgängig
|
||||
</Button>
|
||||
<Button size="small" variant="text" sx={{fontSize: '10px', py: 0.25}}>
|
||||
Speichern
|
||||
</Button>
|
||||
</Toolbar>
|
||||
|
||||
<TableContainer sx={{flex: 1}}>
|
||||
<Table size="small" stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5, width: 40}}>KNr</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5, width: 40}} align="center">+</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5, width: 60}}
|
||||
align="center">Menge</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5, width: 40}} align="center">-</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5}}>Buchungstext</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5, width: 70}}
|
||||
align="right">Betrag</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5, width: 70}}
|
||||
align="right">Gebucht</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{mockVerkaufArtikel.map((artikel, idx) => {
|
||||
const menge = verkaufMengen[artikel.text] || 0;
|
||||
const betrag = menge * artikel.einzelpreis;
|
||||
return (
|
||||
<TableRow
|
||||
key={idx}
|
||||
sx={{
|
||||
'&:nth-of-type(odd)': {bgcolor: 'action.hover'},
|
||||
bgcolor: idx === 0 || idx === 1 ? '#FFFFCC' : undefined,
|
||||
}}
|
||||
>
|
||||
<TableCell sx={{fontSize: '10px', py: 0.5}}>{artikel.knr}</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', px: 0.25, py: 0.5}} align="center">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleMengeChange(artikel.text, 1)}
|
||||
sx={{width: 20, height: 20}}
|
||||
>
|
||||
<AddIcon sx={{fontSize: 14}}/>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', px: 0.5, py: 0.5}} align="center">
|
||||
<TextField
|
||||
size="small"
|
||||
value={menge}
|
||||
onChange={(e) => setVerkaufMengen(prev => ({
|
||||
...prev,
|
||||
[artikel.text]: Math.max(0, parseInt(e.target.value) || 0),
|
||||
}))}
|
||||
sx={{
|
||||
width: 50,
|
||||
'& .MuiInputBase-input': {
|
||||
textAlign: 'center',
|
||||
fontSize: '10px',
|
||||
py: 0.25,
|
||||
px: 0.5,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', px: 0.25, py: 0.5}} align="center">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleMengeChange(artikel.text, -1)}
|
||||
sx={{width: 20, height: 20}}
|
||||
>
|
||||
<RemoveIcon sx={{fontSize: 14}}/>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', py: 0.5}}>{artikel.text}</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: betrag > 0 ? 600 : 400, py: 0.5}} align="right">
|
||||
{betrag.toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', py: 0.5}} align="right">{artikel.gebucht}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
)}
|
||||
|
||||
{tabValue === 1 && (
|
||||
<>
|
||||
<Toolbar variant="dense" sx={{
|
||||
minHeight: 28,
|
||||
px: 1,
|
||||
gap: 1,
|
||||
bgcolor: 'background.paper',
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider'
|
||||
}}>
|
||||
<IconButton size="small" sx={{width: 24, height: 24}}>
|
||||
<RefreshIcon sx={{fontSize: 16}}/>
|
||||
</IconButton>
|
||||
<Typography variant="caption" sx={{fontSize: '10px'}}>
|
||||
Aktualisieren
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{flex: 1, textAlign: 'center', fontWeight: 600, fontSize: '10px'}}>
|
||||
0 Buchungen
|
||||
</Typography>
|
||||
<Button size="small" variant="text" color="error" sx={{fontSize: '10px', py: 0.25}}>
|
||||
Stornieren
|
||||
</Button>
|
||||
</Toolbar>
|
||||
|
||||
<TableContainer sx={{flex: 1}}>
|
||||
<Table size="small" stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5}}>Kopfnr</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5}}>Menge</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5}}>Buchungstext</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5}} align="right">Soll</TableCell>
|
||||
<TableCell sx={{fontSize: '10px', fontWeight: 600, py: 0.5}} align="right">Haben</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} sx={{textAlign: 'center', color: 'text.secondary', fontSize: '10px', py: 2}}>
|
||||
Keine Buchungen vorhanden
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import React, {useState} from 'react'
|
||||
|
||||
const ERROR_IMG_SRC =
|
||||
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODgiIGhlaWdodD0iODgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBvcGFjaXR5PSIuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIzLjciPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjU2IiBoZWlnaHQ9IjU2IiByeD0iNiIvPjxwYXRoIGQ9Im0xNiA1OCAxNi0xOCAzMiAzMiIvPjxjaXJjbGUgY3g9IjUzIiBjeT0iMzUiIHI9IjciLz48L3N2Zz4KCg=='
|
||||
|
||||
export function ImageWithFallback(props: React.ImgHTMLAttributes<HTMLImageElement>) {
|
||||
const [didError, setDidError] = useState(false)
|
||||
|
||||
const handleError = () => {
|
||||
setDidError(true)
|
||||
}
|
||||
|
||||
const {src, alt, style, className, ...rest} = props
|
||||
|
||||
return didError ? (
|
||||
<div
|
||||
className={`inline-block bg-gray-100 text-center align-middle ${className ?? ''}`}
|
||||
style={style}
|
||||
>
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
<img src={ERROR_IMG_SRC} alt="Error loading image" {...rest} data-original-url={src}/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<img src={src} alt={alt} className={className} style={style} {...rest} onError={handleError}/>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user