feat: bug fixes, layout improvements, and new features
Bug fixes: - Remove non-existent `role` column from admin users SQL query (A1) - Fix Nextcloud Talk chat API path v4 → v1 for messages/send/read (A2) - Fix ServiceModeTab sync: useState → useEffect to reflect DB state (A3) - Guard BookStack book_slug with book_id fallback to avoid broken URLs (A4) Layout & UI: - Chat panel: sticky full-height positioning, main content scrolls independently (B1) - Vehicle booking datetime inputs: explicit text color for dark mode (B2) - AnnouncementBanner moved into grid with full-width span (B3) Features: - Per-user widget visibility preferences stored in users.preferences JSONB (C1) - Link collections: grouped external links in admin UI and dashboard widget (C2) - Admin ping history: migration 026, checked_at timestamps, expandable history rows (C4) - Service mode end date picker with scheduled deactivation display (C5) - Vikunja startup config logging and configured:false warnings (C7) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
Box,
|
||||
Fade,
|
||||
} from '@mui/material';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||
import SkeletonCard from '../components/shared/SkeletonCard';
|
||||
@@ -22,6 +23,10 @@ import AdminStatusWidget from '../components/dashboard/AdminStatusWidget';
|
||||
import AnnouncementBanner from '../components/dashboard/AnnouncementBanner';
|
||||
import VehicleBookingQuickAddWidget from '../components/dashboard/VehicleBookingQuickAddWidget';
|
||||
import EventQuickAddWidget from '../components/dashboard/EventQuickAddWidget';
|
||||
import LinksWidget from '../components/dashboard/LinksWidget';
|
||||
import { preferencesApi } from '../services/settings';
|
||||
import { WidgetKey } from '../constants/widgets';
|
||||
|
||||
function Dashboard() {
|
||||
const { user } = useAuth();
|
||||
const isAdmin = user?.groups?.includes('dashboard_admin') ?? false;
|
||||
@@ -33,6 +38,15 @@ function Dashboard() {
|
||||
) ?? false;
|
||||
const [dataLoading, setDataLoading] = useState(true);
|
||||
|
||||
const { data: preferences } = useQuery({
|
||||
queryKey: ['user-preferences'],
|
||||
queryFn: preferencesApi.get,
|
||||
});
|
||||
|
||||
const widgetVisible = (key: WidgetKey) => {
|
||||
return preferences?.widgets?.[key] !== false;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDataLoading(false);
|
||||
@@ -44,7 +58,6 @@ function Dashboard() {
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<Container maxWidth={false} disableGutters>
|
||||
<AnnouncementBanner />
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
@@ -53,6 +66,9 @@ function Dashboard() {
|
||||
alignItems: 'start',
|
||||
}}
|
||||
>
|
||||
{/* Announcement Banner — spans full width, renders null when no banners */}
|
||||
<AnnouncementBanner gridColumn="1 / -1" />
|
||||
|
||||
{/* User Profile Card — full width, contains welcome greeting */}
|
||||
{user && (
|
||||
<Box sx={{ gridColumn: '1 / -1' }}>
|
||||
@@ -69,6 +85,7 @@ function Dashboard() {
|
||||
)}
|
||||
|
||||
{/* Vehicle Status Card */}
|
||||
{widgetVisible('vehicles') && (
|
||||
<Box>
|
||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '300ms' }}>
|
||||
<Box>
|
||||
@@ -76,8 +93,10 @@ function Dashboard() {
|
||||
</Box>
|
||||
</Fade>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Equipment Status Card */}
|
||||
{widgetVisible('equipment') && (
|
||||
<Box>
|
||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '350ms' }}>
|
||||
<Box>
|
||||
@@ -85,9 +104,10 @@ function Dashboard() {
|
||||
</Box>
|
||||
</Fade>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Atemschutz Status Card */}
|
||||
{canViewAtemschutz && (
|
||||
{canViewAtemschutz && widgetVisible('atemschutz') && (
|
||||
<Box>
|
||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '400ms' }}>
|
||||
<Box>
|
||||
@@ -98,6 +118,7 @@ function Dashboard() {
|
||||
)}
|
||||
|
||||
{/* Upcoming Events Widget */}
|
||||
{widgetVisible('events') && (
|
||||
<Box>
|
||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '440ms' }}>
|
||||
<Box>
|
||||
@@ -105,8 +126,10 @@ function Dashboard() {
|
||||
</Box>
|
||||
</Fade>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Nextcloud Talk Widget */}
|
||||
{widgetVisible('nextcloudTalk') && (
|
||||
<Box>
|
||||
{dataLoading ? (
|
||||
<SkeletonCard variant="basic" />
|
||||
@@ -118,8 +141,10 @@ function Dashboard() {
|
||||
</Fade>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* BookStack Recent Pages Widget */}
|
||||
{widgetVisible('bookstackRecent') && (
|
||||
<Box>
|
||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '520ms' }}>
|
||||
<Box>
|
||||
@@ -127,8 +152,10 @@ function Dashboard() {
|
||||
</Box>
|
||||
</Fade>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* BookStack Search Widget */}
|
||||
{widgetVisible('bookstackSearch') && (
|
||||
<Box>
|
||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '560ms' }}>
|
||||
<Box>
|
||||
@@ -136,8 +163,10 @@ function Dashboard() {
|
||||
</Box>
|
||||
</Fade>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Vikunja — My Tasks Widget */}
|
||||
{widgetVisible('vikunjaTasks') && (
|
||||
<Box>
|
||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '600ms' }}>
|
||||
<Box>
|
||||
@@ -145,8 +174,10 @@ function Dashboard() {
|
||||
</Box>
|
||||
</Fade>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Vikunja — Quick Add Widget */}
|
||||
{widgetVisible('vikunjaQuickAdd') && (
|
||||
<Box>
|
||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '640ms' }}>
|
||||
<Box>
|
||||
@@ -154,9 +185,10 @@ function Dashboard() {
|
||||
</Box>
|
||||
</Fade>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Vehicle Booking — Quick Add Widget */}
|
||||
{canWrite && (
|
||||
{canWrite && widgetVisible('vehicleBooking') && (
|
||||
<Box>
|
||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '720ms' }}>
|
||||
<Box>
|
||||
@@ -167,7 +199,7 @@ function Dashboard() {
|
||||
)}
|
||||
|
||||
{/* Event — Quick Add Widget */}
|
||||
{canWrite && (
|
||||
{canWrite && widgetVisible('eventQuickAdd') && (
|
||||
<Box>
|
||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '760ms' }}>
|
||||
<Box>
|
||||
@@ -180,8 +212,19 @@ function Dashboard() {
|
||||
{/* Vikunja — Overdue Notifier (invisible, polling component) */}
|
||||
<VikunjaOverdueNotifier />
|
||||
|
||||
{/* Links Widget */}
|
||||
{widgetVisible('links') && (
|
||||
<Box>
|
||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '790ms' }}>
|
||||
<Box>
|
||||
<LinksWidget />
|
||||
</Box>
|
||||
</Fade>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Admin Status Widget — only for admins */}
|
||||
{isAdmin && (
|
||||
{isAdmin && widgetVisible('adminStatus') && (
|
||||
<Box>
|
||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '680ms' }}>
|
||||
<Box>
|
||||
|
||||
Reference in New Issue
Block a user