Add Help-Floater
This commit is contained in:
parent
629086fdb6
commit
91372c73b4
@ -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()
|
||||
}]
|
||||
|
||||
@ -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 = () => {
|
||||
<Box
|
||||
sx={{
|
||||
position: "fixed",
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
bottom: 24,
|
||||
right: 24,
|
||||
zIndex: 1200,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
<Fab
|
||||
onClick={handleToggleDarkMode}
|
||||
color="inherit"
|
||||
size="large"
|
||||
sx={{
|
||||
bgcolor: "background.paper",
|
||||
boxShadow: 2,
|
||||
"&:hover": {
|
||||
bgcolor: "action.hover",
|
||||
},
|
||||
color: darkMode ? "white" : "black",
|
||||
}}
|
||||
>
|
||||
{darkMode ? <Brightness7 /> : <Brightness4 />}
|
||||
</IconButton>
|
||||
</Fab>
|
||||
</Box>
|
||||
<SnackbarProvider
|
||||
maxSnack={3}
|
||||
@ -351,6 +356,9 @@ const App: React.FC = () => {
|
||||
{process.env.NODE_ENV === "development" && (
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
)}
|
||||
|
||||
{/* Help Button - Always visible */}
|
||||
<HelpButton />
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
</ErrorBoundary>
|
||||
|
||||
192
frontend/src/components/HelpButton/HelpButton.tsx
Normal file
192
frontend/src/components/HelpButton/HelpButton.tsx
Normal file
@ -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<HelpButtonProps> = ({ className }) => {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
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 (
|
||||
<>
|
||||
<Fab
|
||||
color="primary"
|
||||
aria-label="help"
|
||||
onClick={handleClick}
|
||||
className={className}
|
||||
size="large"
|
||||
sx={{
|
||||
position: "fixed",
|
||||
bottom: 96,
|
||||
right: 24,
|
||||
zIndex: 1300,
|
||||
boxShadow: 3,
|
||||
"&:hover": {
|
||||
boxShadow: 6,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{open ? <Close /> : <Help />}
|
||||
</Fab>
|
||||
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
}}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
minWidth: 280,
|
||||
boxShadow: 3,
|
||||
borderRadius: 2,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box
|
||||
sx={{
|
||||
px: 2,
|
||||
py: 1.5,
|
||||
backgroundColor: "primary.main",
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
fontWeight="bold"
|
||||
sx={{ color: "white" }}
|
||||
>
|
||||
Hilfe & Kontakt
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ opacity: 0.9 }}>
|
||||
STUPA Reutlingen University
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Phone Contact */}
|
||||
<MenuItem onClick={handlePhoneClick} sx={{ py: 1.5 }}>
|
||||
<ListItemIcon>
|
||||
<Phone color="primary" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
<Typography variant="body1" fontWeight="medium">
|
||||
STUPA-Büro anrufen
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
+49 7121 271 1099
|
||||
</Typography>
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
<Divider variant="inset" component="li" />
|
||||
|
||||
{/* Booking Link */}
|
||||
<MenuItem onClick={handleBookingClick} sx={{ py: 1.5 }}>
|
||||
<ListItemIcon>
|
||||
<Event color="primary" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
<Typography variant="body1" fontWeight="medium">
|
||||
Termin vereinbaren
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Über Reclaim buchen
|
||||
</Typography>
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
<Divider variant="inset" component="li" />
|
||||
|
||||
{/* Feedback Form */}
|
||||
<MenuItem onClick={handleFeedbackClick} sx={{ py: 1.5 }}>
|
||||
<ListItemIcon>
|
||||
<BugReport color="primary" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
<Typography variant="body1" fontWeight="medium">
|
||||
Feedback & Fehler melden
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Formular ausfüllen
|
||||
</Typography>
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Footer Info */}
|
||||
<Box
|
||||
sx={{
|
||||
px: 2,
|
||||
py: 1,
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.mode === "dark" ? "grey.900" : "grey.50",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
align="center"
|
||||
display="block"
|
||||
>
|
||||
Bei technischen Problemen oder Fragen zur Antragstellung
|
||||
</Typography>
|
||||
</Box>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HelpButton;
|
||||
1
frontend/src/components/HelpButton/index.ts
Normal file
1
frontend/src/components/HelpButton/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from "./HelpButton";
|
||||
@ -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 */}
|
||||
<Grid container spacing={3} sx={{ mb: 4 }}>
|
||||
<Grid item xs={12} sm={6} md={2.4}>
|
||||
<Grid item xs={12} sm={6} md={2}>
|
||||
<Card>
|
||||
<CardContent sx={{ textAlign: "center" }}>
|
||||
<Typography variant="h4" color="primary">
|
||||
@ -166,7 +170,7 @@ const AdminDashboard: React.FC = () => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={2.4}>
|
||||
<Grid item xs={12} sm={6} md={2}>
|
||||
<Card>
|
||||
<CardContent sx={{ textAlign: "center" }}>
|
||||
<Typography variant="h4" color="info.main">
|
||||
@ -178,7 +182,7 @@ const AdminDashboard: React.FC = () => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={2.4}>
|
||||
<Grid item xs={12} sm={6} md={2}>
|
||||
<Card>
|
||||
<CardContent sx={{ textAlign: "center" }}>
|
||||
<Typography variant="h4" color="warning.main">
|
||||
@ -190,7 +194,7 @@ const AdminDashboard: React.FC = () => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={2.4}>
|
||||
<Grid item xs={12} sm={6} md={2}>
|
||||
<Card>
|
||||
<CardContent sx={{ textAlign: "center" }}>
|
||||
<Typography variant="h4" color="success.main">
|
||||
@ -202,7 +206,7 @@ const AdminDashboard: React.FC = () => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={2.4}>
|
||||
<Grid item xs={12} sm={6} md={2}>
|
||||
<Card>
|
||||
<CardContent sx={{ textAlign: "center" }}>
|
||||
<Typography variant="h4" color="error.main">
|
||||
@ -214,6 +218,29 @@ const AdminDashboard: React.FC = () => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6} md={2}>
|
||||
<Card>
|
||||
<CardContent sx={{ textAlign: "center" }}>
|
||||
<Typography
|
||||
variant="h5"
|
||||
color="secondary.main"
|
||||
sx={{
|
||||
fontWeight: "bold",
|
||||
fontSize: { xs: "1rem", sm: "1.25rem" },
|
||||
}}
|
||||
>
|
||||
{stats.totalAmount.toLocaleString("de-DE", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
maximumFractionDigits: 0,
|
||||
})}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Gesamtsumme
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Filters and Search */}
|
||||
@ -285,6 +312,7 @@ const AdminDashboard: React.FC = () => {
|
||||
<TableCell>Projektname</TableCell>
|
||||
<TableCell>Typ</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell align="right">Summe</TableCell>
|
||||
<TableCell>Erstellt</TableCell>
|
||||
<TableCell>Geändert</TableCell>
|
||||
<TableCell align="right">Aktionen</TableCell>
|
||||
@ -293,7 +321,7 @@ const AdminDashboard: React.FC = () => {
|
||||
<TableBody>
|
||||
{applicationList.length === 0 && !isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} align="center">
|
||||
<TableCell colSpan={8} align="center">
|
||||
<Typography color="text.secondary">
|
||||
Keine Anträge gefunden
|
||||
</Typography>
|
||||
@ -340,6 +368,14 @@ const AdminDashboard: React.FC = () => {
|
||||
<MenuItem value="rejected">Abgelehnt</MenuItem>
|
||||
</TextField>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography variant="body2" sx={{ fontWeight: "medium" }}>
|
||||
{(application.total_amount || 0).toLocaleString("de-DE", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
})}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{dayjs(application.created_at).format("DD.MM.YY HH:mm")}
|
||||
|
||||
@ -151,6 +151,7 @@ export interface ApplicationListItem {
|
||||
variant: string;
|
||||
status: string;
|
||||
project_name?: string;
|
||||
total_amount?: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user