Files
DeineDorfApp-Admin-Panel/src/pages/Company/EditCompanyPage.jsx
2026-04-17 09:57:01 +02:00

356 lines
17 KiB
JavaScript

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) {
// ✅ err.response.data kann ein Objekt sein → .error extrahieren
const serverMessage = err.response?.data?.error || err.response?.data;
setError(typeof serverMessage === 'string' ? serverMessage : '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) {
// ✅ Fehlermeldung korrekt aus Axios-Response extrahieren
const serverMessage = err.response?.data?.error || err.response?.data;
const message = typeof serverMessage === 'string'
? serverMessage
: err.message || 'Fehler beim Speichern';
setError(message);
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">
{typeof error === 'string' ? error : JSON.stringify(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 />
<Alert severity="info" sx={{ maxWidth: 560, mb: 3 }}>
Website muss mit "https://" anfangen
</Alert>
<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>
<Button
component="label"
size="small"
sx={{ mt: 0.5, px: 0, minWidth: 0, fontSize: 12 }}
>
Bild ersetzen
<input type="file" hidden accept="image/jpeg,image/png,image/webp" 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/jpeg,image/png,image/webp" onChange={handleTitleImageChange} />
</Button>
)}
</Box>
<Divider />
{/* ── Weitere Bilder ── */}
<Box>
<Typography variant="subtitle2" fontWeight={500} mb={1.5}>
Weitere Bilder
</Typography>
<Stack spacing={1.5}>
{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>
))}
{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/jpeg,image/png,image/webp" 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>
);
}