Skip to content

Commit 5ab0d87

Browse files
committed
control raport draft
1 parent f8f93d3 commit 5ab0d87

File tree

17 files changed

+660
-14
lines changed

17 files changed

+660
-14
lines changed

src/client/components/Admin/Summary/Summary.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { useMemo, useState } from 'react'
22
import { MRT_Row, MaterialReactTable, useMaterialReactTable, type MRT_ColumnDef } from 'material-react-table'
3-
import { Box, Button, IconButton, Typography } from '@mui/material'
3+
import { Box, Button, IconButton, Typography, Tooltip } from '@mui/material'
44
import DeleteIcon from '@mui/icons-material/Delete'
55
import FileDownloadIcon from '@mui/icons-material/FileDownload'
6+
import WarningIcon from '@mui/icons-material/Warning'
67
import { utils, writeFile } from 'xlsx'
78
import { Link, useNavigate } from 'react-router-dom'
89
import { enqueueSnackbar } from 'notistack'
10+
import { useTranslation } from 'react-i18next'
911
import { useEntries } from '../../../hooks/useEntry'
1012
import useQuestions from '../../../hooks/useQuestions'
1113
import useDeleteEntryMutation from '../../../hooks/useDeleteEntryMutation'
@@ -36,11 +38,21 @@ const additionalColumnNames: TableValues = {
3638

3739
const Table = ({ tableValues, questionTitles, isOutdated, entries }: TableProps) => {
3840
const deleteMutation = useDeleteEntryMutation()
41+
const { t } = useTranslation()
3942

4043
const isTestVersion = (id: string) => {
4144
return entries.find(e => e.id === Number(id))?.testVersion || false
4245
}
4346

47+
const needsControlReport = (id: string) => {
48+
const entry = entries.find(e => e.id === Number(id))
49+
if (!entry) {
50+
return false
51+
}
52+
const totalRisk = entry.data?.risks?.find((r: any) => r.id === 'total')?.level
53+
return totalRisk === 3 && (!entry.controlReports || entry.controlReports.length === 0)
54+
}
55+
4456
const columns = useMemo<MRT_ColumnDef<TableValues>[]>(() => {
4557
const outdatedWarning = { paddingRight: 10, color: 'red', fontSize: 'x-large' }
4658
return tableValues.length
@@ -63,7 +75,14 @@ const Table = ({ tableValues, questionTitles, isOutdated, entries }: TableProps)
6375
{columnId === '3' ? (
6476
<Box>
6577
{isOutdated(row.getValue('id')) && <span style={outdatedWarning}>!</span>}
66-
<Link to={`/admin/entry/${row.getValue('id')}`}>{cell.getValue<string>()}</Link>
78+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
79+
<Link to={`/admin/entry/${row.getValue('id')}`}>{cell.getValue<string>()}</Link>
80+
{needsControlReport(row.getValue('id')) && (
81+
<Tooltip title={t('controlReport:noReportsWarning')}>
82+
<WarningIcon sx={{ color: '#e74c3c', fontSize: '1.2rem' }} />
83+
</Tooltip>
84+
)}
85+
</Box>
6786
{isTestVersion(row.getValue('id')) && (
6887
<Box
6988
component="div"
@@ -80,7 +99,7 @@ const Table = ({ tableValues, questionTitles, isOutdated, entries }: TableProps)
8099
),
81100
}))
82101
: []
83-
}, [tableValues, questionTitles, isOutdated])
102+
}, [tableValues, questionTitles, isOutdated, isTestVersion, needsControlReport, t])
84103

