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 ? : }
+
+
+
+ >
+ );
+};
+
+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 = () => {
+
+
+ {(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;
}