From d8bb4e34d36caf1fca3b10b967397cc570c40d2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikko=20Haapam=C3=A4ki?= Date: Tue, 8 Mar 2022 13:51:48 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Aloituskuulutuksen=20lataaminen=20kansa?= =?UTF-8?q?laisen=20k=C3=A4ytt=C3=B6liittym=C3=A4st=C3=A4=20(#135)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/__snapshots__/api.test.ts.snap | 66 +++ backend/integrationtest/api/api.test.ts | 27 +- backend/src/asiakirja/abstractPdf.ts | 2 +- backend/src/config.ts | 1 + backend/src/email/emailTemplates.ts | 2 +- backend/src/endDateCalculator/bankHolidays.ts | 5 +- .../endDateCalculatorHandler.ts | 30 +- backend/src/files/fileService.ts | 16 +- backend/src/handler/projektiAdapter.ts | 23 +- .../src/handler/projektiAdapterJulkinen.ts | 124 ++++++ backend/src/handler/projektiHandler.ts | 54 ++- backend/src/util/dateUtil.ts | 21 +- .../__snapshots__/apiHandler.test.ts.snap | 69 ++++ backend/test/apiHandler.test.ts | 13 + .../endDateCalculator.test.ts | 12 + backend/test/fixture/userFixture.ts | 11 +- common/abstractApi.ts | 7 + deployment/bin/runGraphQLIntrospection.ts | 34 ++ deployment/lib/config.ts | 4 +- deployment/lib/hassu-backend.ts | 11 +- deployment/lib/hassu-pipeline.ts | 2 +- graphql/operations.graphql | 2 +- graphql/types.graphql | 24 +- package-lock.json | 55 +++ package.json | 4 +- src/components/FormatDate.tsx | 16 + src/components/form/DatePicker.tsx | 3 +- src/components/notification/Notification.tsx | 3 + .../projekti/ProjektiKuulutuskielet.tsx | 2 +- .../ProjektiSunnittelusopimusTiedot.tsx | 2 +- .../AloituskuulutusLukunakyma.tsx | 66 +-- .../AloituskuulutusPDFEsikatselu.tsx | 2 +- .../AloituskuulutusTiedostot.tsx | 36 +- .../aloituskuulutus/aloitusKuulutusUtil.ts | 21 + src/hooks/useProjektiJulkinen.tsx | 13 + .../suunnitelma/[oid]/aloituskuulutus.tsx | 110 +++++ .../projekti/[oid]/aloituskuulutus.tsx | 10 +- src/pages/yllapito/projekti/[oid]/index.tsx | 2 +- src/schemas/aloituskuulutus.ts | 37 +- src/services/api/commonApi.ts | 12 +- src/services/api/developerApi.ts | 42 +- src/services/api/fragmentTypes.json | 382 ++++++++++++++++++ src/services/config.ts | 1 + src/stories/Notification.stories.tsx | 7 + .../notification/Notification.module.css | 8 + tailwind.config.js | 2 + 46 files changed, 1240 insertions(+), 156 deletions(-) create mode 100644 backend/integrationtest/api/__snapshots__/api.test.ts.snap create mode 100644 backend/src/handler/projektiAdapterJulkinen.ts create mode 100755 deployment/bin/runGraphQLIntrospection.ts create mode 100644 src/components/FormatDate.tsx create mode 100644 src/components/projekti/aloituskuulutus/aloitusKuulutusUtil.ts create mode 100644 src/hooks/useProjektiJulkinen.tsx create mode 100644 src/pages/suunnitelma/[oid]/aloituskuulutus.tsx create mode 100644 src/services/api/fragmentTypes.json create mode 100644 src/services/config.ts diff --git a/backend/integrationtest/api/__snapshots__/api.test.ts.snap b/backend/integrationtest/api/__snapshots__/api.test.ts.snap new file mode 100644 index 000000000..f376528f6 --- /dev/null +++ b/backend/integrationtest/api/__snapshots__/api.test.ts.snap @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Api should search, load and save a project 1`] = ` +Object { + "__typename": "ProjektiJulkinen", + "aloitusKuulutusJulkaisut": Array [ + Object { + "__typename": "AloitusKuulutusJulkaisuJulkinen", + "aloituskuulutusPDFt": Object { + "SUOMI": Object { + "aloituskuulutusIlmoitusPDFPath": "/tiedostot/suunnitelma/1.2.246.578.5.1.2978288874.2711575506/aloituskuulutus/ILMOITUS TOIMIVALTAISEN VIRANOMAISEN KUULUTUKSESTA HASSU AUTOMAATTITESTIPROJEKTI1.pdf", + "aloituskuulutusPDFPath": "/tiedostot/suunnitelma/1.2.246.578.5.1.2978288874.2711575506/aloituskuulutus/KUULUTUS SUUNNITTELUN ALOITTAMISESTA HASSU AUTOMAATTITESTIPROJEKTI1.pdf", + }, + "__typename": "AloitusKuulutusPDFt", + }, + "elyKeskus": "Pirkanmaa", + "hankkeenKuvaus": Object { + "RUOTSI": "På Svenska", + "SAAME": "Saameksi", + "SUOMI": "Lorem Ipsum", + "__typename": "HankkeenKuvaukset", + }, + "kielitiedot": Object { + "__typename": "Kielitiedot", + "ensisijainenKieli": "SUOMI", + }, + "kuulutusPaiva": "2022-01-02", + "siirtyySuunnitteluVaiheeseen": "2022-01-01", + "suunnitteluSopimus": undefined, + "velho": Object { + "__typename": "Velho", + "kunnat": Array [ + "Helsinki", + " Vantaa", + ], + "nimi": "HASSU AUTOMAATTITESTIPROJEKTI1", + "tilaajaOrganisaatio": "Väylävirasto", + "tyyppi": "TIE", + "vaylamuoto": Array [ + "tie", + ], + }, + "yhteystiedot": Array [ + Object { + "__typename": "Yhteystieto", + "etunimi": "A-tunnus1", + "organisaatio": "CGI Suomi Oy", + "puhelinnumero": "123", + "sahkoposti": "mikko.haapamki@cgi.com", + "sukunimi": "Hassu", + }, + Object { + "__typename": "Yhteystieto", + "etunimi": "Marko", + "organisaatio": "Kajaani", + "puhelinnumero": "0293121213", + "sahkoposti": "markku.koi@koi.com", + "sukunimi": "Koi", + }, + ], + }, + ], + "euRahoitus": false, + "oid": "1.2.246.578.5.1.2978288874.2711575506", +} +`; diff --git a/backend/integrationtest/api/api.test.ts b/backend/integrationtest/api/api.test.ts index 93b059c94..f1bdc8bf5 100644 --- a/backend/integrationtest/api/api.test.ts +++ b/backend/integrationtest/api/api.test.ts @@ -11,6 +11,8 @@ import { ProjektiRooli, SuunnitteluSopimus, SuunnitteluSopimusInput, + TilasiirtymaToiminto, + TilasiirtymaTyyppi, } from "../../../common/graphql/apiModel"; import fs from "fs"; import axios from "axios"; @@ -63,11 +65,10 @@ describe("Api", () => { const { oid, projekti } = await readProjektiFromVelho(); // Expect that projektipaallikko is found - expect( - projekti.kayttoOikeudet?.filter( - (kayttaja) => kayttaja.rooli === ProjektiRooli.PROJEKTIPAALLIKKO && kayttaja.email - ) - ).is.not.empty; + const projektiPaallikko = projekti.kayttoOikeudet + ?.filter((kayttaja) => kayttaja.rooli === ProjektiRooli.PROJEKTIPAALLIKKO && kayttaja.email) + .pop(); + expect(projektiPaallikko).is.not.empty; const kayttoOikeudet = projekti.kayttoOikeudet?.map((value) => ({ rooli: value.rooli, @@ -172,6 +173,18 @@ describe("Api", () => { expect(updatedProjekti2.suunnitteluSopimus).to.be.undefined; expect(updatedProjekti2.kielitiedot).eql(kielitiedot); + userFixture.loginAsProjektiKayttaja(projektiPaallikko); + await api.siirraTila({ + oid, + tyyppi: TilasiirtymaTyyppi.ALOITUSKUULUTUS, + toiminto: TilasiirtymaToiminto.LAHETA_HYVAKSYTTAVAKSI, + }); + await api.siirraTila({ oid, tyyppi: TilasiirtymaTyyppi.ALOITUSKUULUTUS, toiminto: TilasiirtymaToiminto.HYVAKSY }); + + userFixture.logout(); + const publicProjekti = await loadProjektiFromDatabase(oid); + expect(publicProjekti).toMatchSnapshot(); + // Finally delete the projekti const archiveResult = await projektiArchive.archiveProjekti(oid); expect(archiveResult.oid).to.be.equal(oid); @@ -188,12 +201,12 @@ describe("Api", () => { async function loadProjektiFromDatabase(oid: string) { const savedProjekti = await api.lataaProjekti(oid); - expect(savedProjekti.tallennettu).to.be.true; + expect(!savedProjekti.tallennettu || savedProjekti.tallennettu).to.be.true; return savedProjekti; } async function searchProjectsFromVelhoAndPickFirst(): Promise { - const searchResult = await api.getVelhoSuunnitelmasByName("HASSUTESTIPROJEKTI"); + const searchResult = await api.getVelhoSuunnitelmasByName("HASSU AUTOMAATTITESTIPROJEKTI1"); // tslint:disable-next-line:no-unused-expression expect(searchResult).not.to.be.empty; diff --git a/backend/src/asiakirja/abstractPdf.ts b/backend/src/asiakirja/abstractPdf.ts index 614848f96..ad2780a15 100644 --- a/backend/src/asiakirja/abstractPdf.ts +++ b/backend/src/asiakirja/abstractPdf.ts @@ -1,4 +1,4 @@ -import { deburr } from "lodash"; +import deburr from "lodash/deburr"; import log from "loglevel"; import { PDF } from "../../../common/graphql/apiModel"; diff --git a/backend/src/config.ts b/backend/src/config.ts index 4685cc4a1..9ebf6caca 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -35,6 +35,7 @@ const config = { emailsOn: process.env.EMAILS_ON, emailsTo: process.env.EMAILS_TO, + isProd: (): boolean => process.env.ENVIRONMENT == "prod", }; process.env.AWS_XRAY_CONTEXT_MISSING = "LOG_ERROR"; diff --git a/backend/src/email/emailTemplates.ts b/backend/src/email/emailTemplates.ts index f15481c09..e313130de 100644 --- a/backend/src/email/emailTemplates.ts +++ b/backend/src/email/emailTemplates.ts @@ -1,4 +1,4 @@ -import { get } from "lodash"; +import get from "lodash/get"; import { config } from "../config"; import { DBProjekti } from "../database/model/projekti"; import { EmailOptions } from "./email"; diff --git a/backend/src/endDateCalculator/bankHolidays.ts b/backend/src/endDateCalculator/bankHolidays.ts index 327a7edb3..7a977421f 100644 --- a/backend/src/endDateCalculator/bankHolidays.ts +++ b/backend/src/endDateCalculator/bankHolidays.ts @@ -9,7 +9,8 @@ export class BankHolidays { } isBankHoliday(date: Dayjs) { - const isWeekened = date.day() === 0 || date.day() === 6; - return isWeekened || !!this.bankHolidays.find((bankHoliday) => bankHoliday.isSame(date)); + const pureDate = date.set("hours", 0).set("minutes", 0); + const isWeekened = pureDate.day() === 0 || pureDate.day() === 6; + return isWeekened || !!this.bankHolidays.find((bankHoliday) => bankHoliday.isSame(pureDate)); } } diff --git a/backend/src/endDateCalculator/endDateCalculatorHandler.ts b/backend/src/endDateCalculator/endDateCalculatorHandler.ts index 86911dcb2..359fd272d 100644 --- a/backend/src/endDateCalculator/endDateCalculatorHandler.ts +++ b/backend/src/endDateCalculator/endDateCalculatorHandler.ts @@ -1,18 +1,32 @@ import { LaskePaattymisPaivaQueryVariables } from "../../../common/graphql/apiModel"; -import { dateToString, parseDate } from "../util/dateUtil"; +import { dateTimeToString, dateToString, ISO_DATE_FORMAT, parseDate } from "../util/dateUtil"; import { Dayjs } from "dayjs"; import { bankHolidaysClient } from "./bankHolidaysClient"; +import { config } from "../config"; export async function calculateEndDate({ alkupaiva }: LaskePaattymisPaivaQueryVariables) { - const start = parseDate(alkupaiva); - if (start.isValid()) { - let endDate: Dayjs = start.add(1 + 30, "day"); - const bankHolidays = await bankHolidaysClient.getBankHolidays(); - while (bankHolidays.isBankHoliday(endDate)) { - endDate = endDate.add(1, "day"); + let start: Dayjs; + // Only accept dates in prod, but allow datetimes in other environments + const isDateOnly = config.isProd() || alkupaiva.length == ISO_DATE_FORMAT.length; + if (isDateOnly) { + start = parseDate(alkupaiva); + if (!start.isValid()) { + new Error("Alkupäivän pitää olla muotoa YYYY-MM-DD tai YYYY-MM-DDTHH:mm"); } + } else { + start = parseDate(alkupaiva); + if (!start.isValid()) { + new Error("Alkupäivän pitää olla muotoa YYYY-MM-DDTHH:mm"); + } + } + let endDate: Dayjs = start.add(1 + 30, "day"); + const bankHolidays = await bankHolidaysClient.getBankHolidays(); + while (bankHolidays.isBankHoliday(endDate)) { + endDate = endDate.add(1, "day"); + } + if (isDateOnly) { return dateToString(endDate); } else { - throw new Error("Alkupäivän pitää olla muotoa YYYY-MM-DD"); + return dateTimeToString(endDate); } } diff --git a/backend/src/files/fileService.ts b/backend/src/files/fileService.ts index ed8fbb836..7527b1f99 100644 --- a/backend/src/files/fileService.ts +++ b/backend/src/files/fileService.ts @@ -52,19 +52,19 @@ export class FileService { const sourceFileProperties = await this.getUploadedSourceFileInformation(filePath); const fileNameFromUpload = FileService.getFileNameFromPath(filePath); - const targetPath = - FileService.getProjektiDirectory(param.oid) + `/${param.targetFilePathInProjekti}/${fileNameFromUpload}`; + const targetPath = `/${param.targetFilePathInProjekti}/${fileNameFromUpload}`; + const targetBucketPath = FileService.getProjektiDirectory(param.oid) + targetPath; try { await getS3Client().send( new CopyObjectCommand({ ...sourceFileProperties, Bucket: config.yllapitoBucketName, - Key: targetPath, + Key: targetBucketPath, MetadataDirective: "REPLACE", }) ); log.info( - `Copied uploaded file (${sourceFileProperties.ContentType}) ${sourceFileProperties.CopySource} to ${targetPath}` + `Copied uploaded file (${sourceFileProperties.ContentType}) ${sourceFileProperties.CopySource} to ${targetBucketPath}` ); return targetPath; } catch (e) { @@ -99,6 +99,10 @@ export class FileService { return `yllapito/tiedostot/projekti/${oid}`; } + private static getPublicProjektiDirectory(oid: string) { + return `tiedostot/suunnitelma/${oid}`; + } + async getUploadedSourceFileInformation( uploadedFileSource: string ): Promise<{ ContentType: string; CopySource: string }> { @@ -177,6 +181,10 @@ export class FileService { getYllapitoPathForProjektiFile(oid: string, path: string): string | undefined { return path ? `/${FileService.getProjektiDirectory(oid)}${path}` : undefined; } + + getPublicPathForProjektiFile(oid: string, path: string): string | undefined { + return path ? `/${FileService.getPublicProjektiDirectory(oid)}${path}` : undefined; + } } export const fileService = new FileService(); diff --git a/backend/src/handler/projektiAdapter.ts b/backend/src/handler/projektiAdapter.ts index 6fea12575..6ce0bc1c3 100644 --- a/backend/src/handler/projektiAdapter.ts +++ b/backend/src/handler/projektiAdapter.ts @@ -44,7 +44,7 @@ export class ProjektiAdapter { kayttoOikeudet: KayttoOikeudetManager.adaptAPIKayttoOikeudet(kayttoOikeudet), tyyppi: velho?.tyyppi || dbProjekti.tyyppi, // remove usage of projekti.tyyppi after all data has been migrated to new format aloitusKuulutus: adaptAloitusKuulutus(aloitusKuulutus), - suunnitteluSopimus: adaptSuunnitteluSopimus(suunnitteluSopimus), + suunnitteluSopimus: adaptSuunnitteluSopimus(dbProjekti.oid, suunnitteluSopimus), liittyvatSuunnitelmat: adaptLiittyvatSuunnitelmat(liittyvatSuunnitelmat), aloitusKuulutusJulkaisut: adaptAloitusKuulutusJulkaisut(dbProjekti.oid, aloitusKuulutusJulkaisut), velho: { @@ -104,7 +104,7 @@ function adaptLiittyvatSuunnitelmat(suunnitelmat?: Suunnitelma[] | null): API.Su return suunnitelmat as undefined | null; } -function adaptKielitiedot(kielitiedot?: Kielitiedot | null): API.Kielitiedot | undefined | null { +export function adaptKielitiedot(kielitiedot?: Kielitiedot | null): API.Kielitiedot | undefined | null { if (kielitiedot) { return { ...kielitiedot, @@ -181,10 +181,15 @@ function adaptAloitusKuulutus(kuulutus?: AloitusKuulutus | null): API.AloitusKuu } function adaptSuunnitteluSopimus( + oid: string, suunnitteluSopimus?: SuunnitteluSopimus | null ): API.SuunnitteluSopimus | undefined | null { if (suunnitteluSopimus) { - return { __typename: "SuunnitteluSopimus", ...suunnitteluSopimus }; + return { + __typename: "SuunnitteluSopimus", + ...suunnitteluSopimus, + logo: fileService.getYllapitoPathForProjektiFile(oid, suunnitteluSopimus.logo), + }; } return suunnitteluSopimus as undefined | null; } @@ -193,14 +198,14 @@ function removeUndefinedFields(object: API.Projekti): Partial { return pickBy(object, (value) => value !== undefined); } -function adaptYhteystiedot(yhteystiedot: Yhteystieto[]): API.Yhteystieto[] { +export function adaptYhteystiedot(yhteystiedot: Yhteystieto[]): API.Yhteystieto[] { if (yhteystiedot) { return yhteystiedot.map((yt) => ({ __typename: "Yhteystieto", ...yt })); } return []; } -function adaptJulkaisuPDFPaths( +export function adaptJulkaisuPDFPaths( oid: string, aloitusKuulutusPDFS: LocalizedMap ): AloitusKuulutusPDFt | undefined { @@ -224,7 +229,7 @@ function adaptJulkaisuPDFPaths( return { __typename: "AloitusKuulutusPDFt", SUOMI: result[Kieli.SUOMI], ...result }; } -function adaptHankkeenKuvaus(hankkeenKuvaus: LocalizedMap): HankkeenKuvaukset { +export function adaptHankkeenKuvaus(hankkeenKuvaus: LocalizedMap): HankkeenKuvaukset { return { __typename: "HankkeenKuvaukset", SUOMI: hankkeenKuvaus.SUOMI, @@ -232,7 +237,7 @@ function adaptHankkeenKuvaus(hankkeenKuvaus: LocalizedMap): HankkeenKuva }; } -function adaptAloitusKuulutusJulkaisut( +export function adaptAloitusKuulutusJulkaisut( oid: string, aloitusKuulutusJulkaisut?: AloitusKuulutusJulkaisu[] | null ): API.AloitusKuulutusJulkaisu[] | undefined { @@ -246,7 +251,7 @@ function adaptAloitusKuulutusJulkaisut( hankkeenKuvaus: adaptHankkeenKuvaus(julkaisu.hankkeenKuvaus), yhteystiedot: adaptYhteystiedot(yhteystiedot), velho: adaptVelho(velho), - suunnitteluSopimus: adaptSuunnitteluSopimus(suunnitteluSopimus), + suunnitteluSopimus: adaptSuunnitteluSopimus(oid, suunnitteluSopimus), kielitiedot: adaptKielitiedot(kielitiedot), aloituskuulutusPDFt: adaptJulkaisuPDFPaths(oid, julkaisu.aloituskuulutusPDFt), }; @@ -255,7 +260,7 @@ function adaptAloitusKuulutusJulkaisut( return undefined; } -function adaptVelho(velho: Velho): API.Velho { +export function adaptVelho(velho: Velho): API.Velho { return { __typename: "Velho", ...velho }; } diff --git a/backend/src/handler/projektiAdapterJulkinen.ts b/backend/src/handler/projektiAdapterJulkinen.ts new file mode 100644 index 000000000..74cf70bee --- /dev/null +++ b/backend/src/handler/projektiAdapterJulkinen.ts @@ -0,0 +1,124 @@ +import { + AloitusKuulutusJulkaisu, + AloitusKuulutusPDF, + DBProjekti, + LocalizedMap, + SuunnitteluSopimus, +} from "../database/model/projekti"; +import * as API from "../../../common/graphql/apiModel"; +import { AloitusKuulutusPDFt, AloitusKuulutusTila, Kieli, ProjektiJulkinen } from "../../../common/graphql/apiModel"; +import pickBy from "lodash/pickBy"; +import dayjs from "dayjs"; +import { adaptHankkeenKuvaus, adaptKielitiedot, adaptVelho, adaptYhteystiedot } from "./projektiAdapter"; +import { fileService } from "../files/fileService"; +import { log } from "../logger"; +import { parseDate } from "../util/dateUtil"; + +class ProjektiAdapterJulkinen { + public adaptProjekti(dbProjekti: DBProjekti): API.ProjektiJulkinen | undefined { + const aloitusKuulutusJulkaisut = this.adaptAloitusKuulutusJulkaisut( + dbProjekti.oid, + dbProjekti.aloitusKuulutusJulkaisut + ); + + if (!checkIfAloitusKuulutusJulkaisutIsPublic(aloitusKuulutusJulkaisut)) { + return undefined; + } + + const projekti: ProjektiJulkinen = { + __typename: "ProjektiJulkinen", + oid: dbProjekti.oid, + euRahoitus: dbProjekti.euRahoitus, + aloitusKuulutusJulkaisut, + }; + return removeUndefinedFields(projekti) as API.ProjektiJulkinen; + } + + adaptAloitusKuulutusJulkaisut( + oid: string, + aloitusKuulutusJulkaisut?: AloitusKuulutusJulkaisu[] | null + ): API.AloitusKuulutusJulkaisuJulkinen[] | undefined { + if (aloitusKuulutusJulkaisut) { + return aloitusKuulutusJulkaisut + .filter((julkaisu) => julkaisu.tila == AloitusKuulutusTila.HYVAKSYTTY) + .map((julkaisu) => { + const { yhteystiedot, velho, suunnitteluSopimus, kielitiedot } = julkaisu; + + return { + __typename: "AloitusKuulutusJulkaisuJulkinen", + kuulutusPaiva: julkaisu.kuulutusPaiva, + elyKeskus: julkaisu.elyKeskus, + siirtyySuunnitteluVaiheeseen: julkaisu.siirtyySuunnitteluVaiheeseen, + hankkeenKuvaus: adaptHankkeenKuvaus(julkaisu.hankkeenKuvaus), + yhteystiedot: adaptYhteystiedot(yhteystiedot), + velho: adaptVelho(velho), + suunnitteluSopimus: this.adaptSuunnitteluSopimus(oid, suunnitteluSopimus), + kielitiedot: adaptKielitiedot(kielitiedot), + aloituskuulutusPDFt: this.adaptJulkaisuPDFPaths(oid, julkaisu.aloituskuulutusPDFt), + }; + }); + } + return undefined; + } + + adaptSuunnitteluSopimus( + oid: string, + suunnitteluSopimus?: SuunnitteluSopimus | null + ): API.SuunnitteluSopimus | undefined | null { + if (suunnitteluSopimus) { + return { + __typename: "SuunnitteluSopimus", + ...suunnitteluSopimus, + logo: fileService.getPublicPathForProjektiFile(oid, suunnitteluSopimus.logo), + }; + } + return suunnitteluSopimus as undefined | null; + } + + adaptJulkaisuPDFPaths( + oid: string, + aloitusKuulutusPDFS: LocalizedMap + ): AloitusKuulutusPDFt | undefined { + if (!aloitusKuulutusPDFS) { + return undefined; + } + + const result = {}; + for (const kieli in aloitusKuulutusPDFS) { + result[kieli] = { + aloituskuulutusPDFPath: fileService.getPublicPathForProjektiFile( + oid, + aloitusKuulutusPDFS[kieli].aloituskuulutusPDFPath + ), + aloituskuulutusIlmoitusPDFPath: fileService.getPublicPathForProjektiFile( + oid, + aloitusKuulutusPDFS[kieli].aloituskuulutusIlmoitusPDFPath + ), + } as AloitusKuulutusPDF; + } + return { __typename: "AloitusKuulutusPDFt", SUOMI: result[Kieli.SUOMI], ...result }; + } +} + +function checkIfAloitusKuulutusJulkaisutIsPublic(aloitusKuulutusJulkaisut: API.AloitusKuulutusJulkaisuJulkinen[]): boolean { + if (!(aloitusKuulutusJulkaisut && aloitusKuulutusJulkaisut.length == 1)) { + log.info("Projektilla ei ole hyväksyttyä aloituskuulutusta"); + return false; + } + + const julkaisu = aloitusKuulutusJulkaisut[0]; + if (julkaisu.kuulutusPaiva && parseDate(julkaisu.kuulutusPaiva).isAfter(dayjs())) { + log.info("Projektin aloituskuulutuksen kuulutuspäivä on tulevaisuudessa", { + kuulutusPaiva: parseDate(julkaisu.kuulutusPaiva).toISOString(), + now: dayjs().toISOString(), + }); + return false; + } + return true; +} + +function removeUndefinedFields(object: API.ProjektiJulkinen): Partial { + return pickBy(object, (value) => value !== undefined); +} + +export const projektiAdapterJulkinen = new ProjektiAdapterJulkinen(); diff --git a/backend/src/handler/projektiHandler.ts b/backend/src/handler/projektiHandler.ts index 43e5514b9..e19b67524 100644 --- a/backend/src/handler/projektiHandler.ts +++ b/backend/src/handler/projektiHandler.ts @@ -1,5 +1,11 @@ import { projektiDatabase } from "../database/projektiDatabase"; -import { requirePermissionLuku, requirePermissionLuonti, requirePermissionMuokkaa, requireVaylaUser } from "../user"; +import { + getVaylaUser, + requirePermissionLuku, + requirePermissionLuonti, + requirePermissionMuokkaa, + requireVaylaUser, +} from "../user"; import { velho } from "../velho/velhoClient"; import * as API from "../../../common/graphql/apiModel"; import { @@ -22,6 +28,8 @@ import { sendEmail } from "../email/email"; import { createPerustamisEmail } from "../email/emailTemplates"; import { requireAdmin } from "../user/userService"; import { projektiArchive } from "../archive/projektiArchiveService"; +import { NotFoundError } from "../error/NotFoundError"; +import { projektiAdapterJulkinen } from "./projektiAdapterJulkinen"; /** * Function to determine the status of the projekti @@ -61,22 +69,40 @@ function applyStatus(projekti: Projekti, param: { saved?: boolean }) { return projekti; } -export async function loadProjekti(oid: string): Promise { - const vaylaUser = requirePermissionLuku(); +export async function loadProjekti(oid: string): Promise { + const vaylaUser = getVaylaUser(); if (vaylaUser) { - log.info("Loading projekti", { oid }); - const projektiFromDB = await projektiDatabase.loadProjektiByOid(oid); - if (projektiFromDB) { - projektiFromDB.tallennettu = true; - return applyStatus(projektiAdapter.adaptProjekti(projektiFromDB), { saved: true }); - } else { - requirePermissionLuonti(); - const projekti = await createProjektiFromVelho(oid, vaylaUser); - return projektiAdapter.adaptProjekti(projekti); - } + return await loadProjektiYllapito(oid, vaylaUser); } else { - throw new Error("Public access not implemented yet"); + return await loadProjektiJulkinen(oid); + } +} + +async function loadProjektiYllapito(oid: string, vaylaUser: NykyinenKayttaja): Promise { + requirePermissionLuku(); + log.info("Loading projekti", { oid }); + const projektiFromDB = await projektiDatabase.loadProjektiByOid(oid); + if (projektiFromDB) { + projektiFromDB.tallennettu = true; + return applyStatus(projektiAdapter.adaptProjekti(projektiFromDB), { saved: true }); + } else { + requirePermissionLuonti(); + const projekti = await createProjektiFromVelho(oid, vaylaUser); + return projektiAdapter.adaptProjekti(projekti); + } +} + +async function loadProjektiJulkinen(oid: string): Promise { + const projektiFromDB = await projektiDatabase.loadProjektiByOid(oid); + if (projektiFromDB) { + const adaptedProjekti = projektiAdapterJulkinen.adaptProjekti(projektiFromDB); + if (adaptedProjekti) { + return adaptedProjekti; + } + log.info("Projektilla ei ole julkista sisältöä", { oid }); + throw new NotFoundError("Projektilla ei ole julkista sisältöä: " + oid); } + throw new NotFoundError("Projektia ei löydy: " + oid); } export async function arkistoiProjekti(oid: string): Promise { diff --git a/backend/src/util/dateUtil.ts b/backend/src/util/dateUtil.ts index b50a507df..a87bb73b5 100644 --- a/backend/src/util/dateUtil.ts +++ b/backend/src/util/dateUtil.ts @@ -1,15 +1,28 @@ import dayjs, { Dayjs } from "dayjs"; - +import tz from "dayjs/plugin/timezone"; import customParseFormat from "dayjs/plugin/customParseFormat"; +import utc from "dayjs/plugin/utc"; +dayjs.extend(utc); +dayjs.extend(tz); dayjs.extend(customParseFormat); +const DEFAULT_TIMEZONE = "Europe/Helsinki"; +dayjs.tz.setDefault(DEFAULT_TIMEZONE); -const dateFormat = "YYYY-MM-DD"; +export const ISO_DATE_FORMAT = "YYYY-MM-DD"; +const DATE_TIME_FORMAT = "YYYY-MM-DDTHH:mm"; export function parseDate(date: string) { - return dayjs(date, dateFormat, true); + if (date.length == ISO_DATE_FORMAT.length) { + return dayjs(date, ISO_DATE_FORMAT, true).tz(DEFAULT_TIMEZONE, true); + } + return dayjs(date, DATE_TIME_FORMAT, true).tz(DEFAULT_TIMEZONE, true); } export function dateToString(date: Dayjs) { - return date.format(dateFormat); + return date.format(ISO_DATE_FORMAT); +} + +export function dateTimeToString(date: Dayjs) { + return date.format(DATE_TIME_FORMAT); } diff --git a/backend/test/__snapshots__/apiHandler.test.ts.snap b/backend/test/__snapshots__/apiHandler.test.ts.snap index 26952fff4..a64d24663 100644 --- a/backend/test/__snapshots__/apiHandler.test.ts.snap +++ b/backend/test/__snapshots__/apiHandler.test.ts.snap @@ -559,3 +559,72 @@ Object { }, } `; + +exports[`apiHandler handleEvent tallennaProjekti should modify permissions from a project successfully 11`] = ` +Object { + "description": "Public version of the projekti", + "projekti": Object { + "__typename": "ProjektiJulkinen", + "aloitusKuulutusJulkaisut": Array [ + Object { + "__typename": "AloitusKuulutusJulkaisuJulkinen", + "aloituskuulutusPDFt": Object { + "SAAME": Object { + "aloituskuulutusIlmoitusPDFPath": "/tiedostot/suunnitelma/1/aloituskuulutus/ILMOITUS TOIMIVALTAISEN VIRANOMAISEN KUULUTUKSESTA Projektin nimi saameksi.pdf", + "aloituskuulutusPDFPath": "/tiedostot/suunnitelma/1/aloituskuulutus/KUULUTUS SUUNNITTELUN ALOITTAMISESTA Projektin nimi saameksi.pdf", + }, + "SUOMI": Object { + "aloituskuulutusIlmoitusPDFPath": "/tiedostot/suunnitelma/1/aloituskuulutus/ILMOITUS TOIMIVALTAISEN VIRANOMAISEN KUULUTUKSESTA Testiprojekti 1.pdf", + "aloituskuulutusPDFPath": "/tiedostot/suunnitelma/1/aloituskuulutus/KUULUTUS SUUNNITTELUN ALOITTAMISESTA Testiprojekti 1.pdf", + }, + "__typename": "AloitusKuulutusPDFt", + }, + "elyKeskus": "Pirkanmaa", + "hankkeenKuvaus": Object { + "RUOTSI": "På Svenska", + "SAAME": "Saameksi", + "SUOMI": "Lorem Ipsum", + "__typename": "HankkeenKuvaukset", + }, + "kielitiedot": Object { + "__typename": "Kielitiedot", + "ensisijainenKieli": "SUOMI", + "projektinNimiVieraskielella": "Projektin nimi saameksi", + "toissijainenKieli": "SAAME", + }, + "kuulutusPaiva": "2022-01-02", + "siirtyySuunnitteluVaiheeseen": "2022-01-01", + "suunnitteluSopimus": null, + "velho": Object { + "__typename": "Velho", + "kunnat": undefined, + "nimi": "Testiprojekti 1", + "tilaajaOrganisaatio": undefined, + "tyyppi": "TIE", + "vaylamuoto": undefined, + }, + "yhteystiedot": Array [ + Object { + "__typename": "Yhteystieto", + "etunimi": "Pekka", + "organisaatio": "Väylävirasto", + "puhelinnumero": "11", + "sahkoposti": "pekka.projari@vayla.fi", + "sukunimi": "Projari", + }, + Object { + "__typename": "Yhteystieto", + "etunimi": "Marko", + "organisaatio": "Kajaani", + "puhelinnumero": "0293121213", + "sahkoposti": "markku.koi@koi.com", + "sukunimi": "Koi", + }, + ], + }, + ], + "euRahoitus": false, + "oid": "1", + }, +} +`; diff --git a/backend/test/apiHandler.test.ts b/backend/test/apiHandler.test.ts index 787823078..aae0424a4 100644 --- a/backend/test/apiHandler.test.ts +++ b/backend/test/apiHandler.test.ts @@ -26,6 +26,7 @@ import { Kayttajas } from "../src/personSearch/kayttajas"; import { AwsClientStub, mockClient } from "aws-sdk-client-mock"; import { getS3Client } from "../src/aws/clients"; import { PutObjectCommand, PutObjectCommandInput, S3Client } from "@aws-sdk/client-s3"; +import { NotFoundError } from "../src/error/NotFoundError"; const { expect, assert } = require("chai"); @@ -292,6 +293,11 @@ describe("apiHandler", () => { } ); + // Verify that projekti is not visible for anonymous users + userFixture.logout(); + await expect(api.lataaProjekti(fixture.projekti1.oid)).to.eventually.be.rejectedWith(NotFoundError); + userFixture.loginAs(UserFixture.pekkaProjari); + // Send aloituskuulutus to be approved const oid = projekti.oid; await api.siirraTila({ @@ -355,6 +361,13 @@ describe("apiHandler", () => { // Verify the end result using snapshot expect(await api.lataaProjekti(oid)).toMatchSnapshot(); + + // Verify the public result using snapshot + userFixture.logout(); + expect({ + description: "Public version of the projekti", + projekti: await api.lataaProjekti(oid), + }).toMatchSnapshot(); }); }); diff --git a/backend/test/endDateCalculator/endDateCalculator.test.ts b/backend/test/endDateCalculator/endDateCalculator.test.ts index 4fa1b7cde..c5c7ee090 100644 --- a/backend/test/endDateCalculator/endDateCalculator.test.ts +++ b/backend/test/endDateCalculator/endDateCalculator.test.ts @@ -37,6 +37,18 @@ describe("Api", () => { ).to.be.equal("2022-12-27"); }); + it("should calculate correct end date over holidays in dev and test environments with time 00:00", async () => { + expect( + await calculateEndDate({ alkupaiva: "2022-11-23T00:00", tyyppi: LaskuriTyyppi.KUULUTUKSEN_PAATTYMISPAIVA }) + ).to.be.equal("2022-12-27T00:00"); + }); + + it("should calculate correct end date over holidays in dev and test environments with time 12:34", async () => { + expect( + await calculateEndDate({ alkupaiva: "2022-11-23T12:34", tyyppi: LaskuriTyyppi.KUULUTUKSEN_PAATTYMISPAIVA }) + ).to.be.equal("2022-12-27T12:34"); + }); + it("should calculate correct end date over weekend", async () => { expect( await calculateEndDate({ alkupaiva: "2022-11-02", tyyppi: LaskuriTyyppi.KUULUTUKSEN_PAATTYMISPAIVA }) diff --git a/backend/test/fixture/userFixture.ts b/backend/test/fixture/userFixture.ts index 89422251a..c347eaf62 100644 --- a/backend/test/fixture/userFixture.ts +++ b/backend/test/fixture/userFixture.ts @@ -1,6 +1,6 @@ import * as sinon from "sinon"; import * as Sinon from "sinon"; -import { NykyinenKayttaja, VaylaKayttajaTyyppi } from "../../../common/graphql/apiModel"; +import { NykyinenKayttaja, ProjektiKayttaja, VaylaKayttajaTyyppi } from "../../../common/graphql/apiModel"; export class UserFixture { private sinonStub: Sinon.SinonStub; @@ -16,6 +16,15 @@ export class UserFixture { this.userService.identifyMockUser(vaylaUser); } + loginAsProjektiKayttaja(projektiKayttaja: ProjektiKayttaja) { + this.userService.identifyMockUser({ + __typename: "NykyinenKayttaja", + uid: projektiKayttaja.kayttajatunnus, + roolit: ["hassu_kayttaja", "Atunnukset"], + vaylaKayttajaTyyppi: VaylaKayttajaTyyppi.A_TUNNUS, + }); + } + public logout() { this.userService.identifyMockUser(undefined); } diff --git a/common/abstractApi.ts b/common/abstractApi.ts index b614e0fa9..1c18a8c02 100644 --- a/common/abstractApi.ts +++ b/common/abstractApi.ts @@ -14,6 +14,7 @@ import { NykyinenKayttaja, PDF, Projekti, + ProjektiJulkinen, SiirraTilaMutationVariables, TallennaProjektiInput, TallennaProjektiMutationVariables, @@ -108,6 +109,12 @@ export abstract class AbstractApi { } as LataaProjektiQueryVariables); } + async lataaProjektiJulkinen(oid: string): Promise { + return await this.callAPI(apiConfig.lataaProjekti, { + oid, + } as LataaProjektiQueryVariables); + } + async tallennaProjekti(input: TallennaProjektiInput) { return await this.callYllapitoAPI(apiConfig.tallennaProjekti, { projekti: input, diff --git a/deployment/bin/runGraphQLIntrospection.ts b/deployment/bin/runGraphQLIntrospection.ts new file mode 100755 index 000000000..593cd2f83 --- /dev/null +++ b/deployment/bin/runGraphQLIntrospection.ts @@ -0,0 +1,34 @@ +#!/usr/bin/env node +/* tslint:disable:no-console */ +import fs from "fs"; +import { api } from "../../src/services/api/developerApi"; +import { OperationType } from "../../common/abstractApi"; +import { Config } from "../lib/config"; + +process.env.AWS_SDK_LOAD_CONFIG = "true"; + +if (Config.isDeveloperEnvironment()) { + api + .callAPI({ + name: "__schema" as any, + graphql: ` + { + __schema { + types { + kind + name + possibleTypes { + name + } + } + } + } +`, + operationType: OperationType.Query + }) + .then((value: any) => { + let schema = { __schema: value }; + fs.writeFileSync("src/services/api/fragmentTypes.json", JSON.stringify(schema, null, 2) + "\n"); + }) + .catch((reason: any) => console.log(reason)); +} diff --git a/deployment/lib/config.ts b/deployment/lib/config.ts index f48f7436f..464226d78 100644 --- a/deployment/lib/config.ts +++ b/deployment/lib/config.ts @@ -131,7 +131,7 @@ export class Config extends BaseConfig { ? process.env.BUILD_BRANCH : await execShellCommand("git rev-parse --abbrev-ref HEAD"); - if (this.isDeveloperEnvironment()) { + if (Config.isDeveloperEnvironment()) { this.frontendDomainName = (await readFrontendStackOutputs()).CloudfrontPrivateDNSName || "please-re-run-backend-deployment"; } else { @@ -151,7 +151,7 @@ export class Config extends BaseConfig { return this.branch; } - public isDeveloperEnvironment() { + public static isDeveloperEnvironment() { return !Config.isPermanentEnvironment() && "feature" !== Config.env; } } diff --git a/deployment/lib/hassu-backend.ts b/deployment/lib/hassu-backend.ts index 672ed1270..d4050e8ee 100644 --- a/deployment/lib/hassu-backend.ts +++ b/deployment/lib/hassu-backend.ts @@ -57,7 +57,7 @@ export class HassuBackendStack extends cdk.Stack { const api = this.createAPI(config); const commonEnvironmentVariables = await this.getCommonEnvironmentVariables(config); const personSearchUpdaterLambda = await this.createPersonSearchUpdaterLambda(commonEnvironmentVariables); - const backendLambda = await this.createBackendLambda(config, commonEnvironmentVariables, personSearchUpdaterLambda); + const backendLambda = await this.createBackendLambda(commonEnvironmentVariables, personSearchUpdaterLambda); this.attachDatabaseToBackend(backendLambda); HassuBackendStack.mapApiResolversToLambda(api, backendLambda); this.configureOpenSearchAccess(projektiSearchIndexer, backendLambda); @@ -65,7 +65,7 @@ export class HassuBackendStack extends cdk.Stack { new cdk.CfnOutput(this, "AppSyncAPIKey", { value: api.apiKey || "", }); - if (config.isDeveloperEnvironment()) { + if (Config.isDeveloperEnvironment()) { new cdk.CfnOutput(this, "AppSyncAPIURL", { value: api.graphqlUrl || "", }); @@ -94,7 +94,7 @@ export class HassuBackendStack extends cdk.Stack { private createAPI(config: Config) { let defaultAuthorization: AuthorizationMode; - if (config.isDeveloperEnvironment()) { + if (Config.isDeveloperEnvironment()) { defaultAuthorization = { authorizationType: appsync.AuthorizationType.IAM, }; @@ -118,7 +118,7 @@ export class HassuBackendStack extends cdk.Stack { authorizationConfig: { defaultAuthorization }, xrayEnabled: true, }); - if (!config.isDeveloperEnvironment()) { + if (!Config.isDeveloperEnvironment()) { new WafConfig(this, "Hassu-WAF", { api, allowedAddresses: Fn.split("\n", config.getInfraParameter("WAFAllowedAddresses")), @@ -168,12 +168,11 @@ export class HassuBackendStack extends cdk.Stack { } private async createBackendLambda( - config: Config, commonEnvironmentVariables: Record, personSearchUpdaterLambda: NodejsFunction ) { let define; - if (config.isDeveloperEnvironment()) { + if (Config.isDeveloperEnvironment()) { define = { // Replace strings during build time "process.env.USER_IDENTIFIER_FUNCTIONS": JSON.stringify("../../developer/identifyIAMUser"), diff --git a/deployment/lib/hassu-pipeline.ts b/deployment/lib/hassu-pipeline.ts index 70b71df94..c4c6976d4 100644 --- a/deployment/lib/hassu-pipeline.ts +++ b/deployment/lib/hassu-pipeline.ts @@ -71,7 +71,7 @@ export class HassuPipelineStack extends Stack { reportBucket.grantRead(oai); } - if (config.isFeatureBranch() && !config.isDeveloperEnvironment()) { + if (config.isFeatureBranch() && !Config.isDeveloperEnvironment()) { webhookFilters = [codebuild.FilterGroup.inEventOf(codebuild.EventAction.PUSH).andBranchIs("feature/*")]; reportBuildStatus = true; } else { diff --git a/graphql/operations.graphql b/graphql/operations.graphql index f986f2081..6d4bcc59f 100644 --- a/graphql/operations.graphql +++ b/graphql/operations.graphql @@ -2,7 +2,7 @@ type Query { nykyinenKayttaja: NykyinenKayttaja listaaKayttajat(hakuehto: ListaaKayttajatInput!): [Kayttaja!] listaaProjektit: [Projekti!] - lataaProjekti(oid: String!): Projekti + lataaProjekti(oid: String!): IProjekti listaaVelhoProjektit(nimi: String!, requireExactMatch: Boolean): [VelhoHakuTulos!] esikatseleAsiakirjaPDF( oid: String! diff --git a/graphql/types.graphql b/graphql/types.graphql index fd8b9f3fd..cf422ae21 100644 --- a/graphql/types.graphql +++ b/graphql/types.graphql @@ -52,7 +52,11 @@ enum AsiakirjaTyyppi { ILMOITUS_KUULUTUKSESTA } -type Projekti { +interface IProjekti { + oid: String! +} + +type Projekti implements IProjekti { oid: String! muistiinpano: String vaihe: String @@ -91,6 +95,12 @@ type Velho { linkki: String } +type ProjektiJulkinen implements IProjekti { + oid: String! + aloitusKuulutusJulkaisut: [AloitusKuulutusJulkaisuJulkinen!] + euRahoitus: Boolean +} + type Suunnitelma { asiatunnus: String! nimi: String! @@ -151,6 +161,18 @@ type AloitusKuulutusJulkaisu { hyvaksyja: String } +type AloitusKuulutusJulkaisuJulkinen { + kuulutusPaiva: String + siirtyySuunnitteluVaiheeseen: String + hankkeenKuvaus: HankkeenKuvaukset + elyKeskus: String + yhteystiedot: [Yhteystieto!]! + velho: Velho! + suunnitteluSopimus: SuunnitteluSopimus + kielitiedot: Kielitiedot + aloituskuulutusPDFt: AloitusKuulutusPDFt +} + type IlmoituksenVastaanottajat { kunnat: [KuntaVastaanottaja!] viranomaiset: [ViranomaisVastaanottaja!] diff --git a/package-lock.json b/package-lock.json index e3101994b..f5e92c65d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/react-fontawesome": "0.1.16", "@hookform/resolvers": "2.7.1", + "@mui/icons-material": "5.4.4", "@mui/material": "5.2.5", "@types/nodemailer": "6.4.4", "@types/pdfkit": "0.12.3", @@ -21808,6 +21809,42 @@ "@emotion/memoize": "^0.7.4" } }, + "node_modules/@mui/icons-material": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.4.4.tgz", + "integrity": "sha512-7zoRpjO8vsd+bPvXq6rtXu0V8Saj70X09dtTQogZmxQKabrYW3g7+Yym7SCRA7IYVF3ysz2AvdQrGD1P/sGepg==", + "dependencies": { + "@babel/runtime": "^7.17.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@mui/material": "^5.0.0", + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/icons-material/node_modules/@babel/runtime": { + "version": "7.17.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.2.tgz", + "integrity": "sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==", + "dependencies": { + "regenerator-runtime": "^0.13.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@mui/material": { "version": "5.2.5", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.2.5.tgz", @@ -84677,6 +84714,24 @@ } } }, + "@mui/icons-material": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.4.4.tgz", + "integrity": "sha512-7zoRpjO8vsd+bPvXq6rtXu0V8Saj70X09dtTQogZmxQKabrYW3g7+Yym7SCRA7IYVF3ysz2AvdQrGD1P/sGepg==", + "requires": { + "@babel/runtime": "^7.17.2" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.17.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.2.tgz", + "integrity": "sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + } + } + }, "@mui/material": { "version": "5.2.5", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.2.5.tgz", diff --git a/package.json b/package.json index 8fb393f19..472fe90b3 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/react-fontawesome": "0.1.16", "@hookform/resolvers": "2.7.1", + "@mui/icons-material": "5.4.4", "@mui/material": "5.2.5", "@types/nodemailer": "6.4.4", "@types/pdfkit": "0.12.3", @@ -163,6 +164,7 @@ "localstack": "docker-compose -f deployment/localstack/docker-compose.yml up -d && cross-env ENVIRONMENT=localstack TS_NODE_PROJECT=\"./tsconfig.cdk.json\" cdklocal bootstrap --template deployment/localstack/localstack-bootstrap.yml && cross-env ENVIRONMENT=localstack TS_NODE_PROJECT=\"./tsconfig.cdk.json\" cdklocal deploy database --require-approval never", "localstack:stop": "docker-compose -f deployment/localstack/docker-compose.yml down", "setupenvironment": "ts-node --project=tsconfig.cdk.json deployment/bin/setupEnvironment.ts", + "graphQLIntrospection": "ts-node --project=tsconfig.cdk.json -r dotenv/config deployment/bin/runGraphQLIntrospection.ts dotenv_config_path=.env.local", "test": "npm-run-all --aggregate-output --parallel \"test:!(backend)\"", "husky:test": "npm-run-all --aggregate-output --parallel \"test:!(backend|integration)\"", "test:frontend": "jest --config \"./jest.frontend.config.js\"", @@ -175,7 +177,7 @@ "deploy:database": "cdk --app \"ts-node --project=tsconfig.cdk.json ./deployment/bin/hassu\" deploy database --require-approval never", "postdeploy:database": "npm run setupenvironment", "deploy:backend": "cdk --app \"ts-node --project=tsconfig.cdk.json ./deployment/bin/hassu\" deploy backend --require-approval never", - "postdeploy:backend": "npm run setupenvironment", + "postdeploy:backend": "npm run setupenvironment && npm run graphQLIntrospection", "predeploy:frontend": "rimraf build .next", "deploy:frontend": "cdk --app \"ts-node --project=tsconfig.cdk.json ./deployment/bin/hassu\" deploy frontend --require-approval never", "postdeploy:frontend": "npm run setupenvironment", diff --git a/src/components/FormatDate.tsx b/src/components/FormatDate.tsx new file mode 100644 index 000000000..ff9bce8f0 --- /dev/null +++ b/src/components/FormatDate.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import dayjs from "dayjs"; + +interface Props { + date?: string | null; +} + +const FormatDate = ({ date }: Props) => { + if (!date) { + return <>; + } else { + return <>{dayjs(date).format("DD.MM.YYYY")}; + } +}; + +export default React.forwardRef(FormatDate); diff --git a/src/components/form/DatePicker.tsx b/src/components/form/DatePicker.tsx index efab09563..747dc6a61 100644 --- a/src/components/form/DatePicker.tsx +++ b/src/components/form/DatePicker.tsx @@ -2,6 +2,7 @@ import classNames from "classnames"; import React from "react"; import { FieldError } from "react-hook-form"; import FormGroup from "./FormGroup"; +import { isDevEnvironment } from "@services/config"; interface Props { error?: FieldError; @@ -30,7 +31,7 @@ const DatePicker = ( className={formGroupClassName} > )} + {!hideIcon && type==NotificationType.INFO_GREEN && ()} {children} ); diff --git a/src/components/projekti/ProjektiKuulutuskielet.tsx b/src/components/projekti/ProjektiKuulutuskielet.tsx index 921faf6d6..47bf84139 100644 --- a/src/components/projekti/ProjektiKuulutuskielet.tsx +++ b/src/components/projekti/ProjektiKuulutuskielet.tsx @@ -4,7 +4,7 @@ import { useFormContext } from "react-hook-form"; import FormGroup from "@components/form/FormGroup"; import Select from "@components/form/Select"; import { Kieli } from "@services/api"; -import { lowerCase } from "lodash"; +import lowerCase from "lodash/lowerCase"; export default function ProjektiKuulutuskielet(): ReactElement { const { diff --git a/src/components/projekti/ProjektiSunnittelusopimusTiedot.tsx b/src/components/projekti/ProjektiSunnittelusopimusTiedot.tsx index e8d712c5e..328b9e5e3 100644 --- a/src/components/projekti/ProjektiSunnittelusopimusTiedot.tsx +++ b/src/components/projekti/ProjektiSunnittelusopimusTiedot.tsx @@ -37,7 +37,7 @@ export default function ProjektiPerustiedot({ projekti }: Props): ReactElement { useEffect(() => { getKuntaLista(); setHasSuunnitteluSopimus(!!projekti?.suunnitteluSopimus); - setLogoUrl((projekti?.suunnitteluSopimus?.logo && "/" + projekti?.suunnitteluSopimus?.logo) || undefined); + setLogoUrl(projekti?.suunnitteluSopimus?.logo || undefined); }, [projekti]); return ( diff --git a/src/components/projekti/aloituskuulutus/AloituskuulutusLukunakyma.tsx b/src/components/projekti/aloituskuulutus/AloituskuulutusLukunakyma.tsx index d14c8dd9c..b17d518cc 100644 --- a/src/components/projekti/aloituskuulutus/AloituskuulutusLukunakyma.tsx +++ b/src/components/projekti/aloituskuulutus/AloituskuulutusLukunakyma.tsx @@ -1,10 +1,14 @@ import { AloitusKuulutusJulkaisu, AloitusKuulutusTila, Kieli, ViranomaisVastaanottajaInput } from "@services/api"; import React, { ReactElement } from "react"; import Notification, { NotificationType } from "@components/notification/Notification"; -import { capitalize, replace, lowerCase } from "lodash"; +import capitalize from "lodash/capitalize"; +import replace from "lodash/replace"; +import lowerCase from "lodash/lowerCase"; import AloituskuulutusPDFEsikatselu from "./AloituskuulutusPDFEsikatselu"; import AloituskuulutusTiedostot from "./AloituskuulutusTiedostot"; import IlmoituksenVastaanottajat from "./IlmoituksenVastaanottajat"; +import { examineKuulutusPaiva } from "@components/projekti/aloituskuulutus/aloitusKuulutusUtil"; +import FormatDate from "@components/FormatDate"; interface Props { oid?: string; @@ -19,32 +23,38 @@ export default function AloituskuulutusLukunakyma({ isLoadingProjekti, kirjaamoOsoitteet, }: Props): ReactElement { - const muotoilePvm = (pvm: string | null | undefined) => { - if (!pvm) { - return; - } - return new Date(pvm).toLocaleDateString("fi"); - }; + if (!aloituskuulutusjulkaisu || !oid) { + return <>; + } + + let { kuulutusPaiva, published } = examineKuulutusPaiva(aloituskuulutusjulkaisu); return ( <> - {aloituskuulutusjulkaisu?.tila === AloitusKuulutusTila.HYVAKSYTTY && ( + {!published && aloituskuulutusjulkaisu.tila === AloitusKuulutusTila.HYVAKSYTTY && ( - Kuulutusta ei ole vielä julkaistu. Kuulutuspäivä {muotoilePvm(aloituskuulutusjulkaisu.kuulutusPaiva)} + Kuulutusta ei ole vielä julkaistu. Kuulutuspäivä {kuulutusPaiva} + + )} + {published && aloituskuulutusjulkaisu.tila === AloitusKuulutusTila.HYVAKSYTTY && ( + + Aloituskuulutus on julkaistu {kuulutusPaiva}. Projekti näytetään kuulutuspäivästä lasketun määräajan jälkeen + palvelun julkisella puolella suunnittelussa olevana. Kuulutusvaihe päättyy{" "} + . )} - {aloituskuulutusjulkaisu?.tila !== AloitusKuulutusTila.HYVAKSYTTY && ( + {aloituskuulutusjulkaisu.tila !== AloitusKuulutusTila.HYVAKSYTTY && ( Aloituskuulutus on hyväksyttävänä projektipäälliköllä. Jos kuulutusta tarvitsee muokata, ota yhteys projektipäällikköön. )} - {aloituskuulutusjulkaisu?.suunnitteluSopimus && ( + {aloituskuulutusjulkaisu.suunnitteluSopimus && ( Hankkeesta on tehty suunnittelusopimus kunnan kanssa

- {capitalize(aloituskuulutusjulkaisu?.suunnitteluSopimus.kunta)} + {capitalize(aloituskuulutusjulkaisu.suunnitteluSopimus.kunta)}
{capitalize(aloituskuulutusjulkaisu.suunnitteluSopimus.etunimi)}{" "} {capitalize(aloituskuulutusjulkaisu.suunnitteluSopimus.sukunimi)}, puh.{" "} @@ -57,12 +67,14 @@ export default function AloituskuulutusLukunakyma({

Kuulutuspäivä

Kuulutusvaihe päättyy

-

{muotoilePvm(aloituskuulutusjulkaisu?.kuulutusPaiva)}

-

{muotoilePvm(aloituskuulutusjulkaisu?.siirtyySuunnitteluVaiheeseen)}

+

{kuulutusPaiva}

+

+ +

Kuulutuksessa esitettävät yhteystiedot

- {aloituskuulutusjulkaisu?.yhteystiedot?.map((yhteistieto, index) => ( + {aloituskuulutusjulkaisu.yhteystiedot?.map((yhteistieto, index) => (

{capitalize(yhteistieto?.etunimi)} {capitalize(yhteistieto?.sukunimi)}, puh. {yhteistieto?.puhelinnumero},{" "} {yhteistieto?.sahkoposti ? replace(yhteistieto?.sahkoposti, "@", "[at]") : ""} @@ -72,32 +84,32 @@ export default function AloituskuulutusLukunakyma({

Tiivistetty hankkeen sisällönkuvaus ensisijaisella kielellä. ( - {lowerCase(aloituskuulutusjulkaisu?.kielitiedot?.ensisijainenKieli)}) + {lowerCase(aloituskuulutusjulkaisu.kielitiedot?.ensisijainenKieli)})

- {aloituskuulutusjulkaisu?.kielitiedot?.ensisijainenKieli === Kieli.SUOMI - ? aloituskuulutusjulkaisu?.hankkeenKuvaus?.SUOMI - : aloituskuulutusjulkaisu?.hankkeenKuvaus?.RUOTSI} + {aloituskuulutusjulkaisu.kielitiedot?.ensisijainenKieli === Kieli.SUOMI + ? aloituskuulutusjulkaisu.hankkeenKuvaus?.SUOMI + : aloituskuulutusjulkaisu.hankkeenKuvaus?.RUOTSI}

- {aloituskuulutusjulkaisu?.kielitiedot?.toissijainenKieli && ( + {aloituskuulutusjulkaisu.kielitiedot?.toissijainenKieli && (

Tiivistetty hankkeen sisällönkuvaus toissijaisella kielellä. ( - {lowerCase(aloituskuulutusjulkaisu?.kielitiedot?.toissijainenKieli)}) + {lowerCase(aloituskuulutusjulkaisu.kielitiedot?.toissijainenKieli)})

- {aloituskuulutusjulkaisu?.kielitiedot?.toissijainenKieli === Kieli.SUOMI - ? aloituskuulutusjulkaisu?.hankkeenKuvaus?.SUOMI - : aloituskuulutusjulkaisu?.hankkeenKuvaus?.RUOTSI} + {aloituskuulutusjulkaisu.kielitiedot?.toissijainenKieli === Kieli.SUOMI + ? aloituskuulutusjulkaisu.hankkeenKuvaus?.SUOMI + : aloituskuulutusjulkaisu.hankkeenKuvaus?.RUOTSI}

)} - {aloituskuulutusjulkaisu?.tila !== AloitusKuulutusTila.HYVAKSYTTY && ( + {aloituskuulutusjulkaisu.tila !== AloitusKuulutusTila.HYVAKSYTTY && ( )} - {aloituskuulutusjulkaisu?.tila === AloitusKuulutusTila.HYVAKSYTTY && ( - + {aloituskuulutusjulkaisu.tila === AloitusKuulutusTila.HYVAKSYTTY && ( + )}
; + } const getPdft = (kieli: Kieli | undefined | null) => { if (!aloituskuulutusjulkaisu || !aloituskuulutusjulkaisu.aloituskuulutusPDFt || !kieli) { return undefined; @@ -23,6 +28,13 @@ export default function AloituskuulutusTiedostot({ aloituskuulutusjulkaisu }: Pr return path.substring(path.lastIndexOf("/") + 1); }; + let { kuulutusPaiva, published } = examineKuulutusPaiva(aloituskuulutusjulkaisu); + + let aloitusKuulutusHref: string | undefined; + if (published) { + aloitusKuulutusHref = + window.location.protocol + "//" + window.location.host + "/suunnitelma/" + oid + "/aloituskuulutus"; + } return ( <>
@@ -55,18 +67,12 @@ export default function AloituskuulutusTiedostot({ aloituskuulutusjulkaisu }: Pr {toissijaisetPDFt && (
- + {parseFilename(toissijaisetPDFt.aloituskuulutusPDFPath)}
- + {parseFilename(toissijaisetPDFt.aloituskuulutusIlmoitusPDFPath)}
@@ -77,10 +83,10 @@ export default function AloituskuulutusTiedostot({ aloituskuulutusjulkaisu }: Pr

Kuulutus julkisella puolella

-

- Linkki julkiselle puolelle muodostetaan kuulutuspäivänä. Kuulutuspäivä on{" "} - {dayjs(aloituskuulutusjulkaisu?.kuulutusPaiva).format("DD.MM.YYYY")}. -

+ {!published && ( +

Linkki julkiselle puolelle muodostetaan kuulutuspäivänä. Kuulutuspäivä on {kuulutusPaiva}.

+ )} + {published && Kuulutus palvelun julkisella puolella}
diff --git a/src/components/projekti/aloituskuulutus/aloitusKuulutusUtil.ts b/src/components/projekti/aloituskuulutus/aloitusKuulutusUtil.ts new file mode 100644 index 000000000..5b1f8bc00 --- /dev/null +++ b/src/components/projekti/aloituskuulutus/aloitusKuulutusUtil.ts @@ -0,0 +1,21 @@ +import { AloitusKuulutusJulkaisu } from "../../../../common/graphql/apiModel"; +import dayjs from "dayjs"; + +export function examineKuulutusPaiva(aloituskuulutusjulkaisu: AloitusKuulutusJulkaisu) { + let kuulutusPaiva: string | undefined; + let published: boolean; + const date = aloituskuulutusjulkaisu?.kuulutusPaiva; + if (date) { + let parsedDate = dayjs(date); + if (date.length == 10) { + kuulutusPaiva = parsedDate.format("DD.MM.YYYY"); + } else { + kuulutusPaiva = parsedDate.format("DD.MM.YYYY HH:mm"); + } + published = parsedDate.isBefore(dayjs()); + } else { + published = false; + kuulutusPaiva = undefined; + } + return { kuulutusPaiva, published }; +} diff --git a/src/hooks/useProjektiJulkinen.tsx b/src/hooks/useProjektiJulkinen.tsx new file mode 100644 index 000000000..5922adcf1 --- /dev/null +++ b/src/hooks/useProjektiJulkinen.tsx @@ -0,0 +1,13 @@ +import useSWR from "swr"; +import { api, apiConfig, ProjektiJulkinen } from "@services/api"; + +export function useProjektiJulkinen(oid?: string) { + return useSWR([apiConfig.lataaProjekti.graphql, oid], projektiLoader); +} + +async function projektiLoader(_: string, oid: string | undefined): Promise { + if (!oid) { + return; + } + return await api.lataaProjektiJulkinen(oid); +} diff --git a/src/pages/suunnitelma/[oid]/aloituskuulutus.tsx b/src/pages/suunnitelma/[oid]/aloituskuulutus.tsx new file mode 100644 index 000000000..5b66bbb55 --- /dev/null +++ b/src/pages/suunnitelma/[oid]/aloituskuulutus.tsx @@ -0,0 +1,110 @@ +import { useRouter } from "next/router"; +import React, { ReactElement } from "react"; +import { useProjektiJulkinen } from "../../../hooks/useProjektiJulkinen"; +import FormatDate from "@components/FormatDate"; +import useTranslation from "next-translate/useTranslation"; +import { AloitusKuulutusJulkaisuJulkinen, Kieli } from "../../../../common/graphql/apiModel"; +import ExtLink from "@components/ExtLink"; + +function formatYhteystiedotText(kuulutus: AloitusKuulutusJulkaisuJulkinen) { + const yhteystiedotList = kuulutus.yhteystiedot.map( + (yt) => + yt.etunimi + + " " + + yt.sukunimi + + ", puh. " + + yt.puhelinnumero + + ", " + + yt.sahkoposti + + " (" + + yt.organisaatio + + ")" + ); + + if (yhteystiedotList.length == 1) { + return yhteystiedotList[0]; + } else { + return ( + yhteystiedotList.slice(0, yhteystiedotList.length - 1).join(", ") + + " ja " + + yhteystiedotList[yhteystiedotList.length - 1] + ); + } +} + +export default function AloituskuulutusJulkinen(): ReactElement { + const router = useRouter(); + const { t } = useTranslation("projekti"); + const oid = typeof router.query.oid === "string" ? router.query.oid : undefined; + const { data: projekti } = useProjektiJulkinen(oid); + + if (!projekti) { + return
; + } + if (!projekti.aloitusKuulutusJulkaisut || !projekti.aloitusKuulutusJulkaisut[0]) { + return
; + } + const kuulutus = projekti.aloitusKuulutusJulkaisut[0]; + const velho = kuulutus.velho; + const suunnitteluSopimus = kuulutus.suunnitteluSopimus; + + let sijainti = ""; + if (velho.maakunnat) { + sijainti = sijainti + velho.maakunnat.join(", ") + "; "; + } + if (velho.kunnat) { + sijainti = sijainti + velho.kunnat.join(", "); + } + const yhteystiedot = formatYhteystiedotText(kuulutus); + + let aloituskuulutusPDFPath = + kuulutus.aloituskuulutusPDFt?.[kuulutus.kielitiedot?.ensisijainenKieli || Kieli.SUOMI]?.aloituskuulutusPDFPath; + let kuulutusFileName = aloituskuulutusPDFPath?.replace(/.*\//, "").replace(/\.\w+$/, ""); + let kuulutusFileExt = aloituskuulutusPDFPath?.replace(/.*\./, ""); + return ( + <> +

Yhteyshenkilöt

+ {kuulutus.yhteystiedot.map((yt) => ( +
+

{yt.organisaatio}

+

+ + {yt.etunimi} {yt.sukunimi} + +

+

{yt.puhelinnumero}

+

{yt.sahkoposti}

+
+ ))} + {suunnitteluSopimus && ( +
+

{suunnitteluSopimus.logo}

+

{suunnitteluSopimus.kunta}

+

PROJEKTIPÄÄLLIKKÖ

+

+ + {suunnitteluSopimus.etunimi} {suunnitteluSopimus.sukunimi} + +

+

{suunnitteluSopimus.puhelinnumero}

+

{suunnitteluSopimus.email}

+
+ )} +

Kuulutus suunnittelun aloittamisesta

+

+ Nähtävilläoloaika - + +

+

Hankkeen sijainti {sijainti}

+

Suunnitelman tyyppi {velho?.tyyppi && t(`projekti-tyyppi.${velho?.tyyppi}`)}

+

Suunnitteluhankkeen kuvaus

+

{kuulutus.hankkeenKuvaus?.[kuulutus.kielitiedot?.ensisijainenKieli || Kieli.SUOMI]}

+

Yhteystiedot

+

Lisätietoja antavat {yhteystiedot}

+

Ladattava kuulutus

+ {kuulutusFileName} ({kuulutusFileExt}) ( + - + ){projekti.euRahoitus &&

EU-logo tähän

} + + ); +} diff --git a/src/pages/yllapito/projekti/[oid]/aloituskuulutus.tsx b/src/pages/yllapito/projekti/[oid]/aloituskuulutus.tsx index beac72ee9..bc6ab686a 100644 --- a/src/pages/yllapito/projekti/[oid]/aloituskuulutus.tsx +++ b/src/pages/yllapito/projekti/[oid]/aloituskuulutus.tsx @@ -27,7 +27,9 @@ import { getProjektiValidationSchema, ProjektiTestType } from "src/schemas/proje import ProjektiErrorNotification from "@components/projekti/ProjektiErrorNotification"; import KuulutuksenYhteystiedot from "@components/projekti/aloituskuulutus/KuulutuksenYhteystiedot"; import deleteFieldArrayIds from "src/util/deleteFieldArrayIds"; -import { cloneDeep, find } from "lodash"; +import cloneDeep from "lodash/cloneDeep"; +import find from "lodash/find"; +import lowerCase from "lodash/lowerCase"; import useSnackbars from "src/hooks/useSnackbars"; import { aloituskuulutusSchema } from "src/schemas/aloituskuulutus"; import AloituskuulutusLukunakyma from "@components/projekti/aloituskuulutus/AloituskuulutusLukunakyma"; @@ -36,7 +38,6 @@ import { GetServerSideProps } from "next"; import { setupLambdaMonitoring } from "backend/src/aws/monitoring"; import { DialogContent, DialogTitle } from "@mui/material"; import TextInput from "@components/form/TextInput"; -import { lowerCase } from "lodash"; import dayjs from "dayjs"; import HassuDialog from "@components/HassuDialog"; import { GetParameterCommandOutput, SSMClient } from "@aws-sdk/client-ssm"; @@ -520,9 +521,8 @@ export default function Aloituskuulutus({ { - let validDate = false; - try { - const dateString2 = new Date(dateString!).toISOString().split("T")[0]; - if (dateString2 === dateString) { - validDate = true; - } - } catch { - validDate = false; + if (!dateString) { + return false; } - return validDate; + return validateDate(dateString); }) .test("not-in-past", "Kuulutuspäivää ei voida asettaa menneisyyteen", (dateString) => { // KuulutusPaiva is not required when saved as a draft. @@ -47,16 +55,7 @@ export const aloituskuulutusSchema = Yup.object().shape({ if (!dateString) { return true; } - let validDate = false; - try { - const dateString2 = new Date(dateString!).toISOString().split("T")[0]; - if (dateString2 === dateString) { - validDate = true; - } - } catch { - validDate = false; - } - return validDate; + return validateDate(dateString); }), esitettavatYhteystiedot: Yup.array() .notRequired() diff --git a/src/services/api/commonApi.ts b/src/services/api/commonApi.ts index a57a3d47c..88e3f7a2d 100644 --- a/src/services/api/commonApi.ts +++ b/src/services/api/commonApi.ts @@ -4,9 +4,15 @@ import { ApolloLink, FetchResult, GraphQLRequest } from "apollo-link"; import { setContext } from "apollo-link-context"; import gql from "graphql-tag"; import ApolloClient, { DefaultOptions } from "apollo-client"; -import { InMemoryCache } from "apollo-cache-inmemory"; +import { InMemoryCache, IntrospectionFragmentMatcher } from "apollo-cache-inmemory"; import log from "loglevel"; import { onError } from "apollo-link-error"; +import * as introspectionQueryResultData from "./fragmentTypes.json"; +import { IntrospectionResultData } from "apollo-cache-inmemory/lib/types"; + +const fragmentMatcher = new IntrospectionFragmentMatcher({ + introspectionQueryResultData: introspectionQueryResultData as IntrospectionResultData, +}); const defaultOptions: DefaultOptions = { watchQuery: { @@ -56,12 +62,12 @@ export class API extends AbstractApi { this.publicClient = new ApolloClient({ link: ApolloLink.from(publicLinks), - cache: new InMemoryCache(), + cache: new InMemoryCache({ fragmentMatcher }), defaultOptions, }); this.authenticatedClient = new ApolloClient({ link: ApolloLink.from(authenticatedLinks), - cache: new InMemoryCache(), + cache: new InMemoryCache({ fragmentMatcher }), defaultOptions, }); } diff --git a/src/services/api/developerApi.ts b/src/services/api/developerApi.ts index d58214e1d..9c94e597e 100644 --- a/src/services/api/developerApi.ts +++ b/src/services/api/developerApi.ts @@ -1,15 +1,18 @@ import awsExports from "../../aws-exports"; import { createAuthLink } from "aws-appsync-auth-link"; import { createHttpLink } from "apollo-link-http"; -import { API } from "@services/api/commonApi"; +import { API } from "./commonApi"; import { setContext } from "apollo-link-context"; +import fetch from "node-fetch"; const AWS = require("aws-sdk"); -AWS.config.update({ - region: awsExports.aws_appsync_region, -}); +if (awsExports.aws_appsync_region) { + AWS.config.update({ + region: awsExports.aws_appsync_region, + }); +} -if (typeof window !== "undefined") { +if (typeof window !== "undefined" && awsExports.AWS_ACCESS_KEY_ID) { AWS.config.update({ credentials: new AWS.Credentials({ accessKeyId: awsExports.AWS_ACCESS_KEY_ID, @@ -46,20 +49,20 @@ function getParamOrDefault(params: URLSearchParams | undefined, name: string, de return defaultValue; } +const authenticatedVaylaUser = setContext((_, { headers }) => { + let params: URLSearchParams | undefined; + if (typeof window !== "undefined") { + params = window.location?.search ? new URLSearchParams(window.location.search) : undefined; + } + return { + headers: { + ...headers, + "x-hassudev-uid": getParamOrDefault(params, "x-hassudev-uid", process.env["x-hassudev-uid"]), + "x-hassudev-roles": getParamOrDefault(params, "x-hassudev-roles", process.env["x-hassudev-roles"]), + }, + }; +}); const links = [ - setContext((_, { headers }) => { - let params: URLSearchParams | undefined; - if (typeof window !== "undefined") { - params = window.location?.search ? new URLSearchParams(window.location.search) : undefined; - } - return { - headers: { - ...headers, - "x-hassudev-uid": getParamOrDefault(params, "x-hassudev-uid", process.env["x-hassudev-uid"]), - "x-hassudev-roles": getParamOrDefault(params, "x-hassudev-roles", process.env["x-hassudev-roles"]), - }, - }; - }), createAuthLink({ url: awsExports.aws_appsync_graphqlEndpoint, region: awsExports.aws_appsync_region, @@ -67,7 +70,8 @@ const links = [ }), createHttpLink({ uri: awsExports.aws_appsync_graphqlEndpoint, + fetch: fetch as any, }), ]; -export const api = new API(links, links, false); +export const api = new API(links, [authenticatedVaylaUser].concat(links), false); diff --git a/src/services/api/fragmentTypes.json b/src/services/api/fragmentTypes.json new file mode 100644 index 000000000..66449b485 --- /dev/null +++ b/src/services/api/fragmentTypes.json @@ -0,0 +1,382 @@ +{ + "__schema": { + "types": [ + { + "kind": "OBJECT", + "name": "Query", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "NykyinenKayttaja", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "SCALAR", + "name": "String", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "ENUM", + "name": "VaylaKayttajaTyyppi", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "Kayttaja", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "SCALAR", + "name": "Boolean", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "INPUT_OBJECT", + "name": "ListaaKayttajatInput", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "Projekti", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "ENUM", + "name": "ProjektiTyyppi", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "ENUM", + "name": "Status", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "ENUM", + "name": "Viranomainen", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "AloitusKuulutus", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "HankkeenKuvaukset", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "Yhteystieto", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "IlmoituksenVastaanottajat", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "KuntaVastaanottaja", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "ViranomaisVastaanottaja", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "ENUM", + "name": "IlmoitettavaViranomainen", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "AloitusKuulutusJulkaisu", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "Velho", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "SuunnitteluSopimus", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "Kielitiedot", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "ENUM", + "name": "Kieli", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "AloitusKuulutusPDFt", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "AloitusKuulutusPDF", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "ENUM", + "name": "AloitusKuulutusTila", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "ProjektiKayttaja", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "ENUM", + "name": "ProjektiRooli", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "Suunnitelma", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "INTERFACE", + "name": "IProjekti", + "possibleTypes": [ + { + "name": "Projekti", + "__typename": "__Type" + }, + { + "name": "ProjektiJulkinen", + "__typename": "__Type" + } + ], + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "VelhoHakuTulos", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "PDF", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "ENUM", + "name": "AsiakirjaTyyppi", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "INPUT_OBJECT", + "name": "TallennaProjektiInput", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "INPUT_OBJECT", + "name": "SuunnitelmaInput", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "INPUT_OBJECT", + "name": "AloitusKuulutusInput", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "INPUT_OBJECT", + "name": "HankkeenKuvauksetInput", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "INPUT_OBJECT", + "name": "YhteystietoInput", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "INPUT_OBJECT", + "name": "IlmoituksenVastaanottajatInput", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "INPUT_OBJECT", + "name": "KuntaVastaanottajaInput", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "INPUT_OBJECT", + "name": "ViranomaisVastaanottajaInput", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "INPUT_OBJECT", + "name": "SuunnitteluSopimusInput", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "INPUT_OBJECT", + "name": "ProjektiKayttajaInput", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "INPUT_OBJECT", + "name": "KielitiedotInput", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "LatausTiedot", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "ENUM", + "name": "LaskuriTyyppi", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "Mutation", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "INPUT_OBJECT", + "name": "TilaSiirtymaInput", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "ENUM", + "name": "TilasiirtymaTyyppi", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "ENUM", + "name": "TilasiirtymaToiminto", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "ArkistointiTunnus", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "ProjektiJulkinen", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "AloitusKuulutusJulkaisuJulkinen", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "__Schema", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "__Type", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "ENUM", + "name": "__TypeKind", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "__Field", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "__InputValue", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "__EnumValue", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "OBJECT", + "name": "__Directive", + "possibleTypes": null, + "__typename": "__Type" + }, + { + "kind": "ENUM", + "name": "__DirectiveLocation", + "possibleTypes": null, + "__typename": "__Type" + } + ], + "__typename": "__Schema" + } +} diff --git a/src/services/config.ts b/src/services/config.ts new file mode 100644 index 000000000..42cb5f30e --- /dev/null +++ b/src/services/config.ts @@ -0,0 +1 @@ +export const isDevEnvironment = process.env.INFRA_ENVIRONMENT == "dev" || process.env.INFRA_ENVIRONMENT == "test"; diff --git a/src/stories/Notification.stories.tsx b/src/stories/Notification.stories.tsx index 16f94687c..4099b1b92 100644 --- a/src/stories/Notification.stories.tsx +++ b/src/stories/Notification.stories.tsx @@ -49,6 +49,13 @@ Info.args = { type: NotificationType.INFO, }; +export const InfoGreen = Template.bind({}); +// More on args: https://storybook.js.org/docs/react/writing-stories/args +InfoGreen.args = { + children: "Ohjeet", + type: NotificationType.INFO_GREEN, +}; + export const Warn = Template.bind({}); // More on args: https://storybook.js.org/docs/react/writing-stories/args Warn.args = { diff --git a/src/styles/notification/Notification.module.css b/src/styles/notification/Notification.module.css index ff868b792..7968ad822 100644 --- a/src/styles/notification/Notification.module.css +++ b/src/styles/notification/Notification.module.css @@ -18,6 +18,10 @@ @apply text-turquoise; } +.notification .start-icon.info-green { + @apply text-green-darker; +} + .notification .start-icon.warn { @apply text-orange-dark; } @@ -38,6 +42,10 @@ @apply bg-gray-lightest border-gray; } +.notification.info-green { + @apply bg-green-lightest border-green-darker; +} + .notification.warn { @apply bg-orange-light border-orange-dark; } diff --git a/tailwind.config.js b/tailwind.config.js index 6073b4425..6ff27ca4f 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -42,7 +42,9 @@ module.exports = { turquoise: "#00b0cc", green: { dark: "#207a43", + darker: "#54AC54", DEFAULT: "#8dcb6d", + lightest: "#F5FFEF" }, orange: { dark: "#F0AD4E",