diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 6b41eb92..8a1a0ed1 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -3410,6 +3410,13 @@ "label": "Projekt", "none": "Kein Projekt" }, + "events": { + "title": "Events", + "none": "Diesem Projekt sind noch keine Events zugeordnet.", + "searchPlaceholder": "Event zuordnen — nach Name suchen…", + "attached": "Event zugeordnet", + "attachFailed": "Event konnte nicht zugeordnet werden" + }, "feed": { "title": "Aktivität", "empty": "Diesem Projekt ist noch nichts zugeordnet.", diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index dccf833f..9baa4d41 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -3410,6 +3410,13 @@ "label": "Project", "none": "No project" }, + "events": { + "title": "Events", + "none": "No events grouped under this project yet.", + "searchPlaceholder": "Attach an event — search by name…", + "attached": "Event attached", + "attachFailed": "Could not attach event" + }, "feed": { "title": "Activity", "empty": "Nothing rolled up to this project yet.", diff --git a/frontend/src/pages/admin/projects/ProjectCockpitPage.tsx b/frontend/src/pages/admin/projects/ProjectCockpitPage.tsx index 3efcbf03..bbfc65c0 100644 --- a/frontend/src/pages/admin/projects/ProjectCockpitPage.tsx +++ b/frontend/src/pages/admin/projects/ProjectCockpitPage.tsx @@ -13,7 +13,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { toast } from 'react-toastify'; import { Mail, FileText, ScrollText, Receipt, Image as ImageIcon, Clock, - X, Send, RotateCw, Ban, Eye, Save, ArrowLeft, + X, Send, RotateCw, Ban, Eye, Save, ArrowLeft, Plus, Search, } from 'lucide-react'; import { Button, Card, Input, Loading } from '../../../components/common'; import { @@ -21,6 +21,7 @@ import { type ProjectOverview, type EmailPreview, } from '../../../services/projects.service'; +import { eventsService } from '../../../services/events.service'; import { useLocalizedDate } from '../../../hooks/useLocalizedDate'; import { formatMoneyMinor } from '../../../utils/money'; @@ -63,6 +64,7 @@ export const ProjectCockpitPage: React.FC = () => { const [editName, setEditName] = useState(null); const [preview, setPreview] = useState(null); const [previewLoading, setPreviewLoading] = useState(false); + const [eventSearch, setEventSearch] = useState(''); const { data, isLoading } = useQuery({ queryKey: ['project-overview', projectId], @@ -95,6 +97,25 @@ export const ProjectCockpitPage: React.FC = () => { onError: (err: any) => toast.error(err?.response?.data?.error || (t('projects.toast.emailActionFailed', 'Action failed') as string)), }); + // Event search for the "attach event" control (results exclude events + // already on this project). + const { data: eventResults } = useQuery({ + queryKey: ['project-event-search', eventSearch], + queryFn: () => eventsService.getEvents(1, 10, undefined, eventSearch), + enabled: eventSearch.trim().length >= 2, + }); + + const attachEventMutation = useMutation({ + mutationFn: (eventId: number) => projectsService.assignEvent(projectId as number, eventId), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['project-overview', projectId] }); + qc.invalidateQueries({ queryKey: ['projects'] }); + setEventSearch(''); + toast.success(t('projects.events.attached', 'Event attached') as string); + }, + onError: (err: any) => toast.error(err?.response?.data?.error || (t('projects.events.attachFailed', 'Could not attach event') as string)), + }); + const openPreview = async (emailId: number) => { setPreviewLoading(true); try { @@ -201,6 +222,49 @@ export const ProjectCockpitPage: React.FC = () => { + {/* Events in this project + attach control */} + +

{t('projects.events.title', 'Events')}

+ {data.events.length === 0 ? ( +

{t('projects.events.none', 'No events grouped under this project yet.')}

+ ) : ( +
    + {data.events.map((ev) => ( +
  • + {ev.event_name} + {ev.event_date ? format(ev.event_date) : '—'} +
  • + ))} +
+ )} +
+ + setEventSearch(e.target.value)} + placeholder={t('projects.events.searchPlaceholder', 'Attach an event — search by name…') as string} + className="pl-9" + /> + {eventSearch.trim().length >= 2 && eventResults?.events && eventResults.events.length > 0 && ( +
+ {eventResults.events + .filter((ev: any) => !data.events.some((existing) => existing.id === ev.id)) + .map((ev: any) => ( + + ))} +
+ )} +
+
+ {/* Milestone timeline */} {milestones && milestones.length > 0 && (