rework from modal to page
This commit is contained in:
@@ -3,24 +3,24 @@ import {
|
||||
Box, Tab, Tabs, Typography, Table, TableBody, TableCell, TableContainer,
|
||||
TableHead, TableRow, Paper, Chip, IconButton, Button, Dialog, DialogTitle,
|
||||
DialogContent, DialogActions, TextField, MenuItem, Select, FormControl,
|
||||
InputLabel, Collapse, Divider, CircularProgress, FormControlLabel, Switch,
|
||||
InputLabel, CircularProgress, FormControlLabel, Switch,
|
||||
Autocomplete,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Add as AddIcon, Delete as DeleteIcon, ExpandMore, ExpandLess,
|
||||
BugReport, FiberNew, HelpOutline, Send as SendIcon,
|
||||
Circle as CircleIcon, Edit as EditIcon, Refresh as RefreshIcon,
|
||||
Add as AddIcon, Delete as DeleteIcon,
|
||||
BugReport, FiberNew, HelpOutline,
|
||||
Circle as CircleIcon, Edit as EditIcon,
|
||||
DragIndicator, Check as CheckIcon, Close as CloseIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||
import ChatAwareFab from '../components/shared/ChatAwareFab';
|
||||
import { useNotification } from '../contexts/NotificationContext';
|
||||
import { usePermissionContext } from '../contexts/PermissionContext';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { issuesApi } from '../services/issues';
|
||||
import type { Issue, IssueComment, IssueTyp, CreateIssuePayload, UpdateIssuePayload, IssueFilters, AssignableMember, IssueStatusDef, IssuePriorityDef } from '../types/issue.types';
|
||||
import type { Issue, IssueTyp, IssueFilters, AssignableMember, IssueStatusDef, IssuePriorityDef } from '../types/issue.types';
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
@@ -76,338 +76,18 @@ function TabPanel({ children, value, index }: TabPanelProps) {
|
||||
return <Box sx={{ pt: 3 }}>{children}</Box>;
|
||||
}
|
||||
|
||||
// ── Comment Section ──
|
||||
|
||||
function CommentSection({ issueId, canComment }: { issueId: number; canComment: boolean }) {
|
||||
const queryClient = useQueryClient();
|
||||
const { showError } = useNotification();
|
||||
const [text, setText] = useState('');
|
||||
|
||||
const { data: comments = [], isLoading } = useQuery({
|
||||
queryKey: ['issues', issueId, 'comments'],
|
||||
queryFn: () => issuesApi.getComments(issueId),
|
||||
});
|
||||
|
||||
const addMut = useMutation({
|
||||
mutationFn: (inhalt: string) => issuesApi.addComment(issueId, inhalt),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['issues', issueId, 'comments'] });
|
||||
setText('');
|
||||
},
|
||||
onError: () => showError('Kommentar konnte nicht erstellt werden'),
|
||||
});
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>Kommentare</Typography>
|
||||
{isLoading ? (
|
||||
<CircularProgress size={20} />
|
||||
) : comments.length === 0 ? (
|
||||
<Typography variant="body2" color="text.secondary">Noch keine Kommentare</Typography>
|
||||
) : (
|
||||
comments.map((c: IssueComment) => (
|
||||
<Box key={c.id} sx={{ mb: 1.5, p: 1.5, bgcolor: 'action.hover', borderRadius: 1 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{c.autor_name || 'Unbekannt'} - {formatDate(c.created_at)}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mt: 0.5, whiteSpace: 'pre-wrap' }}>{c.inhalt}</Typography>
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
{canComment && (
|
||||
<Box sx={{ display: 'flex', gap: 1, mt: 1 }}>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
placeholder="Kommentar schreiben..."
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey && text.trim()) {
|
||||
e.preventDefault();
|
||||
addMut.mutate(text.trim());
|
||||
}
|
||||
}}
|
||||
multiline
|
||||
maxRows={4}
|
||||
/>
|
||||
<IconButton
|
||||
color="primary"
|
||||
disabled={!text.trim() || addMut.isPending}
|
||||
onClick={() => addMut.mutate(text.trim())}
|
||||
>
|
||||
<SendIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Issue Row ──
|
||||
|
||||
function IssueRow({
|
||||
issue,
|
||||
userId,
|
||||
hasEdit,
|
||||
hasChangeStatus,
|
||||
hasDelete,
|
||||
members,
|
||||
statuses,
|
||||
priorities,
|
||||
onDelete,
|
||||
}: {
|
||||
issue: Issue;
|
||||
userId: string;
|
||||
hasEdit: boolean;
|
||||
hasChangeStatus: boolean;
|
||||
hasDelete: boolean;
|
||||
members: AssignableMember[];
|
||||
statuses: IssueStatusDef[];
|
||||
priorities: IssuePriorityDef[];
|
||||
onDelete: (id: number) => void;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [reopenOpen, setReopenOpen] = useState(false);
|
||||
const [reopenComment, setReopenComment] = useState('');
|
||||
const queryClient = useQueryClient();
|
||||
const { showSuccess, showError } = useNotification();
|
||||
|
||||
const isOwner = issue.erstellt_von === userId;
|
||||
const isAssignee = issue.zugewiesen_an === userId;
|
||||
const canDelete = hasDelete || isOwner;
|
||||
const canComment = isOwner || isAssignee || hasChangeStatus || hasEdit;
|
||||
|
||||
// Determine status change capability
|
||||
const canChangeStatus = hasEdit || hasChangeStatus || isAssignee;
|
||||
const ownerOnlyErledigt = isOwner && !canChangeStatus;
|
||||
|
||||
// Build allowed statuses from dynamic list
|
||||
const allowedStatuses = useMemo(() => {
|
||||
const active = statuses.filter(s => s.aktiv);
|
||||
if (hasEdit) return active.filter(s => !s.benoetigt_typ_freigabe || issue.typ_erlaubt_abgelehnt);
|
||||
if (hasChangeStatus || isAssignee) return active.filter(s => !s.benoetigt_typ_freigabe || issue.typ_erlaubt_abgelehnt);
|
||||
if (isOwner) return active.filter(s => s.schluessel === issue.status || s.ist_abschluss);
|
||||
return active.filter(s => s.schluessel === issue.status);
|
||||
}, [statuses, hasEdit, hasChangeStatus, isAssignee, isOwner, issue.status, issue.typ_erlaubt_abgelehnt]);
|
||||
|
||||
const updateMut = useMutation({
|
||||
mutationFn: (data: UpdateIssuePayload) => issuesApi.updateIssue(issue.id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['issues'] });
|
||||
showSuccess('Issue aktualisiert');
|
||||
},
|
||||
onError: () => showError('Fehler beim Aktualisieren'),
|
||||
});
|
||||
|
||||
// Owner on erledigt issue: show reopen button instead of status select
|
||||
const currentStatusDef = statuses.find(s => s.schluessel === issue.status);
|
||||
const isTerminal = currentStatusDef?.ist_abschluss ?? (issue.status === 'erledigt');
|
||||
const showReopenButton = ownerOnlyErledigt && isTerminal;
|
||||
|
||||
const initialStatusKey = statuses.find(s => s.ist_initial)?.schluessel ?? 'offen';
|
||||
|
||||
const handleReopen = () => {
|
||||
updateMut.mutate({ status: initialStatusKey, kommentar: reopenComment.trim() }, {
|
||||
onSuccess: () => {
|
||||
setReopenOpen(false);
|
||||
setReopenComment('');
|
||||
queryClient.invalidateQueries({ queryKey: ['issues'] });
|
||||
showSuccess('Issue wiedereröffnet');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableRow
|
||||
hover
|
||||
sx={{ cursor: 'pointer', '& > *': { borderBottom: expanded ? 'unset' : undefined } }}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
<TableCell sx={{ width: 80 }}>{formatIssueId(issue)}</TableCell>
|
||||
<TableCell>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{getTypIcon(issue.typ_icon, issue.typ_farbe)}
|
||||
<Typography variant="body2">{issue.titel}</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={issue.typ_name} size="small" variant="outlined" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<CircleIcon sx={{ fontSize: 10, color: getPrioColor(priorities, issue.prioritaet) }} />
|
||||
<Typography variant="body2">{getPrioLabel(priorities, issue.prioritaet)}</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={getStatusLabel(statuses, issue.status)}
|
||||
size="small"
|
||||
color={getStatusColor(statuses, issue.status)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{issue.erstellt_von_name || '-'}</TableCell>
|
||||
<TableCell>{issue.zugewiesen_an_name || '-'}</TableCell>
|
||||
<TableCell>{formatDate(issue.created_at)}</TableCell>
|
||||
<TableCell>
|
||||
<IconButton size="small" onClick={(e) => { e.stopPropagation(); setExpanded(!expanded); }}>
|
||||
{expanded ? <ExpandLess /> : <ExpandMore />}
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} sx={{ py: 0 }}>
|
||||
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
||||
<Box sx={{ p: 2 }}>
|
||||
{issue.beschreibung && (
|
||||
<Typography variant="body2" sx={{ mb: 2, whiteSpace: 'pre-wrap' }}>
|
||||
{issue.beschreibung}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, mb: 2, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
{/* Status control */}
|
||||
{showReopenButton ? (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={(e) => { e.stopPropagation(); setReopenOpen(true); }}
|
||||
>
|
||||
Wiedereröffnen
|
||||
</Button>
|
||||
) : canChangeStatus || isOwner ? (
|
||||
<FormControl size="small" sx={{ minWidth: 160 }}>
|
||||
<InputLabel>Status</InputLabel>
|
||||
<Select
|
||||
value={issue.status}
|
||||
label="Status"
|
||||
onChange={(e) => updateMut.mutate({ status: e.target.value })}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{allowedStatuses.map(s => (
|
||||
<MenuItem key={s.schluessel} value={s.schluessel}>{s.bezeichnung}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
) : null}
|
||||
|
||||
{/* Priority control — only with issues:edit */}
|
||||
{hasEdit && (
|
||||
<FormControl size="small" sx={{ minWidth: 140 }}>
|
||||
<InputLabel>Priorität</InputLabel>
|
||||
<Select
|
||||
value={issue.prioritaet}
|
||||
label="Priorität"
|
||||
onChange={(e) => updateMut.mutate({ prioritaet: e.target.value })}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{priorities.filter(p => p.aktiv).map(p => (
|
||||
<MenuItem key={p.schluessel} value={p.schluessel}>{p.bezeichnung}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
{/* Assignment — only with issues:edit */}
|
||||
{hasEdit && (
|
||||
<Autocomplete
|
||||
size="small"
|
||||
sx={{ minWidth: 200 }}
|
||||
options={members}
|
||||
getOptionLabel={(o) => o.name}
|
||||
value={members.find(m => m.id === issue.zugewiesen_an) || null}
|
||||
onChange={(_e, val) => updateMut.mutate({ zugewiesen_an: val?.id || null })}
|
||||
renderInput={(params) => <TextField {...params} label="Zugewiesen an" size="small" />}
|
||||
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
||||
isOptionEqualToValue={(o, v) => o.id === v.id}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{canDelete && (
|
||||
<Button
|
||||
size="small"
|
||||
color="error"
|
||||
startIcon={<DeleteIcon />}
|
||||
onClick={(e) => { e.stopPropagation(); onDelete(issue.id); }}
|
||||
sx={{ mb: 1 }}
|
||||
>
|
||||
Löschen
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Divider sx={{ my: 1 }} />
|
||||
<CommentSection issueId={issue.id} canComment={canComment} />
|
||||
</Box>
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
{/* Reopen Dialog */}
|
||||
<Dialog open={reopenOpen} onClose={() => setReopenOpen(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Issue wiedereröffnen</DialogTitle>
|
||||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '20px !important' }}>
|
||||
<TextField
|
||||
label="Kommentar (Pflicht)"
|
||||
required
|
||||
multiline
|
||||
rows={3}
|
||||
fullWidth
|
||||
value={reopenComment}
|
||||
onChange={(e) => setReopenComment(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setReopenOpen(false)}>Abbrechen</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={!reopenComment.trim() || updateMut.isPending}
|
||||
onClick={handleReopen}
|
||||
>
|
||||
Wiedereröffnen
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Issue Table ──
|
||||
|
||||
function IssueTable({
|
||||
issues,
|
||||
userId,
|
||||
hasEdit,
|
||||
hasChangeStatus,
|
||||
hasDelete,
|
||||
members,
|
||||
statuses,
|
||||
priorities,
|
||||
}: {
|
||||
issues: Issue[];
|
||||
userId: string;
|
||||
hasEdit: boolean;
|
||||
hasChangeStatus: boolean;
|
||||
hasDelete: boolean;
|
||||
members: AssignableMember[];
|
||||
statuses: IssueStatusDef[];
|
||||
priorities: IssuePriorityDef[];
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const { showSuccess, showError } = useNotification();
|
||||
|
||||
const deleteMut = useMutation({
|
||||
mutationFn: (id: number) => issuesApi.deleteIssue(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['issues'] });
|
||||
showSuccess('Issue gelöscht');
|
||||
},
|
||||
onError: () => showError('Fehler beim Löschen'),
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (issues.length === 0) {
|
||||
return (
|
||||
@@ -430,23 +110,43 @@ function IssueTable({
|
||||
<TableCell>Erstellt von</TableCell>
|
||||
<TableCell>Zugewiesen an</TableCell>
|
||||
<TableCell>Erstellt am</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{issues.map((issue) => (
|
||||
<IssueRow
|
||||
<TableRow
|
||||
key={issue.id}
|
||||
issue={issue}
|
||||
userId={userId}
|
||||
hasEdit={hasEdit}
|
||||
hasChangeStatus={hasChangeStatus}
|
||||
hasDelete={hasDelete}
|
||||
members={members}
|
||||
statuses={statuses}
|
||||
priorities={priorities}
|
||||
onDelete={(id) => deleteMut.mutate(id)}
|
||||
/>
|
||||
hover
|
||||
sx={{ cursor: 'pointer' }}
|
||||
onClick={() => navigate(`/issues/${issue.id}`)}
|
||||
>
|
||||
<TableCell sx={{ width: 80 }}>{formatIssueId(issue)}</TableCell>
|
||||
<TableCell>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{getTypIcon(issue.typ_icon, issue.typ_farbe)}
|
||||
<Typography variant="body2">{issue.titel}</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={issue.typ_name} size="small" variant="outlined" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<CircleIcon sx={{ fontSize: 10, color: getPrioColor(priorities, issue.prioritaet) }} />
|
||||
<Typography variant="body2">{getPrioLabel(priorities, issue.prioritaet)}</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={getStatusLabel(statuses, issue.status)}
|
||||
size="small"
|
||||
color={getStatusColor(statuses, issue.status)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{issue.erstellt_von_name || '-'}</TableCell>
|
||||
<TableCell>{issue.zugewiesen_an_name || '-'}</TableCell>
|
||||
<TableCell>{formatDate(issue.created_at)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
@@ -854,15 +554,12 @@ function IssueSettings() {
|
||||
|
||||
export default function Issues() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { showSuccess, showError } = useNotification();
|
||||
const navigate = useNavigate();
|
||||
const { hasPermission } = usePermissionContext();
|
||||
const { user } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const canViewAll = hasPermission('issues:view_all');
|
||||
const hasEdit = hasPermission('issues:edit');
|
||||
const hasChangeStatus = hasPermission('issues:change_status');
|
||||
const hasDeletePerm = hasPermission('issues:delete');
|
||||
const hasEditSettings = hasPermission('issues:edit_settings');
|
||||
const canCreate = hasPermission('issues:create');
|
||||
const userId = user?.id || '';
|
||||
@@ -884,8 +581,6 @@ export default function Issues() {
|
||||
const [showDoneMine, setShowDoneMine] = useState(false);
|
||||
const [showDoneAssigned, setShowDoneAssigned] = useState(false);
|
||||
const [filters, setFilters] = useState<IssueFilters>({});
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [form, setForm] = useState<CreateIssuePayload>({ titel: '', prioritaet: 'mittel' });
|
||||
|
||||
// Fetch all issues for mine/assigned tabs
|
||||
const { data: issues = [], isLoading } = useQuery({
|
||||
@@ -922,20 +617,6 @@ export default function Issues() {
|
||||
enabled: hasEdit,
|
||||
});
|
||||
|
||||
// Default priority: first active, sorted by sort_order
|
||||
const defaultPriority = issuePriorities.find(p => p.aktiv)?.schluessel ?? 'mittel';
|
||||
|
||||
const createMut = useMutation({
|
||||
mutationFn: (data: CreateIssuePayload) => issuesApi.createIssue(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['issues'] });
|
||||
showSuccess('Issue erstellt');
|
||||
setCreateOpen(false);
|
||||
setForm({ titel: '', prioritaet: defaultPriority });
|
||||
},
|
||||
onError: () => showError('Fehler beim Erstellen'),
|
||||
});
|
||||
|
||||
const handleTabChange = (_: unknown, newValue: number) => {
|
||||
setSearchParams({ tab: String(newValue) });
|
||||
};
|
||||
@@ -950,9 +631,6 @@ export default function Issues() {
|
||||
const assignedIssues = issues.filter((i: Issue) => i.zugewiesen_an === userId);
|
||||
const assignedFiltered = showDoneAssigned ? assignedIssues : assignedIssues.filter((i: Issue) => !isDone(i));
|
||||
|
||||
// Default typ_id to first active type
|
||||
const defaultTypId = types.find(t => t.aktiv)?.id;
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<Box sx={{ p: 3 }}>
|
||||
@@ -972,7 +650,7 @@ export default function Issues() {
|
||||
{isLoading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}><CircularProgress /></Box>
|
||||
) : (
|
||||
<IssueTable issues={myIssuesFiltered} userId={userId} hasEdit={hasEdit} hasChangeStatus={hasChangeStatus} hasDelete={hasDeletePerm} members={members} statuses={issueStatuses} priorities={issuePriorities} />
|
||||
<IssueTable issues={myIssuesFiltered} statuses={issueStatuses} priorities={issuePriorities} />
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
@@ -986,7 +664,7 @@ export default function Issues() {
|
||||
{isLoading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}><CircularProgress /></Box>
|
||||
) : (
|
||||
<IssueTable issues={assignedFiltered} userId={userId} hasEdit={hasEdit} hasChangeStatus={hasChangeStatus} hasDelete={hasDeletePerm} members={members} statuses={issueStatuses} priorities={issuePriorities} />
|
||||
<IssueTable issues={assignedFiltered} statuses={issueStatuses} priorities={issuePriorities} />
|
||||
)}
|
||||
</TabPanel>
|
||||
|
||||
@@ -997,7 +675,7 @@ export default function Issues() {
|
||||
{isFilteredLoading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}><CircularProgress /></Box>
|
||||
) : (
|
||||
<IssueTable issues={filteredIssues} userId={userId} hasEdit={hasEdit} hasChangeStatus={hasChangeStatus} hasDelete={hasDeletePerm} members={members} statuses={issueStatuses} priorities={issuePriorities} />
|
||||
<IssueTable issues={filteredIssues} statuses={issueStatuses} priorities={issuePriorities} />
|
||||
)}
|
||||
</TabPanel>
|
||||
)}
|
||||
@@ -1010,69 +688,12 @@ export default function Issues() {
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Create Issue Dialog */}
|
||||
<Dialog open={createOpen} onClose={() => setCreateOpen(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Neues Issue erstellen</DialogTitle>
|
||||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '20px !important' }}>
|
||||
<TextField
|
||||
label="Titel"
|
||||
required
|
||||
fullWidth
|
||||
value={form.titel}
|
||||
onChange={(e) => setForm({ ...form, titel: e.target.value })}
|
||||
autoFocus
|
||||
/>
|
||||
<TextField
|
||||
label="Beschreibung"
|
||||
multiline
|
||||
rows={4}
|
||||
fullWidth
|
||||
value={form.beschreibung || ''}
|
||||
onChange={(e) => setForm({ ...form, beschreibung: e.target.value })}
|
||||
/>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Typ</InputLabel>
|
||||
<Select
|
||||
value={form.typ_id ?? defaultTypId ?? ''}
|
||||
label="Typ"
|
||||
onChange={(e) => setForm({ ...form, typ_id: Number(e.target.value) })}
|
||||
>
|
||||
{types.filter(t => t.aktiv).map(t => (
|
||||
<MenuItem key={t.id} value={t.id}>{t.name}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Priorität</InputLabel>
|
||||
<Select
|
||||
value={form.prioritaet || defaultPriority}
|
||||
label="Priorität"
|
||||
onChange={(e) => setForm({ ...form, prioritaet: e.target.value })}
|
||||
>
|
||||
{issuePriorities.filter(p => p.aktiv).map(p => (
|
||||
<MenuItem key={p.schluessel} value={p.schluessel}>{p.bezeichnung}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setCreateOpen(false)}>Abbrechen</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={!form.titel.trim() || createMut.isPending}
|
||||
onClick={() => createMut.mutate({ ...form, typ_id: form.typ_id ?? defaultTypId })}
|
||||
>
|
||||
Erstellen
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* FAB */}
|
||||
{canCreate && activeTab === 'mine' && (
|
||||
{canCreate && (
|
||||
<ChatAwareFab
|
||||
color="primary"
|
||||
aria-label="Neues Issue"
|
||||
onClick={() => setCreateOpen(true)}
|
||||
onClick={() => navigate('/issues/neu')}
|
||||
>
|
||||
<AddIcon />
|
||||
</ChatAwareFab>
|
||||
|
||||
Reference in New Issue
Block a user