85104
const columnIds = Object.keys(tableValues[0])
86105

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import { useState } from 'react'
2+
import {
3+
Box,
4+
Button,
5+
Card,
6+
CardContent,
7+
Dialog,
8+
DialogActions,
9+
DialogContent,
10+
DialogTitle,
11+
IconButton,
12+
Typography,
13+
Alert,
14+
} from '@mui/material'
15+
import EditIcon from '@mui/icons-material/Edit'
16+
import DeleteIcon from '@mui/icons-material/Delete'
17+
import AddIcon from '@mui/icons-material/Add'
18+
import { useTranslation } from 'react-i18next'
19+
import { enqueueSnackbar } from 'notistack'
20+
import MDEditor from '@uiw/react-md-editor'
21+
22+
import { ControlReport } from '@types'
23+
import useLoggedInUser from '../../hooks/useLoggedInUser'
24+
import Markdown from '../Common/Markdown'
25+
import apiClient from '../../util/apiClient'
26+
import styles from '../../styles'
27+
28+
interface ControlReportsProps {
29+
entryId: string
30+
controlReports: ControlReport[]
31+
totalRiskLevel: number | null | undefined
32+
onUpdate: () => void
33+
}
34+
35+
const ControlReports = ({ entryId, controlReports, totalRiskLevel, onUpdate }: ControlReportsProps) => {
36+
const { t } = useTranslation()
37+
const { user } = useLoggedInUser()
38+
const [isDialogOpen, setIsDialogOpen] = useState(false)
39+
const [editingReport, setEditingReport] = useState<ControlReport | null>(null)
40+
const [text, setText] = useState('')
41+
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
42+
const [reportToDelete, setReportToDelete] = useState<string | null>(null)
43+
const [isSubmitting, setIsSubmitting] = useState(false)
44+
45+
const { cardStyles } = styles
46+
47+
const isAdmin = user?.isAdmin
48+
const canAddReport = isAdmin && totalRiskLevel === 3
49+
50+
const handleOpenDialog = (report?: ControlReport) => {
51+
if (report) {
52+
setEditingReport(report)
53+
setText(report.text)
54+
} else {
55+
setEditingReport(null)
56+
setText('')
57+
}
58+
setIsDialogOpen(true)
59+
}
60+
61+
const handleCloseDialog = () => {
62+
setIsDialogOpen(false)
63+
setEditingReport(null)
64+
setText('')
65+
}
66+
67+
const handleSave = async () => {
68+
if (!text.trim()) {
69+
enqueueSnackbar(t('controlReport:createError'), { variant: 'error' })
70+
return
71+
}
72+
73+
setIsSubmitting(true)
74+
try {
75+
const payload = {
76+
text,
77+
}
78+
79+
if (editingReport) {
80+
await apiClient.put(`/entries/${entryId}/control-report/${editingReport.id}`, payload)
81+
enqueueSnackbar(t('controlReport:updateSuccess'), { variant: 'success' })
82+
} else {
83+
await apiClient.post(`/entries/${entryId}/control-report`, payload)
84+
enqueueSnackbar(t('controlReport:createSuccess'), { variant: 'success' })
85+
}
86+
87+
handleCloseDialog()
88+
onUpdate()
89+
} catch (error: any) {
90+
const message = editingReport ? t('controlReport:updateError') : t('controlReport:createError')
91+
enqueueSnackbar(error?.response?.data || message, { variant: 'error' })
92+
} finally {
93+
setIsSubmitting(false)
94+
}
95+
}
96+
97+
const handleDeleteClick = (reportId: string) => {
98+
setReportToDelete(reportId)
99+
setDeleteDialogOpen(true)
100+
}
101+
102+
const handleDeleteConfirm = async () => {
103+
if (!reportToDelete) {
104+
return
105+
}
106+
107+
setIsSubmitting(true)
108+
try {
109+
await apiClient.delete(`/entries/${entryId}/control-report/${reportToDelete}`)
110+
enqueueSnackbar(t('controlReport:deleteSuccess'), { variant: 'success' })
111+
setDeleteDialogOpen(false)
112+
setReportToDelete(null)
113+
onUpdate()
114+
} catch (error) {
115+
enqueueSnackbar(t('controlReport:deleteError'), { variant: 'error' })
116+
} finally {
117+
setIsSubmitting(false)
118+
}
119+
}
120+
121+
const formatDate = (dateString: string) =>
122+
`${new Date(dateString).toLocaleDateString()} ${new Date(dateString).toLocaleTimeString()}`
123+
124+
return (
125+
<Box sx={cardStyles.nestedSubSection}>
126+
{canAddReport && (
127+
<Box sx={{ mb: 2 }}>
128+
<Button variant="contained" startIcon={<AddIcon />} onClick={() => handleOpenDialog()}>
129+
{t('controlReport:addButton')}
130+
</Button>
131+
</Box>
132+
)}
133+
134+
<Typography variant="h5" sx={{ fontWeight: 'bold', mb: 2 }}>
135+
{t('controlReport:title')}
136+
</Typography>
137+
138+
{isAdmin && totalRiskLevel !== 3 && controlReports.length === 0 && (
139+
<Alert severity="info" sx={{ mb: 2 }}>
140+
{t('controlReport:riskLevel3Required')}
141+
</Alert>
142+
)}
143+
144+
{controlReports.length === 0 ? (
145+
<Typography variant="body2" color="text.secondary">
146+
{t('controlReport:noReports')}
147+
</Typography>
148+
) : (
149+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
150+
{controlReports.map(report => (
151+
<Card key={report.id} variant="outlined">
152+
<CardContent>
153+
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
154+
<Box>
155+
<Typography variant="caption" color="text.secondary">
156+
{t('controlReport:createdBy')}: {report.createdBy}
157+
</Typography>
158+
<Typography variant="caption" color="text.secondary" sx={{ ml: 2 }}>
159+
{t('controlReport:lastUpdated')}: {formatDate(report.updatedAt)}
160+
</Typography>
161+
</Box>
162+
{isAdmin && (
163+
<Box>
164+
<IconButton size="small" onClick={() => handleOpenDialog(report)} sx={{ mr: 1 }}>
165+
<EditIcon fontSize="small" />
166+
</IconButton>
167+
<IconButton size="small" onClick={() => handleDeleteClick(report.id)} color="error">
168+
<DeleteIcon fontSize="small" />
169+
</IconButton>
170+
</Box>
171+
)}
172+
</Box>
173+
<Markdown>{report.text}</Markdown>
174+
</CardContent>
175+
</Card>
176+
))}
177+
</Box>
178+
)}
179+
180+
{/* Edit/Create Dialog */}
181+
<Dialog open={isDialogOpen} onClose={handleCloseDialog} maxWidth="md" fullWidth>
182+
<DialogTitle>{editingReport ? t('controlReport:editButton') : t('controlReport:addButton')}</DialogTitle>
183+
<DialogContent>
184+
<Typography variant="caption" color="text.secondary" sx={{ mb: 2, display: 'block' }}>
185+
{t('controlReport:markdownSupported')}
186+
</Typography>
187+
188+
<Box sx={{ mb: 2 }}>
189+
<MDEditor value={text} onChange={value => setText(value ?? '')} height={300} />
190+
</Box>
191+
</DialogContent>
192+
<DialogActions>
193+
<Button onClick={handleCloseDialog} disabled={isSubmitting}>
194+
{t('controlReport:cancelButton')}
195+
</Button>
196+
<Button onClick={handleSave} variant="contained" disabled={isSubmitting}>
197+
{t('controlReport:saveButton')}
198+
</Button>
199+
</DialogActions>
200+
</Dialog>
201+
202+
{/* Delete Confirmation Dialog */}
203+
<Dialog open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)}>
204+
<DialogTitle>{t('controlReport:deleteButton')}</DialogTitle>
205+
<DialogContent>
206+
<Typography>{t('controlReport:confirmDelete')}</Typography>
207+
</DialogContent>
208+
<DialogActions>
209+
<Button onClick={() => setDeleteDialogOpen(false)} disabled={isSubmitting}>
210+
{t('controlReport:cancelButton')}
211+
</Button>
212+
<Button onClick={handleDeleteConfirm} color="error" variant="contained" disabled={isSubmitting}>
213+
{t('controlReport:deleteButton')}
214+
</Button>
215+
</DialogActions>
216+
</Dialog>
217+
</Box>
218+
)
219+
}
220+
221+
export default ControlReports

