company + layout verbessert
This commit is contained in:
18
src/App.jsx
18
src/App.jsx
@@ -14,6 +14,8 @@ import EditAttractionPage from './pages/EditAttractionPage';
|
||||
import AttractionPage from './pages/AttractionPage';
|
||||
import HomePage from './pages/HomePage';
|
||||
import EditOrganizationPage from "./pages/EditOrganizationPage.jsx";
|
||||
import CompanyPage from "./pages/CompanyPage.jsx";
|
||||
import EditCompanyPage from "./pages/EditCompanyPage.jsx";
|
||||
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
@@ -81,6 +83,14 @@ export default function App() {
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="companies"
|
||||
element={
|
||||
<PrivateRoute allowedRoles={['ADMIN', 'SITE_OWNER']}>
|
||||
<CompanyPage />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="attractions/:id/edit"
|
||||
element={
|
||||
@@ -89,6 +99,14 @@ export default function App() {
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="companies/:id/edit"
|
||||
element={
|
||||
<PrivateRoute allowedRoles={['ADMIN', 'SITE_OWNER']}>
|
||||
<EditCompanyPage/>
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="organizations/:id/edit"
|
||||
element={
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useNavigate, useLocation, Outlet } from 'react-router-dom';
|
||||
import {
|
||||
Box, Drawer, List, ListItem, ListItemButton, ListItemIcon, ListItemText,
|
||||
AppBar, Toolbar, Typography, IconButton, Divider, Avatar, Tooltip,
|
||||
AppBar, Toolbar, Typography, Divider, Avatar, Tooltip,
|
||||
} from '@mui/material';
|
||||
import PeopleIcon from '@mui/icons-material/People';
|
||||
import ArticleIcon from '@mui/icons-material/Article';
|
||||
@@ -11,24 +11,47 @@ import LogoutIcon from '@mui/icons-material/Logout';
|
||||
import DashboardIcon from '@mui/icons-material/Dashboard';
|
||||
import LocationOnIcon from '@mui/icons-material/LocationOn';
|
||||
import HomeIcon from '@mui/icons-material/Home';
|
||||
import StoreIcon from '@mui/icons-material/Store';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
const DRAWER_WIDTH = 240;
|
||||
|
||||
const navItems = [
|
||||
{ label: 'Home', path: '/', icon: <HomeIcon />, roles: null },
|
||||
{ label: 'Users', path: '/users', icon: <PeopleIcon />, roles: ['ADMIN'] },
|
||||
{ label: 'Neuigkeiten', path: '/posts', icon: <ArticleIcon />, roles: ['ADMIN', 'REPORTER'] },
|
||||
{ label: 'Push', path: '/push', icon: <NotificationsActiveIcon />, roles: ['ADMIN'] },
|
||||
{ label: 'Organization', path: '/organizations', icon: <CorporateFareIcon />, roles: ['ADMIN', 'SITE_OWNER'] },
|
||||
{ label: 'Sehenswürdigkeiten', path: '/attractions', icon: <LocationOnIcon />, roles: ['ADMIN', 'REPORTER'] },
|
||||
const navCategories = [
|
||||
{
|
||||
label: null,
|
||||
items: [
|
||||
{ label: 'Home', path: '/', icon: <HomeIcon />, roles: null },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Administration',
|
||||
items: [
|
||||
{ label: 'Users', path: '/users', icon: <PeopleIcon />, roles: ['ADMIN'] },
|
||||
{ label: 'Push', path: '/push', icon: <NotificationsActiveIcon />, roles: ['ADMIN'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Reporter',
|
||||
items: [
|
||||
{ label: 'Neuigkeiten', path: '/posts', icon: <ArticleIcon />, roles: ['ADMIN', 'REPORTER'] },
|
||||
{ label: 'Sehenswürdigkeiten', path: '/attractions', icon: <LocationOnIcon />, roles: ['ADMIN', 'REPORTER'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Seiten',
|
||||
items: [
|
||||
{ label: 'Unternehmen', path: '/companies', icon: <StoreIcon />, roles: ['ADMIN', 'SITE_OWNER'] },
|
||||
{ label: 'Organization', path: '/organizations', icon: <CorporateFareIcon />, roles: ['ADMIN', 'SITE_OWNER'] },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const allNavItems = navCategories.flatMap((c) => c.items);
|
||||
|
||||
function getActiveLabel(pathname) {
|
||||
// Exakter Match für "/" zuerst prüfen, dann startsWith für den Rest
|
||||
const exact = navItems.find((i) => i.path === pathname);
|
||||
const exact = allNavItems.find((i) => i.path === pathname);
|
||||
if (exact) return exact.label;
|
||||
const partial = navItems.find((i) => i.path !== '/' && pathname.startsWith(i.path));
|
||||
const partial = allNavItems.find((i) => i.path !== '/' && pathname.startsWith(i.path));
|
||||
return partial?.label ?? 'Admin';
|
||||
}
|
||||
|
||||
@@ -37,13 +60,15 @@ export default function Layout() {
|
||||
const location = useLocation();
|
||||
const { logout, role } = useAuth();
|
||||
|
||||
const visibleNavItems = navItems.filter(
|
||||
(item) => !item.roles || item.roles.includes(role)
|
||||
);
|
||||
const visibleCategories = navCategories
|
||||
.map((cat) => ({
|
||||
...cat,
|
||||
items: cat.items.filter((item) => !item.roles || item.roles.includes(role)),
|
||||
}))
|
||||
.filter((cat) => cat.items.length > 0);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
{/* Sidebar */}
|
||||
<Box sx={{ display: 'flex', minHeight: '100vh', bgcolor: 'grey.50' }}>
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
sx={{
|
||||
@@ -54,9 +79,20 @@ export default function Layout() {
|
||||
boxSizing: 'border-box',
|
||||
borderRight: '1px solid',
|
||||
borderColor: 'divider',
|
||||
'&::-webkit-scrollbar': { width: 4 },
|
||||
'&::-webkit-scrollbar-track': { background: 'transparent' },
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: 'transparent',
|
||||
borderRadius: 4,
|
||||
transition: 'background 0.2s',
|
||||
},
|
||||
'&:hover::-webkit-scrollbar-thumb': {
|
||||
background: 'rgba(0,0,0,0.15)',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Logo */}
|
||||
<Toolbar sx={{ px: 2 }}>
|
||||
<DashboardIcon sx={{ mr: 1, color: 'primary.main' }} />
|
||||
<Typography variant="h6" fontWeight={600} color="primary.main">
|
||||
@@ -64,26 +100,51 @@ export default function Layout() {
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
<Divider />
|
||||
<List sx={{ px: 1, pt: 1 }}>
|
||||
{visibleNavItems.map((item) => (
|
||||
<ListItem key={item.path} disablePadding sx={{ mb: 0.5 }}>
|
||||
<ListItemButton
|
||||
selected={
|
||||
item.path === '/'
|
||||
? location.pathname === '/'
|
||||
: location.pathname.startsWith(item.path)
|
||||
}
|
||||
onClick={() => navigate(item.path)}
|
||||
sx={{ borderRadius: 2 }}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 36 }}>{item.icon}</ListItemIcon>
|
||||
<ListItemText primary={item.label} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
{/* Navigation */}
|
||||
<List sx={{ px: 1, pt: 1, pb: 1 }}>
|
||||
{visibleCategories.map((cat, catIdx) => (
|
||||
<Box key={catIdx} sx={{ mt: catIdx === 0 ? 0 : 2 }}>
|
||||
{cat.label && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
px: 1.5,
|
||||
pb: 0.5,
|
||||
display: 'block',
|
||||
color: 'text.disabled',
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.08em',
|
||||
}}
|
||||
>
|
||||
{cat.label}
|
||||
</Typography>
|
||||
)}
|
||||
{cat.items.map((item) => (
|
||||
<ListItem key={item.path} disablePadding sx={{ mb: 0.5 }}>
|
||||
<ListItemButton
|
||||
selected={
|
||||
item.path === '/'
|
||||
? location.pathname === '/'
|
||||
: location.pathname.startsWith(item.path)
|
||||
}
|
||||
onClick={() => navigate(item.path)}
|
||||
sx={{ borderRadius: 2 }}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 36 }}>{item.icon}</ListItemIcon>
|
||||
<ListItemText primary={item.label} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</Box>
|
||||
))}
|
||||
</List>
|
||||
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Divider />
|
||||
|
||||
{/* Logout */}
|
||||
<List sx={{ px: 1, py: 1 }}>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton onClick={logout} sx={{ borderRadius: 2 }}>
|
||||
|
||||
211
src/pages/CompanyPage.jsx
Normal file
211
src/pages/CompanyPage.jsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
Box, Typography, Alert, IconButton, Tooltip,
|
||||
Dialog, DialogTitle, DialogContent, DialogActions,
|
||||
Button, Stack, TextField, CircularProgress
|
||||
} from '@mui/material';
|
||||
import { DataGrid } from '@mui/x-data-grid';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import { Chip } from '@mui/material';
|
||||
import axiosInstance from '../api/axiosInstance';
|
||||
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
|
||||
export default function CompanyPage() {
|
||||
const [rows, setRows] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [creatingError, setCreatingError] = useState('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [createForm, setCreateForm] = useState({
|
||||
name: '',
|
||||
email: ''
|
||||
});
|
||||
|
||||
const loadCompanies = async () => {
|
||||
try {
|
||||
const { data } = await axiosInstance.get('/company');
|
||||
setRows(data);
|
||||
} catch {
|
||||
setError('Fehler beim Laden der Unternehmen');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadCompanies();
|
||||
}, []);
|
||||
|
||||
const handleFormChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setCreateForm((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
setCreating(true);
|
||||
setCreatingError('');
|
||||
|
||||
try {
|
||||
const jsonPayload = {
|
||||
name: createForm.name,
|
||||
ownerEmail: createForm.email
|
||||
};
|
||||
|
||||
await axiosInstance.post('/company', jsonPayload);
|
||||
|
||||
await loadCompanies(); // Liste neu laden
|
||||
handleCreateClose();
|
||||
|
||||
} catch (err) {
|
||||
setCreatingError(err.response?.data?.message ?? 'Erstellen fehlgeschlagen');
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateClose = () => {
|
||||
setOpen(false);
|
||||
setCreateForm({ name: '', email: '' });
|
||||
setCreatingError('');
|
||||
};
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
if (!window.confirm('Unternehmen wirklich löschen?')) return;
|
||||
|
||||
try {
|
||||
await axiosInstance.delete(`/company/${id}`);
|
||||
setRows((prev) => prev.filter((r) => r.id !== id));
|
||||
} catch (err){
|
||||
alert(err.response?.data?.message ?? 'Löschen fehlgeschlagen');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (id) => {
|
||||
navigate(`/companies/${id}/edit`);
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ field: 'id', headerName: 'ID', width: 70 },
|
||||
{ field: 'name', headerName: 'Name', flex: 1.5 },
|
||||
{
|
||||
field: 'ownerEmail',
|
||||
headerName: 'Verwalter',
|
||||
flex: 2,
|
||||
renderCell: ({ value }) => (
|
||||
<Box sx={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', color: 'text.secondary', fontSize: 13 }}>
|
||||
{value}
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'active',
|
||||
headerName: 'Status',
|
||||
width: 150,
|
||||
renderCell: ({ value }) => (
|
||||
<Chip
|
||||
label={value ? 'Aktiv' : 'Inaktiv'}
|
||||
color={value ? 'success' : 'default'}
|
||||
size="small"
|
||||
variant={value ? 'filled' : 'outlined'}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
headerName: '',
|
||||
width: 120,
|
||||
sortable: false,
|
||||
renderCell: ({ row }) => (
|
||||
<Tooltip title="">
|
||||
<IconButton title="löschen" size="small" color="error" onClick={() => handleDelete(row.id)}>
|
||||
<DeleteOutlineIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton title="bearbeiten" size="small" color="grey" onClick={() => handleEdit(row.id)}>
|
||||
<EditOutlinedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* Header */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
|
||||
<Typography variant="h5" fontWeight={600}>Unternehmen</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
Unternehmen erstellen
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
||||
|
||||
{/* Tabelle */}
|
||||
<Box sx={{ bgcolor: 'white', borderRadius: 2, border: '1px solid', borderColor: 'divider' }}>
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
autoHeight
|
||||
pageSizeOptions={[25, 50, 100]}
|
||||
initialState={{ pagination: { paginationModel: { pageSize: 25 } } }}
|
||||
disableRowSelectionOnClick
|
||||
sx={{ border: 'none' }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Create Dialog */}
|
||||
<Dialog open={open} onClose={handleCreateClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle sx={{ fontWeight: 600}}>Unternehmen erstellen</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<Stack spacing={2.5} sx={{ mt: 1 }}>
|
||||
{creatingError && <Alert severity="error">{creatingError}</Alert>}
|
||||
|
||||
<TextField
|
||||
label="Name"
|
||||
name="name"
|
||||
value={createForm.name}
|
||||
onChange={handleFormChange}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Verwalter Email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={createForm.email}
|
||||
onChange={handleFormChange}
|
||||
fullWidth
|
||||
/>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions sx={{ px: 3, pb: 2.5 }}>
|
||||
<Button onClick={handleCreateClose} disabled={creating}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleCreate}
|
||||
disabled={creating}
|
||||
startIcon={creating ? <CircularProgress size={16} color="inherit" /> : null}
|
||||
>
|
||||
Erstellen
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
345
src/pages/EditCompanyPage.jsx
Normal file
345
src/pages/EditCompanyPage.jsx
Normal file
@@ -0,0 +1,345 @@
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {useState} from "react";
|
||||
import {
|
||||
Alert,
|
||||
Box, Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Checkbox, Chip, CircularProgress, Divider,
|
||||
FormControlLabel,
|
||||
IconButton,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||
import StarIcon from "@mui/icons-material/Star";
|
||||
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
|
||||
import AddPhotoAlternateOutlinedIcon from "@mui/icons-material/AddPhotoAlternateOutlined";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
import axiosInstance from "../api/axiosInstance.js";
|
||||
|
||||
const EMPTY_FORM = {
|
||||
description: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
address: '',
|
||||
website: '',
|
||||
contactPerson: '',
|
||||
active: false,
|
||||
}
|
||||
|
||||
|
||||
export default function EditCompanyPage() {
|
||||
const navigate = useNavigate();
|
||||
const [form, setForm] = useState(EMPTY_FORM);
|
||||
const [name, setName] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const { id } = useParams();
|
||||
|
||||
// Titelbild
|
||||
const [titleImageUrl, setTitleImageUrl] = useState(null);
|
||||
const [titleImageFile, setTitleImageFile] = useState(null);
|
||||
const [titleImagePreview, setTitleImagePreview] = useState(null);
|
||||
|
||||
// Weitere Bilder
|
||||
const [otherImageUrls, setOtherImageUrls] = useState([]);
|
||||
const [otherFiles, setOtherFiles] = useState([]);
|
||||
const [otherPreviews, setOtherPreviews] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await axiosInstance.get('/company/' + id);
|
||||
const data = response.data;
|
||||
|
||||
setName(data.name || '');
|
||||
setForm({
|
||||
description: data.description || '',
|
||||
phone: data.number || '',
|
||||
email: data.email || '',
|
||||
address: data.address || '',
|
||||
website: data.website || '',
|
||||
contactPerson: data.contactPerson || '',
|
||||
active: data.active || false,
|
||||
});
|
||||
|
||||
const imgs = data.pictures || [];
|
||||
setTitleImageUrl(imgs[0] || null);
|
||||
setOtherImageUrls(imgs.slice(1));
|
||||
} catch (err) {
|
||||
setError(err.response?.data || 'Fehler beim Laden');
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [id]);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value, type, checked } = e.target;
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
[name]: type === 'checkbox' ? checked : value
|
||||
}));
|
||||
};
|
||||
|
||||
// Titelbild-Handler
|
||||
const handleTitleImageChange = (e) => {
|
||||
const f = e.target.files[0];
|
||||
if (!f) return;
|
||||
if (titleImagePreview) URL.revokeObjectURL(titleImagePreview);
|
||||
setTitleImageFile(f);
|
||||
setTitleImagePreview(URL.createObjectURL(f));
|
||||
e.target.value = '';
|
||||
};
|
||||
|
||||
const handleRemoveTitleImage = () => {
|
||||
if (titleImagePreview) URL.revokeObjectURL(titleImagePreview);
|
||||
setTitleImageUrl(null);
|
||||
setTitleImageFile(null);
|
||||
setTitleImagePreview(null);
|
||||
};
|
||||
|
||||
// Weitere Bilder-Handler
|
||||
const handleOtherFilesChange = (e) => {
|
||||
const selected = Array.from(e.target.files);
|
||||
setOtherFiles((prev) => [...prev, ...selected]);
|
||||
const newPreviews = selected.map((f) => URL.createObjectURL(f));
|
||||
setOtherPreviews((prev) => [...prev, ...newPreviews]);
|
||||
e.target.value = '';
|
||||
};
|
||||
|
||||
const handleRemoveOtherFile = (index) => {
|
||||
URL.revokeObjectURL(otherPreviews[index]);
|
||||
setOtherFiles((prev) => prev.filter((_, i) => i !== index));
|
||||
setOtherPreviews((prev) => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleRemoveOtherUrl = (index) => {
|
||||
setOtherImageUrls((prev) => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setSaving(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append(
|
||||
'data',
|
||||
new Blob([JSON.stringify({
|
||||
...form,
|
||||
existingTitleImage: titleImageUrl || null,
|
||||
existingOtherImages: otherImageUrls,
|
||||
})], { type: 'application/json' })
|
||||
);
|
||||
|
||||
if (titleImageFile) formData.append('images', titleImageFile);
|
||||
otherFiles.forEach((f) => formData.append('images', f));
|
||||
|
||||
await axiosInstance.put(`/company/${id}`, formData, {
|
||||
headers: { 'Content-Type': undefined }
|
||||
});
|
||||
navigate('/companies');
|
||||
} catch (err) {
|
||||
setError(err.response?.data || 'Fehler beim Speichern');
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const hasTitleImage = titleImageUrl || titleImageFile;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
||||
<IconButton onClick={() => navigate('/companies')} size="small">
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h5" fontWeight={600}>Unternehmen bearbeiten</Typography>
|
||||
</Box>
|
||||
|
||||
<Card sx={{ maxWidth: 680 }}>
|
||||
<CardContent sx={{ p: 3 }}>
|
||||
<Box component="form" onSubmit={handleSubmit}>
|
||||
<Stack spacing={3}>
|
||||
{error && <Alert severity="error">{error}</Alert>}
|
||||
|
||||
<TextField label="Name" value={name} fullWidth InputProps={{ readOnly: true }} helperText="Der Name kann nicht geändert werden." />
|
||||
<TextField label="Beschreibung" name="description" value={form.description} onChange={handleChange} required fullWidth autoFocus multiline rows={6} />
|
||||
<TextField label="Telefonnummer" name="phone" value={form.phone} onChange={handleChange} required fullWidth />
|
||||
<TextField label="Email Adresse" name="email" value={form.email} onChange={handleChange} required fullWidth />
|
||||
<TextField label="Adresse" name="address" value={form.address} onChange={handleChange} required fullWidth />
|
||||
<TextField label="Webseite" name="website" value={form.website} onChange={handleChange} required fullWidth />
|
||||
<TextField label="Kontaktperson" name="contactPerson" value={form.contactPerson} onChange={handleChange} required fullWidth />
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name="active"
|
||||
checked={form.active}
|
||||
onChange={(e) =>
|
||||
setForm((prev) => ({ ...prev, active: e.target.checked }))
|
||||
}
|
||||
/>
|
||||
}
|
||||
label="Aktiv"
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* ── Titelbild ── */}
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.5 }}>
|
||||
<Chip
|
||||
icon={<StarIcon sx={{ fontSize: '13px !important' }} />}
|
||||
label="Titelbild"
|
||||
size="small"
|
||||
color="primary"
|
||||
sx={{ height: 22, fontSize: 11 }}
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Wird als Vorschaubild des Unternehmens verwendet
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{hasTitleImage ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex', alignItems: 'center', gap: 1.5,
|
||||
p: 1, borderRadius: 1,
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.main',
|
||||
bgcolor: 'primary.50',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src={titleImagePreview || titleImageUrl}
|
||||
alt="Titelbild"
|
||||
sx={{ width: 72, height: 72, objectFit: 'cover', borderRadius: 1, flexShrink: 0 }}
|
||||
/>
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{titleImageFile ? titleImageFile.name : titleImageUrl}
|
||||
</Typography>
|
||||
{/* Titelbild tauschen ohne zu löschen */}
|
||||
<Button
|
||||
component="label"
|
||||
size="small"
|
||||
sx={{ mt: 0.5, px: 0, minWidth: 0, fontSize: 12 }}
|
||||
>
|
||||
Bild ersetzen
|
||||
<input type="file" hidden accept="image/*" onChange={handleTitleImageChange} />
|
||||
</Button>
|
||||
</Box>
|
||||
<IconButton size="small" color="error" onClick={handleRemoveTitleImage}>
|
||||
<DeleteOutlineIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
) : (
|
||||
<Button
|
||||
component="label"
|
||||
startIcon={<AddPhotoAlternateOutlinedIcon />}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{ alignSelf: 'flex-start' }}
|
||||
>
|
||||
Titelbild hochladen
|
||||
<input type="file" hidden accept="image/*" onChange={handleTitleImageChange} />
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* ── Weitere Bilder ── */}
|
||||
<Box>
|
||||
<Typography variant="subtitle2" fontWeight={500} mb={1.5}>
|
||||
Weitere Bilder
|
||||
</Typography>
|
||||
|
||||
<Stack spacing={1.5}>
|
||||
{/* Bestehende weitere Bilder */}
|
||||
{otherImageUrls.map((src, i) => (
|
||||
<Box
|
||||
key={`existing-${i}`}
|
||||
sx={{
|
||||
display: 'flex', alignItems: 'center', gap: 1.5,
|
||||
p: 1, borderRadius: 1,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src={src}
|
||||
alt={`Bild ${i + 1}`}
|
||||
sx={{ width: 64, height: 64, objectFit: 'cover', borderRadius: 1, flexShrink: 0 }}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{src}
|
||||
</Typography>
|
||||
<IconButton size="small" color="error" onClick={() => handleRemoveOtherUrl(i)}>
|
||||
<DeleteOutlineIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
{/* Neu hinzugefügte weitere Bilder */}
|
||||
{otherPreviews.map((src, i) => (
|
||||
<Box
|
||||
key={`new-${i}`}
|
||||
sx={{
|
||||
display: 'flex', alignItems: 'center', gap: 1.5,
|
||||
p: 1, borderRadius: 1,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src={src}
|
||||
alt={`Neues Bild ${i + 1}`}
|
||||
sx={{ width: 64, height: 64, objectFit: 'cover', borderRadius: 1, flexShrink: 0 }}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{otherFiles[i]?.name}
|
||||
</Typography>
|
||||
<IconButton size="small" color="error" onClick={() => handleRemoveOtherFile(i)}>
|
||||
<DeleteOutlineIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
<Button
|
||||
component="label"
|
||||
startIcon={<AddPhotoAlternateOutlinedIcon />}
|
||||
size="small"
|
||||
sx={{ alignSelf: 'flex-start' }}
|
||||
>
|
||||
Bild hinzufügen
|
||||
<input type="file" hidden accept="image/*" multiple onChange={handleOtherFilesChange} />
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'flex-end', pt: 1 }}>
|
||||
<Button onClick={() => navigate('/companies')} disabled={saving}>Abbrechen</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={saving}
|
||||
startIcon={saving ? <CircularProgress size={16} color="inherit" /> : null}
|
||||
>
|
||||
Speichern
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user