feat: add issue kanban/attachments/deadlines, dashboard widget DnD, and checklisten system

This commit is contained in:
Matthias Hochmeister
2026-03-28 15:19:41 +01:00
parent a1cda5be51
commit 0c2ea829aa
42 changed files with 4804 additions and 201 deletions

View File

@@ -4,13 +4,14 @@ import {
TableHead, TableRow, Paper, Chip, IconButton, Button, Dialog, DialogTitle,
DialogContent, DialogActions, TextField, MenuItem, Select, FormControl,
InputLabel, CircularProgress, FormControlLabel, Switch,
Autocomplete,
Autocomplete, ToggleButtonGroup, ToggleButton,
} from '@mui/material';
import {
Add as AddIcon, Delete as DeleteIcon,
BugReport, FiberNew, HelpOutline,
Circle as CircleIcon, Edit as EditIcon,
DragIndicator, Check as CheckIcon, Close as CloseIcon,
ViewList as ViewListIcon, ViewKanban as ViewKanbanIcon,
} from '@mui/icons-material';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useSearchParams, useNavigate } from 'react-router-dom';
@@ -21,6 +22,7 @@ import { usePermissionContext } from '../contexts/PermissionContext';
import { useAuth } from '../contexts/AuthContext';
import { issuesApi } from '../services/issues';
import type { Issue, IssueTyp, IssueFilters, AssignableMember, IssueStatusDef, IssuePriorityDef } from '../types/issue.types';
import KanbanBoard from '../components/issues/KanbanBoard';
// ── Helpers ──
@@ -555,8 +557,10 @@ function IssueSettings() {
export default function Issues() {
const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { hasPermission } = usePermissionContext();
const { user } = useAuth();
const { showSuccess, showError } = useNotification();
const canViewAll = hasPermission('issues:view_all');
const hasEdit = hasPermission('issues:edit');
@@ -581,6 +585,27 @@ export default function Issues() {
const [showDoneMine, setShowDoneMine] = useState(false);
const [showDoneAssigned, setShowDoneAssigned] = useState(false);
const [filters, setFilters] = useState<IssueFilters>({});
const [viewMode, setViewMode] = useState<'list' | 'kanban'>(() => {
try { return (localStorage.getItem('issues-view-mode') as 'list' | 'kanban') || 'list'; }
catch { return 'list'; }
});
const handleViewModeChange = (_: unknown, val: 'list' | 'kanban' | null) => {
if (!val) return;
setViewMode(val);
localStorage.setItem('issues-view-mode', val);
};
// Mutation for kanban drag-and-drop status change
const updateStatusMut = useMutation({
mutationFn: ({ id, status }: { id: number; status: string }) =>
issuesApi.updateIssue(id, { status }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['issues'] });
showSuccess('Status aktualisiert');
},
onError: () => showError('Fehler beim Aktualisieren'),
});
// Fetch all issues for mine/assigned tabs
const { data: issues = [], isLoading } = useQuery({
@@ -634,7 +659,22 @@ export default function Issues() {
return (
<DashboardLayout>
<Box sx={{ p: 3 }}>
<Typography variant="h5" gutterBottom>Issues</Typography>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Typography variant="h5">Issues</Typography>
<ToggleButtonGroup
value={viewMode}
exclusive
onChange={handleViewModeChange}
size="small"
>
<ToggleButton value="list" aria-label="Listenansicht">
<ViewListIcon fontSize="small" />
</ToggleButton>
<ToggleButton value="kanban" aria-label="Kanban-Ansicht">
<ViewKanbanIcon fontSize="small" />
</ToggleButton>
</ToggleButtonGroup>
</Box>
<Tabs value={tab} onChange={handleTabChange} sx={{ mb: 0 }}>
{tabs.map((t, i) => <Tab key={i} label={t.label} />)}
@@ -642,13 +682,24 @@ export default function Issues() {
{/* Tab 0: Meine Issues */}
<TabPanel value={tab} index={0}>
<FormControlLabel
control={<Switch checked={showDoneMine} onChange={(e) => setShowDoneMine(e.target.checked)} size="small" />}
label="Erledigte anzeigen"
sx={{ mb: 1 }}
/>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 1 }}>
{viewMode === 'list' && (
<FormControlLabel
control={<Switch checked={showDoneMine} onChange={(e) => setShowDoneMine(e.target.checked)} size="small" />}
label="Erledigte anzeigen"
/>
)}
</Box>
{isLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}><CircularProgress /></Box>
) : viewMode === 'kanban' ? (
<KanbanBoard
issues={myIssuesFiltered}
statuses={issueStatuses}
priorities={issuePriorities}
onNavigate={(id) => navigate(`/issues/${id}`)}
onStatusChange={(id, status) => updateStatusMut.mutate({ id, status })}
/>
) : (
<IssueTable issues={myIssuesFiltered} statuses={issueStatuses} priorities={issuePriorities} />
)}
@@ -656,13 +707,24 @@ export default function Issues() {
{/* Tab 1: Zugewiesene Issues */}
<TabPanel value={tab} index={1}>
<FormControlLabel
control={<Switch checked={showDoneAssigned} onChange={(e) => setShowDoneAssigned(e.target.checked)} size="small" />}
label="Erledigte anzeigen"
sx={{ mb: 1 }}
/>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 1 }}>
{viewMode === 'list' && (
<FormControlLabel
control={<Switch checked={showDoneAssigned} onChange={(e) => setShowDoneAssigned(e.target.checked)} size="small" />}
label="Erledigte anzeigen"
/>
)}
</Box>
{isLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}><CircularProgress /></Box>
) : viewMode === 'kanban' ? (
<KanbanBoard
issues={assignedFiltered}
statuses={issueStatuses}
priorities={issuePriorities}
onNavigate={(id) => navigate(`/issues/${id}`)}
onStatusChange={(id, status) => updateStatusMut.mutate({ id, status })}
/>
) : (
<IssueTable issues={assignedFiltered} statuses={issueStatuses} priorities={issuePriorities} />
)}
@@ -674,6 +736,14 @@ export default function Issues() {
<FilterBar filters={filters} onChange={setFilters} types={types} members={members} statuses={issueStatuses} priorities={issuePriorities} />
{isFilteredLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}><CircularProgress /></Box>
) : viewMode === 'kanban' ? (
<KanbanBoard
issues={filteredIssues}
statuses={issueStatuses}
priorities={issuePriorities}
onNavigate={(id) => navigate(`/issues/${id}`)}
onStatusChange={(id, status) => updateStatusMut.mutate({ id, status })}
/>
) : (
<IssueTable issues={filteredIssues} statuses={issueStatuses} priorities={issuePriorities} />
)}