src/client/components/UserPage/UserEntry.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import useSurvey from '../../hooks/useSurvey'
1010
import MuiComponentProvider from '../Common/MuiComponentProvider'
1111
import RenderAnswersDOM from '../ResultPage/RenderAnswersDOM'
1212
import SendSummaryEmail from '../ResultPage/SendSummaryEmail'
13+
import ControlReports from './ControlReports'
1314

1415
interface TabPanelProps {
1516
children: React.ReactNode
@@ -49,7 +50,7 @@ const TabPanel = (props: TabPanelProps) => {
4950
const UserEntry = () => {
5051
const { entryId } = useParams()
5152
const { survey } = useSurvey()
52-
const { entry } = useEntry(entryId)
53+
const { entry, refetch } = useEntry(entryId)
5354
const [tabValue, setTabValue] = useState(0)
5455
const { t } = useTranslation()
5556
const navigate = useNavigate()
@@ -106,6 +107,21 @@ const UserEntry = () => {
106107
</Button>
107108
)}
108109
</Box>
110+
{entry.data.risks.find(r => r.id === 'total')?.level === 3 && (
111+
<>
112+
{(!entry.controlReports || entry.controlReports.length === 0) && (
113+
<Alert severity="error" sx={{ mb: 2 }}>
114+
{t('controlReport:noReportsWarning')}
115+
</Alert>
116+
)}
117+
<ControlReports
118+
entryId={entryId}
119+
controlReports={entry.controlReports ?? []}
120+
totalRiskLevel={entry.data.risks.find(r => r.id === 'total')?.level}
121+
onUpdate={refetch}
122+
/>
123+
</>
124+
)}
109125
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
110126
<Tabs value={tabValue} onChange={handleChange} data-testid="version-tabs">
111127
<Tab

src/client/components/UserPage/UserPage.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import {
1111
TableHead,
1212
TableRow,
1313
Typography,
14+
Tooltip,
1415
} from '@mui/material'
1516
import DeleteIcon from '@mui/icons-material/Delete'
17+
import WarningIcon from '@mui/icons-material/Warning'
1618
import { useTranslation } from 'react-i18next'
1719
import { Link } from 'react-router-dom'
1820
import { enqueueSnackbar } from 'notistack'
@@ -110,7 +112,15 @@ const UserPage = () => {
110112
>
111113
<TableCell component="th" scope="row">
112114
<Box>
113-
<Link to={`/user/${entry.id.toString()}`}>{entry.data.answers['3']}</Link>
115+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
116+
<Link to={`/user/${entry.id.toString()}`}>{entry.data.answers['3']}</Link>
117+
{entry.data.risks.find(r => r.id === 'total')?.level === 3 &&
118+
(!entry.controlReports || entry.controlReports.length === 0) && (
119+
<Tooltip title={t('controlReport:noReportsWarning')}>
120+
<WarningIcon sx={{ color: '#e74c3c', fontSize: '1.2rem' }} />
121+
</Tooltip>
122+
)}
123+
</Box>
114124
{entry.testVersion && (
115125
<Box
116126
component="div"

0 commit comments

Comments
 (0)