From a8e155f5d570757500e943f9e4e2f75c34686a16 Mon Sep 17 00:00:00 2001 From: eddy Date: Sun, 12 Apr 2026 18:52:25 +0200 Subject: [PATCH] company + layout verbessert --- src/App.jsx | 18 ++ src/components/Layout.jsx | 125 ++++++++---- src/pages/CompanyPage.jsx | 211 +++++++++++++++++++++ src/pages/EditCompanyPage.jsx | 345 ++++++++++++++++++++++++++++++++++ 4 files changed, 667 insertions(+), 32 deletions(-) create mode 100644 src/pages/CompanyPage.jsx create mode 100644 src/pages/EditCompanyPage.jsx diff --git a/src/App.jsx b/src/App.jsx index 77672c6..ff82c56 100644 --- a/src/App.jsx +++ b/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() { } /> + + + + } + /> } /> + + + + } + /> , roles: null }, - { label: 'Users', path: '/users', icon: , roles: ['ADMIN'] }, - { label: 'Neuigkeiten', path: '/posts', icon: , roles: ['ADMIN', 'REPORTER'] }, - { label: 'Push', path: '/push', icon: , roles: ['ADMIN'] }, - { label: 'Organization', path: '/organizations', icon: , roles: ['ADMIN', 'SITE_OWNER'] }, - { label: 'Sehenswürdigkeiten', path: '/attractions', icon: , roles: ['ADMIN', 'REPORTER'] }, +const navCategories = [ + { + label: null, + items: [ + { label: 'Home', path: '/', icon: , roles: null }, + ], + }, + { + label: 'Administration', + items: [ + { label: 'Users', path: '/users', icon: , roles: ['ADMIN'] }, + { label: 'Push', path: '/push', icon: , roles: ['ADMIN'] }, + ], + }, + { + label: 'Reporter', + items: [ + { label: 'Neuigkeiten', path: '/posts', icon: , roles: ['ADMIN', 'REPORTER'] }, + { label: 'Sehenswürdigkeiten', path: '/attractions', icon: , roles: ['ADMIN', 'REPORTER'] }, + ], + }, + { + label: 'Seiten', + items: [ + { label: 'Unternehmen', path: '/companies', icon: , roles: ['ADMIN', 'SITE_OWNER'] }, + { label: 'Organization', path: '/organizations', icon: , 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 ( - - {/* Sidebar */} + + {/* Logo */} @@ -64,26 +100,51 @@ export default function Layout() { - - {visibleNavItems.map((item) => ( - - navigate(item.path)} - sx={{ borderRadius: 2 }} - > - {item.icon} - - - + + {/* Navigation */} + + {visibleCategories.map((cat, catIdx) => ( + + {cat.label && ( + + {cat.label} + + )} + {cat.items.map((item) => ( + + navigate(item.path)} + sx={{ borderRadius: 2 }} + > + {item.icon} + + + + ))} + ))} + + + {/* Logout */} diff --git a/src/pages/CompanyPage.jsx b/src/pages/CompanyPage.jsx new file mode 100644 index 0000000..d01370e --- /dev/null +++ b/src/pages/CompanyPage.jsx @@ -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 }) => ( + + {value} + + ), + }, + { + field: 'active', + headerName: 'Status', + width: 150, + renderCell: ({ value }) => ( + + ), + }, + { + field: 'actions', + headerName: '', + width: 120, + sortable: false, + renderCell: ({ row }) => ( + + handleDelete(row.id)}> + + + handleEdit(row.id)}> + + + + ), + }, + ]; + + return ( + + {/* Header */} + + Unternehmen + + + + {error && {error}} + + {/* Tabelle */} + + + + + {/* Create Dialog */} + + Unternehmen erstellen + + + + {creatingError && {creatingError}} + + + + + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/src/pages/EditCompanyPage.jsx b/src/pages/EditCompanyPage.jsx new file mode 100644 index 0000000..68bca8f --- /dev/null +++ b/src/pages/EditCompanyPage.jsx @@ -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 ( + + + navigate('/companies')} size="small"> + + + Unternehmen bearbeiten + + + + + + + {error && {error}} + + + + + + + + + + setForm((prev) => ({ ...prev, active: e.target.checked })) + } + /> + } + label="Aktiv" + /> + + + + {/* ── Titelbild ── */} + + + } + label="Titelbild" + size="small" + color="primary" + sx={{ height: 22, fontSize: 11 }} + /> + + Wird als Vorschaubild des Unternehmens verwendet + + + + {hasTitleImage ? ( + + + + + {titleImageFile ? titleImageFile.name : titleImageUrl} + + {/* Titelbild tauschen ohne zu löschen */} + + + + + + + ) : ( + + )} + + + + + {/* ── Weitere Bilder ── */} + + + Weitere Bilder + + + + {/* Bestehende weitere Bilder */} + {otherImageUrls.map((src, i) => ( + + + + {src} + + handleRemoveOtherUrl(i)}> + + + + ))} + + {/* Neu hinzugefügte weitere Bilder */} + {otherPreviews.map((src, i) => ( + + + + {otherFiles[i]?.name} + + handleRemoveOtherFile(i)}> + + + + ))} + + + + + + + + + + + + + + + ); +} \ No newline at end of file