Skip to content

Commit c14b796

Browse files
committed
refactor: DRY Responsibles
1 parent 78d6228 commit c14b796

File tree

4 files changed

+173
-70
lines changed

4 files changed

+173
-70
lines changed

src/client/pages/Organisation/Organisation.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { LoadingProgress } from '../../components/common/LoadingProgress'
2626
import OrganisationLogs from './OrganisationLogs'
2727
import SemesterOverview from './SemesterOverview'
2828
import Responsibles from './Responsibles'
29+
import ResponsiblesXlsx from './ResponsiblesXlsx'
2930
import Title from '../../components/common/Title'
3031
import { RouterTab, RouterTabs } from '../../components/common/RouterTabs'
3132
import ErrorView from '../../components/common/ErrorView'
@@ -173,6 +174,7 @@ const Organisation = () => {
173174
)}
174175

175176
<Route path="/responsibles" element={<Responsibles />} />
177+
<Route path="/responsibles/xlsx" element={<ResponsiblesXlsx />} />
176178

177179
<Route
178180
path="/logs"

src/client/pages/Organisation/Responsibles.jsx

Lines changed: 4 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import React, { useMemo, useEffect } from 'react'
22
import { useTranslation } from 'react-i18next'
33
import { useParams } from 'react-router'
44
import { useSearchParams } from 'react-router-dom'
5-
import { orderBy } from 'lodash-es'
6-
import { useQuery } from '@tanstack/react-query'
75
import { writeFileXLSX, utils } from 'xlsx'
86
import { format } from 'date-fns'
97
import {
@@ -30,8 +28,8 @@ import { LoadingProgress } from '../../components/common/LoadingProgress'
3028
import { YearSemesterPeriodSelector } from '../../components/common/YearSemesterPeriodSelector'
3129
import useHistoryState from '../../hooks/useHistoryState'
3230
import { getYearRange } from '../../util/yearUtils'
33-
import apiClient from '../../util/apiClient'
3431
import { NorButton } from '../../components/common/NorButton'
32+
import { useOrganisationFeedbackTargets, getCourseRealisationName, generateTeacherStats } from './responsiblesUtils'
3533

3634
const styles = {
3735
filtersHead: {
@@ -42,26 +40,6 @@ const styles = {
4240
},
4341
}
4442

45-
const useOrganisationFeedbackTargets = ({ code, startDate, endDate, enabled }) => {
46-
const queryKey = ['organisationFeedbackTargets', code, startDate, endDate]
47-
48-
const queryFn = async () => {
49-
const { data: feedbackTargets } = await apiClient.get(`/feedback-targets/for-organisation/${code}`, {
50-
params: { startDate, endDate },
51-
})
52-
53-
return feedbackTargets
54-
}
55-
56-
return useQuery({
57-
queryKey,
58-
queryFn,
59-
enabled,
60-
refetchOnWindowFocus: false,
61-
refetchOnMount: false,
62-
})
63-
}
64-
6543
const Filters = React.memo(({ startDate, endDate, onChange, timeOption, setTimeOption }) => {
6644
const { t } = useTranslation()
6745
const [open, setOpen] = React.useState(false)
@@ -131,12 +109,6 @@ const Responsibles = () => {
131109
setSearchParams(params, { replace: true })
132110
}, [startDate, endDate, timeOption])
133111

134-
const getCourseRealisationName = fbt =>
135-
fbt.courseRealisation.name[i18n.language] ||
136-
fbt.courseRealisation.name.fi ||
137-
fbt.courseRealisation.name.en ||
138-
fbt.courseRealisation.name.sv
139-
140112
const handleDateChange = (newStart, newEnd) => {
141113
setStartDate(newStart)
142114
setEndDate(newEnd)
@@ -161,45 +133,7 @@ const Responsibles = () => {
161133
enabled: startDate !== null,
162134
})
163135

164-
const teacherStats = useMemo(() => {
165-
if (!feedbackTargets || !Array.isArray(feedbackTargets)) return []
166-
167-
const stats = new Map()
168-
169-
feedbackTargets.forEach(([_year, months]) => {
170-
if (!Array.isArray(months)) return
171-
172-
months.forEach(([_month, days]) => {
173-
if (!Array.isArray(days)) return
174-
175-
days.forEach(([_date, fbts]) => {
176-
if (!Array.isArray(fbts)) return
177-
178-
fbts.forEach(fbt => {
179-
if (fbt.responsibleTeachers && Array.isArray(fbt.responsibleTeachers)) {
180-
fbt.responsibleTeachers.forEach(teacher => {
181-
if (stats.has(teacher.id)) {
182-
stats.get(teacher.id).count++
183-
stats.get(teacher.id).feedbackTargets.push(fbt)
184-
} else {
185-
stats.set(teacher.id, {
186-
id: teacher.id,
187-
firstName: teacher.firstName,
188-
lastName: teacher.lastName,
189-
email: teacher.email,
190-
count: 1,
191-
feedbackTargets: [fbt],
192-
})
193-
}
194-
})
195-
}
196-
})
197-
})
198-
})
199-
})
200-
201-
return orderBy(Array.from(stats.values()), ['lastName', 'firstName'], ['asc', 'asc'])
202-
}, [feedbackTargets])
136+
const teacherStats = useMemo(() => generateTeacherStats(feedbackTargets), [feedbackTargets])
203137

204138
const exportSummary = () => {
205139
const headers = [t('common:lastName'), t('common:firstName'), t('organisationSettings:feedbackTargetCount')]
@@ -234,7 +168,7 @@ const Responsibles = () => {
234168
teacher.lastName,
235169
teacher.firstName,
236170
fbt.courseUnit.courseCode,
237-
getCourseRealisationName(fbt),
171+
getCourseRealisationName(fbt, i18n),
238172
new Date(fbt.courseRealisation.startDate).toLocaleDateString(i18n.language),
239173
new Date(fbt.courseRealisation.endDate).toLocaleDateString(i18n.language),
240174
])
@@ -328,7 +262,7 @@ const Responsibles = () => {
328262
{teacher.feedbackTargets.map(fbt => (
329263
<TableRow key={fbt.id}>
330264
<TableCell>{fbt.courseUnit.courseCode}</TableCell>
331-
<TableCell>{getCourseRealisationName(fbt)}</TableCell>
265+
<TableCell>{getCourseRealisationName(fbt, i18n)}</TableCell>
332266
<TableCell>
333267
{new Date(fbt.courseRealisation.startDate).toLocaleDateString(i18n.language)}
334268
</TableCell>
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import React, { useEffect } from 'react'
2+
import { useTranslation } from 'react-i18next'
3+
import { useParams, useSearchParams, useNavigate } from 'react-router-dom'
4+
import { writeFileXLSX, utils } from 'xlsx'
5+
6+
import { LoadingProgress } from '../../components/common/LoadingProgress'
7+
import { getYearRange } from '../../util/yearUtils'
8+
import { useOrganisationFeedbackTargets, getCourseRealisationName, generateTeacherStats } from './responsiblesUtils'
9+
10+
const ResponsiblesXlsx = () => {
11+
const { t, i18n } = useTranslation()
12+
const { code } = useParams()
13+
const [searchParams] = useSearchParams()
14+
const navigate = useNavigate()
15+
16+
const studyYearRange = getYearRange(new Date())
17+
const startDate = searchParams.get('startDate') || studyYearRange.start
18+
const endDate = searchParams.get('endDate') || studyYearRange.end
19+
20+
const {
21+
data: feedbackTargets,
22+
isLoading,
23+
isSuccess,
24+
} = useOrganisationFeedbackTargets({
25+
code,
26+
startDate,
27+
endDate,
28+
enabled: startDate !== null,
29+
})
30+
31+
useEffect(() => {
32+
if (!isSuccess || !feedbackTargets) return
33+
34+
// Generate teacher stats
35+
const teacherStats = generateTeacherStats(feedbackTargets)
36+
37+
// Generate detailed export
38+
const data = []
39+
40+
// Headers
41+
data.push([
42+
t('common:lastName'),
43+
t('common:firstName'),
44+
t('common:code'),
45+
t('common:name'),
46+
t('organisationSettings:startDate'),
47+
t('organisationSettings:endDate'),
48+
])
49+
50+
// Data rows
51+
teacherStats.forEach(teacher => {
52+
teacher.feedbackTargets.forEach(fbt => {
53+
data.push([
54+
teacher.lastName,
55+
teacher.firstName,
56+
fbt.courseUnit.courseCode,
57+
getCourseRealisationName(fbt, i18n),
58+
new Date(fbt.courseRealisation.startDate).toLocaleDateString(i18n.language),
59+
new Date(fbt.courseRealisation.endDate).toLocaleDateString(i18n.language),
60+
])
61+
})
62+
})
63+
64+
const worksheet = utils.aoa_to_sheet(data)
65+
const workbook = utils.book_new()
66+
utils.book_append_sheet(workbook, worksheet, t('organisationSettings:detailed'))
67+
68+
const fileName = `${code}_${t('organisationSettings:exportFilePrefix')}_${t('organisationSettings:detailed')}.xlsx`
69+
writeFileXLSX(workbook, fileName)
70+
71+
// Redirect back to responsibles page after download
72+
const params = new URLSearchParams()
73+
if (startDate) params.set('startDate', startDate)
74+
if (endDate) params.set('endDate', endDate)
75+
const option = searchParams.get('option')
76+
if (option) params.set('option', option)
77+
78+
navigate(`/organisations/${code}/responsibles?${params.toString()}`, { replace: true })
79+
}, [isSuccess, feedbackTargets, code, startDate, endDate, t, i18n, navigate, searchParams])
80+
81+
if (isLoading) {
82+
return <LoadingProgress />
83+
}
84+
85+
return null
86+
}
87+
88+
export default ResponsiblesXlsx
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { useQuery } from '@tanstack/react-query'
2+
import { orderBy } from 'lodash-es'
3+
import apiClient from '../../util/apiClient'
4+
5+
export const useOrganisationFeedbackTargets = ({ code, startDate, endDate, enabled }) => {
6+
const queryKey = ['organisationFeedbackTargets', code, startDate, endDate]
7+
8+
const queryFn = async () => {
9+
const { data: feedbackTargets } = await apiClient.get(`/feedback-targets/for-organisation/${code}`, {
10+
params: { startDate, endDate },
11+
})
12+
13+
return feedbackTargets
14+
}
15+
16+
return useQuery({
17+
queryKey,
18+
queryFn,
19+
enabled,
20+
refetchOnWindowFocus: false,
21+
refetchOnMount: false,
22+
})
23+
}
24+
25+
export const getCourseRealisationName = (fbt, i18n) => {
26+
const name =
27+
fbt.courseRealisation.name[i18n.language] ||
28+
fbt.courseRealisation.name.fi ||
29+
fbt.courseRealisation.name.en ||
30+
fbt.courseRealisation.name.sv
31+
32+
// Check for pattern "X | X, Y" and convert to "X, Y"
33+
const parts = name.split(' | ')
34+
if (parts.length === 2 && parts[1].startsWith(`${parts[0]}, `)) {
35+
return parts[1]
36+
}
37+
38+
return name
39+
}
40+
41+
export const generateTeacherStats = feedbackTargets => {
42+
if (!feedbackTargets || !Array.isArray(feedbackTargets)) return []
43+
44+
const stats = new Map()
45+
46+
feedbackTargets.forEach(([_year, months]) => {
47+
if (!Array.isArray(months)) return
48+
49+
months.forEach(([_month, days]) => {
50+
if (!Array.isArray(days)) return
51+
52+
days.forEach(([_date, fbts]) => {
53+
if (!Array.isArray(fbts)) return
54+
55+
fbts.forEach(fbt => {
56+
if (fbt.responsibleTeachers && Array.isArray(fbt.responsibleTeachers)) {
57+
fbt.responsibleTeachers.forEach(teacher => {
58+
if (stats.has(teacher.id)) {
59+
stats.get(teacher.id).count++
60+
stats.get(teacher.id).feedbackTargets.push(fbt)
61+
} else {
62+
stats.set(teacher.id, {
63+
id: teacher.id,
64+
firstName: teacher.firstName,
65+
lastName: teacher.lastName,
66+
email: teacher.email,
67+
count: 1,
68+
feedbackTargets: [fbt],
69+
})
70+
}
71+
})
72+
}
73+
})
74+
})
75+
})
76+
})
77+
78+
return orderBy(Array.from(stats.values()), ['lastName', 'firstName'], ['asc', 'asc'])
79+
}

0 commit comments

Comments
 (0)