From 91372c73b45b23603372778a77a1b52672db57bd Mon Sep 17 00:00:00 2001 From: Frederik Beimgraben Date: Mon, 1 Sep 2025 17:22:38 +0200 Subject: [PATCH] Add Help-Floater --- backend/src/service_api.py | 38 +++- frontend/src/App.tsx | 20 +- .../src/components/HelpButton/HelpButton.tsx | 192 ++++++++++++++++++ frontend/src/components/HelpButton/index.ts | 1 + frontend/src/pages/AdminDashboard.tsx | 48 ++++- frontend/src/types/api.ts | 1 + 6 files changed, 279 insertions(+), 21 deletions(-) create mode 100644 frontend/src/components/HelpButton/HelpButton.tsx create mode 100644 frontend/src/components/HelpButton/index.ts diff --git a/backend/src/service_api.py b/backend/src/service_api.py index 7a43d2c7..dd2cb4ad 100644 --- a/backend/src/service_api.py +++ b/backend/src/service_api.py @@ -422,7 +422,7 @@ def _inject_meta_for_render(payload: Dict[str, Any], pa_id: str, pa_key: Optiona p2["pa"]["meta"]["key"] = pa_key # Calculate total amount from costs dynamically - project = p2.get("pa", {}).get("project", {}) + project = p2.get("pa", {}).get("project", {}) or {} costs = project.get("costs", []) total_amount = 0.0 @@ -446,7 +446,7 @@ def _sanitize_payload_for_db(payload: Dict[str, Any]) -> Dict[str, Any]: meta["key"] = None # Remove calculated total from database storage - project = p2.get("pa", {}).get("project", {}) + project = p2.get("pa", {}).get("project", {}) or {} if "totals" in project and "requestedAmountEur" in project["totals"]: del project["totals"]["requestedAmountEur"] @@ -517,8 +517,8 @@ def create_application( financing_data = project_data.get("financing", {}) # Check which financing type has actual content (not just empty structure) - qsm_data = financing_data.get("qsm", {}) - vsm_data = financing_data.get("vsm", {}) + qsm_data = financing_data.get("qsm", {}) or {} + vsm_data = financing_data.get("vsm", {}) or {} # QSM has 'code' and 'flags' fields when filled has_qsm_content = bool(qsm_data.get("code") or qsm_data.get("flags")) @@ -527,7 +527,7 @@ def create_application( # Also check institution fields (VSM-specific) # Note: Institution name alone doesn't determine variant, as QSM can also have institution name - institution_data = pa_data.get("applicant", {}).get("institution", {}) + institution_data = pa_data.get("applicant", {}).get("institution", {}) or {} has_institution_type = bool(institution_data.get("type")) # Only type is VSM-specific # Determine variant based on which fields have actual content @@ -746,12 +746,21 @@ def list_applications( rows = db.execute(q).scalars().all() result = [] for r in rows: - # Extract project name from payload if available + # Extract project name and calculate total amount from payload if available project_name = "" + total_amount = 0.0 if r.payload_json: try: payload = json.loads(r.payload_json) if isinstance(r.payload_json, str) else r.payload_json - project_name = payload.get("pa", {}).get("project", {}).get("name", "") + project = payload.get("pa", {}).get("project", {}) + project_name = project.get("name", "") + # Calculate total from costs + costs = project.get("costs", []) + for cost in costs: + if isinstance(cost, dict) and "amountEur" in cost: + amount = cost.get("amountEur") + if amount is not None and isinstance(amount, (int, float)): + total_amount += float(amount) except: pass @@ -760,6 +769,7 @@ def list_applications( "variant": "VSM" if r.variant == "COMMON" else r.variant, "status": r.status, "project_name": project_name, + "total_amount": total_amount, "created_at": r.created_at.isoformat(), "updated_at": r.updated_at.isoformat() }) @@ -772,12 +782,21 @@ def list_applications( app_row: Application = auth.get("app") if not app_row: raise HTTPException(status_code=404, detail="Application not found") - # Extract project name from payload if available + # Extract project name and calculate total amount from payload if available project_name = "" + total_amount = 0.0 if app_row.payload_json: try: payload = json.loads(app_row.payload_json) if isinstance(app_row.payload_json, str) else app_row.payload_json - project_name = payload.get("pa", {}).get("project", {}).get("name", "") + project = payload.get("pa", {}).get("project", {}) + project_name = project.get("name", "") + # Calculate total from costs + costs = project.get("costs", []) + for cost in costs: + if isinstance(cost, dict) and "amountEur" in cost: + amount = cost.get("amountEur") + if amount is not None and isinstance(amount, (int, float)): + total_amount += float(amount) except: pass @@ -786,6 +805,7 @@ def list_applications( "variant": "VSM" if app_row.variant == "COMMON" else app_row.variant, "status": app_row.status, "project_name": project_name, + "total_amount": total_amount, "created_at": app_row.created_at.isoformat(), "updated_at": app_row.updated_at.isoformat() }] diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ebfa766b..efd50600 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,7 +10,7 @@ import CssBaseline from "@mui/material/CssBaseline"; import { SnackbarProvider } from "notistack"; import { QueryClient, QueryClientProvider } from "react-query"; import { ReactQueryDevtools } from "react-query/devtools"; -import { IconButton, Box } from "@mui/material"; +import { Fab, Box } from "@mui/material"; import { Brightness4, Brightness7 } from "@mui/icons-material"; // Components @@ -25,6 +25,7 @@ import HelpPage from "./pages/HelpPage"; import EditApplicationPage from "./pages/EditApplicationPage"; import ErrorBoundary from "./components/ErrorBoundary/ErrorBoundary"; import LoadingSpinner from "./components/LoadingSpinner/LoadingSpinner"; +import HelpButton from "./components/HelpButton"; // Store import { useApplicationStore } from "./store/applicationStore"; @@ -245,24 +246,28 @@ const App: React.FC = () => { - {darkMode ? : } - + { {process.env.NODE_ENV === "development" && ( )} + + {/* Help Button - Always visible */} + diff --git a/frontend/src/components/HelpButton/HelpButton.tsx b/frontend/src/components/HelpButton/HelpButton.tsx new file mode 100644 index 00000000..78996518 --- /dev/null +++ b/frontend/src/components/HelpButton/HelpButton.tsx @@ -0,0 +1,192 @@ +import React, { useState } from "react"; +import { + Fab, + Menu, + MenuItem, + ListItemIcon, + ListItemText, + Typography, + Divider, + Box, +} from "@mui/material"; +import { Help, Phone, Event, BugReport, Close } from "@mui/icons-material"; + +interface HelpButtonProps { + className?: string; +} + +const HelpButton: React.FC = ({ className }) => { + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handlePhoneClick = () => { + window.location.href = "tel:+4971212711099"; + handleClose(); + }; + + const handleBookingClick = () => { + window.open( + "https://app.reclaim.ai/m/verfasste-studierendenschaft/help", + "_blank", + "noopener,noreferrer", + ); + handleClose(); + }; + + const handleFeedbackClick = () => { + window.open( + "https://cloud.reutlingen.university/apps/forms/s/gZwfmxt7q2YBDXm5dBwicqmn", + "_blank", + "noopener,noreferrer", + ); + handleClose(); + }; + + return ( + <> + + {open ? : } + + + + {/* Header */} + + + Hilfe & Kontakt + + + STUPA Reutlingen University + + + + + + {/* Phone Contact */} + + + + + + + STUPA-Büro anrufen + + + +49 7121 271 1099 + + + + + + + {/* Booking Link */} + + + + + + + Termin vereinbaren + + + Über Reclaim buchen + + + + + + + {/* Feedback Form */} + + + + + + + Feedback & Fehler melden + + + Formular ausfüllen + + + + + + + {/* Footer Info */} + + theme.palette.mode === "dark" ? "grey.900" : "grey.50", + }} + > + + Bei technischen Problemen oder Fragen zur Antragstellung + + + + + ); +}; + +export default HelpButton; diff --git a/frontend/src/components/HelpButton/index.ts b/frontend/src/components/HelpButton/index.ts new file mode 100644 index 00000000..3456ccb2 --- /dev/null +++ b/frontend/src/components/HelpButton/index.ts @@ -0,0 +1 @@ +export { default } from "./HelpButton"; diff --git a/frontend/src/pages/AdminDashboard.tsx b/frontend/src/pages/AdminDashboard.tsx index 92d844b1..ae733a9f 100644 --- a/frontend/src/pages/AdminDashboard.tsx +++ b/frontend/src/pages/AdminDashboard.tsx @@ -126,6 +126,10 @@ const AdminDashboard: React.FC = () => { .length, approved: applicationList.filter((app) => app.status === "approved").length, rejected: applicationList.filter((app) => app.status === "rejected").length, + totalAmount: applicationList.reduce( + (sum, app) => sum + (app.total_amount || 0), + 0, + ), }; return ( @@ -154,7 +158,7 @@ const AdminDashboard: React.FC = () => { {/* Statistics Cards */} - + @@ -166,7 +170,7 @@ const AdminDashboard: React.FC = () => { - + @@ -178,7 +182,7 @@ const AdminDashboard: React.FC = () => { - + @@ -190,7 +194,7 @@ const AdminDashboard: React.FC = () => { - + @@ -202,7 +206,7 @@ const AdminDashboard: React.FC = () => { - + @@ -214,6 +218,29 @@ const AdminDashboard: React.FC = () => { + + + + + {stats.totalAmount.toLocaleString("de-DE", { + style: "currency", + currency: "EUR", + maximumFractionDigits: 0, + })} + + + Gesamtsumme + + + + {/* Filters and Search */} @@ -285,6 +312,7 @@ const AdminDashboard: React.FC = () => { Projektname Typ Status + Summe Erstellt Geändert Aktionen @@ -293,7 +321,7 @@ const AdminDashboard: React.FC = () => { {applicationList.length === 0 && !isLoading ? ( - + Keine Anträge gefunden @@ -340,6 +368,14 @@ const AdminDashboard: React.FC = () => { Abgelehnt + + + {(application.total_amount || 0).toLocaleString("de-DE", { + style: "currency", + currency: "EUR", + })} + + {dayjs(application.created_at).format("DD.MM.YY HH:mm")} diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index f4233a9c..967af9af 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -151,6 +151,7 @@ export interface ApplicationListItem { variant: string; status: string; project_name?: string; + total_amount?: number; created_at: string; updated_at: string; }