-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
1,632 additions
and
331 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { Workbook } from 'excel4node'; | ||
import { z } from 'zod'; | ||
|
||
import { getFilterFragment } from '@backend/components/project/search'; | ||
import { getPool, sql } from '@backend/db'; | ||
import { logger } from '@backend/logging'; | ||
|
||
import { translations } from '@shared/language'; | ||
import { dateStringSchema } from '@shared/schema/common'; | ||
import { ProjectSearch } from '@shared/schema/project'; | ||
|
||
import { buildSheet } from '.'; | ||
|
||
const projectReportFragment = sql.fragment` | ||
SELECT | ||
project.project_name AS "detailplanProjectName", | ||
project_detailplan.detailplan_id AS "detailplanProjectDetailplanId", | ||
(SELECT text_fi FROM app.code WHERE id = project_detailplan.subtype) AS "detailplanProjectSubtype", | ||
project_detailplan.diary_id AS "detailplanProjectDiaryId", | ||
(SELECT name FROM app.user WHERE id = project_detailplan.preparer) AS "detailplanProjectPreparer", | ||
(SELECT name FROM app.user WHERE id = project_detailplan.technical_planner) AS "detailplanProjectTechnicalPlanner", | ||
project_detailplan.district AS "detailplanProjectDistrict", | ||
project_detailplan.block_name AS "detailplanProjectBlockName", | ||
project_detailplan.address_text AS "detailplanProjectAddressText", | ||
project_detailplan.initiative_date AS "detailplanProjectInitiativeDate" | ||
FROM app.project_detailplan | ||
INNER JOIN app.project ON (project_detailplan.id = project.id AND project.deleted IS FALSE) | ||
`; | ||
|
||
const reportRowSchema = z.object({ | ||
detailplanProjectDetailplanId: z.number(), | ||
detailplanProjectSubtype: z.string().nullish(), | ||
detailplanProjectDiaryId: z.string().nullish(), | ||
detailplanProjectPreparer: z.string(), | ||
detailplanProjectTechnicalPlanner: z.string().nullish(), | ||
detailplanProjectDistrict: z.string(), | ||
detailplanProjectBlockName: z.string(), | ||
detailplanProjectAddressText: z.string(), | ||
detailplanProjectName: z.string(), | ||
detailplanProjectInitiativeDate: dateStringSchema.nullish(), | ||
}); | ||
|
||
export async function buildDetailplanCatalogSheet(workbook: Workbook, searchParams: ProjectSearch) { | ||
const reportQuery = sql.type(reportRowSchema)` | ||
${projectReportFragment} | ||
${getFilterFragment(searchParams)} | ||
`; | ||
|
||
const reportResult = await getPool().any(reportQuery); | ||
logger.debug(`Fetched ${reportResult.length} rows for the detailplan catalog`); | ||
const rows = z.array(reportRowSchema).parse(reportResult); | ||
|
||
buildSheet({ | ||
workbook, | ||
sheetTitle: translations['fi']['report.detailplanCatalog'], | ||
rows, | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import { Workbook } from 'excel4node'; | ||
|
||
import { getPool, sql } from '@backend/db'; | ||
import { logger } from '@backend/logging'; | ||
|
||
import { TranslationKey, translations } from '@shared/language'; | ||
import { ProjectSearch } from '@shared/schema/project'; | ||
import { Suffix } from '@shared/util-types'; | ||
|
||
import { buildDetailplanCatalogSheet } from './detailplanProject'; | ||
import { buildInvestmentProjectReportSheet } from './investmentProject'; | ||
|
||
type ReportColumnKey = Partial<Suffix<TranslationKey, 'report.columns.'>>; | ||
type ReportFieldValue = string | number | Date | null; | ||
|
||
/** | ||
* Generic function for building a worksheet into a workbook with given rows. | ||
* | ||
* All row keys must be defined in localizations under the prefix `"report.columns."`. | ||
*/ | ||
export function buildSheet<ColumnKey extends ReportColumnKey>({ | ||
workbook, | ||
sheetTitle, | ||
rows, | ||
}: { | ||
workbook: Workbook; | ||
sheetTitle: string; | ||
rows: { [field in ColumnKey]?: ReportFieldValue }[]; | ||
}) { | ||
const headerStyle = workbook.createStyle({ | ||
font: { | ||
bold: true, | ||
}, | ||
}); | ||
|
||
const reportFields = Object.keys(rows[0]) as ReportColumnKey[]; | ||
const headers = reportFields.map((field) => translations['fi'][`report.columns.${field}`]); | ||
const worksheet = workbook.addWorksheet(sheetTitle); | ||
|
||
headers.forEach((header, index) => { | ||
worksheet | ||
.cell(1, index + 1) | ||
.string(header) | ||
.style(headerStyle); | ||
}); | ||
|
||
rows.forEach((row, rowIndex) => { | ||
Object.values<ReportFieldValue | undefined>(row).forEach((value, column) => { | ||
// Skip empty values | ||
if (value == null) { | ||
return; | ||
} | ||
|
||
const cell = worksheet.cell(rowIndex + 2, column + 1); | ||
if (value instanceof Date) { | ||
cell.date(value); | ||
} else if (typeof value === 'number') { | ||
cell.number(value); | ||
} else { | ||
cell.string(value); | ||
} | ||
}); | ||
}); | ||
} | ||
|
||
/** | ||
* Builds a report for given search parameters. Saves it temporarily into the database for downloading. | ||
* @param jobId Job ID | ||
* @param searchParams Search parameters | ||
*/ | ||
export async function buildReport(jobId: string, searchParams: ProjectSearch) { | ||
try { | ||
const workbook = new Workbook({ | ||
dateFormat: 'd.m.yyyy', | ||
}); | ||
|
||
logger.debug( | ||
`Running report queries for job ${jobId} with data: ${JSON.stringify(searchParams)}` | ||
); | ||
|
||
// Build each sheet in desired order | ||
await buildInvestmentProjectReportSheet(workbook, searchParams); | ||
await buildDetailplanCatalogSheet(workbook, searchParams); | ||
|
||
const buffer = await workbook.writeToBuffer(); | ||
|
||
logger.debug(`Saving report to database, ${buffer.length} bytes...`); | ||
const queryResult = await getPool().query(sql.untyped` | ||
INSERT INTO app.report_file (pgboss_job_id, report_filename, report_data) | ||
VALUES (${jobId}, 'raportti.xlsx', ${sql.binary(buffer)}) | ||
`); | ||
logger.debug(`Report saved to database, ${queryResult.rowCount} rows affected.`); | ||
} catch (error) { | ||
// Log and rethrow the error to make the job state failed | ||
logger.error(error); | ||
throw error; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
/** | ||
* Extracts string literal type with given suffix from given base string literal. | ||
* | ||
* Example: | ||
* `Suffix<"a.foo", "a.bar", "b.baz", "a.">` = `"foo" | "bar"` | ||
*/ | ||
export type Suffix<Base extends string, Prefix extends string> = Base extends `${Prefix}${infer R}` | ||
? R | ||
: never; |