diff --git a/apps/judicial-system/backend/migrations/20240904100012-update-institution.js b/apps/judicial-system/backend/migrations/20240904100012-update-institution.js new file mode 100644 index 0000000000000..d107dfba7de4f --- /dev/null +++ b/apps/judicial-system/backend/migrations/20240904100012-update-institution.js @@ -0,0 +1,48 @@ +'use strict' + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('institution', 'address', { + type: Sequelize.STRING, + allowNull: true, + }) + + const institutionsToUpdate = [ + { + name: 'Héraðsdómur Reykjavíkur', + address: 'Dómhúsið við Lækjartorg, Reykjavík', + }, + { name: 'Héraðsdómur Reykjaness', address: 'Fjarðargata 9, Hafnarfirði' }, + { + name: 'Héraðsdómur Vesturlands', + address: 'Bjarnarbraut 8, Borgarnesi', + }, + { name: 'Héraðsdómur Vestfjarða', address: 'Hafnarstræti 9, Ísafirði' }, + { + name: 'Héraðsdómur Norðurlands vestra', + address: 'Skagfirðingabraut 21, Sauðárkróki', + }, + { + name: 'Héraðsdómur Norðurlands eystra', + address: 'Hafnarstræti 107, 4. hæð, Akureyri', + }, + { name: 'Héraðsdómur Austurlands', address: 'Lyngás 15, Egilsstöðum' }, + { name: 'Héraðsdómur Suðurlands', address: 'Austurvegur 4, Selfossi' }, + ] + + await queryInterface.sequelize.transaction(async (transaction) => { + for (const institution of institutionsToUpdate) { + await queryInterface.bulkUpdate( + 'institution', + { address: institution.address }, + { name: institution.name }, + { transaction }, + ) + } + }) + }, + + down: async (queryInterface) => { + await queryInterface.removeColumn('institution', 'address') + }, +} diff --git a/apps/judicial-system/backend/src/app/formatters/confirmedIndictmentPdf.ts b/apps/judicial-system/backend/src/app/formatters/confirmedIndictmentPdf.ts index 1c663df0edfdc..f5798cf308496 100644 --- a/apps/judicial-system/backend/src/app/formatters/confirmedIndictmentPdf.ts +++ b/apps/judicial-system/backend/src/app/formatters/confirmedIndictmentPdf.ts @@ -4,14 +4,14 @@ import { formatDate, lowercase } from '@island.is/judicial-system/formatters' import { calculatePt, + Confirmation, drawTextWithEllipsisPDFKit, - IndictmentConfirmation, smallFontSize, } from './pdfHelpers' import { PDFKitCoatOfArms } from './PDFKitCoatOfArms' export const createConfirmedIndictment = async ( - confirmation: IndictmentConfirmation, + confirmation: Confirmation, indictmentPDF: Buffer, ): Promise => { const pdfDoc = await PDFDocument.load(indictmentPDF) diff --git a/apps/judicial-system/backend/src/app/formatters/index.ts b/apps/judicial-system/backend/src/app/formatters/index.ts index 084da81ca6b56..120d7279c56e3 100644 --- a/apps/judicial-system/backend/src/app/formatters/index.ts +++ b/apps/judicial-system/backend/src/app/formatters/index.ts @@ -29,7 +29,7 @@ export { formatPostponedCourtDateEmailNotification, stripHtmlTags, } from './formatters' -export { IndictmentConfirmation } from './pdfHelpers' +export { Confirmation } from './pdfHelpers' export { getRequestPdfAsBuffer, getRequestPdfAsString } from './requestPdf' export { getRulingPdfAsBuffer, getRulingPdfAsString } from './rulingPdf' export { createCaseFilesRecord } from './caseFilesRecordPdf' diff --git a/apps/judicial-system/backend/src/app/formatters/indictmentPdf.ts b/apps/judicial-system/backend/src/app/formatters/indictmentPdf.ts index c19da2711a7f1..5ab70b81c81a4 100644 --- a/apps/judicial-system/backend/src/app/formatters/indictmentPdf.ts +++ b/apps/judicial-system/backend/src/app/formatters/indictmentPdf.ts @@ -16,7 +16,7 @@ import { addNormalPlusJustifiedText, addNormalPlusText, addNormalText, - IndictmentConfirmation, + Confirmation, setTitle, } from './pdfHelpers' @@ -52,7 +52,7 @@ const roman = (num: number) => { export const createIndictment = async ( theCase: Case, formatMessage: FormatMessage, - confirmation?: IndictmentConfirmation, + confirmation?: Confirmation, ): Promise => { const doc = new PDFDocument({ size: 'A4', diff --git a/apps/judicial-system/backend/src/app/formatters/pdfHelpers.ts b/apps/judicial-system/backend/src/app/formatters/pdfHelpers.ts index d348032ebc350..4f44aa249e93a 100644 --- a/apps/judicial-system/backend/src/app/formatters/pdfHelpers.ts +++ b/apps/judicial-system/backend/src/app/formatters/pdfHelpers.ts @@ -5,7 +5,7 @@ import { formatDate, lowercase } from '@island.is/judicial-system/formatters' import { coatOfArms } from './coatOfArms' import { policeStar } from './policeStar' -export interface IndictmentConfirmation { +export interface Confirmation { actor: string title?: string institution: string @@ -22,6 +22,10 @@ export const largeFontSize = 18 export const hugeFontSize = 26 export const giganticFontSize = 33 +const lightGray = '#FAFAFA' +const darkGray = '#CBCBCB' +const gold = '#ADA373' + const setFont = (doc: PDFKit.PDFDocument, font?: string) => { if (font) { doc.font(font) @@ -106,13 +110,105 @@ export const addPoliceStar = (doc: PDFKit.PDFDocument) => { doc.scale(25).translate(-270, -70) } +export const addConfirmation = ( + doc: PDFKit.PDFDocument, + confirmation: Confirmation, +) => { + const pageMargin = calculatePt(18) + const shaddowHeight = calculatePt(70) + const coatOfArmsWidth = calculatePt(105) + const coatOfArmsX = pageMargin + calculatePt(8) + const titleHeight = calculatePt(24) + const titleX = coatOfArmsX + coatOfArmsWidth + calculatePt(8) + const institutionWidth = calculatePt(160) + const confirmedByWidth = institutionWidth + calculatePt(48) + const shaddowWidth = institutionWidth + confirmedByWidth + coatOfArmsWidth + const titleWidth = institutionWidth + confirmedByWidth + + // Draw the shadow + doc + .rect(pageMargin, pageMargin + calculatePt(8), shaddowWidth, shaddowHeight) + .fill(lightGray) + .stroke() + + // Draw the coat of arms + doc + .rect(coatOfArmsX, pageMargin, coatOfArmsWidth, shaddowHeight) + .fillAndStroke('white', darkGray) + + addCoatOfArms(doc, calculatePt(49), calculatePt(24)) + + // Draw the title + doc + .rect(coatOfArmsX + coatOfArmsWidth, pageMargin, titleWidth, titleHeight) + .fillAndStroke(lightGray, darkGray) + doc.fill('black') + doc.font('Times-Bold') + doc + .fontSize(calculatePt(smallFontSize)) + .text('Réttarvörslugátt', titleX, pageMargin + calculatePt(9)) + doc.font('Times-Roman') + // The X value here is approx. 8px after the title + doc.text('Rafræn staðfesting', calculatePt(210), pageMargin + calculatePt(9)) + doc.text( + formatDate(confirmation.date) || '', + shaddowWidth - calculatePt(24), + pageMargin + calculatePt(9), + ) + + // Draw the institution + doc + .rect( + coatOfArmsX + coatOfArmsWidth, + pageMargin + titleHeight, + institutionWidth, + shaddowHeight - titleHeight, + ) + .fillAndStroke('white', darkGray) + doc.fill('black') + doc.font('Times-Bold') + doc.text('Dómstóll', titleX, pageMargin + titleHeight + calculatePt(10)) + doc.font('Times-Roman') + drawTextWithEllipsis( + doc, + confirmation.institution, + titleX, + pageMargin + titleHeight + calculatePt(22), + institutionWidth - calculatePt(16), + ) + + // Draw the actor + doc + .rect( + coatOfArmsX + coatOfArmsWidth + institutionWidth, + pageMargin + titleHeight, + confirmedByWidth, + shaddowHeight - titleHeight, + ) + .fillAndStroke('white', darkGray) + doc.fill('black') + doc.font('Times-Bold') + doc.text( + 'Samþykktaraðili', + titleX + institutionWidth, + pageMargin + titleHeight + calculatePt(10), + ) + doc.font('Times-Roman') + doc.text( + `${confirmation.actor}${ + confirmation.title ? `, ${lowercase(confirmation.title)}` : '' + }`, + titleX + institutionWidth, + pageMargin + titleHeight + calculatePt(22), + ) + + doc.fillColor('black') +} + export const addIndictmentConfirmation = ( doc: PDFKit.PDFDocument, - confirmation: IndictmentConfirmation, + confirmation: Confirmation, ) => { - const lightGray = '#FAFAFA' - const darkGray = '#CBCBCB' - const gold = '#ADA373' const pageMargin = calculatePt(18) const shaddowHeight = calculatePt(90) const coatOfArmsWidth = calculatePt(105) diff --git a/apps/judicial-system/backend/src/app/formatters/subpoenaPdf.ts b/apps/judicial-system/backend/src/app/formatters/subpoenaPdf.ts index 40af05fe4ba9f..4af8001053e8e 100644 --- a/apps/judicial-system/backend/src/app/formatters/subpoenaPdf.ts +++ b/apps/judicial-system/backend/src/app/formatters/subpoenaPdf.ts @@ -13,6 +13,7 @@ import { subpoena as strings } from '../messages' import { Case } from '../modules/case' import { Defendant } from '../modules/defendant' import { + addConfirmation, addEmptyLines, addFooter, addHugeHeading, @@ -22,28 +23,6 @@ import { setTitle, } from './pdfHelpers' -type DistrictCourts = - | 'Héraðsdómur Reykjavíkur' - | 'Héraðsdómur Reykjaness' - | 'Héraðsdómur Vesturlands' - | 'Héraðsdómur Vestfjarða' - | 'Héraðsdómur Norðurlands vestra' - | 'Héraðsdómur Norðurlands eystra' - | 'Héraðsdómur Austurlands' - | 'Héraðsdómur Suðurlands' - -// TODO: Move to databas -const DistrictCourtLocation: Record = { - 'Héraðsdómur Reykjavíkur': 'Dómhúsið við Lækjartorg, Reykjavík', - 'Héraðsdómur Reykjaness': 'Fjarðargata 9, Hafnarfirði', - 'Héraðsdómur Vesturlands': 'Bjarnarbraut 8, Borgarnesi', - 'Héraðsdómur Vestfjarða': 'Hafnarstræti 9, Ísafirði', - 'Héraðsdómur Norðurlands vestra': 'Skagfirðingabraut 21, Sauðárkróki', - 'Héraðsdómur Norðurlands eystra': 'Hafnarstræti 107, 4. hæð, Akureyri', - 'Héraðsdómur Austurlands': 'Lyngás 15, Egilsstöðum', - 'Héraðsdómur Suðurlands': 'Austurvegur 4, Selfossi', -} - export const createSubpoena = ( theCase: Case, defendant: Defendant, @@ -71,6 +50,11 @@ export const createSubpoena = ( doc.on('data', (chunk) => sinc.push(chunk)) setTitle(doc, formatMessage(strings.title)) + + if (dateLog) { + addEmptyLines(doc, 5) + } + addNormalText(doc, `${theCase.court?.name}`, 'Times-Bold', true) addNormalRightAlignedText( @@ -86,7 +70,7 @@ export const createSubpoena = ( if (theCase.court?.name) { addNormalText( doc, - DistrictCourtLocation[theCase.court.name as DistrictCourts], + theCase.court.address || 'Ekki skráð', // the latter shouldn't happen, if it does we have an problem with the court data 'Times-Roman', ) } @@ -170,6 +154,15 @@ export const createSubpoena = ( addFooter(doc) + if (dateLog) { + addConfirmation(doc, { + actor: theCase.judge?.name || '', + title: theCase.judge?.title, + institution: theCase.judge?.institution?.name || '', + date: dateLog.created, + }) + } + doc.end() return new Promise((resolve) => diff --git a/apps/judicial-system/backend/src/app/modules/case/case.controller.ts b/apps/judicial-system/backend/src/app/modules/case/case.controller.ts index cca384ac525ad..7b0da96f91d1e 100644 --- a/apps/judicial-system/backend/src/app/modules/case/case.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/case/case.controller.ts @@ -68,7 +68,7 @@ import { prosecutorRule, publicProsecutorStaffRule, } from '../../guards' -import { CaseEvent, EventService } from '../event' +import { EventService } from '../event' import { UserService } from '../user' import { CreateCaseDto } from './dto/createCase.dto' import { TransitionCaseDto } from './dto/transitionCase.dto' @@ -99,8 +99,8 @@ import { prosecutorUpdateRule, publicProsecutorStaffUpdateRule, } from './guards/rolesRules' -import { CaseInterceptor } from './interceptors/case.interceptor' import { CaseListInterceptor } from './interceptors/caseList.interceptor' +import { CompletedAppealAccessedInterceptor } from './interceptors/completedAppealAccessed.interceptor' import { Case } from './models/case.model' import { SignatureConfirmationResponse } from './models/signatureConfirmation.response' import { transitionCase } from './state/case.state' @@ -465,7 +465,7 @@ export class CaseController { ) @Get('case/:caseId') @ApiOkResponse({ type: Case, description: 'Gets an existing case' }) - @UseInterceptors(CaseInterceptor) + @UseInterceptors(CompletedAppealAccessedInterceptor) getById(@Param('caseId') caseId: string, @CurrentCase() theCase: Case): Case { this.logger.debug(`Getting case ${caseId} by id`) @@ -545,6 +545,7 @@ export class CaseController { @RolesRules( prosecutorRule, prosecutorRepresentativeRule, + publicProsecutorStaffRule, districtCourtJudgeRule, districtCourtRegistrarRule, districtCourtAssistantRule, @@ -700,6 +701,7 @@ export class CaseController { @RolesRules( prosecutorRule, prosecutorRepresentativeRule, + publicProsecutorStaffRule, districtCourtJudgeRule, districtCourtRegistrarRule, districtCourtAssistantRule, diff --git a/apps/judicial-system/backend/src/app/modules/case/case.service.ts b/apps/judicial-system/backend/src/app/modules/case/case.service.ts index 65a7daca3a847..eabf3a6cee0bb 100644 --- a/apps/judicial-system/backend/src/app/modules/case/case.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/case.service.ts @@ -56,7 +56,7 @@ import { import { AwsS3Service } from '../aws-s3' import { CourtService } from '../court' import { Defendant, DefendantService } from '../defendant' -import { CaseEvent, EventService } from '../event' +import { EventService } from '../event' import { EventLog, EventLogService } from '../event-log' import { CaseFile, FileService } from '../file' import { IndictmentCount } from '../indictment-count' diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts b/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts index d49a4ea4a9640..7228bb2fda08b 100644 --- a/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts +++ b/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts @@ -5,7 +5,6 @@ import { CaseDecision, CaseState, CaseType, - DateType, getIndictmentVerdictAppealDeadline, IndictmentCaseReviewDecision, InstitutionType, diff --git a/apps/judicial-system/backend/src/app/modules/case/guards/limitedAccessCaseExists.guard.ts b/apps/judicial-system/backend/src/app/modules/case/guards/limitedAccessCaseExists.guard.ts index 460480edf5f84..f92e78361b749 100644 --- a/apps/judicial-system/backend/src/app/modules/case/guards/limitedAccessCaseExists.guard.ts +++ b/apps/judicial-system/backend/src/app/modules/case/guards/limitedAccessCaseExists.guard.ts @@ -14,7 +14,7 @@ export class LimitedAccessCaseExistsGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest() - const caseId = request.params.caseId + const caseId: string = request.params.caseId if (!caseId) { throw new BadRequestException('Missing case id') diff --git a/apps/judicial-system/backend/src/app/modules/case/interceptors/caseFile.interceptor.ts b/apps/judicial-system/backend/src/app/modules/case/interceptors/caseFile.interceptor.ts new file mode 100644 index 0000000000000..d7d74fb30a5c8 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/case/interceptors/caseFile.interceptor.ts @@ -0,0 +1,53 @@ +import { Observable } from 'rxjs' +import { map } from 'rxjs/operators' + +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from '@nestjs/common' + +import { + CaseAppealState, + CaseFileCategory, + isDefenceUser, + isPrisonStaffUser, + isPrisonSystemUser, + User, +} from '@island.is/judicial-system/types' + +import { Case } from '../models/case.model' + +@Injectable() +export class CaseFileInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const request = context.switchToHttp().getRequest() + const user: User = request.user + + return next.handle().pipe( + map((data: Case) => { + if (isDefenceUser(user)) { + return data + } + + if ( + isPrisonStaffUser(user) || + data.appealState !== CaseAppealState.COMPLETED + ) { + data.caseFiles?.splice(0, data.caseFiles.length) + } else if (isPrisonSystemUser(user)) { + data.caseFiles?.splice( + 0, + data.caseFiles.length, + ...data.caseFiles.filter( + (cf) => cf.category === CaseFileCategory.APPEAL_RULING, + ), + ) + } + + return data + }), + ) + } +} diff --git a/apps/judicial-system/backend/src/app/modules/case/interceptors/case.interceptor.ts b/apps/judicial-system/backend/src/app/modules/case/interceptors/completedAppealAccessed.interceptor.ts similarity index 94% rename from apps/judicial-system/backend/src/app/modules/case/interceptors/case.interceptor.ts rename to apps/judicial-system/backend/src/app/modules/case/interceptors/completedAppealAccessed.interceptor.ts index 6beab2bc39151..5ff8d84bff3fd 100644 --- a/apps/judicial-system/backend/src/app/modules/case/interceptors/case.interceptor.ts +++ b/apps/judicial-system/backend/src/app/modules/case/interceptors/completedAppealAccessed.interceptor.ts @@ -20,7 +20,7 @@ import { EventLogService } from '../../event-log' import { Case } from '../models/case.model' @Injectable() -export class CaseInterceptor implements NestInterceptor { +export class CompletedAppealAccessedInterceptor implements NestInterceptor { constructor(private readonly eventLogService: EventLogService) {} intercept(context: ExecutionContext, next: CallHandler): Observable { diff --git a/apps/judicial-system/backend/src/app/modules/case/internalCase.controller.ts b/apps/judicial-system/backend/src/app/modules/case/internalCase.controller.ts index 9149585ce4970..0d73789d2686f 100644 --- a/apps/judicial-system/backend/src/app/modules/case/internalCase.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/case/internalCase.controller.ts @@ -23,7 +23,7 @@ import { restrictionCases, } from '@island.is/judicial-system/types' -import { CaseEvent, EventService } from '../event' +import { EventService } from '../event' import { DeliverDto } from './dto/deliver.dto' import { DeliverCancellationNoticeDto } from './dto/deliverCancellationNotice.dto' import { InternalCasesDto } from './dto/internalCases.dto' diff --git a/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts b/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts index ebee2422a7419..235d74f860a0c 100644 --- a/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts @@ -53,7 +53,7 @@ import { AwsS3Service } from '../aws-s3' import { CourtDocumentFolder, CourtService } from '../court' import { courtSubtypes } from '../court/court.service' import { Defendant, DefendantService } from '../defendant' -import { CaseEvent, EventService } from '../event' +import { EventService } from '../event' import { CaseFile, FileService } from '../file' import { IndictmentCount, IndictmentCountService } from '../indictment-count' import { Institution } from '../institution' diff --git a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts index 7e17c55bab85e..42c90d58ec5e9 100644 --- a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts @@ -40,7 +40,7 @@ import { import { nowFactory } from '../../factories' import { defenderRule, prisonSystemStaffRule } from '../../guards' -import { CaseEvent, EventService } from '../event' +import { EventService } from '../event' import { User } from '../user' import { TransitionCaseDto } from './dto/transitionCase.dto' import { UpdateCaseDto } from './dto/updateCase.dto' @@ -53,7 +53,8 @@ import { CaseWriteGuard } from './guards/caseWrite.guard' import { LimitedAccessCaseExistsGuard } from './guards/limitedAccessCaseExists.guard' import { RequestSharedWithDefenderGuard } from './guards/requestSharedWithDefender.guard' import { defenderTransitionRule, defenderUpdateRule } from './guards/rolesRules' -import { CaseInterceptor } from './interceptors/case.interceptor' +import { CaseFileInterceptor } from './interceptors/caseFile.interceptor' +import { CompletedAppealAccessedInterceptor } from './interceptors/completedAppealAccessed.interceptor' import { Case } from './models/case.model' import { transitionCase } from './state/case.state' import { @@ -85,7 +86,7 @@ export class LimitedAccessCaseController { type: Case, description: 'Gets a limited set of properties of an existing case', }) - @UseInterceptors(CaseInterceptor) + @UseInterceptors(CompletedAppealAccessedInterceptor, CaseFileInterceptor) async getById( @Param('caseId') caseId: string, @CurrentCase() theCase: Case, diff --git a/apps/judicial-system/backend/src/app/modules/case/pdf.service.ts b/apps/judicial-system/backend/src/app/modules/case/pdf.service.ts index 8289b8d636ec0..6b956856ebb1b 100644 --- a/apps/judicial-system/backend/src/app/modules/case/pdf.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/pdf.service.ts @@ -22,6 +22,7 @@ import { } from '@island.is/judicial-system/types' import { + Confirmation, createCaseFilesRecord, createIndictment, createSubpoena, @@ -29,7 +30,6 @@ import { getCustodyNoticePdfAsBuffer, getRequestPdfAsBuffer, getRulingPdfAsBuffer, - IndictmentConfirmation, } from '../../formatters' import { AwsS3Service } from '../aws-s3' import { Defendant } from '../defendant' @@ -206,7 +206,7 @@ export class PdfService { ) } - let confirmation: IndictmentConfirmation | undefined = undefined + let confirmation: Confirmation | undefined = undefined if (hasIndictmentCaseBeenSubmittedToCourt(theCase.state)) { if (theCase.indictmentHash) { diff --git a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCaseFilesRecordPdfRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCaseFilesRecordPdfRolesRules.spec.ts index 98e04d53d4f82..05822ae9a72c7 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCaseFilesRecordPdfRolesRules.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCaseFilesRecordPdfRolesRules.spec.ts @@ -4,6 +4,7 @@ import { districtCourtRegistrarRule, prosecutorRepresentativeRule, prosecutorRule, + publicProsecutorStaffRule, } from '../../../../guards' import { CaseController } from '../../case.controller' @@ -19,9 +20,10 @@ describe('CaseController - Get case files record pdf rules', () => { }) it('should give permission to roles', () => { - expect(rules).toHaveLength(5) + expect(rules).toHaveLength(6) expect(rules).toContain(prosecutorRule) expect(rules).toContain(prosecutorRepresentativeRule) + expect(rules).toContain(publicProsecutorStaffRule) expect(rules).toContain(districtCourtJudgeRule) expect(rules).toContain(districtCourtRegistrarRule) expect(rules).toContain(districtCourtAssistantRule) diff --git a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getIndictmentPdfRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getIndictmentPdfRolesRules.spec.ts index 170857ab4dcae..6fee0d26b903f 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getIndictmentPdfRolesRules.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getIndictmentPdfRolesRules.spec.ts @@ -4,6 +4,7 @@ import { districtCourtRegistrarRule, prosecutorRepresentativeRule, prosecutorRule, + publicProsecutorStaffRule, } from '../../../../guards' import { CaseController } from '../../case.controller' @@ -19,9 +20,10 @@ describe('CaseController - Get indictment pdf rules', () => { }) it('should give permission to roles', () => { - expect(rules).toHaveLength(5) + expect(rules).toHaveLength(6) expect(rules).toContain(prosecutorRule) expect(rules).toContain(prosecutorRepresentativeRule) + expect(rules).toContain(publicProsecutorStaffRule) expect(rules).toContain(districtCourtJudgeRule) expect(rules).toContain(districtCourtRegistrarRule) expect(rules).toContain(districtCourtAssistantRule) diff --git a/apps/judicial-system/backend/src/app/modules/file/file.controller.ts b/apps/judicial-system/backend/src/app/modules/file/file.controller.ts index f507d84863e28..340d782f767bf 100644 --- a/apps/judicial-system/backend/src/app/modules/file/file.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/file/file.controller.ts @@ -37,6 +37,7 @@ import { prisonSystemStaffRule, prosecutorRepresentativeRule, prosecutorRule, + publicProsecutorStaffRule, } from '../../guards' import { Case, @@ -133,6 +134,7 @@ export class FileController { @RolesRules( prosecutorRule, prosecutorRepresentativeRule, + publicProsecutorStaffRule, districtCourtJudgeRule, districtCourtRegistrarRule, districtCourtAssistantRule, diff --git a/apps/judicial-system/backend/src/app/modules/file/guards/caseFileCategory.ts b/apps/judicial-system/backend/src/app/modules/file/guards/caseFileCategory.ts index 5455ad7976b08..2d8d88353f358 100644 --- a/apps/judicial-system/backend/src/app/modules/file/guards/caseFileCategory.ts +++ b/apps/judicial-system/backend/src/app/modules/file/guards/caseFileCategory.ts @@ -22,3 +22,5 @@ export const defenderCaseFileCategoriesForIndictmentCases = [ CaseFileCategory.PROSECUTOR_CASE_FILE, CaseFileCategory.DEFENDANT_CASE_FILE, ] + +export const prisonAdminCaseFileCategories = [CaseFileCategory.APPEAL_RULING] diff --git a/apps/judicial-system/backend/src/app/modules/file/guards/limitedAccessViewCaseFile.guard.ts b/apps/judicial-system/backend/src/app/modules/file/guards/limitedAccessViewCaseFile.guard.ts index 3526675d69026..a8c2f8295ea7f 100644 --- a/apps/judicial-system/backend/src/app/modules/file/guards/limitedAccessViewCaseFile.guard.ts +++ b/apps/judicial-system/backend/src/app/modules/file/guards/limitedAccessViewCaseFile.guard.ts @@ -7,11 +7,10 @@ import { } from '@nestjs/common' import { - CaseFileCategory, isCompletedCase, isDefenceUser, isIndictmentCase, - isPrisonSystemUser, + isPrisonAdminUser, isRequestCase, User, } from '@island.is/judicial-system/types' @@ -21,6 +20,7 @@ import { CaseFile } from '../models/file.model' import { defenderCaseFileCategoriesForIndictmentCases, defenderCaseFileCategoriesForRestrictionAndInvestigationCases, + prisonAdminCaseFileCategories, } from './caseFileCategory' @Injectable() @@ -65,14 +65,13 @@ export class LimitedAccessViewCaseFileGuard implements CanActivate { } } - if (isPrisonSystemUser(user)) { - if ( - isCompletedCase(theCase.state) && - caseFile.category && - caseFile.category === CaseFileCategory.APPEAL_RULING - ) { - return true - } + if ( + caseFile.category && + isCompletedCase(theCase.state) && + isPrisonAdminUser(user) && + prisonAdminCaseFileCategories.includes(caseFile.category) + ) { + return true } throw new ForbiddenException(`Forbidden for ${user.role}`) diff --git a/apps/judicial-system/backend/src/app/modules/file/guards/test/limitedAccessViewCaseFileGuard.spec.ts b/apps/judicial-system/backend/src/app/modules/file/guards/test/limitedAccessViewCaseFileGuard.spec.ts index dd31ac1d78161..e4e7672dc2d6d 100644 --- a/apps/judicial-system/backend/src/app/modules/file/guards/test/limitedAccessViewCaseFileGuard.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/guards/test/limitedAccessViewCaseFileGuard.spec.ts @@ -229,27 +229,19 @@ describe('Limited Access View Case File Guard', () => { describe.each(allowedCaseFileCategories)( 'prison system users can view %s', (category) => { - let thenPrison: Then let thenPrisonAdmin: Then beforeEach(() => { - mockRequest.mockImplementationOnce(() => ({ - user: prisonUser, - case: { type, state }, - caseFile: { category }, - })) mockRequest.mockImplementationOnce(() => ({ user: prisonAdminUser, case: { type, state }, caseFile: { category }, })) - thenPrison = givenWhenThen() thenPrisonAdmin = givenWhenThen() }) it('should activate', () => { - expect(thenPrison.result).toBe(true) expect(thenPrisonAdmin.result).toBe(true) }) }, diff --git a/apps/judicial-system/backend/src/app/modules/file/guards/test/viewCaseFileGuard.spec.ts b/apps/judicial-system/backend/src/app/modules/file/guards/test/viewCaseFileGuard.spec.ts index d27014f8a5d10..e29e254f3c2d4 100644 --- a/apps/judicial-system/backend/src/app/modules/file/guards/test/viewCaseFileGuard.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/guards/test/viewCaseFileGuard.spec.ts @@ -12,6 +12,7 @@ import { districtCourtRoles, InstitutionType, prosecutionRoles, + publicProsecutorRoles, User, UserRole, } from '@island.is/judicial-system/types' @@ -210,6 +211,59 @@ describe('View Case File Guard', () => { }) }) + describe.each(publicProsecutorRoles)('role %s', (role) => { + describe.each(completedCaseStates)('%s cases', (state) => { + let then: Then + + beforeEach(() => { + mockRequest.mockImplementationOnce(() => ({ + user: { + role, + institution: { + type: InstitutionType.PROSECUTORS_OFFICE, + id: '8f9e2f6d-6a00-4a5e-b39b-95fd110d762e', + }, + }, + case: { state }, + })) + + then = givenWhenThen() + }) + + it('should activate', () => { + expect(then.result).toBe(true) + }) + }) + + describe.each( + Object.values(CaseState).filter( + (state) => !completedCaseStates.includes(state), + ), + )('%s cases', (state) => { + let then: Then + + beforeEach(() => { + mockRequest.mockImplementationOnce(() => ({ + user: { + role, + institution: { + type: InstitutionType.PROSECUTORS_OFFICE, + id: '8f9e2f6d-6a00-4a5e-b39b-95fd110d762e', + }, + }, + case: { state }, + })) + + then = givenWhenThen() + }) + + it('should throw ForbiddenException', () => { + expect(then.error).toBeInstanceOf(ForbiddenException) + expect(then.error.message).toBe(`Forbidden for ${role}`) + }) + }) + }) + describe.each(Object.keys(CaseState))('in state %s', (state) => { describe.each( Object.keys(UserRole).filter( diff --git a/apps/judicial-system/backend/src/app/modules/file/guards/viewCaseFile.guard.ts b/apps/judicial-system/backend/src/app/modules/file/guards/viewCaseFile.guard.ts index 466cfb67357ed..dd00dfcdcbaad 100644 --- a/apps/judicial-system/backend/src/app/modules/file/guards/viewCaseFile.guard.ts +++ b/apps/judicial-system/backend/src/app/modules/file/guards/viewCaseFile.guard.ts @@ -14,6 +14,7 @@ import { isDistrictCourtUser, isPrisonSystemUser, isProsecutionUser, + isPublicProsecutorUser, User, } from '@island.is/judicial-system/types' @@ -44,6 +45,10 @@ export class ViewCaseFileGuard implements CanActivate { return true } + if (isPublicProsecutorUser(user) && isCompletedCase(theCase.state)) { + return true + } + if ( isDistrictCourtUser(user) && ([CaseState.SUBMITTED, CaseState.RECEIVED].includes(theCase.state) || diff --git a/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrlRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrlRolesRules.spec.ts index dd24bcd6d2651..86d57470734e6 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrlRolesRules.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrlRolesRules.spec.ts @@ -8,6 +8,7 @@ import { prisonSystemStaffRule, prosecutorRepresentativeRule, prosecutorRule, + publicProsecutorStaffRule, } from '../../../../guards' import { FileController } from '../../file.controller' @@ -23,9 +24,10 @@ describe('FileController - Get case file signed url rules', () => { }) it('should give permission to roles', () => { - expect(rules).toHaveLength(9) + expect(rules).toHaveLength(10) expect(rules).toContain(prosecutorRule) expect(rules).toContain(prosecutorRepresentativeRule) + expect(rules).toContain(publicProsecutorStaffRule) expect(rules).toContain(districtCourtJudgeRule) expect(rules).toContain(districtCourtRegistrarRule) expect(rules).toContain(districtCourtAssistantRule) diff --git a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/createPresignedPostGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/createPresignedPostGuards.spec.ts index 310e6b9f70c2a..fcb2e12d9efd9 100644 --- a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/createPresignedPostGuards.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/createPresignedPostGuards.spec.ts @@ -4,11 +4,7 @@ import { restrictionCases, } from '@island.is/judicial-system/types' -import { - CaseCompletedGuard, - CaseTypeGuard, - CaseWriteGuard, -} from '../../../case' +import { CaseTypeGuard, CaseWriteGuard } from '../../../case' import { LimitedAccessFileController } from '../../limitedAccessFile.controller' describe('LimitedAccessFileController - Create presigned post guards', () => { diff --git a/apps/judicial-system/backend/src/app/modules/institution/institution.model.ts b/apps/judicial-system/backend/src/app/modules/institution/institution.model.ts index be40cbdadab27..202c29f3f76da 100644 --- a/apps/judicial-system/backend/src/app/modules/institution/institution.model.ts +++ b/apps/judicial-system/backend/src/app/modules/institution/institution.model.ts @@ -62,4 +62,8 @@ export class Institution extends Model { @Column({ type: DataType.STRING, allowNull: true }) @ApiPropertyOptional({ type: String }) nationalId?: string + + @Column({ type: DataType.STRING, allowNull: true }) + @ApiPropertyOptional({ type: String }) + address?: string } diff --git a/apps/judicial-system/backend/src/app/modules/notification/internalNotification.service.ts b/apps/judicial-system/backend/src/app/modules/notification/internalNotification.service.ts index 2c6aa5278d6d1..ebce85337c80b 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/internalNotification.service.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/internalNotification.service.ts @@ -73,10 +73,9 @@ import { } from '../../formatters' import { notifications } from '../../messages' import { type Case, DateLog } from '../case' -import { ExplanatoryComment } from '../case/models/explanatoryComment.model' import { CourtService } from '../court' import { type Defendant, DefendantService } from '../defendant' -import { CaseEvent, EventService } from '../event' +import { EventService } from '../event' import { DeliverResponse } from './models/deliver.response' import { Notification, Recipient } from './models/notification.model' import { BaseNotificationService } from './baseNotification.service' diff --git a/apps/judicial-system/backend/src/app/modules/notification/notification.service.ts b/apps/judicial-system/backend/src/app/modules/notification/notification.service.ts index 8a9e78671c958..8103b1106f145 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/notification.service.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/notification.service.ts @@ -11,7 +11,7 @@ import { type User } from '@island.is/judicial-system/types' import { CaseState, NotificationType } from '@island.is/judicial-system/types' import { type Case } from '../case' -import { CaseEvent, EventService } from '../event' +import { EventService } from '../event' import { SendNotificationResponse } from './models/sendNotification.response' @Injectable() diff --git a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.spec.tsx b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.spec.tsx index fdb75baa1555c..65ec62d224eda 100644 --- a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.spec.tsx +++ b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.spec.tsx @@ -1,7 +1,6 @@ import { render, screen } from '@testing-library/react' import { - CaseDecision, CaseFileCategory, CaseType, } from '@island.is/judicial-system-web/src/graphql/schema' diff --git a/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.tsx b/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.tsx index 9908cf73787a6..c554555f7593e 100644 --- a/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.tsx +++ b/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.tsx @@ -5,7 +5,6 @@ import { useRouter } from 'next/router' import { Box, Option, Select, Text } from '@island.is/island-ui/core' import * as constants from '@island.is/judicial-system/consts' import { formatDate } from '@island.is/judicial-system/formatters' -import { isCompletedCase } from '@island.is/judicial-system/types' import { core, titles } from '@island.is/judicial-system-web/messages' import { BlueBox, diff --git a/apps/native/app/android/link-assets-manifest.json b/apps/native/app/android/link-assets-manifest.json index d9434d82b38ce..96ae18202a1be 100644 --- a/apps/native/app/android/link-assets-manifest.json +++ b/apps/native/app/android/link-assets-manifest.json @@ -38,7 +38,7 @@ "sha1": "8bf01afac8fc3e072eb36667374393b8566a044d" }, { - "path": "assets/fonts/IBMPlexSans-Regular.ttf", + "path": "assets/fonts/IBMPlexSans.ttf", "sha1": "ad38f2d6870ca73533f36bdb958cd8083a49c1a8" }, { diff --git a/apps/native/app/ios/IslandApp/Info.plist b/apps/native/app/ios/IslandApp/Info.plist index b074b56c5c003..24d36d07c197a 100644 --- a/apps/native/app/ios/IslandApp/Info.plist +++ b/apps/native/app/ios/IslandApp/Info.plist @@ -55,7 +55,7 @@ Used for license scanning capabilities UIAppFonts - IBMPlexSans-Regular.ttf + IBMPlexSans.ttf IBMPlexSans-Italic.ttf IBMPlexSans-Bold.ttf IBMPlexSans-BoldItalic.ttf diff --git a/apps/native/app/ios/link-assets-manifest.json b/apps/native/app/ios/link-assets-manifest.json index d9434d82b38ce..96ae18202a1be 100644 --- a/apps/native/app/ios/link-assets-manifest.json +++ b/apps/native/app/ios/link-assets-manifest.json @@ -38,7 +38,7 @@ "sha1": "8bf01afac8fc3e072eb36667374393b8566a044d" }, { - "path": "assets/fonts/IBMPlexSans-Regular.ttf", + "path": "assets/fonts/IBMPlexSans.ttf", "sha1": "ad38f2d6870ca73533f36bdb958cd8083a49c1a8" }, { diff --git a/apps/native/app/src/graphql/client.ts b/apps/native/app/src/graphql/client.ts index 7ac9267a0ae0a..fbac7b3719a10 100644 --- a/apps/native/app/src/graphql/client.ts +++ b/apps/native/app/src/graphql/client.ts @@ -159,6 +159,9 @@ const cache = new InMemoryCache({ userNotifications: { merge: true, }, + getUserProfile: { + merge: true, + }, }, }, DocumentV2: { diff --git a/apps/native/app/src/messages/en.ts b/apps/native/app/src/messages/en.ts index 389df9de9346d..57df2e066d8d7 100644 --- a/apps/native/app/src/messages/en.ts +++ b/apps/native/app/src/messages/en.ts @@ -490,8 +490,8 @@ export const en: TranslatedMessages = { 'edit.phone.inputlabel': 'Phone number', 'edit.phone.button': 'Save', 'edit.phone.button.empty': 'Save empty', - 'edit.phone.button.error': 'Error', - 'edit.phone.button.errorMessage': 'Could not send verification code', + 'edit.phone.error': 'Error', + 'edit.phone.errorMessage': 'Could not send verification code', // edit email 'edit.email.screenTitle': 'Edit Email', @@ -499,8 +499,8 @@ export const en: TranslatedMessages = { 'edit.email.inputlabel': 'Email', 'edit.email.button': 'Save', 'edit.email.button.empty': 'Save empty', - 'edit.email.button.error': 'Error', - 'edit.email.button.errorMessage': 'Could not send verification code', + 'edit.email.error': 'Error', + 'edit.email.errorMessage': 'Could not send verification code', // edit bank info 'edit.bankinfo.screenTitle': 'Edit Bank Info', @@ -510,6 +510,8 @@ export const en: TranslatedMessages = { 'edit.bankinfo.inputlabel.book': 'Hb.', 'edit.bankinfo.inputlabel.number': 'Account number', 'edit.bankinfo.button': 'Save', + 'edit.bankinfo.error': 'Error', + 'edit.bankinfo.errorMessage': 'Could not save bank info', // edit confirm 'edit.confirm.screenTitle': 'Confirm edit', @@ -523,6 +525,8 @@ export const en: TranslatedMessages = { 'edit.confirm.inputlabel': 'Security number', 'edit.cancel.button': 'Cancel', 'edit.confirm.button': 'Confirm', + 'edit.confirm.error': 'Error', + 'edit.confirm.errorMessage': 'Could not update information', // air discount 'airDiscount.screenTitle': 'Air discount scheme', diff --git a/apps/native/app/src/messages/is.ts b/apps/native/app/src/messages/is.ts index 502f6f0ebc577..3af0414f90f16 100644 --- a/apps/native/app/src/messages/is.ts +++ b/apps/native/app/src/messages/is.ts @@ -490,8 +490,8 @@ export const is = { 'edit.phone.inputlabel': 'Símanúmer', 'edit.phone.button': 'Vista', 'edit.phone.button.empty': 'Vista tómt', - 'edit.phone.button.error': 'Villa', - 'edit.phone.button.errorMessage': 'Gat ekki sent staðfestingarkóða', + 'edit.phone.error': 'Villa', + 'edit.phone.errorMessage': 'Gat ekki sent staðfestingarkóða', // edit email 'edit.email.screenTitle': 'Breyta Netfangi', @@ -499,8 +499,8 @@ export const is = { 'edit.email.inputlabel': 'Netfang', 'edit.email.button': 'Vista', 'edit.email.button.empty': 'Vista tómt', - 'edit.email.button.error': 'Villa', - 'edit.email.button.errorMessage': 'Gat ekki sent staðfestingarkóða', + 'edit.email.error': 'Villa', + 'edit.email.errorMessage': 'Gat ekki sent staðfestingarkóða', // edit bank info 'edit.bankinfo.screenTitle': 'Breyta banka upplýsingum', @@ -510,6 +510,8 @@ export const is = { 'edit.bankinfo.inputlabel.book': 'Hb.', 'edit.bankinfo.inputlabel.number': 'Reikningsnúmer', 'edit.bankinfo.button': 'Vista', + 'edit.bankinfo.error': 'Villa', + 'edit.bankinfo.errorMessage': 'Gat ekki vistað reikningsupplýsingar', // edit confirm 'edit.confirm.screenTitle': 'Staðfesta aðgerð', @@ -523,6 +525,8 @@ export const is = { 'edit.confirm.inputlabel': 'Öryggisnúmer', 'edit.cancel.button': 'Hætta við', 'edit.confirm.button': 'Staðfesta', + 'edit.confirm.error': 'Villa', + 'edit.confirm.errorMessage': 'Gat ekki uppfært upplýsingar', // air discount 'airDiscount.screenTitle': 'Loftbrú', diff --git a/apps/native/app/src/screens/home/home.tsx b/apps/native/app/src/screens/home/home.tsx index 40fa0f020189b..c8ec245d3cd5d 100644 --- a/apps/native/app/src/screens/home/home.tsx +++ b/apps/native/app/src/screens/home/home.tsx @@ -34,6 +34,7 @@ import { ApplicationsModule } from './applications-module' import { HelloModule } from './hello-module' import { InboxModule } from './inbox-module' import { OnboardingModule } from './onboarding-module' +import { usePreferencesStore } from '../../stores/preferences-store' interface ListItem { id: string @@ -99,6 +100,9 @@ export const MainHomeScreen: NavigationFunctionComponent = ({ useAndroidNotificationPermission() const syncToken = useNotificationsStore(({ syncToken }) => syncToken) const checkUnseen = useNotificationsStore(({ checkUnseen }) => checkUnseen) + const getAndSetLocale = usePreferencesStore( + ({ getAndSetLocale }) => getAndSetLocale, + ) const [refetching, setRefetching] = useState(false) const flatListRef = useRef(null) const ui = useUiStore() @@ -126,6 +130,8 @@ export const MainHomeScreen: NavigationFunctionComponent = ({ // Sync push tokens and unseen notifications syncToken() checkUnseen() + // Get user locale from server + getAndSetLocale() // Handle initial notification handleInitialNotification() diff --git a/apps/native/app/src/screens/inbox/inbox.tsx b/apps/native/app/src/screens/inbox/inbox.tsx index 6b97574ba2cc6..6356e3f4c6267 100644 --- a/apps/native/app/src/screens/inbox/inbox.tsx +++ b/apps/native/app/src/screens/inbox/inbox.tsx @@ -8,7 +8,6 @@ import { TopLine, InboxCard, } from '@ui' -import { setBadgeCountAsync } from 'expo-notifications' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useIntl } from 'react-intl' import { @@ -371,7 +370,6 @@ export const InboxScreen: NavigationFunctionComponent<{ badgeColor: theme.color.red400, }, }) - setBadgeCountAsync(unreadCount) }, [intl, theme, unreadCount]) const keyExtractor = useCallback((item: ListItem) => { diff --git a/apps/native/app/src/screens/settings/edit-bank-info.tsx b/apps/native/app/src/screens/settings/edit-bank-info.tsx index ea92a495db8e9..a6de5f0f03e85 100644 --- a/apps/native/app/src/screens/settings/edit-bank-info.tsx +++ b/apps/native/app/src/screens/settings/edit-bank-info.tsx @@ -119,7 +119,7 @@ export const EditBankInfoScreen: NavigationFunctionComponent = ({ }) if (!res.data) { - throw new Error('Faild to update') + throw new Error('Failed to update') } Navigation.dismissModal(componentId) @@ -128,7 +128,10 @@ export const EditBankInfoScreen: NavigationFunctionComponent = ({ throw new Error('Failed to update') } } catch (e) { - Alert.alert('Villa', 'Gat ekki vistað reikningsupplýsingar') + Alert.alert( + intl.formatMessage({ id: 'edit.bankinfo.error' }), + intl.formatMessage({ id: 'edit.bankinfo.errorMessage' }), + ) } }} /> diff --git a/apps/native/app/src/screens/settings/edit-confirm.tsx b/apps/native/app/src/screens/settings/edit-confirm.tsx index a7e3c22a3527a..c624d83e259ab 100644 --- a/apps/native/app/src/screens/settings/edit-confirm.tsx +++ b/apps/native/app/src/screens/settings/edit-confirm.tsx @@ -53,10 +53,13 @@ export const EditConfirmScreen: NavigationFunctionComponent = ({ Navigation.dismissModal(parentComponentId) } } else { - throw new Error('Failed') + throw new Error('Failed to update profile') } } catch (e) { - Alert.alert('Villa', 'Gat ekki uppfært upplýsingar') + Alert.alert( + intl.formatMessage({ id: 'edit.confirm.error' }), + intl.formatMessage({ id: 'edit.confirm.errorMessage' }), + ) } setLoading(false) } diff --git a/apps/native/app/src/screens/settings/edit-email.tsx b/apps/native/app/src/screens/settings/edit-email.tsx index b2d5a0df0020e..12994c6d11f9e 100644 --- a/apps/native/app/src/screens/settings/edit-email.tsx +++ b/apps/native/app/src/screens/settings/edit-email.tsx @@ -100,7 +100,6 @@ export const EditEmailScreen: NavigationFunctionComponent<{ }) if (res.data) { Navigation.dismissModal(componentId) - console.log(res.data, 'Uppfærði tómt netfang') } else { throw new Error('Failed to delete email') } diff --git a/apps/native/app/src/screens/settings/settings.tsx b/apps/native/app/src/screens/settings/settings.tsx index 5b54f1980738f..5b4bf194d1aee 100644 --- a/apps/native/app/src/screens/settings/settings.tsx +++ b/apps/native/app/src/screens/settings/settings.tsx @@ -14,7 +14,6 @@ import { Image, Linking, Platform, - Pressable, ScrollView, Switch, TouchableOpacity, @@ -151,6 +150,8 @@ export const SettingsScreen: NavigationFunctionComponent = ({ }).then(({ selectedItem }: any) => { if (selectedItem) { setLocale(selectedItem.id) + const locale = selectedItem.id === 'is-IS' ? 'is' : 'en' + updateLocale(locale) } }) } @@ -166,7 +167,7 @@ export const SettingsScreen: NavigationFunctionComponent = ({ }, 330) }, []) - function updateDocumentNotifications(value: boolean) { + const updateDocumentNotifications = (value: boolean) => { client .mutate({ mutation: UpdateProfileDocument, @@ -198,7 +199,7 @@ export const SettingsScreen: NavigationFunctionComponent = ({ }) } - function updateEmailNotifications(value: boolean) { + const updateEmailNotifications = (value: boolean) => { client .mutate({ mutation: UpdateProfileDocument, @@ -230,6 +231,30 @@ export const SettingsScreen: NavigationFunctionComponent = ({ }) } + const updateLocale = (value: string) => { + client + .mutate({ + mutation: UpdateProfileDocument, + update(cache, { data }) { + cache.modify({ + fields: { + getUserProfile: (existing) => { + return { ...existing, ...data?.updateProfile } + }, + }, + }) + }, + variables: { + input: { + locale: value, + }, + }, + }) + .catch(() => { + // noop + }) + } + useEffect(() => { if (userProfile) { setDocumentNotifications( diff --git a/apps/native/app/src/stores/notifications-store.ts b/apps/native/app/src/stores/notifications-store.ts index 768a537b1ab68..37c673adca2ee 100644 --- a/apps/native/app/src/stores/notifications-store.ts +++ b/apps/native/app/src/stores/notifications-store.ts @@ -18,6 +18,7 @@ import { } from '../graphql/types/schema' import { ComponentRegistry } from '../utils/component-registry' import { getRightButtons } from '../utils/get-main-root' +import { setBadgeCountAsync } from 'expo-notifications' export interface Notification { id: string @@ -114,6 +115,7 @@ export const notificationsStore = create( }, updateNavigationUnseenCount(unseenCount: number) { set({ unseenCount }) + setBadgeCountAsync(unseenCount) Navigation.mergeOptions(ComponentRegistry.HomeScreen, { topBar: { diff --git a/apps/native/app/src/stores/preferences-store.ts b/apps/native/app/src/stores/preferences-store.ts index a456d69faec2a..c44a4d945850e 100644 --- a/apps/native/app/src/stores/preferences-store.ts +++ b/apps/native/app/src/stores/preferences-store.ts @@ -5,6 +5,12 @@ import { persist } from 'zustand/middleware' import create, { State } from 'zustand/vanilla' import { getDefaultOptions } from '../utils/get-default-options' import { getThemeWithPreferences } from '../utils/get-theme-with-preferences' +import { getApolloClientAsync } from '../graphql/client' +import { + GetProfileDocument, + GetProfileQuery, + GetProfileQueryVariables, +} from '../graphql/types/schema' export type Locale = 'en-US' | 'is-IS' | 'en-IS' | 'is-US' export type ThemeMode = 'dark' | 'light' | 'efficient' @@ -29,6 +35,7 @@ export interface PreferencesStore extends State { appearanceMode: AppearanceMode appLockTimeout: number setLocale(locale: Locale): void + getAndSetLocale(): void setAppearanceMode(appearanceMode: AppearanceMode): void setUseBiometrics(useBiometrics: boolean): void dismiss(key: string, value?: boolean): void @@ -61,6 +68,24 @@ export const preferencesStore = create( persist( (set, get) => ({ ...(defaultPreferences as PreferencesStore), + async getAndSetLocale() { + const client = await getApolloClientAsync() + + try { + const res = await client.query< + GetProfileQuery, + GetProfileQueryVariables + >({ + query: GetProfileDocument, + }) + + const locale = res.data?.getUserProfile?.locale + const appLocale = locale === 'en' ? 'en-US' : 'is-IS' + set({ locale: appLocale }) + } catch (err) { + // noop + } + }, setLocale(locale: Locale) { if (!availableLocales.includes(locale)) { throw new Error('Not supported locale') diff --git a/apps/native/app/src/ui/lib/accordion/accordion-item.tsx b/apps/native/app/src/ui/lib/accordion/accordion-item.tsx index a2792cdaa45fe..4305671910e28 100644 --- a/apps/native/app/src/ui/lib/accordion/accordion-item.tsx +++ b/apps/native/app/src/ui/lib/accordion/accordion-item.tsx @@ -98,7 +98,7 @@ export function AccordionItem({ > {icon && {icon}} - {title} + {title} { return ( - + {label} diff --git a/apps/native/app/src/ui/lib/list/list-item.tsx b/apps/native/app/src/ui/lib/list/list-item.tsx index 8f77cd93bc131..5454e11729949 100644 --- a/apps/native/app/src/ui/lib/list/list-item.tsx +++ b/apps/native/app/src/ui/lib/list/list-item.tsx @@ -117,7 +117,6 @@ export function ListItem({ variant="body3" numberOfLines={1} ellipsizeMode="tail" - style={{ fontWeight: '300' }} > {title} diff --git a/apps/services/auth/delegation-api/infra/delegation-api.ts b/apps/services/auth/delegation-api/infra/delegation-api.ts index e68fc0951175f..1ceff205f6d3b 100644 --- a/apps/services/auth/delegation-api/infra/delegation-api.ts +++ b/apps/services/auth/delegation-api/infra/delegation-api.ts @@ -90,5 +90,10 @@ export const serviceSetup = (services: { public: false, }, }) - .grantNamespaces('nginx-ingress-internal', 'islandis', 'service-portal') + .grantNamespaces( + 'nginx-ingress-internal', + 'islandis', + 'service-portal', + 'user-notification-worker', + ) } diff --git a/apps/services/regulations-admin-backend/src/main.ts b/apps/services/regulations-admin-backend/src/main.ts index 8d3c254eaaff3..b0f1f43fea556 100644 --- a/apps/services/regulations-admin-backend/src/main.ts +++ b/apps/services/regulations-admin-backend/src/main.ts @@ -7,4 +7,5 @@ bootstrap({ appModule: AppModule, name: 'regulations-admin-backend', openApi, + jsonBodyLimit: '300kb', }) diff --git a/apps/web/components/Organization/MarkdownText/MarkdownText.tsx b/apps/web/components/Organization/MarkdownText/MarkdownText.tsx index d2266de2df6cf..a28544a9f807a 100644 --- a/apps/web/components/Organization/MarkdownText/MarkdownText.tsx +++ b/apps/web/components/Organization/MarkdownText/MarkdownText.tsx @@ -1,17 +1,29 @@ +import React from 'react' import Markdown from 'markdown-to-jsx' + import { Bullet, BulletList, Text, TextProps } from '@island.is/island-ui/core' -import React from 'react' + import * as styles from './MarkdownText.css' interface MarkdownTextProps { children: string color?: TextProps['color'] variant?: TextProps['variant'] + replaceNewLinesWithBreaks?: boolean } export const MarkdownText: React.FC< React.PropsWithChildren -> = ({ children, color = null, variant = 'default' }) => { +> = ({ + children, + color = null, + variant = 'default', + replaceNewLinesWithBreaks = true, +}) => { + const processedChildren = replaceNewLinesWithBreaks + ? (children as string).replace(/\n/gi, '
') + : children + return (
- {(children as string).replace(/\n/gi, '
')} + {processedChildren}
) diff --git a/apps/web/components/connected/ParentalLeaveCalculator/ParentalLeaveCalculator.css.ts b/apps/web/components/connected/ParentalLeaveCalculator/ParentalLeaveCalculator.css.ts new file mode 100644 index 0000000000000..e7006fe6fb859 --- /dev/null +++ b/apps/web/components/connected/ParentalLeaveCalculator/ParentalLeaveCalculator.css.ts @@ -0,0 +1,7 @@ +import { style } from '@vanilla-extract/css' + +import { theme } from '@island.is/island-ui/theme' + +export const resultBorder = style({ + border: `1px dashed ${theme.color.blue300}`, +}) diff --git a/apps/web/components/connected/ParentalLeaveCalculator/ParentalLeaveCalculator.tsx b/apps/web/components/connected/ParentalLeaveCalculator/ParentalLeaveCalculator.tsx new file mode 100644 index 0000000000000..9132ba9f85f68 --- /dev/null +++ b/apps/web/components/connected/ParentalLeaveCalculator/ParentalLeaveCalculator.tsx @@ -0,0 +1,1089 @@ +import { type PropsWithChildren, useMemo, useRef, useState } from 'react' +import { useIntl } from 'react-intl' +import NumberFormat from 'react-number-format' +import { + parseAsInteger, + parseAsString, + parseAsStringEnum, + useQueryState, +} from 'next-usequerystate' +import { z } from 'zod' + +import { + AlertMessage, + Box, + Button, + GridColumn, + GridRow, + Inline, + Input, + type Option, + RadioButton, + Select, + Stack, + Table, + Text, + Tooltip, +} from '@island.is/island-ui/core' +import { sortAlpha } from '@island.is/shared/utils' +import type { ConnectedComponent } from '@island.is/web/graphql/schema' +import { formatCurrency as formatCurrencyUtil } from '@island.is/web/utils/currency' + +import { MarkdownText } from '../../Organization' +import { translations as t } from './translations.strings' +import * as styles from './ParentalLeaveCalculator.css' + +interface FieldProps { + heading: string + headingTooltip?: string + description?: string + tooltip?: string +} + +const Field = ({ + heading, + headingTooltip, + description, + tooltip, + children, +}: PropsWithChildren) => { + return ( + + + {heading} + {headingTooltip && } + + {description && ( + + {description} + {tooltip && } + + )} + {children} + + ) +} + +enum Status { + PARENTAL_LEAVE = 'parentalLeave', + STUDENT = 'student', + OUTSIDE_WORKFORCE = 'outsideWorkForce', +} + +enum WorkPercentage { + OPTION_1 = 'option1', + OPTION_2 = 'option2', +} + +enum ParentalLeavePeriod { + MONTH = 'month', + THREE_WEEKS = 'threeWeeks', + TWO_WEEKS = 'twoWeeks', +} + +enum Screen { + FORM = 'form', + RESULTS = 'results', +} + +enum LegalDomicileInIceland { + YES = 'y', + NO = 'n', +} + +interface ParentalLeaveCalculatorProps { + slice: ConnectedComponent +} + +interface ScreenProps extends ParentalLeaveCalculatorProps { + changeScreen: () => void +} + +const FormScreen = ({ slice, changeScreen }: ScreenProps) => { + const { formatMessage } = useIntl() + + const statusOptions = useMemo[]>(() => { + return [ + { + label: formatMessage(t.status.parentalLeaveOption), + value: Status.PARENTAL_LEAVE, + }, + { + label: formatMessage(t.status.studentOption), + value: Status.STUDENT, + }, + { + label: formatMessage(t.status.outsideWorkforceOption), + value: Status.OUTSIDE_WORKFORCE, + }, + ] + }, [formatMessage]) + + const yearOptions = useMemo[]>(() => { + const keys = Object.keys(slice.configJson?.yearConfig || {}).map(Number) + keys.sort() + return keys.map((key) => ({ + label: String(key), + value: key, + })) + }, [slice.configJson?.yearConfig]) + + const additionalPensionFundingOptions = useMemo< + Option[] + >(() => { + const options: number[] = slice.configJson + ?.additionalPensionFundingOptions ?? [1, 2, 3, 4] + + return [ + { value: null, label: formatMessage(t.additionalPensionFunding.none) }, + ...options.map((option) => ({ + label: `${option} ${formatMessage( + t.additionalPensionFunding.optionSuffix, + )}`, + value: option, + })), + ] + }, [formatMessage, slice.configJson?.additionalPensionFundingOptions]) + + const unionOptions = useMemo[]>(() => { + const options: { label: string; percentage: number }[] = slice.configJson + ?.unionOptions + ? [...slice.configJson.unionOptions] + : [] + + options.sort(sortAlpha('label')) + + return [ + { + value: null, + label: formatMessage(t.union.none), + }, + ...options.map((option) => ({ + label: option.label, + value: option.label, + })), + ] + }, [formatMessage, slice.configJson?.unionOptions]) + + const parentalLeavePeriodOptions = useMemo[]>(() => { + return [ + { + label: formatMessage(t.parentalLeavePeriod.monthOption), + value: ParentalLeavePeriod.MONTH, + }, + { + label: formatMessage(t.parentalLeavePeriod.threeWeeksOption), + value: ParentalLeavePeriod.THREE_WEEKS, + }, + { + label: formatMessage(t.parentalLeavePeriod.twoWeeksOption), + value: ParentalLeavePeriod.TWO_WEEKS, + }, + ] + }, [formatMessage]) + + const [status, setStatus] = useQueryState( + 'status', + parseAsStringEnum(Object.values(Status)).withDefault(Status.PARENTAL_LEAVE), + ) + const [birthyear, setBirthyear] = useQueryState('birthyear', parseAsInteger) + const [workPercentage, setWorkPercentage] = useQueryState( + 'workPercentage', + parseAsStringEnum(Object.values(WorkPercentage)), + ) + const [income, setIncome] = useQueryState('income', parseAsInteger) + const [ + additionalPensionFundingPercentage, + setAdditionalPensionFundingPercentage, + ] = useQueryState('additionalPensionFunding', parseAsInteger) + const [union, setUnion] = useQueryState('union', parseAsString) + const [personalDiscount, setPersonalDiscount] = useQueryState( + 'personalDiscount', + parseAsInteger.withDefault(100), + ) + const [parentalLeavePeriod, setParentalLeavePeriod] = useQueryState( + 'parentalLeavePeriod', + parseAsStringEnum(Object.values(ParentalLeavePeriod)), + ) + const [parentalLeaveRatio, setParentalLeaveRatio] = useQueryState( + 'parentalLeaveRatio', + parseAsInteger.withDefault(100), + ) + const [legalDomicileInIceland, setLegalDomicileInIceland] = useQueryState( + 'legalDomicileInIceland', + parseAsStringEnum(Object.values(LegalDomicileInIceland)), + ) + + const canCalculate = () => { + let value = + Object.values(Status).includes(status) && + yearOptions.some((year) => year.value === birthyear) + + if (status === Status.OUTSIDE_WORKFORCE) { + value = value && legalDomicileInIceland === LegalDomicileInIceland.YES + } + + if (status === Status.PARENTAL_LEAVE) { + value = + value && + typeof income === 'number' && + income > 0 && + !!workPercentage && + Object.values(WorkPercentage).includes(workPercentage) && + parentalLeavePeriodOptions.some( + (option) => option.value === parentalLeavePeriod, + ) + } + + return value + } + + return ( + + + + { + setBirthyear(option?.value ?? null) + }} + value={yearOptions.find((option) => option.value === birthyear)} + label={formatMessage(t.childBirthYear.label)} + options={yearOptions} + /> + + + {status === Status.PARENTAL_LEAVE && ( + + + + { + setWorkPercentage(WorkPercentage.OPTION_1) + }} + checked={workPercentage === WorkPercentage.OPTION_1} + value={WorkPercentage.OPTION_1} + backgroundColor="white" + large={true} + label={formatMessage(t.workPercentage.option1)} + /> + + + { + setWorkPercentage(WorkPercentage.OPTION_2) + }} + checked={workPercentage === WorkPercentage.OPTION_2} + value={WorkPercentage.OPTION_2} + backgroundColor="white" + large={true} + label={formatMessage(t.workPercentage.option2)} + /> + + + + )} + + {status === Status.PARENTAL_LEAVE && ( + + { + setIncome(Number(value)) + }} + label={formatMessage(t.income.label)} + tooltip={formatMessage(t.income.tooltip)} + value={String(income || '')} + customInput={Input} + name="income" + id="income" + type="text" + inputMode="numeric" + thousandSeparator="." + decimalSeparator="," + suffix={formatMessage(t.income.inputSuffix)} + placeholder={formatMessage(t.income.inputPlaceholder)} + maxLength={ + formatMessage(t.income.inputSuffix).length + + (slice.configJson?.incomeInputMaxLength ?? 12) + } + /> + + )} + + {status === Status.PARENTAL_LEAVE && ( + + { + setUnion(option?.value ?? null) + }} + value={unionOptions.find((option) => option.value === union)} + label={formatMessage(t.union.label)} + options={unionOptions} + /> + + )} + + + { + setPersonalDiscount(Number(value)) + }} + label={formatMessage(t.personalDiscount.label)} + value={String(personalDiscount || '')} + customInput={Input} + name="personalDiscount" + id="personalDiscount" + type="text" + inputMode="numeric" + suffix={formatMessage(t.personalDiscount.suffix)} + placeholder={formatMessage(t.personalDiscount.placeholder)} + format={(value) => { + const maxPersonalDiscount = + slice.configJson?.maxPersonalDiscount ?? 100 + if (Number(value) > maxPersonalDiscount) { + value = String(maxPersonalDiscount) + } + return `${value}${formatMessage(t.personalDiscount.suffix)}` + }} + /> + + + {status === Status.PARENTAL_LEAVE && ( + + setComment(e.target.value)} /> - - + ) } diff --git a/libs/application/templates/official-journal-of-iceland/src/components/comments/CommentList.tsx b/libs/application/templates/official-journal-of-iceland/src/components/comments/CommentList.tsx index 8e8a855006857..7da90fa087602 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/comments/CommentList.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/comments/CommentList.tsx @@ -1,23 +1,29 @@ +import { SkeletonLoader } from '@island.is/island-ui/core' import type { Props as CommentProps } from './Comment' -import { Text } from '@island.is/island-ui/core' import { Comment } from './Comment' import * as styles from './Comments.css' -import { useLocale } from '@island.is/localization' -import { comments as messages } from '../../lib/messages/comments' +import { OJOI_INPUT_HEIGHT } from '../../lib/constants' type Props = { - comments: CommentProps[] + comments?: CommentProps[] + loading?: boolean } -export const CommentsList = ({ comments }: Props) => { - const { formatMessage: f } = useLocale() - if (!comments.length) { - return {f(messages.errors.emptyComments)} +export const CommentsList = ({ comments, loading }: Props) => { + if (loading) { + return ( + + ) } return (
    - {comments.map((comment, index) => ( + {comments?.map((comment, index) => ( ))}
diff --git a/libs/application/templates/official-journal-of-iceland/src/components/communicationChannels/AddChannel.tsx b/libs/application/templates/official-journal-of-iceland/src/components/communicationChannels/AddChannel.tsx index 09073114c40e3..5f5c7ecb3ebb6 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/communicationChannels/AddChannel.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/communicationChannels/AddChannel.tsx @@ -1,22 +1,53 @@ import { Box, Button, Input } from '@island.is/island-ui/core' import { useLocale } from '@island.is/localization' -import { useRef, useState } from 'react' +import { useEffect, useState } from 'react' import { general, publishing } from '../../lib/messages' import * as styles from './AddChannel.css' -import { Channel } from './Channel' import { FormGroup } from '../form/FormGroup' +import { useApplication } from '../../hooks/useUpdateApplication' +import set from 'lodash/set' +import { InputFields } from '../../lib/types' + type Props = { - onAdd: () => void - state: Channel - setState: React.Dispatch> + applicationId: string + defaultEmail?: string + defaultPhone?: string + defaultVisible?: boolean } -export const AddChannel = ({ onAdd, state, setState }: Props) => { +export const AddChannel = ({ + applicationId, + defaultEmail, + defaultPhone, + defaultVisible, +}: Props) => { + const { application, updateApplication } = useApplication({ + applicationId, + }) const { formatMessage: f } = useLocale() - const phoneRef = useRef(null) + const [email, setEmail] = useState('') + const [phone, setPhone] = useState('') + const [isVisible, setIsVisible] = useState(defaultVisible ?? false) + + useEffect(() => { + setEmail(defaultEmail ?? email) + setPhone(defaultPhone ?? phone) + setIsVisible(defaultVisible ?? false) + }, [defaultEmail, defaultPhone, defaultVisible]) + + const onAddChannel = () => { + const currentAnswers = structuredClone(application.answers) + const currentChannels = currentAnswers.advert?.channels ?? [] + const updatedAnswers = set(currentAnswers, InputFields.advert.channels, [ + ...currentChannels, + { email, phone }, + ]) - const [isVisible, setIsVisible] = useState(false) + updateApplication(updatedAnswers) + setEmail('') + setPhone('') + } return ( @@ -32,19 +63,18 @@ export const AddChannel = ({ onAdd, state, setState }: Props) => { size="xs" name="email" type="email" - value={state.email} + value={email} label={f(general.email)} - onChange={(e) => setState({ ...state, email: e.target.value })} + onChange={(e) => setEmail(e.target.value)} /> setState({ ...state, phone: e.target.value })} + onChange={(e) => setPhone(e.target.value)} /> @@ -54,12 +84,13 @@ export const AddChannel = ({ onAdd, state, setState }: Props) => { variant="ghost" onClick={() => { setIsVisible(!isVisible) - setState({ email: '', phone: '' }) + setEmail('') + setPhone('') }} > {f(general.cancel)} - diff --git a/libs/application/templates/official-journal-of-iceland/src/components/communicationChannels/Channel.tsx b/libs/application/templates/official-journal-of-iceland/src/components/communicationChannels/Channel.tsx deleted file mode 100644 index f87569b3e8666..0000000000000 --- a/libs/application/templates/official-journal-of-iceland/src/components/communicationChannels/Channel.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Icon, Table as T } from '@island.is/island-ui/core' - -export type Channel = { - email: string - phone: string -} - -type Props = { - channel: Channel - onEditChannel: (channel: Channel) => void - onRemoveChannel: (channel: Channel) => void -} - -export const Channel = ({ channel, onEditChannel, onRemoveChannel }: Props) => { - return ( - - {channel.email} - {channel.phone} - - - - - - - - ) -} diff --git a/libs/application/templates/official-journal-of-iceland/src/components/communicationChannels/ChannelList.tsx b/libs/application/templates/official-journal-of-iceland/src/components/communicationChannels/ChannelList.tsx index 09417e1c5519f..76c5be4ddf795 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/communicationChannels/ChannelList.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/communicationChannels/ChannelList.tsx @@ -1,21 +1,40 @@ -import { Table as T } from '@island.is/island-ui/core' -import { Channel } from './Channel' +import { Icon, Table as T } from '@island.is/island-ui/core' import { useLocale } from '@island.is/localization' import { general } from '../../lib/messages' +import { useApplication } from '../../hooks/useUpdateApplication' +import { InputFields } from '../../lib/types' +import set from 'lodash/set' type Props = { - channels: Channel[] - onEditChannel: (channel: Channel) => void - onRemoveChannel: (channel: Channel) => void + applicationId: string + onEditChannel: (email?: string, phone?: string) => void } -export const ChannelList = ({ - channels, - onEditChannel, - onRemoveChannel, -}: Props) => { +export const ChannelList = ({ applicationId, onEditChannel }: Props) => { const { formatMessage } = useLocale() - if (channels.length === 0) return null + + const { application, updateApplication } = useApplication({ + applicationId, + }) + + const channels = application.answers.advert?.channels || [] + + const onRemoveChannel = (email?: string) => { + const currentAnswers = structuredClone(application.answers) + const currentChannels = currentAnswers.advert?.channels ?? [] + + const updatedAnswers = set( + currentAnswers, + InputFields.advert.channels, + currentChannels.filter((channel) => channel.email !== email), + ) + + updateApplication(updatedAnswers) + } + + if (channels.length === 0) { + return null + } return ( @@ -29,12 +48,26 @@ export const ChannelList = ({ {channels.map((channel, i) => ( - + + {channel.email} + {channel.phone} + + + + + + + ))} diff --git a/libs/application/templates/official-journal-of-iceland/src/components/form/FormGroup.tsx b/libs/application/templates/official-journal-of-iceland/src/components/form/FormGroup.tsx index 7925a2fea4635..aaff6f3fcf8df 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/form/FormGroup.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/form/FormGroup.tsx @@ -1,22 +1,37 @@ import { Box, Text } from '@island.is/island-ui/core' import * as styles from './FormGroup.css' +import { MessageDescriptor } from 'react-intl' +import { useLocale } from '@island.is/localization' type Props = { - title?: string - intro?: string + title?: string | MessageDescriptor + intro?: string | MessageDescriptor children?: React.ReactNode } export const FormGroup = ({ title, intro, children }: Props) => { + const { formatMessage: f } = useLocale() + + const titleText = title + ? typeof title !== 'string' + ? f(title) + : title + : undefined + const introText = intro + ? typeof intro !== 'string' + ? f(intro) + : intro + : undefined + return ( {(title || intro) && ( - {title && ( - - {title} + {titleText && ( + + {titleText} )} - {intro && {intro}} + {introText && {introText}} )} {children} diff --git a/libs/application/templates/official-journal-of-iceland/src/components/htmlEditor/templates/signatures.ts b/libs/application/templates/official-journal-of-iceland/src/components/htmlEditor/templates/signatures.ts deleted file mode 100644 index b964b5c5dfe78..0000000000000 --- a/libs/application/templates/official-journal-of-iceland/src/components/htmlEditor/templates/signatures.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { HTMLText } from '@island.is/regulations-tools/types' -import { - CommitteeSignatureState, - RegularSignatureState, -} from '../../../lib/types' -import is from 'date-fns/locale/is' -import en from 'date-fns/locale/en-US' -import format from 'date-fns/format' - -type RegularSignatureTemplateParams = { - signatureGroups?: RegularSignatureState - additionalSignature?: string - locale?: 'is' | 'en' -} - -type CommitteeSignatureTemplateParams = { - signature?: CommitteeSignatureState - additionalSignature?: string - locale?: 'is' | 'en' -} - -const signatureTemplate = ( - name?: string, - after?: string, - above?: string, - below?: string, -) => { - if (!name) return '' - return ` -
- ${above ? `

${above}

` : ''} -
-

${name} - ${after ? `${after}` : ''} -

- ${below ? `

${below}

` : ''} -
-
- ` -} - -const additionalTemplate = (additional?: string) => { - if (!additional) return '' - - return `

${additional}

` -} - -const titleTemplate = (title?: string, date?: string, locale = 'is') => { - if (!title && !date) return '' - - return ` -

${title}${title && date ? ', ' : ''}${ - date - ? format(new Date(date), 'd. MMMM yyyy', { - locale: locale === 'is' ? is : en, - }) - : '' - }

` -} - -export const regularSignatureTemplate = ({ - signatureGroups, - additionalSignature, - locale = 'is', -}: RegularSignatureTemplateParams): HTMLText => { - const results = signatureGroups - ?.map((signatureGroup) => { - const className = - signatureGroup?.members?.length === 1 - ? 'signatures single' - : signatureGroup?.members?.length === 2 - ? 'signatures double' - : signatureGroup?.members?.length === 3 - ? 'signatures triple' - : 'signatures' - - return ` -
- ${titleTemplate( - signatureGroup.institution, - signatureGroup.date, - locale, - )} -
- ${signatureGroup?.members - ?.map((s) => signatureTemplate(s.name, s.after, s.above, s.below)) - .join('')} -
-
` - }) - - .join('') - - return additionalSignature - ? ((results + additionalTemplate(additionalSignature)) as HTMLText) - : (results as HTMLText) -} - -export const committeeSignatureTemplate = ({ - signature, - additionalSignature, - locale = 'is', -}: CommitteeSignatureTemplateParams): HTMLText => { - const className = - signature?.members?.length === 1 - ? 'signatures single' - : signature?.members?.length === 2 - ? 'signatures double' - : signature?.members?.length === 3 - ? 'signatures triple' - : 'signatures' - - const html = ` -
- ${titleTemplate(signature?.institution, signature?.date, locale)} -
- ${signatureTemplate( - signature?.chairman.name, - signature?.chairman.after, - signature?.chairman.above, - signature?.chairman.below, - )} -
-
- ${signature?.members - ?.map((s) => signatureTemplate(s.name, '', '', s.below)) - .join('')} -
-
- ` - - return additionalSignature - ? ((html + additionalTemplate(additionalSignature)) as HTMLText) - : (html as HTMLText) -} diff --git a/libs/application/templates/official-journal-of-iceland/src/components/input/OJOIDateController.tsx b/libs/application/templates/official-journal-of-iceland/src/components/input/OJOIDateController.tsx new file mode 100644 index 0000000000000..4e75c4a5c70e5 --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/components/input/OJOIDateController.tsx @@ -0,0 +1,100 @@ +import { SkeletonLoader } from '@island.is/island-ui/core' +import { useLocale } from '@island.is/localization' +import { DatePickerController } from '@island.is/shared/form-fields' +import { MessageDescriptor } from 'react-intl' +import { OJOI_INPUT_HEIGHT } from '../../lib/constants' +import { useApplication } from '../../hooks/useUpdateApplication' +import { useFormContext } from 'react-hook-form' +import set from 'lodash/set' + +type Props = { + name: string + label: string | MessageDescriptor + placeholder: string | MessageDescriptor + defaultValue?: string + loading?: boolean + applicationId: string + disabled?: boolean + excludeDates?: Date[] + minDate?: Date + maxDate?: Date + onChange?: (value: string) => void +} + +export const OJOIDateController = ({ + name, + label, + placeholder, + defaultValue, + loading, + applicationId, + disabled, + excludeDates, + minDate, + maxDate, + onChange, +}: Props) => { + const { formatMessage: f } = useLocale() + const { + debouncedOnUpdateApplicationHandler, + application, + updateApplication, + } = useApplication({ + applicationId, + }) + + const { setValue } = useFormContext() + + if (loading) { + return ( + + ) + } + + // if defaultValue is passed and there is no value set we must set it in the application state + if (defaultValue && !application.answers.advert?.requestedDate) { + setValue(name, defaultValue) + const currentAnswers = structuredClone(application.answers) + const updatedAnswers = set(currentAnswers, name, defaultValue) + updateApplication(updatedAnswers) + } + + const placeholderText = + typeof placeholder === 'string' ? placeholder : f(placeholder) + + const labelText = typeof label === 'string' ? label : f(label) + + const handleChange = (value: string) => { + const currentAnswers = structuredClone(application.answers) + const newAnswers = set(currentAnswers, name, value) + + return newAnswers + } + + return ( + + debouncedOnUpdateApplicationHandler( + handleChange(e), + onChange && (() => onChange(e)), + ) + } + /> + ) +} diff --git a/libs/application/templates/official-journal-of-iceland/src/components/input/OJOIHtmlController.tsx b/libs/application/templates/official-journal-of-iceland/src/components/input/OJOIHtmlController.tsx new file mode 100644 index 0000000000000..d3426936f725a --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/components/input/OJOIHtmlController.tsx @@ -0,0 +1,72 @@ +import { HTMLText } from '@island.is/regulations' +import { Editor, EditorFileUploader } from '@island.is/regulations-tools/Editor' +import { useCallback, useEffect, useRef } from 'react' +import { classes, editorWrapper } from '../htmlEditor/HTMLEditor.css' +import { baseConfig } from '../htmlEditor/config/baseConfig' +import { Box } from '@island.is/island-ui/core' +import { useApplication } from '../../hooks/useUpdateApplication' +import set from 'lodash/set' + +type Props = { + applicationId: string + name: string + defaultValue?: string + onChange?: (value: HTMLText) => void + editorKey?: string +} + +export const OJOIHtmlController = ({ + applicationId, + name, + onChange, + defaultValue, + editorKey, +}: Props) => { + const { debouncedOnUpdateApplicationHandler, application } = useApplication({ + applicationId, + }) + + const valueRef = useRef(() => + defaultValue ? (defaultValue as HTMLText) : ('' as HTMLText), + ) + + const fileUploader = (): EditorFileUploader => async (blob) => { + throw new Error('Not implemented') + } + + const handleChange = (value: HTMLText) => { + const currentAnswers = structuredClone(application.answers) + const newAnswers = set(currentAnswers, name, value) + + onChange && onChange(value) + return newAnswers + } + + const onChangeHandler = () => { + return handleChange(valueRef.current()) + } + + return ( + + { + // add little bit of delay for valueRef to update + setTimeout( + () => debouncedOnUpdateApplicationHandler(onChangeHandler()), + 100, + ) + }} + onBlur={() => debouncedOnUpdateApplicationHandler(onChangeHandler())} + /> + + ) +} diff --git a/libs/application/templates/official-journal-of-iceland/src/components/input/OJOIInputController.tsx b/libs/application/templates/official-journal-of-iceland/src/components/input/OJOIInputController.tsx new file mode 100644 index 0000000000000..54455a5558539 --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/components/input/OJOIInputController.tsx @@ -0,0 +1,82 @@ +import { SkeletonLoader } from '@island.is/island-ui/core' +import { useLocale } from '@island.is/localization' +import { InputController } from '@island.is/shared/form-fields' +import { MessageDescriptor } from 'react-intl' +import { OJOI_INPUT_HEIGHT } from '../../lib/constants' +import { useApplication } from '../../hooks/useUpdateApplication' +import set from 'lodash/set' + +type Props = { + name: string + label: string | MessageDescriptor + placeholder?: string | MessageDescriptor + defaultValue?: string + loading?: boolean + applicationId: string + disabled?: boolean + textarea?: boolean + onChange?: (value: string) => void +} + +export const OJOIInputController = ({ + name, + label, + placeholder, + defaultValue, + loading, + applicationId, + disabled, + textarea, + onChange, +}: Props) => { + const { formatMessage: f } = useLocale() + const { debouncedOnUpdateApplicationHandler, application } = useApplication({ + applicationId, + }) + + const placeholderText = placeholder + ? typeof placeholder === 'string' + ? placeholder + : f(placeholder) + : '' + + const labelText = typeof label === 'string' ? label : f(label) + + const handleChange = (value: string) => { + const currentAnswers = structuredClone(application.answers) + const newAnswers = set(currentAnswers, name, value) + + return newAnswers + } + + if (loading) { + return ( + + ) + } + + return ( + + debouncedOnUpdateApplicationHandler( + handleChange(e.target.value), + onChange && (() => onChange(e.target.value)), + ) + } + /> + ) +} diff --git a/libs/application/templates/official-journal-of-iceland/src/components/input/OJOISelectController.tsx b/libs/application/templates/official-journal-of-iceland/src/components/input/OJOISelectController.tsx new file mode 100644 index 0000000000000..3bb2ec98f5232 --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/components/input/OJOISelectController.tsx @@ -0,0 +1,84 @@ +import { SkeletonLoader } from '@island.is/island-ui/core' +import { useLocale } from '@island.is/localization' +import { SelectController } from '@island.is/shared/form-fields' +import { MessageDescriptor } from 'react-intl' +import { OJOI_INPUT_HEIGHT } from '../../lib/constants' +import { useApplication } from '../../hooks/useUpdateApplication' +import set from 'lodash/set' +import { InputFields } from '../../lib/types' + +type OJOISelectControllerOption = { + label: string + value: string +} + +type Props = { + name: string + label: string | MessageDescriptor + placeholder: string | MessageDescriptor + options?: OJOISelectControllerOption[] + defaultValue?: string + loading?: boolean + applicationId: string + disabled?: boolean + onChange?: (value: string) => void +} + +export const OJOISelectController = ({ + name, + label, + placeholder, + options, + defaultValue, + loading, + applicationId, + disabled, + onChange, +}: Props) => { + const { formatMessage: f } = useLocale() + const { updateApplication, application } = useApplication({ applicationId }) + + const placeholderText = + typeof placeholder === 'string' ? placeholder : f(placeholder) + + const labelText = typeof label === 'string' ? label : f(label) + + const handleChange = (value: string) => { + const currentAnswers = structuredClone(application.answers) + const newAnswers = set(currentAnswers, name, value) + + // we must reset the selected typeId if the department changes + if (name === InputFields.advert.departmentId) { + set(newAnswers, InputFields.advert.typeId, '') + } + + updateApplication(newAnswers) + + onChange && onChange(value) + } + + if (loading) { + return ( + + ) + } + + return ( + handleChange(opt.value)} + /> + ) +} diff --git a/libs/application/templates/official-journal-of-iceland/src/components/property/Property.tsx b/libs/application/templates/official-journal-of-iceland/src/components/property/Property.tsx index f257fdf848cce..35d5a59150005 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/property/Property.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/property/Property.tsx @@ -1,17 +1,31 @@ -import { Box, Text } from '@island.is/island-ui/core' +import { Box, SkeletonLoader, Text } from '@island.is/island-ui/core' import * as styles from './Property.css' +import { OJOI_INPUT_HEIGHT } from '../../lib/constants' type Props = { name?: string value?: string + loading?: boolean } -export const Property = ({ name, value }: Props) => ( - - - {name} - - - {value} +export const Property = ({ name, value, loading = false }: Props) => { + if (!value && !loading) { + return null + } + + return ( + + {loading ? ( + + ) : ( + <> + + {name} + + + {value} + + + )} - -) + ) +} diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddCommitteeMember.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddCommitteeMember.tsx new file mode 100644 index 0000000000000..d094f6b8e98e3 --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddCommitteeMember.tsx @@ -0,0 +1,77 @@ +import { Box, Button } from '@island.is/island-ui/core' +import { signatures } from '../../lib/messages/signatures' +import { useLocale } from '@island.is/localization' +import { useApplication } from '../../hooks/useUpdateApplication' +import { InputFields } from '../../lib/types' +import { + MAXIMUM_COMMITTEE_SIGNATURE_MEMBER_COUNT, + MINIMUM_COMMITTEE_SIGNATURE_MEMBER_COUNT, +} from '../../lib/constants' +import { getCommitteeAnswers, getEmptyMember } from '../../lib/utils' +import set from 'lodash/set' + +type Props = { + applicationId: string +} + +export const AddCommitteeMember = ({ applicationId }: Props) => { + const { formatMessage: f } = useLocale() + const { updateApplication, application, isLoading } = useApplication({ + applicationId, + }) + + const onAddCommitteeMember = () => { + const { signature, currentAnswers } = getCommitteeAnswers( + structuredClone(application.answers), + ) + + if (signature) { + const withExtraMember = { + ...signature, + members: [...(signature.members ?? []), getEmptyMember()], + } + + const updatedAnswers = set( + currentAnswers, + InputFields.signature.committee, + withExtraMember, + ) + + updateApplication(updatedAnswers) + } + } + + const getCurrentCount = () => { + const { signature } = getCommitteeAnswers( + structuredClone(application.answers), + ) + if (signature) { + return signature.members?.length ?? 0 + } + + return 0 + } + + const count = getCurrentCount() + + const isGreaterThanMinimum = count >= MINIMUM_COMMITTEE_SIGNATURE_MEMBER_COUNT + const isLessThanMaximum = count < MAXIMUM_COMMITTEE_SIGNATURE_MEMBER_COUNT + + const isDisabled = !isGreaterThanMinimum || !isLessThanMaximum + + return ( + + + + ) +} diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddRegularMember.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddRegularMember.tsx new file mode 100644 index 0000000000000..4d6aeb79bfc49 --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddRegularMember.tsx @@ -0,0 +1,92 @@ +import { Box, Button } from '@island.is/island-ui/core' +import { signatures } from '../../lib/messages/signatures' +import { useLocale } from '@island.is/localization' +import { useApplication } from '../../hooks/useUpdateApplication' +import { InputFields } from '../../lib/types' +import { + MAXIMUM_REGULAR_SIGNATURE_MEMBER_COUNT, + MINIMUM_REGULAR_SIGNATURE_MEMBER_COUNT, +} from '../../lib/constants' +import { getEmptyMember, getRegularAnswers } from '../../lib/utils' +import set from 'lodash/set' + +type Props = { + applicationId: string + signatureIndex: number +} + +export const AddRegularMember = ({ applicationId, signatureIndex }: Props) => { + const { formatMessage: f } = useLocale() + const { updateApplication, application, isLoading } = useApplication({ + applicationId, + }) + + const onAddMember = () => { + const { signature, currentAnswers } = getRegularAnswers( + structuredClone(application.answers), + ) + + if (signature) { + const doesSignatureExist = signature.at(signatureIndex) + + if (doesSignatureExist !== undefined) { + const updatedRegularSignature = signature.map((signature, index) => { + if (index === signatureIndex) { + return { + ...signature, + members: [...(signature.members ?? []), getEmptyMember()], + } + } + + return signature + }) + + const updatedAnswers = set( + currentAnswers, + InputFields.signature.regular, + updatedRegularSignature, + ) + + updateApplication(updatedAnswers) + } + } + } + + const getCurrentCount = () => { + const { signature } = getRegularAnswers( + structuredClone(application.answers), + ) + if (signature) { + const doesSignatureExist = signature?.at(signatureIndex) + + if (doesSignatureExist !== undefined) { + return doesSignatureExist.members?.length ?? 0 + } + } + + return 0 + } + + const count = getCurrentCount() + + const isGreaterThanMinimum = count >= MINIMUM_REGULAR_SIGNATURE_MEMBER_COUNT + const isLessThanMaximum = count < MAXIMUM_REGULAR_SIGNATURE_MEMBER_COUNT + + const isDisabled = !isGreaterThanMinimum || !isLessThanMaximum + + return ( + + + + ) +} diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddRegularSignature.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddRegularSignature.tsx new file mode 100644 index 0000000000000..1d3ce373b3ed8 --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddRegularSignature.tsx @@ -0,0 +1,81 @@ +import { useLocale } from '@island.is/localization' +import { signatures } from '../../lib/messages/signatures' +import { Box, Button } from '@island.is/island-ui/core' +import { useApplication } from '../../hooks/useUpdateApplication' +import { getValueViaPath } from '@island.is/application/core' +import { InputFields } from '../../lib/types' +import { + getRegularAnswers, + getRegularSignature, + isRegularSignature, +} from '../../lib/utils' +import set from 'lodash/set' +import { + DEFAULT_REGULAR_SIGNATURE_MEMBER_COUNT, + MAXIMUM_REGULAR_SIGNATURE_COUNT, + ONE, +} from '../../lib/constants' + +type Props = { + applicationId: string +} + +export const AddRegularSignature = ({ applicationId }: Props) => { + const { formatMessage: f } = useLocale() + const { updateApplication, application, isLoading } = useApplication({ + applicationId, + }) + + const onAddInstitution = () => { + const { signature, currentAnswers } = getRegularAnswers( + structuredClone(application.answers), + ) + + if (signature) { + const newSignature = getRegularSignature( + ONE, + DEFAULT_REGULAR_SIGNATURE_MEMBER_COUNT, + )?.pop() + + const updatedAnswers = set( + currentAnswers, + InputFields.signature.regular, + [...signature, newSignature], + ) + + updateApplication(updatedAnswers) + } + } + + const getCount = () => { + const currentAnswers = structuredClone(application.answers) + const signature = getValueViaPath( + currentAnswers, + InputFields.signature.regular, + ) + + if (isRegularSignature(signature)) { + return signature?.length ?? 0 + } + + return 0 + } + + const count = getCount() + + return ( + + + + ) +} diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/Additional.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/Additional.tsx index 9772604d59497..12148dc93a808 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/Additional.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/Additional.tsx @@ -1,22 +1,15 @@ import { Box, Text } from '@island.is/island-ui/core' import * as styles from './Signatures.css' -import { InputFields, OJOIFieldBaseProps } from '../../lib/types' -import { InputController } from '@island.is/shared/form-fields' -import { getErrorViaPath } from '@island.is/application/core' import { useLocale } from '@island.is/localization' import { signatures } from '../../lib/messages/signatures' +import { OJOIInputController } from '../input/OJOIInputController' -type Props = Pick & { - signature: string - setSignature: (state: string) => void +type Props = { + applicationId: string + name: string } -export const AdditionalSignature = ({ - application, - errors, - signature, - setSignature, -}: Props) => { +export const AdditionalSignature = ({ applicationId, name }: Props) => { const { formatMessage: f } = useLocale() return ( @@ -25,18 +18,10 @@ export const AdditionalSignature = ({ {f(signatures.headings.additionalSignature)} - setSignature(event.target.value)} + diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/Chairman.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/Chairman.tsx new file mode 100644 index 0000000000000..91b23f17dcafb --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/Chairman.tsx @@ -0,0 +1,106 @@ +import { Box, Text } from '@island.is/island-ui/core' +import { useApplication } from '../../hooks/useUpdateApplication' +import { useLocale } from '@island.is/localization' +import { signatures } from '../../lib/messages/signatures' +import { InputFields } from '../../lib/types' +import { getCommitteeAnswers, getEmptyMember } from '../../lib/utils' +import { memberItemSchema } from '../../lib/dataSchema' +import { SignatureMember } from './Member' +import set from 'lodash/set' +import * as styles from './Signatures.css' +import * as z from 'zod' + +type Props = { + applicationId: string + member?: z.infer +} + +type MemberProperties = ReturnType + +export const Chairman = ({ applicationId, member }: Props) => { + const { formatMessage: f } = useLocale() + const { application, debouncedOnUpdateApplicationHandler } = useApplication({ + applicationId, + }) + + const handleChairmanChange = (value: string, key: keyof MemberProperties) => { + const { signature, currentAnswers } = getCommitteeAnswers( + application.answers, + ) + + if (signature) { + const updatedCommitteeSignature = { + ...signature, + chairman: { ...signature.chairman, [key]: value }, + } + + const updatedSignatures = set( + currentAnswers, + InputFields.signature.committee, + updatedCommitteeSignature, + ) + + return updatedSignatures + } + + return currentAnswers + } + + if (!member) { + return null + } + + return ( + + + {f(signatures.headings.chairman)} + + + + + debouncedOnUpdateApplicationHandler( + handleChairmanChange(e.target.value, 'above'), + ) + } + /> + + debouncedOnUpdateApplicationHandler( + handleChairmanChange(e.target.value, 'after'), + ) + } + /> + + + + debouncedOnUpdateApplicationHandler( + handleChairmanChange(e.target.value, 'name'), + ) + } + /> + + debouncedOnUpdateApplicationHandler( + handleChairmanChange(e.target.value, 'below'), + ) + } + /> + + + + ) +} diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/Committee.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/Committee.tsx index a941b1e3d9f8d..0f71630176907 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/Committee.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/Committee.tsx @@ -1,282 +1,75 @@ -import { Box, Text, Button } from '@island.is/island-ui/core' +import { Box, Text } from '@island.is/island-ui/core' import * as styles from './Signatures.css' +import { InstitutionSignature } from './Institution' +import { SignatureTypes } from '../../lib/constants' +import { CommitteeMember } from './CommitteeMember' +import { useApplication } from '../../hooks/useUpdateApplication' +import { isCommitteeSignature } from '../../lib/utils' +import { Chairman } from './Chairman' +import { getValueViaPath } from '@island.is/application/core' +import { InputFields } from '../../lib/types' +import { AdditionalSignature } from './Additional' import { useLocale } from '@island.is/localization' -import { - CommitteeSignatureState, - InputFields, - OJOIFieldBaseProps, -} from '../../lib/types' -import cloneDeep from 'lodash/cloneDeep' -import { - DatePickerController, - InputController, -} from '@island.is/shared/form-fields' -import { INITIAL_ANSWERS, MEMBER_INDEX } from '../../lib/constants' -import { getErrorViaPath } from '@island.is/application/core' import { signatures } from '../../lib/messages/signatures' +import { AddCommitteeMember } from './AddCommitteeMember' -type ChairmanKey = keyof NonNullable['chairman'] -type MemberKey = keyof NonNullable['members'][0] - -type LocalState = typeof INITIAL_ANSWERS['signature'] -type Props = Pick & { - state: LocalState - setState: (state: LocalState) => void - addSignature?: boolean +type Props = { + applicationId: string } -export const CommitteeSignature = ({ state, setState, errors }: Props) => { +export const CommitteeSignature = ({ applicationId }: Props) => { const { formatMessage: f } = useLocale() + const { application } = useApplication({ + applicationId, + }) - const onCommitteeChairmanChange = (key: ChairmanKey, value: string) => { - const newState = cloneDeep(state) - newState.committee.chairman[key] = value - setState(newState) - } + const getSignature = () => { + const currentAnswers = getValueViaPath( + application.answers, + InputFields.signature.committee, + ) - const onCommitteMemberChange = ( - index: number, - key: MemberKey, - value: string, - ) => { - const newState = cloneDeep(state) - if (!newState.committee.members) return - newState.committee.members[index][key] = value - setState(newState) + if (isCommitteeSignature(currentAnswers)) { + return currentAnswers + } } - const onCommitteeChange = ( - key: keyof Omit, - value: string, - ) => { - const newState = cloneDeep(state) - newState.committee[key] = value - setState(newState) - } - - const onAddCommitteeMember = () => { - const newState = cloneDeep(state) - if (!newState.committee.members) return - newState.committee.members.push({ - name: '', - below: '', - }) - setState(newState) - } + const signature = getSignature() - const onRemoveCommitteeMember = (index: number) => { - const newState = cloneDeep(state) - if (!newState.committee.members) return - newState.committee.members.splice(index, 1) - setState(newState) + if (!signature) { + return null } return ( - - - - onCommitteeChange('institution', e.target.value)} - size="sm" - /> - - - onCommitteeChange('date', date)} - /> - - - - - {f(signatures.headings.chairman)} - - - - - - onCommitteeChairmanChange('above', e.target.value) - } - error={ - errors && - getErrorViaPath( - errors, - InputFields.signature.committee.chairman.above, - ) - } - /> - - - - onCommitteeChairmanChange('name', e.target.value) - } - error={ - errors && - getErrorViaPath( - errors, - InputFields.signature.committee.chairman.name, - ) - } + <> + + + + + + {f(signatures.headings.committeeMembers)} + + + {signature?.members?.map((member, index) => ( + - + ))} + - - - - onCommitteeChairmanChange('after', e.target.value) - } - error={ - errors && - getErrorViaPath( - errors, - InputFields.signature.committee.chairman.after, - ) - } - /> - - - - onCommitteeChairmanChange('below', e.target.value) - } - error={ - errors && - getErrorViaPath( - errors, - InputFields.signature.committee.chairman.below, - ) - } - /> - - - - - - - {f(signatures.headings.committeeMembers)} - - - {state.committee.members?.map((member, index) => { - const namePath = - InputFields.signature.committee.members.name.replace( - MEMBER_INDEX, - `${index}`, - ) - - const belowPath = - InputFields.signature.committee.members.below.replace( - MEMBER_INDEX, - `${index}`, - ) - return ( - - - - - onCommitteMemberChange(index, 'name', e.target.value) - } - error={errors && getErrorViaPath(errors, namePath)} - /> - - - - onCommitteMemberChange(index, 'below', e.target.value) - } - error={errors && getErrorViaPath(errors, belowPath)} - /> - - - {index > 1 && ( - - + + ) } diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/CommitteeMember.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/CommitteeMember.tsx new file mode 100644 index 0000000000000..c153a116f2890 --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/CommitteeMember.tsx @@ -0,0 +1,123 @@ +import { Box } from '@island.is/island-ui/core' +import * as styles from './Signatures.css' +import { useApplication } from '../../hooks/useUpdateApplication' +import { useLocale } from '@island.is/localization' +import { signatures } from '../../lib/messages/signatures' +import { InputFields } from '../../lib/types' +import set from 'lodash/set' +import { getCommitteeAnswers, getEmptyMember } from '../../lib/utils' +import { memberItemSchema } from '../../lib/dataSchema' +import { SignatureMember } from './Member' +import * as z from 'zod' +import { RemoveCommitteeMember } from './RemoveComitteeMember' +import { getValueViaPath } from '@island.is/application/core' + +type Props = { + applicationId: string + memberIndex: number + member?: z.infer +} + +type MemberProperties = ReturnType + +export const CommitteeMember = ({ + applicationId, + memberIndex, + member, +}: Props) => { + const { formatMessage: f } = useLocale() + const { debouncedOnUpdateApplicationHandler, application } = useApplication({ + applicationId, + }) + + const handleMemberChange = ( + value: string, + key: keyof MemberProperties, + memberIndex: number, + ) => { + const { signature, currentAnswers } = getCommitteeAnswers( + application.answers, + ) + + if (signature) { + const updatedCommitteeSignature = { + ...signature, + members: signature?.members?.map((m, i) => { + if (i === memberIndex) { + return { + ...m, + [key]: value, + } + } + + return m + }), + } + + const updatedSignatures = set( + currentAnswers, + InputFields.signature.committee, + updatedCommitteeSignature, + ) + + return updatedSignatures + } + + return currentAnswers + } + + if (!member) { + return null + } + + const getMemberCount = () => { + const { signature } = getCommitteeAnswers(application.answers) + + if (signature) { + return signature.members?.length ?? 0 + } + + return 0 + } + + const isLast = memberIndex === getMemberCount() - 1 + + return ( + + + + + debouncedOnUpdateApplicationHandler( + handleMemberChange(e.target.value, 'name', memberIndex), + ) + } + /> + + + + debouncedOnUpdateApplicationHandler( + handleMemberChange(e.target.value, 'below', memberIndex), + ) + } + /> + + + + + ) +} diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/Institution.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/Institution.tsx new file mode 100644 index 0000000000000..06a94dbf7502c --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/Institution.tsx @@ -0,0 +1,163 @@ +import { + Box, + DatePicker, + Input, + SkeletonLoader, +} from '@island.is/island-ui/core' +import { + OJOI_INPUT_HEIGHT, + SignatureType, + SignatureTypes, +} from '../../lib/constants' +import { useLocale } from '@island.is/localization' +import { signatures } from '../../lib/messages/signatures' +import { useApplication } from '../../hooks/useUpdateApplication' +import set from 'lodash/set' +import { getValueViaPath } from '@island.is/application/core' +import { InputFields } from '../../lib/types' +import * as styles from './Signatures.css' +import { + getCommitteeAnswers, + getRegularAnswers, + getSignatureDefaultValues, + isCommitteeSignature, + isRegularSignature, +} from '../../lib/utils' +import { z } from 'zod' +import { signatureInstitutionSchema } from '../../lib/dataSchema' +import { RemoveRegularSignature } from './RemoveRegularSignature' +type Props = { + applicationId: string + type: SignatureType + signatureIndex?: number +} + +type SignatureInstitutionKeys = z.infer + +export const InstitutionSignature = ({ + applicationId, + type, + signatureIndex, +}: Props) => { + const { formatMessage: f } = useLocale() + const { + debouncedOnUpdateApplicationHandler, + application, + applicationLoading, + } = useApplication({ + applicationId, + }) + + const handleInstitutionChange = ( + value: string, + key: SignatureInstitutionKeys, + signatureIndex?: number, + ) => { + const { signature, currentAnswers } = + type === SignatureTypes.COMMITTEE + ? getCommitteeAnswers(application.answers) + : getRegularAnswers(application.answers) + + if (isRegularSignature(signature)) { + const updatedRegularSignature = signature?.map((signature, index) => { + if (index === signatureIndex) { + return { + ...signature, + [key]: value, + } + } + + return signature + }) + + const updatedSignatures = set( + currentAnswers, + InputFields.signature[type], + updatedRegularSignature, + ) + + return updatedSignatures + } + + if (isCommitteeSignature(signature)) { + const updatedCommitteeSignature = set( + currentAnswers, + InputFields.signature[type], + { + ...signature, + [key]: value, + }, + ) + + return updatedCommitteeSignature + } + + return currentAnswers + } + + if (applicationLoading) { + return + } + + const { institution, date } = getSignatureDefaultValues( + getValueViaPath(application.answers, InputFields.signature[type]), + signatureIndex, + ) + + return ( + + + + + debouncedOnUpdateApplicationHandler( + handleInstitutionChange( + e.target.value, + 'institution', + signatureIndex, + ), + ) + } + /> + + + + date && + debouncedOnUpdateApplicationHandler( + handleInstitutionChange( + date.toISOString(), + 'date', + signatureIndex, + ), + ) + } + /> + + {signatureIndex !== undefined && ( + + )} + + + ) +} diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/Member.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/Member.tsx new file mode 100644 index 0000000000000..fe26e50abf9cd --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/Member.tsx @@ -0,0 +1,28 @@ +import { Input } from '@island.is/island-ui/core' + +type Props = { + name: string + label: string + defaultValue?: string + onChange: ( + e: React.ChangeEvent, + ) => void +} + +export const SignatureMember = ({ + name, + label, + defaultValue, + onChange, +}: Props) => { + return ( + + ) +} diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/Regular.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/Regular.tsx index 6619c1173d69e..0b314fd77ff88 100644 --- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/Regular.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/Regular.tsx @@ -1,296 +1,88 @@ -import { Box, Button, Text } from '@island.is/island-ui/core' +import { Box, Text } from '@island.is/island-ui/core' import * as styles from './Signatures.css' +import { InputFields } from '../../lib/types' +import { useApplication } from '../../hooks/useUpdateApplication' +import { getValueViaPath } from '@island.is/application/core' +import { InstitutionSignature } from './Institution' +import { RegularMember } from './RegularMember' +import { isRegularSignature } from '../../lib/utils' +import { AddRegularMember } from './AddRegularMember' +import { AddRegularSignature } from './AddRegularSignature' +import { AdditionalSignature } from './Additional' +import { signatures as messages } from '../../lib/messages/signatures' import { useLocale } from '@island.is/localization' -import { - InputFields, - OJOIFieldBaseProps, - RegularSignatureState, -} from '../../lib/types' -import cloneDeep from 'lodash/cloneDeep' -import { - INITIAL_ANSWERS, - INSTITUTION_INDEX, - MEMBER_INDEX, -} from '../../lib/constants' -import { getErrorViaPath } from '@island.is/application/core' -import { signatures } from '../../lib/messages/signatures' -import { - DatePickerController, - InputController, -} from '@island.is/shared/form-fields' -type LocalState = typeof INITIAL_ANSWERS['signature'] - -type Props = Pick & { - state: LocalState - setState: (state: LocalState) => void - addSignature?: boolean +type Props = { + applicationId: string } -type Institution = NonNullable[0] - -type InstitutionMember = NonNullable[0] - -type MemberKey = keyof InstitutionMember - -type InstitutionKey = keyof Omit - -export const RegularSignature = ({ state, setState, errors }: Props) => { +export const RegularSignature = ({ applicationId }: Props) => { const { formatMessage: f } = useLocale() - - const onChangeMember = ( - institutionIndex: number, - memberIndex: number, - key: MemberKey, - value: string, - ) => { - const clonedState = cloneDeep(state) - const institution = clonedState.regular.find( - (_, index) => index === institutionIndex, + const { application } = useApplication({ + applicationId, + }) + + const getSignature = () => { + const currentAnswers = getValueViaPath( + application.answers, + InputFields.signature.regular, ) - if (!institution) return - - const member = institution?.members.find( - (_, index) => index === memberIndex, - ) - - if (!member) return - - const updatedMember = { ...member, [key]: value } - institution.members.splice(memberIndex, 1, updatedMember) - clonedState.regular.splice(institutionIndex, 1, institution) - setState(clonedState) + if (isRegularSignature(currentAnswers)) { + return currentAnswers + } } - const onRemoveMember = (institutionIndex: number, memberIndex: number) => { - const clonedState = cloneDeep(state) - const institution = clonedState.regular.find( - (_, index) => index === institutionIndex, - ) + const signatures = getSignature() - if (!institution) return - - institution.members.splice(memberIndex, 1) - clonedState.regular.splice(institutionIndex, 1, institution) - setState(clonedState) - } - - const onAddMember = (institutionIndex: number) => { - const clonedState = cloneDeep(state) - const institution = clonedState.regular.find( - (_, index) => index === institutionIndex, - ) - - if (!institution) return - - institution.members.push({ - above: '', - name: '', - after: '', - below: '', - }) - clonedState.regular.splice(institutionIndex, 1, institution) - setState(clonedState) - } - - const onChangeInstitution = ( - institutionIndex: number, - key: InstitutionKey, - value: string, - ) => { - const clonedState = cloneDeep(state) - const institution = clonedState.regular.find( - (_, index) => index === institutionIndex, - ) - - if (!institution) return - - const updatedInstitution = { ...institution, [key]: value } - clonedState.regular.splice(institutionIndex, 1, updatedInstitution) - setState(clonedState) - } - - const onRemoveInstitution = (institutionIndex: number) => { - const clonedState = cloneDeep(state) - clonedState.regular.splice(institutionIndex, 1) - setState(clonedState) - } - - const onAddInstitution = () => { - const clonedState = cloneDeep(state) - clonedState.regular.push({ - institution: '', - date: '', - members: [ - { - above: '', - name: '', - after: '', - below: '', - }, - ], - }) - setState(clonedState) + if (!signatures) { + return null } return ( - - {state.regular.map((institution, index) => { - const institutionPath = - InputFields.signature.regular.institution.replace( - INSTITUTION_INDEX, - `${index}`, - ) - - const datePath = InputFields.signature.regular.date.replace( - INSTITUTION_INDEX, - `${index}`, - ) - - return ( - - - - - onChangeInstitution(index, 'institution', e.target.value) - } - error={errors && getErrorViaPath(errors, institutionPath)} - size="sm" - /> - - - onChangeInstitution(index, 'date', date)} - error={errors && getErrorViaPath(errors, datePath)} + <> + + {signatures?.map((signature, index) => { + return ( + + + - - - {index > 0 && ( - + ) + })} + + - - ) - })} - - + ) + })} - + + + ) } diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/RegularMember.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/RegularMember.tsx new file mode 100644 index 0000000000000..01ce1ba0ed42c --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/RegularMember.tsx @@ -0,0 +1,153 @@ +import { Box } from '@island.is/island-ui/core' +import * as styles from './Signatures.css' +import { useApplication } from '../../hooks/useUpdateApplication' +import { useLocale } from '@island.is/localization' +import { signatures } from '../../lib/messages/signatures' +import { InputFields } from '../../lib/types' +import set from 'lodash/set' +import { getEmptyMember, getRegularAnswers } from '../../lib/utils' +import { memberItemSchema } from '../../lib/dataSchema' +import { SignatureMember } from './Member' +import * as z from 'zod' +import { RemoveRegularMember } from './RemoveRegularMember' + +type Props = { + applicationId: string + signatureIndex: number + memberIndex: number + member?: z.infer +} + +type MemberProperties = ReturnType + +export const RegularMember = ({ + applicationId, + signatureIndex, + memberIndex, + member, +}: Props) => { + const { formatMessage: f } = useLocale() + const { debouncedOnUpdateApplicationHandler, application } = useApplication({ + applicationId, + }) + + const handleMemberChange = ( + value: string, + key: keyof MemberProperties, + si: number, + mi: number, + ) => { + const { signature, currentAnswers } = getRegularAnswers(application.answers) + + if (signature) { + const updatedRegularSignature = signature.map((s, index) => { + if (index === si) { + return { + ...s, + members: s.members?.map((member, memberIndex) => { + if (memberIndex === mi) { + return { + ...member, + [key]: value, + } + } + + return member + }), + } + } + + return s + }) + + const updatedSignatures = set( + currentAnswers, + InputFields.signature.regular, + updatedRegularSignature, + ) + + return updatedSignatures + } + + return currentAnswers + } + + if (!member) { + return null + } + + return ( + + + + debouncedOnUpdateApplicationHandler( + handleMemberChange( + e.target.value, + 'above', + signatureIndex, + memberIndex, + ), + ) + } + /> + + debouncedOnUpdateApplicationHandler( + handleMemberChange( + e.target.value, + 'after', + signatureIndex, + memberIndex, + ), + ) + } + /> + + + + debouncedOnUpdateApplicationHandler( + handleMemberChange( + e.target.value, + 'name', + signatureIndex, + memberIndex, + ), + ) + } + /> + + debouncedOnUpdateApplicationHandler( + handleMemberChange( + e.target.value, + 'below', + signatureIndex, + memberIndex, + ), + ) + } + /> + + + + ) +} diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveComitteeMember.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveComitteeMember.tsx new file mode 100644 index 0000000000000..43d6f6ba42959 --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveComitteeMember.tsx @@ -0,0 +1,55 @@ +import { Box, Button } from '@island.is/island-ui/core' +import { useApplication } from '../../hooks/useUpdateApplication' +import { InputFields } from '../../lib/types' +import { MINIMUM_COMMITTEE_SIGNATURE_MEMBER_COUNT } from '../../lib/constants' +import set from 'lodash/set' +import * as styles from './Signatures.css' +import { getCommitteeAnswers, isCommitteeSignature } from '../../lib/utils' + +type Props = { + applicationId: string + memberIndex: number +} + +export const RemoveCommitteeMember = ({ + applicationId, + memberIndex, +}: Props) => { + const { updateApplication, application, isLoading } = useApplication({ + applicationId, + }) + + const onRemoveMember = () => { + const { currentAnswers, signature } = getCommitteeAnswers( + application.answers, + ) + + if (isCommitteeSignature(signature)) { + const updatedCommitteeSignature = { + ...signature, + members: signature.members?.filter((_, mi) => mi !== memberIndex), + } + + const updatedAnswers = set( + currentAnswers, + InputFields.signature.committee, + updatedCommitteeSignature, + ) + + updateApplication(updatedAnswers) + } + } + + return ( + + diff --git a/libs/application/templates/official-journal-of-iceland/src/fields/Attachments.tsx b/libs/application/templates/official-journal-of-iceland/src/fields/Attachments.tsx index 864cf1b6d2858..c2bd139df8264 100644 --- a/libs/application/templates/official-journal-of-iceland/src/fields/Attachments.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/fields/Attachments.tsx @@ -1,65 +1,33 @@ -import { Box } from '@island.is/island-ui/core' -import { FormGroup } from '../components/form/FormGroup' -import { UPLOAD_ACCEPT, FILE_SIZE_LIMIT, FileNames } from '../lib/constants' -import { attachments } from '../lib/messages' -import { InputFields, OJOIFieldBaseProps } from '../lib/types' -import { FileUploadController } from '@island.is/application/ui-components' -import { Application } from '@island.is/application/types' -import { RadioController } from '@island.is/shared/form-fields' +import { OJOIFieldBaseProps } from '../lib/types' +import { Box, InputFileUpload } from '@island.is/island-ui/core' +import { useFileUpload } from '../hooks/useFileUpload' +import { ALLOWED_FILE_TYPES, ApplicationAttachmentType } from '../lib/constants' import { useLocale } from '@island.is/localization' -import { getErrorViaPath } from '@island.is/application/core' +import { attachments } from '../lib/messages/attachments' -export const Attachments = ({ application, errors }: OJOIFieldBaseProps) => { +export const Attachments = ({ application }: OJOIFieldBaseProps) => { const { formatMessage: f } = useLocale() - - // TODO: Create wrapper around file upload component to handle file upload + const { files, onChange, onRemove } = useFileUpload({ + applicationId: application.id, + attachmentType: ApplicationAttachmentType.ADDITIONS, + }) return ( - <> - - - - - - - - - - - - - + + + ) } diff --git a/libs/application/templates/official-journal-of-iceland/src/fields/Comments.tsx b/libs/application/templates/official-journal-of-iceland/src/fields/Comments.tsx index 2106b0d876b16..e5a25426b2fba 100644 --- a/libs/application/templates/official-journal-of-iceland/src/fields/Comments.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/fields/Comments.tsx @@ -1,56 +1,80 @@ -import { AlertMessage, Box, SkeletonLoader } from '@island.is/island-ui/core' +import { + AlertMessage, + Box, + SkeletonLoader, + Stack, +} from '@island.is/island-ui/core' import { OJOIFieldBaseProps } from '../lib/types' import { CommentsList } from '../components/comments/CommentList' import { FormGroup } from '../components/form/FormGroup' import { useComments } from '../hooks/useComments' import { useLocale } from '@island.is/localization' -import { comments as messages } from '../lib/messages/comments' +import { + error as errorMessages, + comments as commentMessages, +} from '../lib/messages' +import { OJOI_INPUT_HEIGHT } from '../lib/constants' +import { AddComment } from '../components/comments/AddComment' export const Comments = ({ application }: OJOIFieldBaseProps) => { const { formatMessage: f } = useLocale() - const { comments, error, loading } = useComments({ + const { comments, loading, error } = useComments({ applicationId: application.id, }) - if (error) { - return ( - - - - ) - } + const showCommentsList = comments && comments.length > 0 if (loading) { return ( - - - + ) } return ( - - - - {/* handleAddComment(c)} /> */} - + + + {error && ( + + )} + {!showCommentsList && ( + + )} + {showCommentsList && ( + + ({ + task: comment.task.title, + comment: comment.task.comment as string, + from: comment.task.from ?? undefined, + date: comment.createdAt, + type: 'received', // TODO: Implement sent comments + }))} + /> + + )} + + ) } diff --git a/libs/application/templates/official-journal-of-iceland/src/fields/CommunicationChannels.tsx b/libs/application/templates/official-journal-of-iceland/src/fields/CommunicationChannels.tsx new file mode 100644 index 0000000000000..8cad52cf0c782 --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/fields/CommunicationChannels.tsx @@ -0,0 +1,37 @@ +import { useLocale } from '@island.is/localization' +import { OJOIFieldBaseProps } from '../lib/types' +import { FormGroup } from '../components/form/FormGroup' +import { publishing } from '../lib/messages' +import { ChannelList } from '../components/communicationChannels/ChannelList' +import { AddChannel } from '../components/communicationChannels/AddChannel' +import { useState } from 'react' + +export const CommunicationChannels = ({ application }: OJOIFieldBaseProps) => { + const { formatMessage: f } = useLocale() + + const [email, setEmail] = useState('') + const [phone, setPhone] = useState('') + const [isVisible, setIsVisible] = useState(false) + + return ( + + { + if (email) setEmail(email) + if (phone) setPhone(phone ?? '') + setIsVisible(true) + }} + applicationId={application.id} + /> + + + ) +} diff --git a/libs/application/templates/official-journal-of-iceland/src/fields/Message.tsx b/libs/application/templates/official-journal-of-iceland/src/fields/Message.tsx new file mode 100644 index 0000000000000..f7815955eb2e6 --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/fields/Message.tsx @@ -0,0 +1,18 @@ +import { FormGroup } from '../components/form/FormGroup' +import { OJOIInputController } from '../components/input/OJOIInputController' +import { publishing } from '../lib/messages' +import { InputFields, OJOIFieldBaseProps } from '../lib/types' + +export const Message = ({ application }: OJOIFieldBaseProps) => { + return ( + + + + ) +} diff --git a/libs/application/templates/official-journal-of-iceland/src/fields/Original.tsx b/libs/application/templates/official-journal-of-iceland/src/fields/Original.tsx index f2e12858f5e27..a3d8a9ac7dd94 100644 --- a/libs/application/templates/official-journal-of-iceland/src/fields/Original.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/fields/Original.tsx @@ -1,29 +1,33 @@ -import { FileUploadController } from '@island.is/application/ui-components' -import { FormGroup } from '../components/form/FormGroup' -import { InputFields, OJOIFieldBaseProps } from '../lib/types' -import { Application } from '@island.is/application/types' -import { FILE_SIZE_LIMIT, UPLOAD_ACCEPT } from '../lib/constants' -import { original } from '../lib/messages' +import { OJOIFieldBaseProps } from '../lib/types' +import { ALLOWED_FILE_TYPES, ApplicationAttachmentType } from '../lib/constants' +import { attachments } from '../lib/messages' import { useLocale } from '@island.is/localization' -import { getErrorViaPath } from '@island.is/application/core' +import { InputFileUpload, Box } from '@island.is/island-ui/core' -export const Original = (props: OJOIFieldBaseProps) => { +import { useFileUpload } from '../hooks/useFileUpload' + +export const Original = ({ application }: OJOIFieldBaseProps) => { const { formatMessage: f } = useLocale() + const { files, onChange, onRemove } = useFileUpload({ + applicationId: application.id, + attachmentType: ApplicationAttachmentType.ORIGINAL, + }) return ( - - + - + ) } diff --git a/libs/application/templates/official-journal-of-iceland/src/fields/Preview.tsx b/libs/application/templates/official-journal-of-iceland/src/fields/Preview.tsx index c018afad13013..7385687ab9360 100644 --- a/libs/application/templates/official-journal-of-iceland/src/fields/Preview.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/fields/Preview.tsx @@ -1,45 +1,36 @@ -import { Box, Button, SkeletonLoader } from '@island.is/island-ui/core' +import { + AlertMessage, + Box, + Bullet, + BulletList, + SkeletonLoader, + Stack, + Text, +} from '@island.is/island-ui/core' import { HTMLEditor } from '../components/htmlEditor/HTMLEditor' import { signatureConfig } from '../components/htmlEditor/config/signatureConfig' -import { advertisementTemplate } from '../components/htmlEditor/templates/content' -import { - regularSignatureTemplate, - committeeSignatureTemplate, -} from '../components/htmlEditor/templates/signatures' -import { preview } from '../lib/messages' -import { - OJOIFieldBaseProps, - OfficialJournalOfIcelandGraphqlResponse, -} from '../lib/types' +import { OJOIFieldBaseProps } from '../lib/types' import { useLocale } from '@island.is/localization' -import { useQuery } from '@apollo/client' -import { PDF_QUERY, PDF_URL_QUERY, TYPE_QUERY } from '../graphql/queries' - -export const Preview = (props: OJOIFieldBaseProps) => { - const { formatMessage: f } = useLocale() - const { answers, id } = props.application - const { advert, signature } = answers +import { HTMLText } from '@island.is/regulations-tools/types' +import { getAdvertMarkup, getSignatureMarkup } from '../lib/utils' +import { SignatureTypes } from '../lib/constants' +import { useApplication } from '../hooks/useUpdateApplication' +import { advert, error, preview } from '../lib/messages' +import { useType } from '../hooks/useType' - const { data, loading } = useQuery(TYPE_QUERY, { - variables: { - params: { - id: advert?.type, - }, - }, +export const Preview = ({ application }: OJOIFieldBaseProps) => { + const { application: currentApplication } = useApplication({ + applicationId: application.id, }) - const type = data?.officialJournalOfIcelandType?.type?.title - - const { data: pdfUrlData } = useQuery(PDF_URL_QUERY, { - variables: { - id: id, - }, - }) + const { formatMessage: f } = useLocale() - const { data: pdfData } = useQuery(PDF_QUERY, { - variables: { - id: id, - }, + const { + type, + loading, + error: typeError, + } = useType({ + typeId: currentApplication.answers.advert?.typeId, }) if (loading) { @@ -48,75 +39,63 @@ export const Preview = (props: OJOIFieldBaseProps) => { ) } - const onCopyPreviewLink = () => { - if (!pdfData) { - return - } + const signatureMarkup = getSignatureMarkup({ + signatures: currentApplication.answers.signatures, + type: currentApplication.answers.misc?.signatureType as SignatureTypes, + }) - const url = pdfData.officialJournalOfIcelandApplicationGetPdfUrl.url + const advertMarkup = getAdvertMarkup({ + type: type?.title, + title: currentApplication.answers.advert?.title, + html: currentApplication.answers.advert?.html, + }) - navigator.clipboard.writeText(url) - } + const hasMarkup = + !!currentApplication.answers.advert?.html || + type?.title || + currentApplication.answers.advert?.title - const onOpenPdfPreview = () => { - if (!pdfData) { - return - } - - window.open( - `data:application/pdf,${pdfData.officialJournalOfIcelandApplicationGetPdf.pdf}`, - '_blank', - ) - } + const combinedHtml = hasMarkup + ? (`${advertMarkup}
${signatureMarkup}` as HTMLText) + : (`${signatureMarkup}` as HTMLText) return ( - <> - - {!!pdfUrlData && ( - + + + {typeError && ( + )} - {!!pdfData && ( - + {!hasMarkup && ( + + {f(error.missingHtmlMessage)} + + {f(advert.inputs.department.label)} + {f(advert.inputs.type.label)} + {f(advert.inputs.title.label)} + {f(advert.inputs.editor.label)} + + + } + /> )} - + - + ) } diff --git a/libs/application/templates/official-journal-of-iceland/src/fields/Publishing.tsx b/libs/application/templates/official-journal-of-iceland/src/fields/Publishing.tsx index 5f8ae9da55214..859364996ebf0 100644 --- a/libs/application/templates/official-journal-of-iceland/src/fields/Publishing.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/fields/Publishing.tsx @@ -1,296 +1,136 @@ import { useLocale } from '@island.is/localization' import { FormGroup } from '../components/form/FormGroup' +import { InputFields, OJOIFieldBaseProps } from '../lib/types' +import { error, publishing } from '../lib/messages' +import { OJOIDateController } from '../components/input/OJOIDateController' +import { useApplication } from '../hooks/useUpdateApplication' import { - InputFields, - OfficialJournalOfIcelandGraphqlResponse, - OJOIFieldBaseProps, - Override, -} from '../lib/types' -import { publishing } from '../lib/messages' -import { - DEBOUNCE_INPUT_TIMER, - MINIMUM_WEEKDAYS, - INITIAL_ANSWERS, -} from '../lib/constants' -import { useCallback, useEffect, useState } from 'react' -import { - DatePickerController, - InputController, -} from '@island.is/shared/form-fields' -import { getErrorViaPath } from '@island.is/application/core' -import { Box, Icon, Select, Tag } from '@island.is/island-ui/core' + AlertMessage, + Box, + Select, + SkeletonLoader, + Tag, +} from '@island.is/island-ui/core' +import { useCategories } from '../hooks/useCategories' +import { MINIMUM_WEEKDAYS, OJOI_INPUT_HEIGHT } from '../lib/constants' +import set from 'lodash/set' import addYears from 'date-fns/addYears' import { addWeekdays, getWeekendDates } from '../lib/utils' -import { useMutation, useQuery } from '@apollo/client' -import { CATEGORIES_QUERY } from '../graphql/queries' -import { ChannelList } from '../components/communicationChannels/ChannelList' -import { AddChannel } from '../components/communicationChannels/AddChannel' -import { UPDATE_APPLICATION } from '@island.is/application/graphql' -import debounce from 'lodash/debounce' -import { useFormContext } from 'react-hook-form' - -type LocalState = Override< - typeof INITIAL_ANSWERS['publishing'], - { - contentCategories: CategoryOption[] - communicationChannels: Channel[] - } -> -type Channel = { - email: string - phone: string -} +export const Publishing = ({ application }: OJOIFieldBaseProps) => { + const { formatMessage: f } = useLocale() -type CategoryOption = { - label: string - value: string -} + const { application: currentApplication, updateApplication } = useApplication( + { + applicationId: application.id, + }, + ) -export const Publishing = (props: OJOIFieldBaseProps) => { - const { formatMessage: f, locale } = useLocale() - const { application } = props - const { answers } = application + const { + categories, + error: categoryError, + loading: categoryLoading, + } = useCategories() - const today = new Date() - const maxEndDate = addYears(today, 5) - const minDate = new Date() - if (minDate.getHours() >= 12) { - minDate.setDate(minDate.getDate() + 1) + if (categoryLoading) { + return } - const defaultDate = answers.publishing?.date - ? new Date(answers.publishing.date).toISOString().split('T')[0] - : addWeekdays(today, MINIMUM_WEEKDAYS).toISOString().split('T')[0] - - const { setValue, clearErrors } = useFormContext() - - const [channelState, setChannelState] = useState({ - email: '', - phone: '', - }) + if (categoryError) { + return ( + + ) + } - const [categories, setCategories] = useState([]) + const onCategoryChange = (value?: string) => { + if (!value) { + return + } - const [state, setState] = useState({ - date: answers.publishing?.date ?? '', - contentCategories: - answers.publishing?.contentCategories ?? ([] as CategoryOption[]), - communicationChannels: - answers.publishing?.communicationChannels ?? ([] as Channel[]), - message: answers.publishing?.message ?? '', - }) + const currentAnswers = structuredClone(currentApplication.answers) + const selectedCategories = currentAnswers.advert?.categories || [] - const [updateApplication] = useMutation(UPDATE_APPLICATION) + const newCategories = selectedCategories.includes(value) + ? selectedCategories.filter((c) => c !== value) + : [...selectedCategories, value] - useQuery>( - CATEGORIES_QUERY, - { - variables: { - params: { - pageSize: 1000, - }, - }, - onCompleted: (data) => { - setCategories( - data.officialJournalOfIcelandCategories.categories.map( - (category) => ({ - label: category.title, - value: category.id, - }), - ), - ) - }, - }, - ) - - const onSelect = (opt: CategoryOption) => { - if (!opt.value) return - - const shouldAdd = !state.contentCategories.some( - (category) => category.value === opt.value, + const updatedAnswers = set( + currentAnswers, + InputFields.advert.categories, + newCategories, ) - const updatedCategories = shouldAdd - ? [...state.contentCategories, { label: opt.label, value: opt.value }] - : state.contentCategories.filter( - (category) => category.value !== opt.value, - ) - - setState({ ...state, contentCategories: updatedCategories }) - setValue(InputFields.publishing.contentCategories, updatedCategories) - } - const onEditChannel = (channel: Channel) => { - onRemoveChannel(channel) - setChannelState(channel) + updateApplication(updatedAnswers) } - const onRemoveChannel = (channel: Channel) => { - setState({ - ...state, - communicationChannels: state.communicationChannels.filter( - (c) => c.email !== channel.email, - ), - }) + const defaultCategory = { + label: f(publishing.inputs.contentCategories.placeholder), + value: '', } - const onAddChannel = () => { - if (!channelState.email) return - setState({ - ...state, - communicationChannels: [ - ...state.communicationChannels, - { - email: channelState.email, - phone: channelState.phone, - }, - ], - }) - setChannelState({ email: '', phone: '' }) - } - - const updateHandler = useCallback(async () => { - await updateApplication({ - variables: { - locale, - input: { - skipValidation: true, - id: application.id, - answers: { - ...application.answers, - publishing: state, - }, - }, - }, - }) + const mappedCategories = categories?.map((c) => ({ + label: c.title, + value: c.id, + })) - setValue(InputFields.publishing.date, state.date) - setValue(InputFields.publishing.contentCategories, state.contentCategories) - setValue( - InputFields.publishing.communicationChannels, - state.communicationChannels, - ) - setValue(InputFields.publishing.message, state.message) - }, [ - application.answers, - application.id, - locale, - setValue, - state, - updateApplication, - ]) + const selectedCategories = categories?.filter((c) => + currentApplication.answers.advert?.categories?.includes(c.id), + ) - const updateState = useCallback((newState: typeof state) => { - setState((prev) => ({ ...prev, ...newState })) - }, []) + const today = new Date() + const maxEndDate = addYears(today, 5) + const minDate = new Date() + if (minDate.getHours() >= 12) { + minDate.setDate(minDate.getDate() + 1) + } - useEffect(() => { - updateHandler() - }, [updateHandler]) - const debouncedStateUpdate = debounce(updateState, DEBOUNCE_INPUT_TIMER) + const defaultDate = currentApplication.answers.advert?.requestedDate + ? new Date(currentApplication.answers.advert.requestedDate) + .toISOString() + .split('T')[0] + : addWeekdays(today, MINIMUM_WEEKDAYS).toISOString().split('T')[0] return ( - <> - - - setState({ ...state, date })} - error={ - props.errors && - getErrorViaPath(props.errors, InputFields.publishing.date) - } - /> - - - onCategoryChange(opt?.value)} /> - - - - debouncedStateUpdate({ ...state, message: e.target.value }) - } - textarea - rows={4} - /> - - + + {selectedCategories?.map((c) => ( + onCategoryChange(c.id)} outlined key={c.id}> + {c.title} + + ))} + +
+
) } diff --git a/libs/application/templates/official-journal-of-iceland/src/fields/Signatures.tsx b/libs/application/templates/official-journal-of-iceland/src/fields/Signatures.tsx index 2174c65700a87..881ad65f48efc 100644 --- a/libs/application/templates/official-journal-of-iceland/src/fields/Signatures.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/fields/Signatures.tsx @@ -2,173 +2,57 @@ import { useLocale } from '@island.is/localization' import { FormGroup } from '../components/form/FormGroup' import { InputFields, OJOIFieldBaseProps } from '../lib/types' import { signatures } from '../lib/messages/signatures' +import { useState } from 'react' +import { SignatureType, SignatureTypes } from '../lib/constants' import { Tabs } from '@island.is/island-ui/core' import { CommitteeSignature } from '../components/signatures/Committee' import { RegularSignature } from '../components/signatures/Regular' -import { useCallback, useEffect, useState } from 'react' -import { useMutation } from '@apollo/client' -import { UPDATE_APPLICATION } from '@island.is/application/graphql' -import { DEBOUNCE_INPUT_TIMER, INITIAL_ANSWERS } from '../lib/constants' -import debounce from 'lodash/debounce' +import { useApplication } from '../hooks/useUpdateApplication' +import set from 'lodash/set' import { HTMLEditor } from '../components/htmlEditor/HTMLEditor' -import { - committeeSignatureTemplate, - regularSignatureTemplate, -} from '../components/htmlEditor/templates/signatures' -import { signatureConfig } from '../components/htmlEditor/config/signatureConfig' -import { useFormContext } from 'react-hook-form' -import { AdditionalSignature } from '../components/signatures/Additional' +import { getSignatureMarkup } from '../lib/utils' -type LocalState = typeof INITIAL_ANSWERS['signature'] - -export const Signatures = ({ application, errors }: OJOIFieldBaseProps) => { - const { formatMessage: f, locale } = useLocale() - - const { answers } = application - - const { setValue } = useFormContext() - - const [updateApplication] = useMutation(UPDATE_APPLICATION) - - const [selectedTab, setSelectedTab] = useState( - answers?.signature?.type ?? 'regular', - ) - - const [state, setState] = useState({ - type: answers?.signature?.type ?? 'regular', - signature: answers?.signature?.signature ?? '', - regular: answers?.signature?.regular ?? [ - { - institution: '', - date: '', - members: [ - { - name: '', - above: '', - after: '', - below: '', - }, - ], - }, - ], - committee: answers?.signature?.committee ?? { - institution: '', - date: '', - chairman: { - name: '', - above: '', - after: '', - below: '', - }, - members: [ - { - below: '', - name: '', - }, - ], +export const Signatures = ({ application }: OJOIFieldBaseProps) => { + const { formatMessage: f } = useLocale() + const { updateApplication, application: currentApplication } = useApplication( + { + applicationId: application.id, }, - additional: answers?.signature?.additional ?? '', - }) - - setValue('signature', state) - - const updateHandler = useCallback(async () => { - await updateApplication({ - variables: { - locale, - input: { - skipValidation: true, - id: application.id, - answers: { - ...application.answers, - signature: { - type: state.type, - signature: state.signature, - regular: state.regular, - committee: state.committee, - additional: state.additional, - }, - }, - }, - }, - }) - }, [application.answers, application.id, locale, state, updateApplication]) - - const updateAdditionalSignature = useCallback((newSignature: string) => { - setState((prev) => { - return { - ...prev, - additional: newSignature, - } - }) - }, []) - - const debouncedAdditionalSignatureUpdate = debounce( - updateAdditionalSignature, - DEBOUNCE_INPUT_TIMER, ) - const updateState = useCallback((newState: typeof state) => { - setState((prev) => { - return { - ...prev, - ...newState, - signature: - newState.type === 'regular' - ? regularSignatureTemplate({ - signatureGroups: newState.regular, - additionalSignature: newState.additional, - }) - : committeeSignatureTemplate({ - signature: newState.committee, - additionalSignature: newState.additional, - }), - } - }) - }, []) - - const debouncedStateUpdate = debounce(updateState, DEBOUNCE_INPUT_TIMER) - - const preview = - selectedTab === 'regular' - ? regularSignatureTemplate({ - signatureGroups: state.regular, - additionalSignature: state.additional, - }) - : committeeSignatureTemplate({ - signature: state.committee, - additionalSignature: state.additional, - }) - - useEffect(() => { - updateHandler() - }, [updateHandler]) + const [selectedTab, setSelectedTab] = useState( + (application.answers?.misc?.signatureType as SignatureType) ?? + SignatureTypes.REGULAR, + ) const tabs = [ { - id: 'regular', + id: SignatureTypes.REGULAR, label: f(signatures.tabs.regular), - content: ( - - ), + content: , }, { - id: 'committee', + id: SignatureTypes.COMMITTEE, label: f(signatures.tabs.committee), - content: ( - - ), + content: , }, ] + const onTabChangeHandler = (tabId: string) => { + if (Object.values(SignatureTypes).includes(tabId as SignatureTypes)) { + setSelectedTab(tabId as SignatureType) + + const currentAnswers = structuredClone(application.answers) + const newAnswers = set( + currentAnswers, + InputFields.misc.signatureType, + tabId, + ) + + updateApplication(newAnswers) + } + } + return ( <> { > { - updateState({ ...state, type: id }) - setValue(InputFields.signature.type, id) - setSelectedTab(id) - }} tabs={tabs} label={f(signatures.general.title)} - /> - diff --git a/libs/application/templates/official-journal-of-iceland/src/fields/Summary.tsx b/libs/application/templates/official-journal-of-iceland/src/fields/Summary.tsx index 8cd186a319f55..975ddc0f87e62 100644 --- a/libs/application/templates/official-journal-of-iceland/src/fields/Summary.tsx +++ b/libs/application/templates/official-journal-of-iceland/src/fields/Summary.tsx @@ -1,81 +1,213 @@ import { useUserInfo } from '@island.is/auth/react' -import { Stack } from '@island.is/island-ui/core' -import { useQuery } from '@apollo/client' -import { Property } from '../components/property/Property' import { - DEPARTMENT_QUERY, - GET_PRICE_QUERY, - TYPE_QUERY, -} from '../graphql/queries' -import { summary } from '../lib/messages' + AlertMessage, + Box, + Bullet, + BulletList, + Stack, + Text, +} from '@island.is/island-ui/core' +import { Property } from '../components/property/Property' +import { advert, error, publishing, summary } from '../lib/messages' import { OJOIFieldBaseProps } from '../lib/types' import { useLocale } from '@island.is/localization' import { MINIMUM_WEEKDAYS } from '../lib/constants' -import { addWeekdays } from '../lib/utils' +import { addWeekdays, parseZodIssue } from '../lib/utils' +import { useCategories } from '../hooks/useCategories' +import { + advertValidationSchema, + publishingValidationSchema, + signatureValidationSchema, +} from '../lib/dataSchema' +import { useApplication } from '../hooks/useUpdateApplication' +import { ZodCustomIssue } from 'zod' +import { useType } from '../hooks/useType' +import { useDepartment } from '../hooks/useDepartment' +import { usePrice } from '../hooks/usePrice' +import { useEffect } from 'react' +import { signatures } from '../lib/messages/signatures' -export const Summary = ({ application }: OJOIFieldBaseProps) => { - const { formatMessage: f, formatDate } = useLocale() +export const Summary = ({ + application, + setSubmitButtonDisabled, +}: OJOIFieldBaseProps) => { + const { formatMessage: f, formatDate, formatNumber } = useLocale() + const { application: currentApplication } = useApplication({ + applicationId: application.id, + }) const user = useUserInfo() - const { answers } = application - - const { data, loading } = useQuery(TYPE_QUERY, { - variables: { - params: { - id: application?.answers?.advert?.type, - }, - }, + const { type, loading: loadingType } = useType({ + typeId: currentApplication.answers.advert?.typeId, }) - const { data: priceData } = useQuery(GET_PRICE_QUERY, { - variables: { id: application.id }, + const { price, loading: loadingPrice } = usePrice({ + applicationId: application.id, }) - const price = - priceData?.officialJournalOfIcelandApplicationGetPrice?.price ?? 0 + const { department, loading: loadingDepartment } = useDepartment({ + departmentId: currentApplication.answers.advert?.departmentId, + }) - const type = data?.officialJournalOfIcelandType?.type?.title + const { categories, loading: loadingCategories } = useCategories() - const { data: department } = useQuery(DEPARTMENT_QUERY, { - variables: { - params: { - id: answers?.advert?.department, - }, - }, - }) + const selectedCategories = categories?.filter((c) => + currentApplication.answers?.advert?.categories?.includes(c.id), + ) const today = new Date() const estimatedDate = addWeekdays(today, MINIMUM_WEEKDAYS) + const advertValidationCheck = advertValidationSchema.safeParse( + currentApplication.answers, + ) + + const signatureValidationCheck = signatureValidationSchema.safeParse({ + signatures: currentApplication.answers.signatures, + misc: currentApplication.answers.misc, + }) + + const publishingCheck = publishingValidationSchema.safeParse( + currentApplication.answers.advert, + ) + + useEffect(() => { + if ( + advertValidationCheck.success && + signatureValidationCheck.success && + publishingCheck.success + ) { + setSubmitButtonDisabled && setSubmitButtonDisabled(false) + } else { + setSubmitButtonDisabled && setSubmitButtonDisabled(true) + } + }, [ + advertValidationCheck, + signatureValidationCheck, + publishingCheck, + setSubmitButtonDisabled, + ]) + return ( - - - - - - - - - c.label) - .join(', ')} - /> - + <> + + + + + + + + + + + c.title).join(', ')} + /> + + ) } diff --git a/libs/application/templates/official-journal-of-iceland/src/graphql/queries.ts b/libs/application/templates/official-journal-of-iceland/src/graphql/queries.ts index 92555efea10d3..49fb20dcc52b1 100644 --- a/libs/application/templates/official-journal-of-iceland/src/graphql/queries.ts +++ b/libs/application/templates/official-journal-of-iceland/src/graphql/queries.ts @@ -242,10 +242,83 @@ export const PDF_URL_QUERY = gql` } ` -export const PDF_QUERY = gql` - query PdfDocument($id: String!) { - officialJournalOfIcelandApplicationGetPdf(id: $id) { - pdf +export const GET_PRESIGNED_URL_MUTATION = gql` + mutation GetPresignedUrl( + $input: OfficialJournalOfIcelandApplicationGetPresignedUrlInput! + ) { + officialJournalOfIcelandApplicationGetPresignedUrl(input: $input) { + url + } + } +` + +export const ADD_APPLICATION_ATTACHMENT_MUTATION = gql` + mutation AddApplicationAttachment( + $input: OfficialJournalOfIcelandApplicationAddApplicationAttachmentInput! + ) { + officialJournalOfIcelandApplicationAddAttachment(input: $input) { + success + } + } +` + +export const GET_APPLICATION_ATTACHMENTS_QUERY = gql` + query OfficialJournalOfIcelandApplicationGetAttachments( + $input: OfficialJournalOfIcelandApplicationGetApplicationAttachmentInput! + ) { + officialJournalOfIcelandApplicationGetAttachments(input: $input) { + attachments { + id + originalFileName + fileName + fileFormat + fileExtension + fileLocation + fileSize + } + } + } +` + +export const DELETE_APPLICATION_ATTACHMENT_MUTATION = gql` + mutation DeleteApplicationAttachment( + $input: OfficialJournalOfIcelandApplicationDeleteApplicationAttachmentInput! + ) { + officialJournalOfIcelandApplicationDeleteAttachment(input: $input) { + success + } + } +` + +export const GET_COMMENTS_QUERY = gql` + query GetComments( + $input: OfficialJournalOfIcelandApplicationGetCommentsInput! + ) { + officialJournalOfIcelandApplicationGetComments(input: $input) { + comments { + id + createdAt + internal + type + caseStatus + state + task { + from + to + title + comment + } + } + } + } +` + +export const POST_COMMENT_MUTATION = gql` + mutation AddComment( + $input: OfficialJournalOfIcelandApplicationPostCommentInput! + ) { + officialJournalOfIcelandApplicationPostComment(input: $input) { + success } } ` diff --git a/libs/application/templates/official-journal-of-iceland/src/hooks/useAdvert.ts b/libs/application/templates/official-journal-of-iceland/src/hooks/useAdvert.ts new file mode 100644 index 0000000000000..a2882dab7a46e --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/hooks/useAdvert.ts @@ -0,0 +1,37 @@ +import { useQuery } from '@apollo/client' +import { ADVERT_QUERY } from '../graphql/queries' +import { + OfficialJournalOfIcelandAdvert, + OfficialJournalOfIcelandAdvertResponse, +} from '@island.is/api/schema' + +type AdvertResponse = { + officialJournalOfIcelandAdvert: OfficialJournalOfIcelandAdvertResponse +} + +type Props = { + advertId: string | undefined | null + onCompleted?: (data: OfficialJournalOfIcelandAdvert) => void +} + +export const useAdvert = ({ advertId, onCompleted }: Props) => { + const { data, error, loading } = useQuery(ADVERT_QUERY, { + skip: !advertId, + variables: { + params: { + id: advertId, + }, + }, + onCompleted: (data) => { + if (onCompleted) { + onCompleted(data.officialJournalOfIcelandAdvert.advert) + } + }, + }) + + return { + advert: data?.officialJournalOfIcelandAdvert, + error, + loading, + } +} diff --git a/libs/application/templates/official-journal-of-iceland/src/hooks/useAdverts.ts b/libs/application/templates/official-journal-of-iceland/src/hooks/useAdverts.ts new file mode 100644 index 0000000000000..561491768aaf2 --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/hooks/useAdverts.ts @@ -0,0 +1,67 @@ +import { useQuery } from '@apollo/client' +import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE } from '../lib/constants' +import { ADVERTS_QUERY } from '../graphql/queries' +import { OfficialJournalOfIcelandAdvertsResponse } from '@island.is/api/schema' + +/** + * Fetches adverts from the API + * @param page - The page number + * @param pageSize - The number of items per page + * @param search - The search query + * @param department - The slug of the deparments to filter by + * @param type - The slug of the types to filter by + * @param category - The slug of the categories to filter by + * @param involvedParty - The slug of the involved parties to filter by + * @param dateFrom - The date to filter from + * @param dateTo - The date to filter to + */ +type Props = { + search?: string + page?: number + pageSize?: number + department?: string[] + type?: string[] + category?: string[] + involvedParty?: string[] + dateFrom?: Date + dateTo?: Date +} + +type AdvertsResponse = { + officialJournalOfIcelandAdverts: OfficialJournalOfIcelandAdvertsResponse +} + +export const useAdverts = ({ + page = DEFAULT_PAGE, + pageSize = DEFAULT_PAGE_SIZE, + search, + department, + type, + category, + involvedParty, + dateFrom, + dateTo, +}: Props) => { + const { data, loading, error } = useQuery(ADVERTS_QUERY, { + variables: { + input: { + page, + search, + pageSize, + department, + type, + category, + involvedParty, + dateFrom, + dateTo, + }, + }, + }) + + return { + adverts: data?.officialJournalOfIcelandAdverts.adverts, + paging: data?.officialJournalOfIcelandAdverts.paging, + loading, + error, + } +} diff --git a/libs/application/templates/official-journal-of-iceland/src/hooks/useCategories.ts b/libs/application/templates/official-journal-of-iceland/src/hooks/useCategories.ts new file mode 100644 index 0000000000000..bb454d63eb5db --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/hooks/useCategories.ts @@ -0,0 +1,26 @@ +import { useQuery } from '@apollo/client' +import { CATEGORIES_QUERY } from '../graphql/queries' +import { OfficialJournalOfIcelandAdvertsCategoryResponse } from '@island.is/api/schema' + +type CategoriesResponse = { + officialJournalOfIcelandCategories: OfficialJournalOfIcelandAdvertsCategoryResponse +} + +export const useCategories = () => { + const { data, loading, error } = useQuery( + CATEGORIES_QUERY, + { + variables: { + params: { + pageSize: 1000, + }, + }, + }, + ) + + return { + categories: data?.officialJournalOfIcelandCategories.categories, + loading, + error, + } +} diff --git a/libs/application/templates/official-journal-of-iceland/src/hooks/useComments.ts b/libs/application/templates/official-journal-of-iceland/src/hooks/useComments.ts index d3d3485748c45..76f99f5c6889e 100644 --- a/libs/application/templates/official-journal-of-iceland/src/hooks/useComments.ts +++ b/libs/application/templates/official-journal-of-iceland/src/hooks/useComments.ts @@ -1,21 +1,69 @@ -import { useQuery } from '@apollo/client' -import { GET_APPLICATION_COMMENTS_QUERY } from '../graphql/queries' +import { useMutation, useQuery } from '@apollo/client' +import { OfficialJournalOfIcelandApplicationGetCommentsResponse } from '@island.is/api/schema' +import { POST_COMMENT_MUTATION, GET_COMMENTS_QUERY } from '../graphql/queries' type Props = { applicationId: string } + +type CommentsResponse = { + officialJournalOfIcelandApplicationGetComments: OfficialJournalOfIcelandApplicationGetCommentsResponse +} + +type AddCommentVariables = { + comment: string +} + +type PostCommentResponse = { + officialJournalOfIcelandApplicationPostComment: { + success: boolean + } +} + export const useComments = ({ applicationId }: Props) => { - const { data, loading, error } = useQuery(GET_APPLICATION_COMMENTS_QUERY, { - variables: { - input: { - id: applicationId, + const { data, loading, error, refetch } = useQuery( + GET_COMMENTS_QUERY, + { + variables: { + input: { + id: applicationId, + }, }, }, + ) + + const [ + addCommentMutation, + { + data: addCommentSuccess, + loading: addCommentLoading, + error: addCommentError, + }, + ] = useMutation(POST_COMMENT_MUTATION, { + onCompleted: () => { + refetch() + }, }) + const addComment = (variables: AddCommentVariables) => { + addCommentMutation({ + variables: { + input: { + id: applicationId, + comment: variables.comment, + }, + }, + }) + } + return { comments: data?.officialJournalOfIcelandApplicationGetComments.comments, loading, error, + addComment, + addCommentLoading, + addCommentError, + addCommentSuccess: + addCommentSuccess?.officialJournalOfIcelandApplicationPostComment.success, } } diff --git a/libs/application/templates/official-journal-of-iceland/src/hooks/useDepartment.ts b/libs/application/templates/official-journal-of-iceland/src/hooks/useDepartment.ts new file mode 100644 index 0000000000000..ee9b89e912204 --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/hooks/useDepartment.ts @@ -0,0 +1,30 @@ +import { useQuery } from '@apollo/client' +import { OfficialJournalOfIcelandAdvertEntity } from '@island.is/api/schema' +import { DEPARTMENT_QUERY } from '../graphql/queries' + +type Props = { + departmentId?: string +} + +type DepartmentResponse = { + department: OfficialJournalOfIcelandAdvertEntity +} + +export const useDepartment = ({ departmentId }: Props) => { + const { data, loading, error } = useQuery<{ + officialJournalOfIcelandDepartment: DepartmentResponse + }>(DEPARTMENT_QUERY, { + skip: !departmentId, + variables: { + params: { + id: departmentId, + }, + }, + }) + + return { + department: data?.officialJournalOfIcelandDepartment?.department, + loading, + error, + } +} diff --git a/libs/application/templates/official-journal-of-iceland/src/hooks/useDepartments.ts b/libs/application/templates/official-journal-of-iceland/src/hooks/useDepartments.ts new file mode 100644 index 0000000000000..dee972356a9be --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/hooks/useDepartments.ts @@ -0,0 +1,26 @@ +import { useQuery } from '@apollo/client' +import { DEPARTMENTS_QUERY } from '../graphql/queries' +import { OfficialJournalOfIcelandAdvertsDepartmentsResponse } from '@island.is/api/schema' + +type DepartmentsResponse = { + officialJournalOfIcelandDepartments: OfficialJournalOfIcelandAdvertsDepartmentsResponse +} + +export const useDepartments = () => { + const { data, loading, error } = useQuery( + DEPARTMENTS_QUERY, + { + variables: { + params: { + page: 1, + }, + }, + }, + ) + + return { + departments: data?.officialJournalOfIcelandDepartments.departments, + loading, + error, + } +} diff --git a/libs/application/templates/official-journal-of-iceland/src/hooks/useFileUpload.ts b/libs/application/templates/official-journal-of-iceland/src/hooks/useFileUpload.ts new file mode 100644 index 0000000000000..836ca2ada8771 --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/hooks/useFileUpload.ts @@ -0,0 +1,227 @@ +import { UploadFile } from '@island.is/island-ui/core' +import { + ADD_APPLICATION_ATTACHMENT_MUTATION, + DELETE_APPLICATION_ATTACHMENT_MUTATION, + GET_APPLICATION_ATTACHMENTS_QUERY, + GET_PRESIGNED_URL_MUTATION, +} from '../graphql/queries' +import { useMutation, useQuery } from '@apollo/client' +import { useState } from 'react' +import { ApplicationAttachmentType } from '../lib/constants' + +/** + * + * @param applicationId id of the application + * @param attachmentType type of the attachment used for constructing the presigned URL key + */ +type UseFileUploadProps = { + applicationId: string + attachmentType: ApplicationAttachmentType +} + +type GetPresignedUrlResponse = { + url: string +} + +type AddAttachmentResponse = { + success: boolean +} + +type ApplicationAttachment = { + id: string + fileName: string + originalFileName: string + fileFormat: string + fileExtension: string + fileLocation: string + fileSize: number +} + +type GetAttachmentsResponse = { + attachments: ApplicationAttachment[] +} + +/** + * Hook for uploading files to S3 + * @param props UseFileUploadProps + * @param props.applicationId id of the application + * @param props.attachmentType type of the attachment used for constructing the presigned URL key + */ +export const useFileUpload = ({ + applicationId, + attachmentType, +}: UseFileUploadProps) => { + const [files, setFiles] = useState([]) + + const [getPresignedUrlMutation] = useMutation<{ + officialJournalOfIcelandApplicationGetPresignedUrl: GetPresignedUrlResponse + }>(GET_PRESIGNED_URL_MUTATION) + + const [addApplicationMutation] = useMutation<{ + officialJournalOfIcelandApplicationAddAttachment: AddAttachmentResponse + }>(ADD_APPLICATION_ATTACHMENT_MUTATION, { + onCompleted() { + refetch() + }, + }) + + const { refetch } = useQuery<{ + officialJournalOfIcelandApplicationGetAttachments: GetAttachmentsResponse + }>(GET_APPLICATION_ATTACHMENTS_QUERY, { + variables: { + input: { + applicationId: applicationId, + attachmentType: attachmentType, + }, + }, + fetchPolicy: 'no-cache', + onCompleted(data) { + const currentFiles = + data.officialJournalOfIcelandApplicationGetAttachments.attachments.map( + (attachment) => + ({ + name: attachment.originalFileName, + size: attachment.fileSize, + type: attachment.fileFormat, + key: attachment.fileLocation, + status: 'done', + } as UploadFile), + ) + setFiles((prevFiles) => [...prevFiles, ...currentFiles]) + }, + onError() { + setFiles([]) + }, + }) + + const [deleteApplicationAttachmentMutation] = useMutation<{ + officialJournalOfIcelandApplicationDeleteAttachment: AddAttachmentResponse + }>(DELETE_APPLICATION_ATTACHMENT_MUTATION, { + onCompleted() { + refetch() + }, + }) + + /** + * + * @param newFiles comes from the onChange function on the fileInput component + */ + const onChange = (newFiles: UploadFile[]) => { + newFiles.forEach(async (file) => { + const type = file?.type?.split('/')[1] + const name = file?.name?.split('.').slice(0, -1).join('.') + + if (!type || !name) { + return + } + + const url = await getPresignedUrl(name, type) + + if (!url) { + file.status = 'error' + return + } + + const loc = new URL(url).pathname + + uploadToS3(url, file as File) + addApplicationAttachments(loc, file as File) + + file.key = loc + + setFiles((prevFiles) => [...prevFiles, file]) + }) + } + + /** + * Deletes the file from the database and S3 + */ + const onRemove = async (file: UploadFile) => { + deleteApplicationAttachmentMutation({ + variables: { + input: { + applicationId: applicationId, + key: file.key, + }, + }, + }) + + setFiles(files.filter((f) => f.key !== file.key)) + } + + /** + * Gets a presigned URL for a file + * @param name name of the file ex. myFile + * @param type type of the file ex. pdf, doc, docx... + * @returns + */ + const getPresignedUrl = async (name: string, type: string) => { + const { data } = await getPresignedUrlMutation({ + variables: { + input: { + attachmentType: attachmentType, + applicationId: applicationId, + fileName: name, + fileType: type, + }, + }, + }) + + return data?.officialJournalOfIcelandApplicationGetPresignedUrl.url + } + + /** + * Uploads a file to S3 using a presigned URL + * Used when a presigned URL has been successfully retrieved + * @param preSignedUrl presigned URL + * @param file file to upload + * @param onSuccess callback function to run on success + */ + const uploadToS3 = async (preSignedUrl: string, file: File) => { + await fetch(preSignedUrl, { + headers: { + 'Content-Type': file.type, + 'Content-Length': file.size.toString(), + }, + method: 'PUT', + body: file, + }) + } + + /** + * Adds a record in the database for the uploaded file with the presigned URL. + * Used after the file has been successfully uploaded to S3 + * @param url presigned URL + * @param file file to upload + */ + const addApplicationAttachments = (url: string, file: UploadFile) => { + const type = file?.type?.split('/')[1] + const name = file?.name?.split('.').slice(0, -1).join('.') + if (!type || !name) { + return + } + + addApplicationMutation({ + variables: { + input: { + applicationId: applicationId, + attachmentType: attachmentType, + fileName: name, + originalFileName: file.name, + fileFormat: type, + fileExtension: type, + fileLocation: url, + fileSize: file.size, + }, + }, + onCompleted() { + file.status = 'done' + }, + onError() { + file.status = 'error' + }, + }) + } + + return { files, onChange, onRemove } +} diff --git a/libs/application/templates/official-journal-of-iceland/src/hooks/usePrice.ts b/libs/application/templates/official-journal-of-iceland/src/hooks/usePrice.ts new file mode 100644 index 0000000000000..03551902f72e7 --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/hooks/usePrice.ts @@ -0,0 +1,24 @@ +import { useQuery } from '@apollo/client' +import { GET_PRICE_QUERY } from '../graphql/queries' +import { OfficialJournalOfIcelandApplicationGetPriceResponse } from '@island.is/api/schema' + +type Props = { + applicationId: string +} + +export const usePrice = ({ applicationId }: Props) => { + const { data, loading, error } = useQuery<{ + officialJournalOfIcelandApplicationGetPrice: OfficialJournalOfIcelandApplicationGetPriceResponse + }>(GET_PRICE_QUERY, { + skip: !applicationId, + variables: { + id: applicationId, + }, + }) + + return { + price: data?.officialJournalOfIcelandApplicationGetPrice.price ?? 0, + loading, + error, + } +} diff --git a/libs/application/templates/official-journal-of-iceland/src/hooks/useType.ts b/libs/application/templates/official-journal-of-iceland/src/hooks/useType.ts new file mode 100644 index 0000000000000..40930127c1c71 --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/hooks/useType.ts @@ -0,0 +1,30 @@ +import { useQuery } from '@apollo/client' +import { TYPE_QUERY } from '../graphql/queries' +import { OfficialJournalOfIcelandAdvertType } from '@island.is/api/schema' + +type Props = { + typeId?: string +} + +type TypeResponse = { + type: OfficialJournalOfIcelandAdvertType +} + +export const useType = ({ typeId }: Props) => { + const { data, loading, error } = useQuery<{ + officialJournalOfIcelandType: TypeResponse + }>(TYPE_QUERY, { + skip: !typeId, + variables: { + params: { + id: typeId, + }, + }, + }) + + return { + type: data?.officialJournalOfIcelandType?.type, + loading, + error, + } +} diff --git a/libs/application/templates/official-journal-of-iceland/src/hooks/useTypes.ts b/libs/application/templates/official-journal-of-iceland/src/hooks/useTypes.ts new file mode 100644 index 0000000000000..61d05bb9a6c7a --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/hooks/useTypes.ts @@ -0,0 +1,45 @@ +import { NetworkStatus, useQuery } from '@apollo/client' +import { OfficialJournalOfIcelandAdvertsTypesResponse } from '@island.is/api/schema' + +import { TYPES_QUERY } from '../graphql/queries' + +type UseTypesParams = { + initalDepartmentId?: string +} + +type TypesResponse = { + officialJournalOfIcelandTypes: OfficialJournalOfIcelandAdvertsTypesResponse +} + +type TypesVariables = { + params: { + department: string + page?: number + pageSize?: number + } +} + +export const useTypes = ({ + initalDepartmentId: departmentId, +}: UseTypesParams) => { + const { data, loading, error, refetch, networkStatus } = useQuery< + TypesResponse, + TypesVariables + >(TYPES_QUERY, { + variables: { + params: { + department: departmentId ?? '', + page: 1, + pageSize: 1000, + }, + }, + notifyOnNetworkStatusChange: true, + }) + + return { + useLazyTypes: refetch, + types: data?.officialJournalOfIcelandTypes.types, + loading: loading || networkStatus === NetworkStatus.refetch, + error, + } +} diff --git a/libs/application/templates/official-journal-of-iceland/src/hooks/useUpdateApplication.ts b/libs/application/templates/official-journal-of-iceland/src/hooks/useUpdateApplication.ts new file mode 100644 index 0000000000000..473c12aac10bf --- /dev/null +++ b/libs/application/templates/official-journal-of-iceland/src/hooks/useUpdateApplication.ts @@ -0,0 +1,79 @@ +import { useMutation, useQuery } from '@apollo/client' +import { + UPDATE_APPLICATION, + APPLICATION_APPLICATION, +} from '@island.is/application/graphql' +import { useLocale } from '@island.is/localization' +import { partialSchema } from '../lib/dataSchema' +import { OJOIApplication } from '../lib/types' +import debounce from 'lodash/debounce' +import { DEBOUNCE_INPUT_TIMER } from '../lib/constants' + +type OJOIUseApplicationParams = { + applicationId?: string +} + +export const useApplication = ({ applicationId }: OJOIUseApplicationParams) => { + const { locale } = useLocale() + + const { + data: application, + loading: applicationLoading, + error: applicationError, + refetch: refetchApplication, + } = useQuery(APPLICATION_APPLICATION, { + variables: { + locale: locale, + input: { + id: applicationId, + }, + }, + }) + + const [ + mutation, + { data: updateData, loading: updateLoading, error: updateError }, + ] = useMutation(UPDATE_APPLICATION) + + const updateApplication = async (input: partialSchema, cb?: () => void) => { + await mutation({ + variables: { + locale, + input: { + id: applicationId, + answers: { + ...input, + }, + }, + }, + }) + + cb && cb() + } + + const debouncedUpdateApplication = debounce( + updateApplication, + DEBOUNCE_INPUT_TIMER, + ) + + const debouncedOnUpdateApplicationHandler = ( + input: partialSchema, + cb?: () => void, + ) => { + debouncedUpdateApplication.cancel() + debouncedUpdateApplication(input, cb) + } + + return { + application: application?.applicationApplication as OJOIApplication, + applicationLoading, + applicationError, + updateData, + updateLoading, + updateError, + isLoading: applicationLoading || updateLoading, + debouncedOnUpdateApplicationHandler, + updateApplication, + refetchApplication, + } +} diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/OJOIApplication.ts b/libs/application/templates/official-journal-of-iceland/src/lib/OJOIApplication.ts index f68c75a2a6e11..0c479ddd9e1f4 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/OJOIApplication.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/OJOIApplication.ts @@ -11,7 +11,7 @@ import { InstitutionNationalIds, defineTemplateApi, } from '@island.is/application/types' -import { dataSchema } from './dataSchema' +import { partialSchema } from './dataSchema' import { general } from './messages' import { TemplateApiActions } from './types' import { Features } from '@island.is/feature-flags' @@ -47,7 +47,7 @@ const OJOITemplate: ApplicationTemplate< translationNamespaces: [ ApplicationConfigurations.OfficialJournalOfIceland.translation, ], - dataSchema: dataSchema, + dataSchema: partialSchema, allowMultipleApplicationsInDraft: true, stateMachineOptions: { actions: { @@ -99,18 +99,6 @@ const OJOITemplate: ApplicationTemplate< status: 'inprogress', progress: 0.66, lifecycle: pruneAfterDays(90), - onEntry: [ - defineTemplateApi({ - action: TemplateApiActions.departments, - externalDataId: 'departments', - order: 1, - }), - defineTemplateApi({ - action: TemplateApiActions.types, - externalDataId: 'types', - order: 2, - }), - ], roles: [ { id: Roles.APPLICANT, diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/constants.ts b/libs/application/templates/official-journal-of-iceland/src/lib/constants.ts index a6ae4f0e79c22..4ee1ecaaf486f 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/constants.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/constants.ts @@ -1,6 +1,6 @@ export const emailRegex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i -export const UPLOAD_ACCEPT = '.pdf' +export const ALLOWED_FILE_TYPES = ['.pdf', '.doc', '.docx'] export const FILE_SIZE_LIMIT = 10000000 @@ -12,12 +12,19 @@ export enum AnswerOption { NO = 'no', } +export enum ApplicationAttachmentType { + ORIGINAL = 'frumrit', + ADDITIONS = 'fylgiskjol', +} + +export const DEFAULT_PAGE = 1 +export const DEFAULT_PAGE_SIZE = 10 + export const MINIMUM_WEEKDAYS = 10 export enum Routes { - TEST = 'test', - COMMENTS = 'comments', REQUIREMENTS = 'requirements', + COMMENTS = 'comments', ADVERT = 'advert', SIGNATURE = 'signature', ATTACHMENTS = 'attachments', @@ -26,6 +33,7 @@ export enum Routes { PUBLISHING = 'publishing', SUMMARY = 'summary', COMPLETE = 'complete', + MISC = 'misc', } // this will be replaced with correct values once the api is ready @@ -38,7 +46,7 @@ export enum TypeIds { } export const MEMBER_INDEX = '{memberIndex}' -export const INSTITUTION_INDEX = '{institutionIndex}' +export const SIGNATURE_INDEX = '{institutionIndex}' export const INTERVAL_TIMER = 3000 export const DEBOUNCE_INPUT_TIMER = 333 @@ -48,66 +56,21 @@ export enum FileNames { ADDITIONS = 'additions', } -export const INITIAL_ANSWERS = { - [Routes.TEST]: { - name: '', - department: '', - job: '', - }, - [Routes.REQUIREMENTS]: { - approveExternalData: false, - }, - [Routes.ADVERT]: { - department: '', - type: '', - subType: '', - title: '', - template: '', - document: '', - }, - [Routes.SIGNATURE]: { - type: 'regular', - signature: '', - regular: [ - { - institution: '', - date: '', - members: [ - { - above: '', - name: '', - below: '', - after: '', - }, - ], - }, - ], - committee: { - institution: '', - date: '', - chairman: { - above: '', - name: '', - after: '', - below: '', - }, - members: [ - { - name: '', - below: '', - }, - ], - }, - additional: '', - }, - [Routes.ATTACHMENTS]: { - files: [], - fileNames: [], - }, - [Routes.PUBLISHING]: { - date: '', - contentCategories: [], - communicationChannels: [], - message: '', - }, +export const OJOI_INPUT_HEIGHT = 64 + +export type SignatureType = 'regular' | 'committee' +export enum SignatureTypes { + REGULAR = 'regular', + COMMITTEE = 'committee', } + +export const ONE = 1 +export const MINIMUM_REGULAR_SIGNATURE_MEMBER_COUNT = 1 +export const DEFAULT_REGULAR_SIGNATURE_MEMBER_COUNT = 1 +export const MAXIMUM_REGULAR_SIGNATURE_MEMBER_COUNT = 10 +export const MINIMUM_REGULAR_SIGNATURE_COUNT = 1 +export const DEFAULT_REGULAR_SIGNATURE_COUNT = 1 +export const MAXIMUM_REGULAR_SIGNATURE_COUNT = 3 +export const MINIMUM_COMMITTEE_SIGNATURE_MEMBER_COUNT = 2 +export const DEFAULT_COMMITTEE_SIGNATURE_MEMBER_COUNT = 2 +export const MAXIMUM_COMMITTEE_SIGNATURE_MEMBER_COUNT = 10 diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/dataSchema.ts b/libs/application/templates/official-journal-of-iceland/src/lib/dataSchema.ts index 92cfcb487db0d..b4d6f02e29ad6 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/dataSchema.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/dataSchema.ts @@ -1,197 +1,330 @@ import { z } from 'zod' import { error } from './messages' -import { InputFields } from './types' -import { - TypeIds, - INSTITUTION_INDEX, - MEMBER_INDEX, - FileNames, - AnswerOption, -} from './constants' - -const FileSchema = z.object({ - name: z.string(), - key: z.string(), - url: z.string().optional(), -}) +import { AnswerOption, SignatureTypes } from './constants' +import { institution } from '../components/signatures/Signatures.css' +import { MessageDescriptor } from 'react-intl' + +export const memberItemSchema = z + .object({ + name: z.string().optional(), + before: z.string().optional(), + below: z.string().optional(), + above: z.string().optional(), + after: z.string().optional(), + }) + .partial() + +export const membersSchema = z.array(memberItemSchema).optional() + +export const regularSignatureItemSchema = z + .object({ + date: z.string().optional(), + institution: z.string().optional(), + members: membersSchema.optional(), + html: z.string().optional(), + }) + .partial() + +export const regularSignatureSchema = z + .array(regularSignatureItemSchema) + .optional() + +export const signatureInstitutionSchema = z.enum(['institution', 'date']) + +export const committeeSignatureSchema = regularSignatureItemSchema + .extend({ + chairman: memberItemSchema.optional(), + }) + .partial() + +export const channelSchema = z + .object({ + email: z.string(), + phone: z.string(), + }) + .partial() + +const advertSchema = z + .object({ + departmentId: z.string().optional(), + typeId: z.string().optional(), + title: z.string().optional(), + html: z.string().optional(), + requestedDate: z.string().optional(), + categories: z.array(z.string()).optional(), + channels: z.array(channelSchema).optional(), + message: z.string().optional(), + }) + .partial() -const getPath = (path: string) => path.split('.').slice(1) +const miscSchema = z + .object({ + signatureType: z.string().optional(), + selectedTemplate: z.string().optional(), + }) + .partial() -export const dataSchema = z.object({ +export const partialSchema = z.object({ requirements: z .object({ approveExternalData: z.string(), }) .refine((schema) => schema.approveExternalData === AnswerOption.YES, { params: error.dataGathering, - path: getPath(InputFields.requirements.approveExternalData), + path: ['approveExternalData'], }), - advert: z + advert: advertSchema.optional(), + signatures: z .object({ - department: z.string().optional(), - type: z.string().optional(), - title: z.string().optional(), - document: z.string().optional(), - template: z.string().optional(), - subType: z.string().optional(), + additionalSignature: z.object({ + committee: z.string().optional(), + regular: z.string().optional(), + }), + regular: z.array(regularSignatureItemSchema).optional(), + committee: committeeSignatureSchema.optional(), }) - .superRefine((advert, ctx) => { - if (advert.type === TypeIds.REGLUGERDIR) { - if (!advert.subType) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - params: error.emptyFieldError, - path: getPath(InputFields.advert.subType), - }) - } - } + .partial() + .optional(), + misc: miscSchema.optional(), +}) + +// We make properties optional to throw custom error messages +export const advertValidationSchema = z.object({ + advert: z.object({ + departmentId: z + .string() + .optional() + .refine((value) => value && value.length > 0, { + params: error.missingDepartment, + }), + typeId: z + .string() + .optional() + .refine((value) => value && value.length > 0, { + params: error.missingType, + }), + title: z + .string() + .optional() + .refine((value) => value && value.length > 0, { + params: error.missingTitle, + }), + html: z + .string() + .optional() + .refine((value) => value && value.length > 0, { + params: error.missingHtml, + }), + }), +}) + +export const publishingValidationSchema = z.object({ + requestedDate: z + .string() + .optional() + .refine((value) => value && value.length > 0, { + // TODO: Add date validation + params: error.missingRequestedDate, }), - signature: z - .object({ - type: z.string().optional(), - signature: z.string().optional(), - regular: z - .array( - z.object({ - institution: z.string(), - date: z.string(), - members: z.array( - z.object({ - above: z.string(), - name: z.string(), - below: z.string(), - after: z.string(), - }), - ), - }), - ) - .optional(), - committee: z + categories: z + .array(z.string()) + .optional() + .refine((value) => Array.isArray(value) && value.length > 0, { + params: error.noCategorySelected, + }), +}) + +export const signatureValidationSchema = z + .object({ + signatures: z.object({ + additionalSignature: z .object({ - institution: z.string(), - date: z.string(), - chairman: z.object({ - above: z.string(), - name: z.string(), - below: z.string(), - after: z.string(), - }), - members: z.array( - z.object({ - name: z.string(), - below: z.string(), - }), - ), + committee: z.string().optional(), + regular: z.string().optional(), }) .optional(), - additional: z.string().optional(), + regular: z.array(regularSignatureItemSchema).optional(), + committee: committeeSignatureSchema.optional(), + }), + misc: miscSchema.optional(), + }) + .superRefine((schema, context) => { + const signatureType = schema.misc?.signatureType + + if (!signatureType) { + context.addIssue({ + code: z.ZodIssueCode.custom, + params: error.missingSignatureType, + path: ['misc', 'signatureType'], + }) + } + + let hasRegularIssues = false + let hasCommitteeIssues = false + + if (signatureType === SignatureTypes.REGULAR) { + hasRegularIssues = validateRegularSignature( + schema.signatures.regular, + context, + ) + } + + if (signatureType === SignatureTypes.COMMITTEE) { + hasCommitteeIssues = validateCommitteeSignature( + schema.signatures.committee as z.infer, + context, + ) + } + + if (!hasRegularIssues && !hasCommitteeIssues) { + return false + } + + return true + }) + +const validateMember = ( + schema: z.infer, + context: z.RefinementCtx, + params?: MessageDescriptor, +) => { + if (!schema || !schema.name) { + context.addIssue({ + code: z.ZodIssueCode.custom, + params: params ? params : error.missingSignatureMember, + }) + + return false + } + + return true +} + +const validateInstitutionAndDate = ( + institution: string | undefined, + date: string | undefined, + context: z.RefinementCtx, +) => { + if (!institution) { + context.addIssue({ + code: z.ZodIssueCode.custom, + params: error.missingSignatureInstitution, }) - .superRefine((signature, ctx) => { - switch (signature.type) { - case 'regular': - signature.regular?.forEach((institution, index) => { - // required fields are institution, date, member.name - - if (!institution.institution) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - params: error.emptyFieldError, - path: InputFields.signature.regular.institution - .replace(INSTITUTION_INDEX, `${index}`) - .split('.') - .slice(1), - }) - } - - if (!institution.date) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - params: error.emptyFieldError, - path: InputFields.signature.regular.date - .replace(INSTITUTION_INDEX, `${index}`) - .split('.') - .slice(1), - }) - } - - institution.members?.forEach((member, memberIndex) => { - if (!member.name) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - params: error.emptyFieldError, - path: InputFields.signature.regular.members.name - .replace(INSTITUTION_INDEX, `${index}`) - .replace(MEMBER_INDEX, `${memberIndex}`) - .split('.') - .slice(1), - }) - } - }) - }) - - break - case 'committee': - // required fields are institution, date, chairman.name, members.name - - if (!signature.committee?.institution) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - params: error.emptyFieldError, - path: getPath(InputFields.signature.committee.institution), - }) - } - - if (!signature.committee?.date) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - params: error.emptyFieldError, - path: getPath(InputFields.signature.committee.date), - }) - } - - if (!signature.committee?.chairman.name) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - params: error.emptyFieldError, - path: getPath(InputFields.signature.committee.chairman.name), - }) - } - - signature.committee?.members?.forEach((member, index) => { - if (!member.name) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - params: error.emptyFieldError, - path: InputFields.signature.committee.members.name - .replace(MEMBER_INDEX, `${index}`) - .split('.') - .slice(1), - }) - } - }) - break + + return false + } + + if (!date) { + context.addIssue({ + code: z.ZodIssueCode.custom, + params: error.missingSignatureDate, + }) + + return false + } + + return true +} + +const validateRegularSignature = ( + schema: z.infer, + context: z.RefinementCtx, +) => { + if (!schema || (Array.isArray(schema) && schema.length === 0)) { + context.addIssue({ + code: z.ZodIssueCode.custom, + params: error.signaturesValidationError, + }) + + return false + } + + const validSignatures = schema + ?.map((signature) => { + // institution and date are required + let hasValidInstitutionAndDate = true + let hasValidMembers = true + + hasValidInstitutionAndDate = validateInstitutionAndDate( + signature.institution, + signature.date, + context, + ) + + if (!signature.members && !Array.isArray(signature.members)) { + context.addIssue({ + code: z.ZodIssueCode.custom, + params: error.noSignatureMembers, + }) } - }), - attachments: z.object({ - files: z.array(FileSchema), - fileNames: z.enum([FileNames.ADDITIONS, FileNames.DOCUMENT]), - }), - publishing: z.object({ - date: z.string().optional(), - contentCategories: z.array( - z.object({ - label: z.string(), - value: z.string(), - }), - ), - communicationChannels: z.array( - z.object({ - email: z.string(), - phone: z.string(), - }), - ), - message: z.string().optional(), - }), -}) -export type answerSchemas = z.infer + hasValidMembers = + signature.members + ?.map((member) => validateMember(member, context)) + .every((isValid) => isValid) ?? false + + return hasValidInstitutionAndDate && hasValidMembers + }) + .every((isValid) => isValid) + + return validSignatures +} + +const validateCommitteeSignature = ( + schema: z.infer, + context: z.RefinementCtx, +) => { + if (!schema) { + context.addIssue({ + code: z.ZodIssueCode.custom, + params: error.signaturesValidationError, + }) + } + + let hasValidInstitutionAndDate = true + let hasValidChairman = true + let hasValidMembers = true + + hasValidInstitutionAndDate = validateInstitutionAndDate( + schema.institution, + schema.date, + context, + ) + + hasValidChairman = validateMember( + schema.chairman as z.infer, + context, + error.missingChairmanName, + ) + + hasValidMembers = + schema.members + ?.map((member) => + validateMember(member, context, error.missingCommitteeMemberName), + ) + .every((isValid) => isValid) ?? false + + return hasValidInstitutionAndDate && hasValidChairman && hasValidMembers +} + +type Flatten = T extends any[] ? T[number] : T + +type MapProps = { + [K in keyof T]: T[K] +} + +export type partialSchema = z.infer + +export type partialRegularSignatureSchema = Flatten< + z.infer +> + +export type partialCommitteeSignatureSchema = MapProps< + z.infer +> + +export type validationSchema = z.infer + +export const signatureProperties = committeeSignatureSchema.keyof() + +export const sharedSignatureProperties = signatureProperties diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/messages/advert.ts b/libs/application/templates/official-journal-of-iceland/src/lib/messages/advert.ts index ac24eaca9956f..fcbcd5e3be036 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/messages/advert.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/messages/advert.ts @@ -10,7 +10,7 @@ export const advert = { intro: { id: 'ojoi.application:advert.general.intro', defaultMessage: - 'Veldu deild og tegund birtingar í fellilistanum hér að neðan og skráðu heiti auglýsingar í viðeigandi reit. Tegundarheitið birtist sjálfkrafa í hástöfum í fyrirsögn og titillinn í næstu línu. Efni auglýsinga er sett í ritilinn hér að neðan og skal vanda alla uppsetningu, setja inn töluliði, töflur o.þ.h. Til einföldunar við vinnslu meginmáls getur þú valið sniðmát og aðlagað það að þinni auglýsingu eða sótt eldri auglýsingu og breytt henni.', + 'Veldu deild og tegund birtingar í fellilistanum hér að neðan og skráðu heiti innsendingar í viðeigandi reit. Tegundarheitið birtist sjálfkrafa í hástöfum í fyrirsögn og titillinn í næstu línu. Efni innsendingar er sett í ritilinn hér að neðan og skal vanda alla uppsetningu, setja inn töluliði, töflur o.þ.h. Til einföldunar við vinnslu meginmáls getur þú valið sniðmát og aðlagað það að þinni innsendingu eða sótt eldri innsendingar og breytt henni.', description: 'Intro of the advert form', }, section: { @@ -73,12 +73,12 @@ export const advert = { title: defineMessages({ label: { id: 'ojoi.application:advert.inputs.title.label', - defaultMessage: 'Heiti auglýsingar', + defaultMessage: 'Titill innsendingar', description: 'Label for the title input', }, placeholder: { id: 'ojoi.application:advert.inputs.title.placeholder', - defaultMessage: 'Skráðu heiti auglýsinga', + defaultMessage: 'Skráðu heiti innsendingar', description: 'Placeholder for the title input', }, }), @@ -90,7 +90,7 @@ export const advert = { }, placeholder: { id: 'ojoi.application:advert.inputs.template.placeholder', - defaultMessage: 'Fyrirmynd auglýsinga', + defaultMessage: 'Fyrirmynd innsendingar', description: 'Placeholder for the template input', }, }), diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/messages/comments.ts b/libs/application/templates/official-journal-of-iceland/src/lib/messages/comments.ts index 80697969e7026..c204aad333216 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/messages/comments.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/messages/comments.ts @@ -19,26 +19,26 @@ export const comments = { description: 'Title of comments section', }, }), - errors: defineMessages({ - fetchComments: { - id: 'ojoi.application:comments.errors.fetchComments', - defaultMessage: 'Villa kom upp við að sækja athugasemdir', - description: 'Error fetching comments', + warnings: defineMessages({ + noCommentsTitle: { + id: 'ojoi.application:comments.warnings.noComments', + defaultMessage: 'Engar athugasemdir', + description: 'No comments', }, - fetchCommentsMessage: { - id: 'ojoi.application:comments.errors.fetchCommentsMessage', - defaultMessage: 'Ekki tókst að sækja athugasemdir, reynið aftur síðar', - description: 'Error fetching comments message', + noCommentsMessage: { + id: 'ojoi.application:comments.warnings.noCommentsMessage', + defaultMessage: 'Engar athugasemdir eru skráðar á þessa innsendingu.', + description: 'No comments message', }, - addComment: { - id: 'ojoi.application:comments.errors.addComment', + postCommentFailedTitle: { + id: 'ojoi.application:comments.warnings.postCommentFailedTitle', defaultMessage: 'Ekki tókst að vista athugasemd', - description: 'Error adding comment', + description: 'Post comment failed title', }, - emptyComments: { - id: 'ojoi.application:comments.errors.emptyComments', - defaultMessage: 'Engar athugasemdir eru á þessari umsókn', - description: 'No comments on this application', + postCommentFailedMessage: { + id: 'ojoi.application:comments.warnings.postCommentFailedMessage', + defaultMessage: 'Ekki tókst að vista athugasemd, reyndu aftur síðar.', + description: 'Post comment failed message', }, }), dates: defineMessages({ diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/messages/error.ts b/libs/application/templates/official-journal-of-iceland/src/lib/messages/error.ts index 567d8088de0c8..69b527a4b5e1a 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/messages/error.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/messages/error.ts @@ -1,11 +1,152 @@ import { defineMessages } from 'react-intl' export const error = defineMessages({ + fetchCommentsFailedTitle: { + id: 'ojoi.application:error.fetchCommentsFailedTitle', + defaultMessage: 'Ekki tókst að sækja athugasemdir', + description: 'Error message when fetching comments fails', + }, + fetchCommentsFailedMessage: { + id: 'ojoi.application:error.fetchCommentsFailedMessage', + defaultMessage: + 'Villa kom upp við að sækja athugasemdir, reyndu aftur síðar', + description: 'Error message when fetching comments fails', + }, + fetchAdvertFailed: { + id: 'ojoi.application:error.fetchAdvertFailed', + defaultMessage: 'Ekki tókst að sækja auglýsingu', + description: 'Error message when fetching advert fails', + }, + fetchAdvertFailedMessage: { + id: 'ojoi.application:error.fetchAdvertFailedMessage', + defaultMessage: 'Villa kom upp við að sækja auglýsingu, reyndu aftur síðar', + description: 'Error message when fetching advert fails', + }, + fetchApplicationFailedTitle: { + id: 'ojoi.application:error.fetchApplicationFailedTitle', + defaultMessage: 'Ekki tókst að sækja umsókn', + description: 'Error message when fetching application fails', + }, + fetchApplicationFailedMessage: { + id: 'ojoi.application:error.fetchApplicationFailedMessage', + defaultMessage: 'Villa kom upp við að sækja umsókn, reyndu aftur síðar', + description: 'Error message when fetching application fails', + }, + missingChairmanName: { + id: 'ojoi.application:error.missingChairmanName', + defaultMessage: 'Nafn formanns vantar', + description: 'Error message when chairman name is missing', + }, + missingCommitteeMemberName: { + id: 'ojoi.application:error.missingCommitteeMemberName', + defaultMessage: 'Nafn nefndarmanns vantar', + description: 'Error message when committee member name is missing', + }, + missingSignatureInstitution: { + id: 'ojoi.application:error.missingSignatureInstitution', + defaultMessage: 'Nafn stofnunar vantar', + description: 'Error message when signature institution is missing', + }, + missingSignatureDate: { + id: 'ojoi.application:error.missingSignatureDate', + defaultMessage: 'Dagsetning undirskriftar vantar', + description: 'Error message when signature date is missing', + }, + missingSignatureType: { + id: 'ojoi.application:error.missingSignatureType', + defaultMessage: 'Tegund undirskriftar vantar', + description: 'Error message when signature type is missing', + }, + missingFieldsTitle: { + id: 'ojoi.application:error.missingFieldsTitle', + defaultMessage: 'Fylla þarf út eftirfarandi reiti í {x}', + description: 'Error message when fields are missing', + }, + missingSignatureFieldsMessage: { + id: 'ojoi.application:error.missingSignatureFieldsMessage', + defaultMessage: 'Undirritunarkafli er undir {x}', + description: 'Error message when signature fields are missing', + }, + noSignatureMembers: { + id: 'ojoi.application:error.noSignatureMembers', + defaultMessage: 'Engin undirskriftarmenn valdir', + description: 'Error message when no signature members are selected', + }, + missingSignatureMember: { + id: 'ojoi.application:error.missingSignatureMember', + defaultMessage: 'Nafn undirskriftar meðlims vantar', + description: 'Error message when signature member is missing', + }, + noCategorySelected: { + id: 'ojoi.application:error.noCategorySelected', + defaultMessage: 'Enginn efnisflokkur valinn, vinsamlegast veldu efnisflokk', + description: 'Error message when no category is selected', + }, + missingType: { + id: 'ojoi.application:error.missingType', + defaultMessage: 'Velja þarf tegund innsendingar', + description: 'Error message when type is missing', + }, + missingDepartment: { + id: 'ojoi.application:error.missingDepartment', + defaultMessage: 'Velja þarf deild innsendingar', + description: 'Error message when department is missing', + }, + missingTitle: { + id: 'ojoi.application:error.missingTitle', + defaultMessage: 'Fylla þarf út titill innsendingar', + description: 'Error message when title is missing', + }, + missingHtml: { + id: 'ojoi.application:error.missingHtml', + defaultMessage: 'Innihald innsendingar má ekki vera autt', + description: 'Error message when html is missing', + }, + missingHtmlMessage: { + id: 'ojoi.application:error.missingHtmlMessage', + defaultMessage: 'Innsending samanstendur af eftirfarandi reitum', + description: 'Error message when html is missing', + }, + missingRequestedDate: { + id: 'ojoi.application:error.missingRequestedDate', + defaultMessage: 'Útgáfudagsetning má ekki vera tóm', + description: 'Error message when requested date is missing', + }, + applicationValidationError: { + id: 'ojoi.application:error.applicationValidationError', + defaultMessage: 'Umsókn er ekki rétt útfyllt', + description: 'Error message when application is not valid', + }, + signaturesValidationError: { + id: 'ojoi.application:error.signaturesValidationError', + defaultMessage: 'Undirskriftir eru ekki réttar', + description: 'Error message when signatures are not valid', + }, dataSubmissionErrorTitle: { id: 'ojoi.application:error.dataSubmissionErrorTitle', defaultMessage: 'Villa kom upp við vistun gagna', description: 'Error message when data is not submitted', }, + fetchXFailedTitle: { + id: 'ojoi.application:error.fetchXFailedTitle', + defaultMessage: 'Ekki tókst að sækja {x}', + description: 'Error message when fetching x fails', + }, + fetchXFailedMessage: { + id: 'ojoi.application:error.fetchXFailedMessage', + defaultMessage: 'Villa kom upp við að sækja {x}', + description: 'Error message when fetching x fails', + }, + fetchFailedTitle: { + id: 'ojoi.application:error.fetchFailedTitle', + defaultMessage: 'Ekki tókst að sækja gögn', + description: 'Error message when fetching fails', + }, + fetchFailedMessage: { + id: 'ojoi.application:error.fetchFailedMessage', + defaultMessage: 'Villa kom upp við að sækja gögn', + description: 'Error message when fetching fails', + }, xIsNotValid: { id: 'ojoi.application:error.xIsNotValid', defaultMessage: '{x} er ekki gilt', diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/messages/index.ts b/libs/application/templates/official-journal-of-iceland/src/lib/messages/index.ts index a61e3a9cdf369..125c0744b8c7c 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/messages/index.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/messages/index.ts @@ -9,3 +9,5 @@ export * from './requirements' export * from './preview' export * from './publishing' export * from './summary' +export * from './signatures' +export * from './comments' diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/messages/preview.ts b/libs/application/templates/official-journal-of-iceland/src/lib/messages/preview.ts index d0c399b4025a7..7f12cc6c39871 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/messages/preview.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/messages/preview.ts @@ -19,6 +19,13 @@ export const preview = { description: 'Title of the preview section', }, }), + errors: defineMessages({ + noContent: { + id: 'ojoi.application:preview.errors.noContent', + defaultMessage: 'Innihald innsendingar er ekki útfyllt', + description: 'Error message when content is missing', + }, + }), buttons: defineMessages({ fetchPdf: { id: 'ojoi.application:preview.buttons.fetchPdf', diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/messages/publishing.ts b/libs/application/templates/official-journal-of-iceland/src/lib/messages/publishing.ts index f52271e8c72a6..5e4ccbb90bc1f 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/messages/publishing.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/messages/publishing.ts @@ -68,6 +68,11 @@ export const publishing = { defaultMessage: 'Efnisflokkar', description: 'Label of the content categories input', }, + placeholder: { + id: 'ojoi.application:publishing.inputs.contentCategories.placeholder', + defaultMessage: 'Veldu efnisflokka', + description: 'Placeholder of the content categories input', + }, }), messages: defineMessages({ label: { diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/messages/signatures.ts b/libs/application/templates/official-journal-of-iceland/src/lib/messages/signatures.ts index 037c9a4846375..3c82160a966c2 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/messages/signatures.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/messages/signatures.ts @@ -13,6 +13,11 @@ export const signatures = { 'Hér má velja þá uppsetningu undirrritana sem best á við. Mikilvægt er að tryggja samræmi við frumtexta, til dæmis varðandi stað og dagsetningu.', description: 'Intro of the signatures section', }, + section: { + id: 'ojoi.application:signatures.general.section', + defaultMessage: 'Undirritunarkafl{abbreviation}', + description: 'Title of the signatures section', + }, }), headings: defineMessages({ signedBy: { @@ -84,7 +89,7 @@ export const signatures = { }, placeholder: { id: 'ojoi.application:signatures.inputs.institution.placeholder', - defaultMessage: 'Veldu stofnun', + defaultMessage: 'Nafn stofnunar eða staðsetning', description: 'Placeholder for the institution input', }, }), diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/messages/summary.ts b/libs/application/templates/official-journal-of-iceland/src/lib/messages/summary.ts index 83240b61b3b35..33e74bc4896cf 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/messages/summary.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/messages/summary.ts @@ -32,7 +32,7 @@ export const summary = { }, title: { id: 'ojoi.application:summary.properties.title', - defaultMessage: 'Heiti auglýsingar', + defaultMessage: 'Heiti innsendingar', description: 'Title of the advertisement', }, department: { diff --git a/libs/application/templates/official-journal-of-iceland/src/lib/types.ts b/libs/application/templates/official-journal-of-iceland/src/lib/types.ts index d118cb9707ed4..ab0c6d2a68a37 100644 --- a/libs/application/templates/official-journal-of-iceland/src/lib/types.ts +++ b/libs/application/templates/official-journal-of-iceland/src/lib/types.ts @@ -1,98 +1,50 @@ import { Application, FieldBaseProps } from '@island.is/application/types' -import { type answerSchemas } from './dataSchema' -import { INSTITUTION_INDEX, MEMBER_INDEX, Routes } from './constants' +import { Routes } from './constants' import { - OfficialJournalOfIcelandAdvert, OfficialJournalOfIcelandAdvertEntity, OfficialJournalOfIcelandPaging, } from '@island.is/api/schema' +import { partialSchema } from './dataSchema' export const InputFields = { - [Routes.TEST]: { - name: 'test.name', - department: 'test.department', - job: 'test.job', - }, [Routes.REQUIREMENTS]: { approveExternalData: 'requirements.approveExternalData', }, [Routes.ADVERT]: { - department: 'advert.department', - type: 'advert.type', - subType: 'advert.subType', + departmentId: 'advert.departmentId', + typeId: 'advert.typeId', title: 'advert.title', - template: 'advert.template', - document: 'advert.document', + html: 'advert.html', + requestedDate: 'advert.requestedDate', + categories: 'advert.categories', + channels: 'advert.channels', + message: 'advert.message', }, [Routes.SIGNATURE]: { - type: 'signature.type', - contents: 'signature.contents', - regular: { - institution: `signature.regular-${INSTITUTION_INDEX}.institution`, - date: `signature.regular-${INSTITUTION_INDEX}.date`, - members: { - above: `signature.regular-${INSTITUTION_INDEX}.members-${MEMBER_INDEX}.above`, - name: `signature.regular-${INSTITUTION_INDEX}.members-${MEMBER_INDEX}.name`, - below: `signature.regular-${INSTITUTION_INDEX}.members-${MEMBER_INDEX}.below`, - after: `signature.regular-${INSTITUTION_INDEX}.members-${MEMBER_INDEX}.after`, - }, - }, - committee: { - institution: 'signature.committee.institution', - date: 'signature.committee.date', - chairman: { - above: 'signature.committee.chairman.above', - name: 'signature.committee.chairman.name', - after: 'signature.committee.chairman.after', - below: 'signature.committee.chairman.below', - }, - members: { - name: `signature.committee.members-${MEMBER_INDEX}.name`, - below: `signature.committee.members-${MEMBER_INDEX}.below`, - }, + regular: 'signatures.regular', + committee: 'signatures.committee', + additionalSignature: { + regular: 'signatures.additionalSignature.regular', + committee: 'signatures.additionalSignature.committee', }, - additonalSignature: 'signature.additonalSignature', - }, - [Routes.ATTACHMENTS]: { - files: 'additionsAndDocuments.files', - fileNames: 'additionsAndDocuments.fileNames', }, - [Routes.ORIGINAL]: { - files: 'original.files', + [Routes.MISC]: { + signatureType: 'misc.signatureType', + selectedTemplate: 'misc.selectedTemplate', }, - [Routes.PUBLISHING]: { - date: 'publishing.date', - fastTrack: 'publishing.fastTrack', - contentCategories: 'publishing.contentCategories', - communicationChannels: 'publishing.communicationChannels', - message: 'publishing.message', - }, -} - -export type LocalError = { - type: string - message: string } -type Option = { - id: string - title: string - slug: string -} - -export type AdvertOption = { - [key in Key]: Array