Skip to content

Commit

Permalink
Remove client-side Action spreadsheet file export from dashboard
Browse files Browse the repository at this point in the history
Replace it with the same backend excel export implementation
used for reporting. Also remove related dependencies. Hopefully
this will result in less maintenance in the long run.
  • Loading branch information
tituomin committed Dec 5, 2024
1 parent 4e65ca0 commit 0e4f36c
Show file tree
Hide file tree
Showing 4 changed files with 20 additions and 597 deletions.
187 changes: 5 additions & 182 deletions components/dashboard/ActionStatusExport.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { getActionTaskTermContext, getActionTermContext } from 'common/i18n';
import { cleanActionStatus } from 'common/preprocess';
import { usePlan } from 'context/plan';
import { useTranslations } from 'next-intl';
import {
Expand All @@ -9,193 +7,18 @@ import {
DropdownItem,
} from 'reactstrap';

async function exportActions(
t,
actions,
actionStatuses,
plan,
fileFormat = 'excel'
) {
const Excel = (await import('exceljs')).default;
const fileSaver = (await import('file-saver')).default;
const workbook = new Excel.Workbook();
const worksheet = workbook.addWorksheet(
t('actions', getActionTermContext(plan))
);
const planIds = new Set(actions.map((action) => action.plan.id));
const multiplePlans = planIds.size > 1;
const columns = [
{ header: t('action-identifier'), key: 'id', width: 10 },
{
header: t('action-name-title', getActionTermContext(plan)),
key: 'name',
width: 50,
},
{ header: t('status'), key: 'status', width: 20 },
{
header: t('action-implementation-phase'),
key: 'implementationPhase',
width: 20,
},
{ header: t('action-last-updated'), key: 'lastUpdated', width: 15 },
{
header: t('tasks-on-time', getActionTaskTermContext(plan)),
key: 'ontimeTasks',
width: 10,
},
{
header: t('tasks-late', getActionTaskTermContext(plan)),
key: 'lateTasks',
width: 10,
},
{
header: t('tasks-completed', getActionTaskTermContext(plan)),
key: 'completedTasks',
width: 10,
},
{
header: t('action-tasks', getActionTaskTermContext(plan)),
key: 'tasks',
width: 10,
},
{
header: t('responsible-organizations-primary'),
key: 'primaryResponsibleOrgs',
width: 20,
},
{
header: t('responsible-organizations-collaborator'),
key: 'collaboratorResponsibleOrgs',
width: 20,
},
{
header: t('responsible-organizations-other'),
key: 'otherResponsibleOrgs',
width: 20,
},
];
if (multiplePlans) {
columns.unshift({
header: t('plan'),
key: 'plan',
width: 20,
});
}
// When assigning to worksheet.columns, some magic happens and manipulating worksheet.columns afterwards does not
// yield the desired results. So we prepare the columns array separately before assigning it to worksheet.columns.
worksheet.columns = columns;
actions.forEach((act) => {
const status = cleanActionStatus(act, actionStatuses);
// Remove any soft hyphens in action name (due to `hyphenated: true` when querying the name) as Excel renders
// visible hyphens instead.
const actionName = act.name?.replaceAll('­', '');
let activePhaseName = act.implementationPhase?.name;
if (status != null) {
// FIXME: Duplicated logic from ActionPhase.js
const inactive = [
'cancelled',
'merged',
'postponed',
'completed',
].includes(status.identifier);
if (inactive) activePhaseName = status.name;
}

const tasks = act.tasks;
let tasksCount = tasks.length;
// FIXME: Duplicated logic from ActionStatusTable.js
let ontimeTasks = 0;
let lateTasks = 0;
let completedTasks = 0;
const nowDate = new Date();

tasks.forEach((task) => {
const taskDue = new Date(task.dueAt);
switch (task.state) {
case 'NOT_STARTED':
case 'IN_PROGRESS':
if (taskDue < nowDate) lateTasks += 1;
else ontimeTasks += 1;
break;
case 'COMPLETED':
completedTasks += 1;
break;
default:
tasksCount -= 1;
}
});

const getOrgName = ({ organization }) => organization.name;

const parties = act.responsibleParties;
const primaryResponsibleOrgs = parties
.filter((p) => p.role === 'PRIMARY')
.map(getOrgName);
const collaboratorResponsibleOrgs = parties
.filter((p) => p.role === 'COLLABORATOR')
.map(getOrgName);
const otherResponsibleOrgs = parties
.filter((p) => p.role === null)
.map(getOrgName);

const row = [
act.identifier,
actionName,
status?.name,
activePhaseName,
new Date(act.updatedAt),
ontimeTasks,
lateTasks,
completedTasks,
tasksCount,
primaryResponsibleOrgs.join(';'),
collaboratorResponsibleOrgs.join(';'),
otherResponsibleOrgs.join(';'),
];
if (multiplePlans) {
row.unshift(act.plan.name);
}
worksheet.addRow(row);
});

const today = new Date().toISOString().split('T')[0];
switch (fileFormat) {
case 'excel':
const xls64 = await workbook.xlsx.writeBuffer({ base64: true });
fileSaver.saveAs(
new Blob([xls64], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
}),
`${t('actions', getActionTermContext(plan))}-${today}.xlsx`
);
break;

case 'csv':
const csv64 = await workbook.csv.writeBuffer({ base64: true });
fileSaver.saveAs(
new Blob([csv64], { type: 'text/csv' }),
`${t('actions', getActionTermContext(plan))}-${today}.csv`
);
break;

default:
throw new Error('Unknown file format');
}
}

export default function ActionStatusExport({ actions }) {
const t = useTranslations();
const plan = usePlan();
const { actionStatuses } = plan;
const handleExport = async (format) => {
await exportActions(t, actions, actionStatuses, plan, format);
};
const url = plan.actionReportExportViewUrl;
const csvExportUrl = `${url}?format=csv`;
const excelExportUrl = `${url}?format=xlsx`;
return (
<UncontrolledDropdown>
<DropdownToggle caret>{t('export')}</DropdownToggle>
<DropdownMenu>
<DropdownItem onClick={() => handleExport('excel')}>Excel</DropdownItem>
<DropdownItem onClick={() => handleExport('csv')}>CSV</DropdownItem>
<DropdownItem href={excelExportUrl}>Excel</DropdownItem>
<DropdownItem href={csvExportUrl}>CSV</DropdownItem>
</DropdownMenu>
</UncontrolledDropdown>
);
Expand Down
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@
"docker-secret": "^1.2.4",
"dotenv": "^16.0.0",
"escape-string-regexp": "^5.0.0",
"exceljs": "^4.3.0",
"express-robots-txt": "^1.0.0",
"file-saver": "^2.0.5",
"fontfaceobserver": "^2.1.0",
"framer-motion": "^11.0.24",
"html-react-parser": "^1.4.12",
Expand Down
1 change: 1 addition & 0 deletions queries/get-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const GET_PLAN_CONTEXT = gql`
hideActionIdentifiers
publishedAt
viewUrl(clientUrl: $clientUrl)
actionReportExportViewUrl
primaryActionClassification {
id
identifier
Expand Down
Loading

0 comments on commit 0e4f36c

Please sign in to comment.