diff --git a/cdk/bin/viestinvalityspalvelu.ts b/cdk/bin/viestinvalityspalvelu.ts index 4f0cef14..7bb566cd 100644 --- a/cdk/bin/viestinvalityspalvelu.ts +++ b/cdk/bin/viestinvalityspalvelu.ts @@ -5,7 +5,7 @@ import { SovellusStack } from '../lib/sovellus-stack'; import {PersistenssiStack} from "../lib/persistenssi-stack"; import {LoadtestStack} from "../lib/loadtest-stack"; import {MigraatioStack} from "../lib/migraatio-stack"; -import { AwsSolutionsChecks, NagSuppressions } from 'cdk-nag'; +import { AwsSolutionsChecks } from 'cdk-nag'; const app = new cdk.App(); const environmentName = app.node.tryGetContext("environment"); diff --git a/lambdat/raportointi/src/main/scala/fi/oph/viestinvalitys/raportointi/model/LahetyksetParamValidator.scala b/lambdat/raportointi/src/main/scala/fi/oph/viestinvalitys/raportointi/model/LahetyksetParamValidator.scala index af6122e0..b2571e39 100644 --- a/lambdat/raportointi/src/main/scala/fi/oph/viestinvalitys/raportointi/model/LahetyksetParamValidator.scala +++ b/lambdat/raportointi/src/main/scala/fi/oph/viestinvalitys/raportointi/model/LahetyksetParamValidator.scala @@ -17,7 +17,8 @@ case class LahetyksetParams(alkaen: Optional[String], enintaan: Optional[String], vastaanottajanEmail: Optional[String], organisaatio: Optional[String], - viesti: Optional[String]) + viesti: Optional[String], + palvelu: Optional[String]) object LahetyksetParamValidator { def validateAlkaenUUID(alkaen: Optional[String]): Set[String] = @@ -69,7 +70,7 @@ object LahetyksetParamValidator { .flatMap(virheet => if (hakusanaParam.isPresent && validatedHakusana.isEmpty) Left(virheet.incl(HAKUSANA_INVALID)) else Right(virheet)) .fold(l => l, r => r) - + def validateVastaanottajatParams(params: VastaanottajatParams): Seq[String] = Seq( validateLahetysTunniste(params.lahetysTunniste), @@ -86,6 +87,7 @@ object LahetyksetParamValidator { validateEnintaan(params.enintaan, LAHETYKSET_ENINTAAN_MIN, LAHETYKSET_ENINTAAN_MAX, LAHETYKSET_ENINTAAN_INVALID), validateEmailParam(params.vastaanottajanEmail, VASTAANOTTAJA_INVALID), validateOrganisaatio(params.organisaatio), - validateHakusanaParam(params.viesti) + validateHakusanaParam(params.viesti), + validateHakusanaParam(params.palvelu) ).flatten } diff --git a/lambdat/raportointi/src/main/scala/fi/oph/viestinvalitys/raportointi/resource/LahetysResource.scala b/lambdat/raportointi/src/main/scala/fi/oph/viestinvalitys/raportointi/resource/LahetysResource.scala index b6d8e334..0d1533ab 100644 --- a/lambdat/raportointi/src/main/scala/fi/oph/viestinvalitys/raportointi/resource/LahetysResource.scala +++ b/lambdat/raportointi/src/main/scala/fi/oph/viestinvalitys/raportointi/resource/LahetysResource.scala @@ -16,6 +16,7 @@ import org.slf4j.LoggerFactory import org.springframework.http.{HttpStatus, MediaType, ResponseEntity} import org.springframework.web.bind.annotation.* import org.springframework.web.context.request.{RequestContextHolder, ServletRequestAttributes} +import upickle.default.* import java.util import java.util.{Optional, UUID} @@ -50,8 +51,8 @@ class LahetysResource { @RequestParam(name = VASTAANOTTAJA_PARAM_NAME, required = false) vastaanottajanEmail: Optional[String], @RequestParam(name = ORGANISAATIO_PARAM_NAME, required = false) organisaatio: Optional[String], @RequestParam(name = VIESTI_SISALTO_PARAM_NAME, required = false) viesti: Optional[String], + @RequestParam(name = PALVELU_PARAM_NAME, required = false) palvelu: Optional[String], request: HttpServletRequest): ResponseEntity[PalautaLahetyksetResponse] = - LOG.warn(s"Haetaan lähetyksiä parametreilla vastaanottaja: ${vastaanottajanEmail.toScala.getOrElse("")} viesti: ${viesti.toScala.getOrElse("")}") val securityOperaatiot = new SecurityOperaatiot val kantaOperaatiot = new KantaOperaatiot(DbUtil.database) try @@ -63,7 +64,7 @@ class LahetysResource { else Right(None)) .flatMap(_ => - val virheet = LahetyksetParamValidator.validateLahetyksetParams(LahetyksetParams(alkaen, enintaan, vastaanottajanEmail, organisaatio, viesti)) + val virheet = LahetyksetParamValidator.validateLahetyksetParams(LahetyksetParams(alkaen, enintaan, vastaanottajanEmail, organisaatio, viesti, palvelu)) if (!virheet.isEmpty) Left(ResponseEntity.status(HttpStatus.BAD_REQUEST).body(PalautaLahetyksetFailureResponse(virheet.asJava))) else @@ -77,7 +78,8 @@ class LahetysResource { alkaen = ParametriUtil.asUUID(alkaen), enintaan = ParametriUtil.asInt(enintaan).getOrElse(65535), vastaanottajaHakuLauseke = vastaanottajanEmail.toScala, - sisaltoHakuLauseke = viesti.toScala) // samalla hakusanalla haetaan otsikosta ja sisällöstä + sisaltoHakuLauseke = viesti.toScala, + lahettavaPalveluHakuLauseke = palvelu.toScala) if (lahetykset.isEmpty) // on ok tilanne että haku ei palauta tuloksia Left(ResponseEntity.status(HttpStatus.OK).body(PalautaLahetyksetSuccessResponse(Seq.empty.asJava, Optional.empty))) @@ -396,6 +398,28 @@ class LahetysResource { LOG.error("Vastaanottajien lukeminen epäonnistui", e) ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(VastaanottajatFailureResponse(Seq(RaportointiAPIConstants.VASTAANOTTAJAT_LUKEMINEN_EPAONNISTUI).asJava))) + @GetMapping(path = Array(RaportointiAPIConstants.PALVELUT_PATH), produces = Array(MediaType.APPLICATION_JSON_VALUE)) + @Operation( + summary = "Palauttaa listan lähetyksiä lähettäviä palveluja", + description = "Palauttaa lähettävien palvelujen listauksen käyttöliittymän hakutoimintoa varten", + responses = Array( + new ApiResponse(responseCode = "200", description = "Palauttaa listan palvelunimiä"), + )) + def getLahettavatPalvelut() = { + LOG.info("Haetaan lähettävät palvelut") + val kantaOperaatiot = new KantaOperaatiot(DbUtil.database) + try + // suodatetaan pois swagger-esimerkkirivin palvelu + val palvelut = kantaOperaatiot.getLahettavatPalvelut().filterNot(p => p.equals("Esimerkkipalvelu")) + LOG.info(s"Löytyi ${palvelut.size} palvelua") + ResponseEntity.status(HttpStatus.OK).body(write[List[String]](palvelut)) + catch + case e: Exception => + LOG.error("Lähettävien palvelujen haku epäonnistui", e) + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(write(Map("message" -> e.getMessage))) + } + + def organisaatiorajaus(organisaatio: Optional[String], kayttajanOikeudet: Set[Kayttooikeus], organisaatioClient: OrganisaatioService): Set[Kayttooikeus] = if (organisaatio.isEmpty) kayttajanOikeudet diff --git a/lambdat/raportointi/src/main/scala/fi/oph/viestinvalitys/raportointi/resource/RaportointiApiConstants.scala b/lambdat/raportointi/src/main/scala/fi/oph/viestinvalitys/raportointi/resource/RaportointiApiConstants.scala index 0f812e4f..5ac79013 100644 --- a/lambdat/raportointi/src/main/scala/fi/oph/viestinvalitys/raportointi/resource/RaportointiApiConstants.scala +++ b/lambdat/raportointi/src/main/scala/fi/oph/viestinvalitys/raportointi/resource/RaportointiApiConstants.scala @@ -24,6 +24,7 @@ object RaportointiAPIConstants { final val VASTAANOTTAJA_PARAM_NAME = "vastaanottaja" final val ORGANISAATIO_PARAM_NAME = "organisaatio" final val VIESTI_SISALTO_PARAM_NAME = "viesti" + final val PALVELU_PARAM_NAME = "palvelu" final val VIESTI_PATH = VERSIONED_RAPORTOINTI_API_PREFIX + "/viesti" final val VIESTITUNNISTE_PARAM_NAME = "viestiTunniste" @@ -36,6 +37,7 @@ object RaportointiAPIConstants { final val ORGANISAATIOT_PATH = VERSIONED_RAPORTOINTI_API_PREFIX + "/organisaatiot" final val ORGANISAATIOT_OIKEUDET_PATH = ORGANISAATIOT_PATH + "/oikeudet" final val OMAT_TIEDOT_PATH = VERSIONED_RAPORTOINTI_API_PREFIX + "/omattiedot" + final val PALVELUT_PATH = VERSIONED_RAPORTOINTI_API_PREFIX + "/palvelut" /** * Swagger-kuvauksiin liittyvät vakiot diff --git a/shared/src/main/resources/flyway/V202409120000__create_lahettavapalvelu_index.sql b/shared/src/main/resources/flyway/V202409120000__create_lahettavapalvelu_index.sql new file mode 100644 index 00000000..ed32a5ce --- /dev/null +++ b/shared/src/main/resources/flyway/V202409120000__create_lahettavapalvelu_index.sql @@ -0,0 +1,2 @@ +-- lista lähettävistä palveluista muodostetaan toistaiseksi lähetyksien tiedoista, palvelu cachettaa +CREATE INDEX IF NOT EXISTS lahetykset_lahettavapalvelu_index on lahetykset (lahettavapalvelu asc); \ No newline at end of file diff --git a/shared/src/main/scala/fi/oph/viestinvalitys/business/KantaOperaatiot.scala b/shared/src/main/scala/fi/oph/viestinvalitys/business/KantaOperaatiot.scala index ab7a42cd..320f49fa 100644 --- a/shared/src/main/scala/fi/oph/viestinvalitys/business/KantaOperaatiot.scala +++ b/shared/src/main/scala/fi/oph/viestinvalitys/business/KantaOperaatiot.scala @@ -1116,4 +1116,19 @@ class KantaOperaatiot(db: JdbcBackend.JdbcDatabaseDef) { maskit = maskit.get(tunniste).getOrElse(Map.empty), omistaja = omistaja, prioriteetti = Prioriteetti.valueOf(prioriteetti))).headOption + + /** + * Hakee listan lähettävistä palveluista + * + * @return lista lähetyksille tallennetuista lähettävistä palveluista + */ + def getLahettavatPalvelut(): List[String] = + + val lahettavatPalvelutQuery = sql""" + SELECT DISTINCT lahettavapalvelu + FROM lahetykset + ORDER BY lahettavapalvelu ASC + """.as[String] + Await.result(db.run(lahettavatPalvelutQuery), DB_TIMEOUT).toList + } diff --git a/shared/src/test/scala/fi/oph/viestinvalitys/business/KantaOperaatiotTest.scala b/shared/src/test/scala/fi/oph/viestinvalitys/business/KantaOperaatiotTest.scala index 8f2bf634..5464dc4a 100644 --- a/shared/src/test/scala/fi/oph/viestinvalitys/business/KantaOperaatiotTest.scala +++ b/shared/src/test/scala/fi/oph/viestinvalitys/business/KantaOperaatiotTest.scala @@ -1286,4 +1286,18 @@ class KantaOperaatiotTest { kantaOperaatiot.getRaportointiViestiTunnisteella(viesti.tunniste, kayttajanKayttooikeudet).get) // ei käyttöoikeuksia Assertions.assertEquals(Option.empty, kantaOperaatiot.getRaportointiViestiTunnisteella(viesti2.tunniste, kayttajanKayttooikeudet)) + + /** + * Testataan lähettävien palvelujen listan haku + */ + @Test def testGetLahettavatPalvelut(): Unit = + // lähetyksiä eri palveluilla + val lahetys = this.tallennaLahetys(lahettavaPalvelu = "hakemuspalvelu") + val lahetys2 = this.tallennaLahetys(lahettavaPalvelu = "osoitepalvelu") + val lahetys3 = this.tallennaLahetys(lahettavaPalvelu = "jokupalvelu") + // sama palvelu + val lahetys4 = this.tallennaLahetys(lahettavaPalvelu = "hakemuspalvelu") + // Esimerkkipalvelu on alustettuna kantaan + Assertions.assertEquals(4, kantaOperaatiot.getLahettavatPalvelut().size) + Assertions.assertEquals(List("Esimerkkipalvelu", "hakemuspalvelu", "jokupalvelu", "osoitepalvelu"), kantaOperaatiot.getLahettavatPalvelut()) } diff --git a/viestinvalitys-raportointi/package-lock.json b/viestinvalitys-raportointi/package-lock.json index 0d53af44..7ef3e1a6 100644 --- a/viestinvalitys-raportointi/package-lock.json +++ b/viestinvalitys-raportointi/package-lock.json @@ -15,9 +15,10 @@ "@mui/material-nextjs": "^5.15.11", "@mui/x-data-grid": "^6.18.7", "@mui/x-tree-view": "^6.17.0", - "@opetushallitus/oph-design-system": "github:opetushallitus/oph-design-system#v0.0.2", + "@opetushallitus/oph-design-system": "github:opetushallitus/oph-design-system#v0.0.3", "@tanstack/react-query": "^5.51.5", "@tanstack/react-query-devtools": "^5.51.5", + "date-fns-tz": "^3.1.3", "dompurify": "^3.0.11", "i18next": "^23.11.5", "i18next-chained-backend": "^4.6.2", @@ -1780,9 +1781,8 @@ } }, "node_modules/@opetushallitus/oph-design-system": { - "version": "0.0.2", - "resolved": "git+ssh://git@github.com/opetushallitus/oph-design-system.git#56838f9afdcccea63f91652332c484e133e09f69", - "license": "EUPL-1.2", + "version": "0.0.3", + "resolved": "git+ssh://git@github.com/opetushallitus/oph-design-system.git#4033d6e687bbda8706bdd3eed61a9cd959c4e950", "peerDependencies": { "@mui/material": "^5", "next": "^14", @@ -3949,6 +3949,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-tz": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.1.3.tgz", + "integrity": "sha512-ZfbMu+nbzW0mEzC8VZrLiSWvUIaI3aRHeq33mTe7Y38UctKukgqPR4nTDwcwS4d64Gf8GghnVsroBuMY3eiTeA==", + "peerDependencies": { + "date-fns": "^3.0.0" + } + }, "node_modules/debug": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", diff --git a/viestinvalitys-raportointi/package.json b/viestinvalitys-raportointi/package.json index d2bd9dda..78fffb40 100644 --- a/viestinvalitys-raportointi/package.json +++ b/viestinvalitys-raportointi/package.json @@ -17,9 +17,10 @@ "@mui/material-nextjs": "^5.15.11", "@mui/x-data-grid": "^6.18.7", "@mui/x-tree-view": "^6.17.0", - "@opetushallitus/oph-design-system": "github:opetushallitus/oph-design-system#v0.0.2", + "@opetushallitus/oph-design-system": "github:opetushallitus/oph-design-system#v0.0.3", "@tanstack/react-query": "^5.51.5", "@tanstack/react-query-devtools": "^5.51.5", + "date-fns-tz": "^3.1.3", "dompurify": "^3.0.11", "i18next": "^23.11.5", "i18next-chained-backend": "^4.6.2", diff --git a/viestinvalitys-raportointi/src/app/Haku.tsx b/viestinvalitys-raportointi/src/app/Haku.tsx index a6b5b40b..8acc2899 100644 --- a/viestinvalitys-raportointi/src/app/Haku.tsx +++ b/viestinvalitys-raportointi/src/app/Haku.tsx @@ -1,12 +1,8 @@ 'use client'; import { Box, - FormControl, - FormLabel, InputAdornment, - MenuItem, OutlinedInput, - Select, SelectChangeEvent, } from '@mui/material'; import { useDebouncedCallback } from 'use-debounce'; @@ -14,33 +10,34 @@ import { useQueryState } from 'nuqs'; import { Search } from '@mui/icons-material'; import { NUQS_DEFAULT_OPTIONS } from './lib/constants'; import { useTranslation } from './i18n/clientLocalization'; +import { LahettavaPalveluInput } from './components/LahettavaPalveluInput'; +import { OphFormControl } from './components/OphFormControl'; +import { OphSelect } from '@opetushallitus/oph-design-system'; const HakukenttaSelect = ({ + labelId, value: selectedHakukentta, onChange, }: { + labelId: string; value: string; onChange: (e: SelectChangeEvent) => void; }) => { const { t } = useTranslation(); return ( - + /> ); }; @@ -53,62 +50,91 @@ const HakukenttaInput = ({ }) => { const { t } = useTranslation(); return ( - - {t('lahetykset.haku.mista-haetaan')} - - + ( + + )} + /> ); }; -export default function Haku() { - const [selectedHakukentta, setSelectedHakukentta] = useQueryState("hakukentta", NUQS_DEFAULT_OPTIONS); - const [hakusana, setHakusana] = useQueryState("hakusana", NUQS_DEFAULT_OPTIONS); +export default function Haku({lahettavatPalvelut}: {lahettavatPalvelut: string[]}) { + const [selectedHakukentta, setSelectedHakukentta] = useQueryState( + 'hakukentta', + NUQS_DEFAULT_OPTIONS, + ); + const [hakusana, setHakusana] = useQueryState( + 'hakusana', + NUQS_DEFAULT_OPTIONS, + ); + const [palvelu, setPalvelu] = useQueryState('palvelu', NUQS_DEFAULT_OPTIONS); // päivitetään 3s viiveellä hakuparametrit const handleTypedSearch = useDebouncedCallback((term) => { // päivitetään kun minimipituus täyttyy - if(term.length > 4 || term) { - setHakusana(term) + if (term.length > 4 || term) { + setHakusana(term); } else if (term.length == 0) { - setHakusana(null) // kentän tyhjäys + setHakusana(null); // kentän tyhjäys } }, 3000); const { t } = useTranslation(); return ( - - setSelectedHakukentta(e.target.value)}/> - + - {t('lahetykset.hae')} - { - handleTypedSearch(e.target.value); + setSelectedHakukentta(e.target.value)} + /> + { + return ( + { + handleTypedSearch(e.target.value); + }} + autoFocus={true} + type="text" + endAdornment={ + + + + } + /> + ); }} - autoFocus={true} - type="text" - endAdornment={ - - - - } /> - + + + setPalvelu(e.target.value)} + palvelut={lahettavatPalvelut} /> + ); } diff --git a/viestinvalitys-raportointi/src/app/components/ClientSpinner.tsx b/viestinvalitys-raportointi/src/app/components/ClientSpinner.tsx new file mode 100644 index 00000000..7e11d0c1 --- /dev/null +++ b/viestinvalitys-raportointi/src/app/components/ClientSpinner.tsx @@ -0,0 +1,9 @@ +'use client'; + +import { CircularProgress, CircularProgressProps } from '@mui/material'; +import { useTranslation } from '../i18n/clientLocalization'; + +export const ClientSpinner = (props: CircularProgressProps) => { + const { t } = useTranslation(); + return ; +}; diff --git a/viestinvalitys-raportointi/src/app/components/GreyDivider.tsx b/viestinvalitys-raportointi/src/app/components/GreyDivider.tsx index 9a95ed3f..b8074b55 100644 --- a/viestinvalitys-raportointi/src/app/components/GreyDivider.tsx +++ b/viestinvalitys-raportointi/src/app/components/GreyDivider.tsx @@ -1,10 +1,10 @@ 'use client'; import { Divider } from "@mui/material"; -import { colors } from "../theme"; +import { ophColors } from "@opetushallitus/oph-design-system"; export const GreyDivider = () => { return ( -