From a0aecdfb1b9c4656599f15f99a6e1573d036ffd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Eorkell=20M=C3=A1ni=20=C3=9Eorkelsson?= Date: Sun, 29 Sep 2024 13:25:24 +0000 Subject: [PATCH] feat(assets): Bulk mileage registration (#15774) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Vehicle bulk mileage frontend * feat: omg it works * chore: remove logs * chore: nx format:write update dirty files * fix: more messsages * feat: bad csv parser * fix: failure calblack * feat: refactor logic * chore: nx format:write update dirty files * feat: better org * chore: update config * feat: update client * feat: update with mutation * feat: organize domain and add methods * feat: update domain * fix: better csv parsing * chore: label * chore: nx format:write update dirty files * feat: some ui * chore: nx format:write update dirty files * feat/clearer ui * chore: empty screen * fix: remove buttons * fix: expand callbacks * fix: linting * fix: expand lower * chore: remove console * chore: localize messages * chore: imports * fix: add logos * fix: parsing * fix: reveiw comments * chore: nx format:write update dirty files * fix: more review fixes * chore: review comment v3 * chore: fix func name * chore: review2 gp * chore: add error message * fix:nullechck * fix:review --------- Co-authored-by: Þórður Hafliðason Co-authored-by: andes-it Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- libs/api/domains/vehicles/src/index.ts | 3 +- .../src/lib/api-domains-vehicles.type.ts | 183 ------ .../api/domains/vehicles/src/lib/constants.ts | 2 + ...BulkVehicleMileageRequestOverview.input.ts | 7 + ...etBulkVehicleMileageRequestStatus.input.ts | 7 + .../dto/getPublicVehicleSearchInput.ts | 0 .../{ => lib}/dto/getVehicleDetailInput.ts | 0 .../{ => lib}/dto/getVehicleMileageInput.ts | 0 .../{ => lib}/dto/getVehicleSearchInput.ts | 0 .../{ => lib}/dto/getVehiclesForUserInput.ts | 0 .../src/lib/dto/mileageReading.dto.ts | 9 + .../lib/dto/postBulkVehicleMileage.input.ts | 22 + .../{ => lib}/dto/postVehicleMileageInput.ts | 0 .../src/lib/dto/vehiclesListInputV3.ts | 10 + .../models/getPublicVehicleSearch.model.ts | 0 .../models/getVehicleDetail.model.ts | 0 .../models/getVehicleMileage.model.ts | 0 .../models/getVehicleSearch.model.ts | 0 .../{ => lib}/models/usersVehicles.model.ts | 8 + .../bulkMileageReadingResponse.model.ts | 13 + .../bulkMileageRegistrationJob.model.ts | 37 ++ ...bulkMileageRegistrationJobHistory.model.ts | 8 + ...kMileageRegistrationRequestDetail.model.ts | 22 + ...lkMileageRegistrationRequestError.model.ts | 10 + ...ileageRegistrationRequestOverview.model.ts | 8 + ...kMileageRegistrationRequestStatus.model.ts | 22 + .../v3/currentVehicleListResponse.model.ts | 20 + .../v3/currentVehicleWithMileage.model.ts | 23 + .../src/lib/models/v3/mileageDetails.model.ts | 14 + .../models/v3/mileageRegistration.model.ts | 13 + .../v3/mileageRegistrationHistory.model.ts | 14 + .../src/lib/resolvers/bulkMileage.resolver.ts | 84 +++ .../mileage.resolver.ts} | 6 +- .../shared.resolver.ts} | 2 +- .../src/lib/resolvers/vehicleV3.resolver.ts | 48 ++ .../vehicles.resolver.ts} | 2 +- .../src/lib/services/bulkMileage.service.ts | 143 +++++ .../vehicles.service.ts} | 121 +++- .../utils/basicVehicleInformationMapper.ts | 0 .../vehicles/src/{ => lib}/utils/helpers.ts | 0 ...-vehicles.module.ts => vehicles.module.ts} | 14 +- libs/clients/vehicles-mileage/project.json | 3 +- .../vehicles-mileage/src/clientConfig.json | 563 +++++++++++++++++- libs/feature-flags/src/lib/features.ts | 1 + libs/island-ui/core/src/lib/Icon/Icon.tsx | 7 + libs/island-ui/core/src/lib/IconRC/iconMap.ts | 3 + .../core/src/lib/IconRC/icons/Upload.tsx | 23 + .../src/lib/IconRC/icons/UploadOutline.tsx | 38 ++ .../service-portal/assets/src/lib/messages.ts | 169 ++++++ .../assets/src/lib/navigation.ts | 23 + libs/service-portal/assets/src/lib/paths.ts | 4 + libs/service-portal/assets/src/module.tsx | 45 ++ .../patentVariations/IS.tsx | 1 - .../VehicleBulkMileage.css.ts | 11 + .../VehicleBulkMileage.graphql | 37 ++ .../VehicleBulkMileage/VehicleBulkMileage.tsx | 146 +++++ .../VehicleBulkMileageFileDownloader.tsx | 46 ++ .../VehicleBulkMileageRow.tsx | 160 +++++ .../VehicleBulkMileageSaveButton.tsx | 56 ++ .../VehicleBulkMileageTable.tsx | 88 +++ .../VehicleBulkMileage/mocks/propsDummy.ts | 284 +++++++++ .../src/screens/VehicleBulkMileage/types.ts | 34 ++ .../VehicleBulkMileageJobDetail.graphql | 24 + .../VehicleBulkMileageJobDetail.tsx | 242 ++++++++ .../VehicleBulkMileageJobOverview.graphql | 14 + .../VehicleBulkMileageJobOverview.tsx | 148 +++++ .../VehicleBulkMileageUpload.graphql | 6 + .../VehicleBulkMileageUpload.tsx | 174 ++++++ .../assets/src/utils/makeArrayEven.ts | 2 +- .../assets/src/utils/parseCsvToMileage.ts | 58 ++ .../assets/src/utils/vehicleOwnedMapper.ts | 10 +- .../src/components/LinkButton/LinkButton.tsx | 16 +- .../NestedTable/NestedFullTable.tsx | 76 +++ .../components/NestedTable/NestedTable.css.ts | 18 + libs/service-portal/core/src/index.ts | 1 + libs/service-portal/core/src/lib/messages.ts | 33 + 76 files changed, 3209 insertions(+), 230 deletions(-) delete mode 100644 libs/api/domains/vehicles/src/lib/api-domains-vehicles.type.ts create mode 100644 libs/api/domains/vehicles/src/lib/constants.ts create mode 100644 libs/api/domains/vehicles/src/lib/dto/getBulkVehicleMileageRequestOverview.input.ts create mode 100644 libs/api/domains/vehicles/src/lib/dto/getBulkVehicleMileageRequestStatus.input.ts rename libs/api/domains/vehicles/src/{ => lib}/dto/getPublicVehicleSearchInput.ts (100%) rename libs/api/domains/vehicles/src/{ => lib}/dto/getVehicleDetailInput.ts (100%) rename libs/api/domains/vehicles/src/{ => lib}/dto/getVehicleMileageInput.ts (100%) rename libs/api/domains/vehicles/src/{ => lib}/dto/getVehicleSearchInput.ts (100%) rename libs/api/domains/vehicles/src/{ => lib}/dto/getVehiclesForUserInput.ts (100%) create mode 100644 libs/api/domains/vehicles/src/lib/dto/mileageReading.dto.ts create mode 100644 libs/api/domains/vehicles/src/lib/dto/postBulkVehicleMileage.input.ts rename libs/api/domains/vehicles/src/{ => lib}/dto/postVehicleMileageInput.ts (100%) create mode 100644 libs/api/domains/vehicles/src/lib/dto/vehiclesListInputV3.ts rename libs/api/domains/vehicles/src/{ => lib}/models/getPublicVehicleSearch.model.ts (100%) rename libs/api/domains/vehicles/src/{ => lib}/models/getVehicleDetail.model.ts (100%) rename libs/api/domains/vehicles/src/{ => lib}/models/getVehicleMileage.model.ts (100%) rename libs/api/domains/vehicles/src/{ => lib}/models/getVehicleSearch.model.ts (100%) rename libs/api/domains/vehicles/src/{ => lib}/models/usersVehicles.model.ts (94%) create mode 100644 libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageReadingResponse.model.ts create mode 100644 libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageRegistrationJob.model.ts create mode 100644 libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageRegistrationJobHistory.model.ts create mode 100644 libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageRegistrationRequestDetail.model.ts create mode 100644 libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageRegistrationRequestError.model.ts create mode 100644 libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageRegistrationRequestOverview.model.ts create mode 100644 libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageRegistrationRequestStatus.model.ts create mode 100644 libs/api/domains/vehicles/src/lib/models/v3/currentVehicleListResponse.model.ts create mode 100644 libs/api/domains/vehicles/src/lib/models/v3/currentVehicleWithMileage.model.ts create mode 100644 libs/api/domains/vehicles/src/lib/models/v3/mileageDetails.model.ts create mode 100644 libs/api/domains/vehicles/src/lib/models/v3/mileageRegistration.model.ts create mode 100644 libs/api/domains/vehicles/src/lib/models/v3/mileageRegistrationHistory.model.ts create mode 100644 libs/api/domains/vehicles/src/lib/resolvers/bulkMileage.resolver.ts rename libs/api/domains/vehicles/src/lib/{api-domains-vehicles-mileage.resolver.ts => resolvers/mileage.resolver.ts} (95%) rename libs/api/domains/vehicles/src/lib/{api-domains-vehicles-shared.resolver.ts => resolvers/shared.resolver.ts} (95%) create mode 100644 libs/api/domains/vehicles/src/lib/resolvers/vehicleV3.resolver.ts rename libs/api/domains/vehicles/src/lib/{api-domains-vehicles.resolver.ts => resolvers/vehicles.resolver.ts} (98%) create mode 100644 libs/api/domains/vehicles/src/lib/services/bulkMileage.service.ts rename libs/api/domains/vehicles/src/lib/{api-domains-vehicles.service.ts => services/vehicles.service.ts} (75%) rename libs/api/domains/vehicles/src/{ => lib}/utils/basicVehicleInformationMapper.ts (100%) rename libs/api/domains/vehicles/src/{ => lib}/utils/helpers.ts (100%) rename libs/api/domains/vehicles/src/lib/{api-domains-vehicles.module.ts => vehicles.module.ts} (52%) create mode 100644 libs/island-ui/core/src/lib/IconRC/icons/Upload.tsx create mode 100644 libs/island-ui/core/src/lib/IconRC/icons/UploadOutline.tsx create mode 100644 libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileage.css.ts create mode 100644 libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileage.graphql create mode 100644 libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileage.tsx create mode 100644 libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileageFileDownloader.tsx create mode 100644 libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileageRow.tsx create mode 100644 libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileageSaveButton.tsx create mode 100644 libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileageTable.tsx create mode 100644 libs/service-portal/assets/src/screens/VehicleBulkMileage/mocks/propsDummy.ts create mode 100644 libs/service-portal/assets/src/screens/VehicleBulkMileage/types.ts create mode 100644 libs/service-portal/assets/src/screens/VehicleBulkMileageJobDetail/VehicleBulkMileageJobDetail.graphql create mode 100644 libs/service-portal/assets/src/screens/VehicleBulkMileageJobDetail/VehicleBulkMileageJobDetail.tsx create mode 100644 libs/service-portal/assets/src/screens/VehicleBulkMileageJobOverview/VehicleBulkMileageJobOverview.graphql create mode 100644 libs/service-portal/assets/src/screens/VehicleBulkMileageJobOverview/VehicleBulkMileageJobOverview.tsx create mode 100644 libs/service-portal/assets/src/screens/VehicleBulkMileageUpload/VehicleBulkMileageUpload.graphql create mode 100644 libs/service-portal/assets/src/screens/VehicleBulkMileageUpload/VehicleBulkMileageUpload.tsx create mode 100644 libs/service-portal/assets/src/utils/parseCsvToMileage.ts create mode 100644 libs/service-portal/core/src/components/NestedTable/NestedFullTable.tsx diff --git a/libs/api/domains/vehicles/src/index.ts b/libs/api/domains/vehicles/src/index.ts index 3bf603730d352..3b36038fbea01 100644 --- a/libs/api/domains/vehicles/src/index.ts +++ b/libs/api/domains/vehicles/src/index.ts @@ -1,2 +1 @@ -export * from './lib/api-domains-vehicles.module' -export * from './lib/api-domains-vehicles.service' +export * from './lib/vehicles.module' diff --git a/libs/api/domains/vehicles/src/lib/api-domains-vehicles.type.ts b/libs/api/domains/vehicles/src/lib/api-domains-vehicles.type.ts deleted file mode 100644 index 402acbf3d7651..0000000000000 --- a/libs/api/domains/vehicles/src/lib/api-domains-vehicles.type.ts +++ /dev/null @@ -1,183 +0,0 @@ -/** - * - * @export - * @interface Vehicle - */ -export interface Vehicle { - /** - * - * @type {boolean} - * @memberof Vehicle - */ - isCurrent?: boolean - /** - * - * @type {string} - * @memberof Vehicle - */ - permno?: string | null - /** - * - * @type {string} - * @memberof Vehicle - */ - regno?: string | null - /** - * - * @type {string} - * @memberof Vehicle - */ - vin?: string | null - /** - * - * @type {string} - * @memberof Vehicle - */ - type?: string | null - /** - * - * @type {string} - * @memberof Vehicle - */ - color?: string | null - /** - * - * @type {Date} - * @memberof Vehicle - */ - firstRegDate?: Date | null - /** - * - * @type {string} - * @memberof Vehicle - */ - modelYear?: string | null - /** - * - * @type {string} - * @memberof Vehicle - */ - productYear?: string | null - /** - * - * @type {string} - * @memberof Vehicle - */ - registrationType?: string | null - /** - * - * @type {string} - * @memberof Vehicle - */ - role?: string | null - /** - * - * @type {Date} - * @memberof Vehicle - */ - operatorStartDate?: Date | null - /** - * - * @type {Date} - * @memberof Vehicle - */ - operatorEndDate?: Date | null - /** - * - * @type {boolean} - * @memberof Vehicle - */ - outOfUse?: boolean - /** - * - * @type {boolean} - * @memberof Vehicle - */ - otherOwners?: boolean - /** - * - * @type {string} - * @memberof Vehicle - */ - termination?: string | null - /** - * - * @type {string} - * @memberof Vehicle - */ - buyerPersidno?: string | null - /** - * - * @type {string} - * @memberof Vehicle - */ - ownerPersidno?: string | null - /** - * - * @type {string} - * @memberof Vehicle - */ - vehicleStatus?: string | null - /** - * - * @type {string} - * @memberof Vehicle - */ - useGroup?: string | null - /** - * - * @type {string} - * @memberof Vehicle - */ - vehGroup?: string | null - /** - * - * @type {string} - * @memberof Vehicle - */ - plateStatus?: string | null -} - -/** - * - * @export - * @interface PersidnoLookup - */ -export interface UsersVehicles { - /** - * - * @type {string} - * @memberof PersidnoLookup - */ - persidno?: string | null - /** - * - * @type {string} - * @memberof PersidnoLookup - */ - name?: string | null - /** - * - * @type {string} - * @memberof PersidnoLookup - */ - address?: string | null - /** - * - * @type {string} - * @memberof PersidnoLookup - */ - postStation?: string | null - /** - * - * @type {Array} - * @memberof PersidnoLookup - */ - vehicleList?: Array | null - /** - * - * @type {string} - * @memberof PersidnoLookup - */ - createdTimestamp?: string | null -} diff --git a/libs/api/domains/vehicles/src/lib/constants.ts b/libs/api/domains/vehicles/src/lib/constants.ts new file mode 100644 index 0000000000000..7d200c985f787 --- /dev/null +++ b/libs/api/domains/vehicles/src/lib/constants.ts @@ -0,0 +1,2 @@ +export const ISLAND_IS_ORIGIN_CODE = 'ISLAND.IS' +export const LOG_CATEGORY = 'api-domains-vehicles' diff --git a/libs/api/domains/vehicles/src/lib/dto/getBulkVehicleMileageRequestOverview.input.ts b/libs/api/domains/vehicles/src/lib/dto/getBulkVehicleMileageRequestOverview.input.ts new file mode 100644 index 0000000000000..65b238fb9150c --- /dev/null +++ b/libs/api/domains/vehicles/src/lib/dto/getBulkVehicleMileageRequestOverview.input.ts @@ -0,0 +1,7 @@ +import { Field, ID, InputType } from '@nestjs/graphql' + +@InputType() +export class BulkVehicleMileageRequestOverviewInput { + @Field(() => ID) + guid!: string +} diff --git a/libs/api/domains/vehicles/src/lib/dto/getBulkVehicleMileageRequestStatus.input.ts b/libs/api/domains/vehicles/src/lib/dto/getBulkVehicleMileageRequestStatus.input.ts new file mode 100644 index 0000000000000..077fb1f9fdcf1 --- /dev/null +++ b/libs/api/domains/vehicles/src/lib/dto/getBulkVehicleMileageRequestStatus.input.ts @@ -0,0 +1,7 @@ +import { Field, ID, InputType } from '@nestjs/graphql' + +@InputType() +export class BulkVehicleMileageRequestStatusInput { + @Field(() => ID) + requestId!: string +} diff --git a/libs/api/domains/vehicles/src/dto/getPublicVehicleSearchInput.ts b/libs/api/domains/vehicles/src/lib/dto/getPublicVehicleSearchInput.ts similarity index 100% rename from libs/api/domains/vehicles/src/dto/getPublicVehicleSearchInput.ts rename to libs/api/domains/vehicles/src/lib/dto/getPublicVehicleSearchInput.ts diff --git a/libs/api/domains/vehicles/src/dto/getVehicleDetailInput.ts b/libs/api/domains/vehicles/src/lib/dto/getVehicleDetailInput.ts similarity index 100% rename from libs/api/domains/vehicles/src/dto/getVehicleDetailInput.ts rename to libs/api/domains/vehicles/src/lib/dto/getVehicleDetailInput.ts diff --git a/libs/api/domains/vehicles/src/dto/getVehicleMileageInput.ts b/libs/api/domains/vehicles/src/lib/dto/getVehicleMileageInput.ts similarity index 100% rename from libs/api/domains/vehicles/src/dto/getVehicleMileageInput.ts rename to libs/api/domains/vehicles/src/lib/dto/getVehicleMileageInput.ts diff --git a/libs/api/domains/vehicles/src/dto/getVehicleSearchInput.ts b/libs/api/domains/vehicles/src/lib/dto/getVehicleSearchInput.ts similarity index 100% rename from libs/api/domains/vehicles/src/dto/getVehicleSearchInput.ts rename to libs/api/domains/vehicles/src/lib/dto/getVehicleSearchInput.ts diff --git a/libs/api/domains/vehicles/src/dto/getVehiclesForUserInput.ts b/libs/api/domains/vehicles/src/lib/dto/getVehiclesForUserInput.ts similarity index 100% rename from libs/api/domains/vehicles/src/dto/getVehiclesForUserInput.ts rename to libs/api/domains/vehicles/src/lib/dto/getVehiclesForUserInput.ts diff --git a/libs/api/domains/vehicles/src/lib/dto/mileageReading.dto.ts b/libs/api/domains/vehicles/src/lib/dto/mileageReading.dto.ts new file mode 100644 index 0000000000000..7fbef841c3136 --- /dev/null +++ b/libs/api/domains/vehicles/src/lib/dto/mileageReading.dto.ts @@ -0,0 +1,9 @@ +export interface MileageReadingDto { + isEditing: boolean + canUserRegisterVehicleMileage?: boolean + readings: Array<{ + date?: Date + origin?: string + mileage?: number + }> +} diff --git a/libs/api/domains/vehicles/src/lib/dto/postBulkVehicleMileage.input.ts b/libs/api/domains/vehicles/src/lib/dto/postBulkVehicleMileage.input.ts new file mode 100644 index 0000000000000..40047f8c456ec --- /dev/null +++ b/libs/api/domains/vehicles/src/lib/dto/postBulkVehicleMileage.input.ts @@ -0,0 +1,22 @@ +import { Field, InputType } from '@nestjs/graphql' +import { IsInt, IsString } from 'class-validator' + +@InputType() +export class PostVehicleBulkMileageInput { + @Field({ description: 'Example: "ISLAND.IS"' }) + originCode!: string + + @Field(() => [PostVehicleBulkMileageSingleInput]) + mileageData!: Array +} + +@InputType() +export class PostVehicleBulkMileageSingleInput { + @Field() + @IsString() + vehicleId!: string + + @Field() + @IsInt() + mileageNumber!: number +} diff --git a/libs/api/domains/vehicles/src/dto/postVehicleMileageInput.ts b/libs/api/domains/vehicles/src/lib/dto/postVehicleMileageInput.ts similarity index 100% rename from libs/api/domains/vehicles/src/dto/postVehicleMileageInput.ts rename to libs/api/domains/vehicles/src/lib/dto/postVehicleMileageInput.ts diff --git a/libs/api/domains/vehicles/src/lib/dto/vehiclesListInputV3.ts b/libs/api/domains/vehicles/src/lib/dto/vehiclesListInputV3.ts new file mode 100644 index 0000000000000..15c75e1ccd8e8 --- /dev/null +++ b/libs/api/domains/vehicles/src/lib/dto/vehiclesListInputV3.ts @@ -0,0 +1,10 @@ +import { Field, InputType } from '@nestjs/graphql' + +@InputType() +export class VehiclesListInputV3 { + @Field() + pageSize!: number + + @Field() + page!: number +} diff --git a/libs/api/domains/vehicles/src/models/getPublicVehicleSearch.model.ts b/libs/api/domains/vehicles/src/lib/models/getPublicVehicleSearch.model.ts similarity index 100% rename from libs/api/domains/vehicles/src/models/getPublicVehicleSearch.model.ts rename to libs/api/domains/vehicles/src/lib/models/getPublicVehicleSearch.model.ts diff --git a/libs/api/domains/vehicles/src/models/getVehicleDetail.model.ts b/libs/api/domains/vehicles/src/lib/models/getVehicleDetail.model.ts similarity index 100% rename from libs/api/domains/vehicles/src/models/getVehicleDetail.model.ts rename to libs/api/domains/vehicles/src/lib/models/getVehicleDetail.model.ts diff --git a/libs/api/domains/vehicles/src/models/getVehicleMileage.model.ts b/libs/api/domains/vehicles/src/lib/models/getVehicleMileage.model.ts similarity index 100% rename from libs/api/domains/vehicles/src/models/getVehicleMileage.model.ts rename to libs/api/domains/vehicles/src/lib/models/getVehicleMileage.model.ts diff --git a/libs/api/domains/vehicles/src/models/getVehicleSearch.model.ts b/libs/api/domains/vehicles/src/lib/models/getVehicleSearch.model.ts similarity index 100% rename from libs/api/domains/vehicles/src/models/getVehicleSearch.model.ts rename to libs/api/domains/vehicles/src/lib/models/getVehicleSearch.model.ts diff --git a/libs/api/domains/vehicles/src/models/usersVehicles.model.ts b/libs/api/domains/vehicles/src/lib/models/usersVehicles.model.ts similarity index 94% rename from libs/api/domains/vehicles/src/models/usersVehicles.model.ts rename to libs/api/domains/vehicles/src/lib/models/usersVehicles.model.ts index 7ef0145912728..04476ff794ae1 100644 --- a/libs/api/domains/vehicles/src/models/usersVehicles.model.ts +++ b/libs/api/domains/vehicles/src/lib/models/usersVehicles.model.ts @@ -1,4 +1,5 @@ import { Field, ObjectType } from '@nestjs/graphql' +import { VehicleMileageDetail } from './getVehicleMileage.model' @ObjectType() export class NextInspection { @@ -11,6 +12,7 @@ export class NextInspection { }) nextinspectiondateIfPassedInspectionToday?: Date } + @ObjectType() export class VehiclesVehicle { @Field({ nullable: true }) @@ -243,6 +245,12 @@ export class VehicleListed { @Field({ nullable: true }) nextMainInspection?: Date + + @Field(() => VehicleMileageDetail, { nullable: true }) + lastMileageRegistration?: VehicleMileageDetail + + @Field(() => [VehicleMileageDetail], { nullable: true }) + mileageRegistrationHistory?: Array } @ObjectType() diff --git a/libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageReadingResponse.model.ts b/libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageReadingResponse.model.ts new file mode 100644 index 0000000000000..ef8738dc461f8 --- /dev/null +++ b/libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageReadingResponse.model.ts @@ -0,0 +1,13 @@ +import { Field, ObjectType, ID } from '@nestjs/graphql' + +@ObjectType() +export class VehiclesBulkMileageReadingResponse { + @Field(() => ID, { + description: + 'The GUID of the mileage registration post request. Used to fetch job status', + }) + requestId!: string + + @Field({ nullable: true }) + errorMessage?: string +} diff --git a/libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageRegistrationJob.model.ts b/libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageRegistrationJob.model.ts new file mode 100644 index 0000000000000..a677030b08d6c --- /dev/null +++ b/libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageRegistrationJob.model.ts @@ -0,0 +1,37 @@ +import { Field, ObjectType, ID, GraphQLISODateTime } from '@nestjs/graphql' + +@ObjectType() +export class VehiclesBulkMileageRegistrationJob { + @Field(() => ID) + guid!: string + + @Field({ nullable: true }) + reportingPersonNationalId?: string + + @Field({ nullable: true }) + reportingPersonName?: string + + @Field({ nullable: true }) + originCode?: string + + @Field({ nullable: true }) + originName?: string + + @Field(() => GraphQLISODateTime, { + nullable: true, + description: 'When was the bulk request requested?', + }) + dateRequested?: Date + + @Field(() => GraphQLISODateTime, { + nullable: true, + description: 'When did the bulk request start executing?', + }) + dateStarted?: Date + + @Field(() => GraphQLISODateTime, { + nullable: true, + description: 'When did the bulk request execution finish', + }) + dateFinished?: Date +} diff --git a/libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageRegistrationJobHistory.model.ts b/libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageRegistrationJobHistory.model.ts new file mode 100644 index 0000000000000..f0ea92c3cc347 --- /dev/null +++ b/libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageRegistrationJobHistory.model.ts @@ -0,0 +1,8 @@ +import { Field, ObjectType } from '@nestjs/graphql' +import { VehiclesBulkMileageRegistrationJob } from './bulkMileageRegistrationJob.model' + +@ObjectType() +export class VehiclesBulkMileageRegistrationJobHistory { + @Field(() => [VehiclesBulkMileageRegistrationJob]) + history!: Array +} diff --git a/libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageRegistrationRequestDetail.model.ts b/libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageRegistrationRequestDetail.model.ts new file mode 100644 index 0000000000000..804f11bcb9518 --- /dev/null +++ b/libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageRegistrationRequestDetail.model.ts @@ -0,0 +1,22 @@ +import { Field, ObjectType, ID, Int } from '@nestjs/graphql' +import { VehiclesBulkMileageRegistrationRequestError } from './bulkMileageRegistrationRequestError.model' + +@ObjectType() +export class VehiclesBulkMileageRegistrationRequestDetail { + @Field(() => ID) + guid!: string + + @Field() + vehicleId!: string + + @Field(() => Int, { nullable: true }) + mileage?: number + + @Field({ nullable: true }) + returnCode?: string + + @Field(() => [VehiclesBulkMileageRegistrationRequestError], { + nullable: true, + }) + errors?: Array +} diff --git a/libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageRegistrationRequestError.model.ts b/libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageRegistrationRequestError.model.ts new file mode 100644 index 0000000000000..a289df9b6cfa5 --- /dev/null +++ b/libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageRegistrationRequestError.model.ts @@ -0,0 +1,10 @@ +import { Field, ObjectType } from '@nestjs/graphql' + +@ObjectType() +export class VehiclesBulkMileageRegistrationRequestError { + @Field({ nullable: true }) + code?: string + + @Field({ nullable: true }) + message?: string +} diff --git a/libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageRegistrationRequestOverview.model.ts b/libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageRegistrationRequestOverview.model.ts new file mode 100644 index 0000000000000..56b6f18ed3d51 --- /dev/null +++ b/libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageRegistrationRequestOverview.model.ts @@ -0,0 +1,8 @@ +import { Field, ObjectType } from '@nestjs/graphql' +import { VehiclesBulkMileageRegistrationRequestDetail } from './bulkMileageRegistrationRequestDetail.model' + +@ObjectType() +export class VehiclesBulkMileageRegistrationRequestOverview { + @Field(() => [VehiclesBulkMileageRegistrationRequestDetail]) + requests!: Array +} diff --git a/libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageRegistrationRequestStatus.model.ts b/libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageRegistrationRequestStatus.model.ts new file mode 100644 index 0000000000000..587fdf36f1ee8 --- /dev/null +++ b/libs/api/domains/vehicles/src/lib/models/v3/bulkMileage/bulkMileageRegistrationRequestStatus.model.ts @@ -0,0 +1,22 @@ +import { Field, ID, Int, ObjectType } from '@nestjs/graphql' + +@ObjectType() +export class VehiclesBulkMileageRegistrationRequestStatus { + @Field(() => ID) + requestId!: string + + @Field(() => Int, { nullable: true }) + jobsSubmitted?: number + + @Field(() => Int, { nullable: true }) + jobsFinished?: number + + @Field(() => Int, { nullable: true }) + jobsRemaining?: number + + @Field(() => Int, { nullable: true }) + jobsValid?: number + + @Field(() => Int, { nullable: true }) + jobsErrored?: number +} diff --git a/libs/api/domains/vehicles/src/lib/models/v3/currentVehicleListResponse.model.ts b/libs/api/domains/vehicles/src/lib/models/v3/currentVehicleListResponse.model.ts new file mode 100644 index 0000000000000..e1932c9b7c0d2 --- /dev/null +++ b/libs/api/domains/vehicles/src/lib/models/v3/currentVehicleListResponse.model.ts @@ -0,0 +1,20 @@ +import { Field, Int, ObjectType } from '@nestjs/graphql' +import { VehicleCurrentWithMileage } from './currentVehicleWithMileage.model' + +@ObjectType() +export class VehiclesCurrentListResponse { + @Field(() => Int) + pageNumber!: number + + @Field(() => Int) + pageSize!: number + + @Field(() => Int) + totalPages!: number + + @Field(() => Int) + totalRecords!: number + + @Field(() => [VehicleCurrentWithMileage], { nullable: true }) + data?: Array +} diff --git a/libs/api/domains/vehicles/src/lib/models/v3/currentVehicleWithMileage.model.ts b/libs/api/domains/vehicles/src/lib/models/v3/currentVehicleWithMileage.model.ts new file mode 100644 index 0000000000000..3884dd8d5be5c --- /dev/null +++ b/libs/api/domains/vehicles/src/lib/models/v3/currentVehicleWithMileage.model.ts @@ -0,0 +1,23 @@ +import { Field, ObjectType } from '@nestjs/graphql' +import { MileageDetails } from './mileageDetails.model' + +@ObjectType() +export class VehicleCurrentWithMileage { + @Field() + vehicleId!: string + + @Field({ nullable: true }) + registrationNumber?: string + + @Field({ nullable: true }) + userRole?: string + + @Field({ nullable: true }) + type?: string + + @Field({ nullable: true }) + color?: string + + @Field(() => MileageDetails, { nullable: true }) + mileageDetails?: MileageDetails +} diff --git a/libs/api/domains/vehicles/src/lib/models/v3/mileageDetails.model.ts b/libs/api/domains/vehicles/src/lib/models/v3/mileageDetails.model.ts new file mode 100644 index 0000000000000..e2dd8408f6967 --- /dev/null +++ b/libs/api/domains/vehicles/src/lib/models/v3/mileageDetails.model.ts @@ -0,0 +1,14 @@ +import { Field, ObjectType } from '@nestjs/graphql' +import { MileageRegistrationHistory } from './mileageRegistrationHistory.model' + +@ObjectType('VehiclesMileageDetails') +export class MileageDetails { + @Field() + canRegisterMileage!: boolean + + @Field() + requiresMileageRegistration!: boolean + + @Field(() => MileageRegistrationHistory, { nullable: true }) + mileageRegistrations?: MileageRegistrationHistory +} diff --git a/libs/api/domains/vehicles/src/lib/models/v3/mileageRegistration.model.ts b/libs/api/domains/vehicles/src/lib/models/v3/mileageRegistration.model.ts new file mode 100644 index 0000000000000..1585acb1d3811 --- /dev/null +++ b/libs/api/domains/vehicles/src/lib/models/v3/mileageRegistration.model.ts @@ -0,0 +1,13 @@ +import { Field, GraphQLISODateTime, Int, ObjectType } from '@nestjs/graphql' + +@ObjectType('VehiclesMileageRegistration') +export class MileageRegistration { + @Field() + originCode!: string + + @Field(() => Int) + mileage!: number + + @Field(() => GraphQLISODateTime) + date!: Date +} diff --git a/libs/api/domains/vehicles/src/lib/models/v3/mileageRegistrationHistory.model.ts b/libs/api/domains/vehicles/src/lib/models/v3/mileageRegistrationHistory.model.ts new file mode 100644 index 0000000000000..dde6de0404c5d --- /dev/null +++ b/libs/api/domains/vehicles/src/lib/models/v3/mileageRegistrationHistory.model.ts @@ -0,0 +1,14 @@ +import { Field, ObjectType } from '@nestjs/graphql' +import { MileageRegistration } from './mileageRegistration.model' + +@ObjectType('VehiclesMileageRegistrationHistory') +export class MileageRegistrationHistory { + @Field() + vehicleId!: string + + @Field(() => MileageRegistration, { nullable: true }) + lastMileageRegistration?: MileageRegistration + + @Field(() => [MileageRegistration], { nullable: true }) + mileageRegistrationHistory?: Array +} diff --git a/libs/api/domains/vehicles/src/lib/resolvers/bulkMileage.resolver.ts b/libs/api/domains/vehicles/src/lib/resolvers/bulkMileage.resolver.ts new file mode 100644 index 0000000000000..a551c3dc893af --- /dev/null +++ b/libs/api/domains/vehicles/src/lib/resolvers/bulkMileage.resolver.ts @@ -0,0 +1,84 @@ +import { Args, Mutation, Query, Resolver } from '@nestjs/graphql' +import { UseGuards } from '@nestjs/common' +import { + IdsUserGuard, + ScopesGuard, + Scopes, + CurrentUser, +} from '@island.is/auth-nest-tools' +import type { User } from '@island.is/auth-nest-tools' +import { ApiScope } from '@island.is/auth/scopes' +import { Audit } from '@island.is/nest/audit' +import { + FeatureFlagGuard, + FeatureFlag, + Features, +} from '@island.is/nest/feature-flags' +import { PostVehicleBulkMileageInput } from '../dto/postBulkVehicleMileage.input' +import { BulkMileageService } from '../services/bulkMileage.service' +import { VehiclesBulkMileageReadingResponse } from '../models/v3/bulkMileage/bulkMileageReadingResponse.model' +import { VehiclesBulkMileageRegistrationRequestOverview } from '../models/v3/bulkMileage/bulkMileageRegistrationRequestOverview.model' +import { VehiclesBulkMileageRegistrationJobHistory } from '../models/v3/bulkMileage/bulkMileageRegistrationJobHistory.model' +import { VehiclesBulkMileageRegistrationRequestStatus } from '../models/v3/bulkMileage/bulkMileageRegistrationRequestStatus.model' +import { BulkVehicleMileageRequestStatusInput } from '../dto/getBulkVehicleMileageRequestStatus.input' +import { BulkVehicleMileageRequestOverviewInput } from '../dto/getBulkVehicleMileageRequestOverview.input' + +@UseGuards(IdsUserGuard, ScopesGuard, FeatureFlagGuard) +@FeatureFlag(Features.servicePortalVehicleBulkMileagePageEnabled) +@Resolver() +@Audit({ namespace: '@island.is/api/vehicles' }) +@Scopes(ApiScope.vehicles) +export class VehiclesBulkMileageResolver { + constructor(private readonly bulkService: BulkMileageService) {} + + @Query(() => VehiclesBulkMileageRegistrationJobHistory, { + name: 'vehicleBulkMileageRegistrationJobHistory', + nullable: true, + }) + @Audit() + getVehicleMileageRegistrationJobHistory(@CurrentUser() user: User) { + return this.bulkService.getBulkMileageRegistrationJobHistory(user) + } + + @Query(() => VehiclesBulkMileageRegistrationRequestStatus, { + name: 'vehicleBulkMileageRegistrationRequestStatus', + nullable: true, + }) + @Audit() + getVehicleMileageRegistrationRequestStatus( + @CurrentUser() user: User, + @Args('input') input: BulkVehicleMileageRequestStatusInput, + ) { + return this.bulkService.getBulkMileageRegistrationRequestStatus( + user, + input.requestId, + ) + } + + @Query(() => VehiclesBulkMileageRegistrationRequestOverview, { + name: 'vehicleBulkMileageRegistrationRequestOverview', + nullable: true, + }) + @Audit() + getVehicleMileageRegistrationRequestOverview( + @CurrentUser() user: User, + @Args('input') input: BulkVehicleMileageRequestOverviewInput, + ) { + return this.bulkService.getBulkMileageRegistrationRequestOverview( + user, + input.guid, + ) + } + + @Mutation(() => VehiclesBulkMileageReadingResponse, { + name: 'vehicleBulkMileagePost', + nullable: true, + }) + @Audit() + postBulkMileageReading( + @Args('input') input: PostVehicleBulkMileageInput, + @CurrentUser() user: User, + ) { + return this.bulkService.postBulkMileageReading(user, input) + } +} diff --git a/libs/api/domains/vehicles/src/lib/api-domains-vehicles-mileage.resolver.ts b/libs/api/domains/vehicles/src/lib/resolvers/mileage.resolver.ts similarity index 95% rename from libs/api/domains/vehicles/src/lib/api-domains-vehicles-mileage.resolver.ts rename to libs/api/domains/vehicles/src/lib/resolvers/mileage.resolver.ts index c22385922c476..b95126c94212c 100644 --- a/libs/api/domains/vehicles/src/lib/api-domains-vehicles-mileage.resolver.ts +++ b/libs/api/domains/vehicles/src/lib/resolvers/mileage.resolver.ts @@ -17,7 +17,7 @@ import { import type { User } from '@island.is/auth-nest-tools' import { ApiScope } from '@island.is/auth/scopes' import { Audit } from '@island.is/nest/audit' -import { VehiclesService } from './api-domains-vehicles.service' +import { VehiclesService } from '../services/vehicles.service' import { VehicleMileageDetail, VehicleMileageOverview, @@ -88,9 +88,9 @@ export class VehiclesMileageResolver { mileage: Number(input.mileage ?? input.mileageNumber), }) - if (!res) return undefined + if (!res?.length) return undefined - return mileageDetailConstructor(res) + return mileageDetailConstructor(res[0]) } @ResolveField('canRegisterMileage', () => Boolean, { diff --git a/libs/api/domains/vehicles/src/lib/api-domains-vehicles-shared.resolver.ts b/libs/api/domains/vehicles/src/lib/resolvers/shared.resolver.ts similarity index 95% rename from libs/api/domains/vehicles/src/lib/api-domains-vehicles-shared.resolver.ts rename to libs/api/domains/vehicles/src/lib/resolvers/shared.resolver.ts index 3feb413764ec9..9b48d1f8f0c4f 100644 --- a/libs/api/domains/vehicles/src/lib/api-domains-vehicles-shared.resolver.ts +++ b/libs/api/domains/vehicles/src/lib/resolvers/shared.resolver.ts @@ -4,7 +4,7 @@ import type { Logger } from '@island.is/logging' import { IdsUserGuard, ScopesGuard } from '@island.is/auth-nest-tools' import type { User } from '@island.is/auth-nest-tools' import { Audit } from '@island.is/nest/audit' -import { VehiclesService } from './api-domains-vehicles.service' +import { VehiclesService } from '../services/vehicles.service' import { VehicleMileageDetail } from '../models/getVehicleMileage.model' import { VehiclesDetail } from '../models/getVehicleDetail.model' import { LOGGER_PROVIDER } from '@island.is/logging' diff --git a/libs/api/domains/vehicles/src/lib/resolvers/vehicleV3.resolver.ts b/libs/api/domains/vehicles/src/lib/resolvers/vehicleV3.resolver.ts new file mode 100644 index 0000000000000..6b90138ec10d2 --- /dev/null +++ b/libs/api/domains/vehicles/src/lib/resolvers/vehicleV3.resolver.ts @@ -0,0 +1,48 @@ +import { Args, Query, Resolver } from '@nestjs/graphql' +import { UseGuards } from '@nestjs/common' +import { + IdsUserGuard, + ScopesGuard, + Scopes, + CurrentUser, +} from '@island.is/auth-nest-tools' +import type { User } from '@island.is/auth-nest-tools' +import { ApiScope } from '@island.is/auth/scopes' +import { Audit } from '@island.is/nest/audit' +import { VehiclesService } from '../services/vehicles.service' +import { VehiclesCurrentListResponse } from '../models/v3/currentVehicleListResponse.model' +import { VehiclesListInputV3 } from '../dto/vehiclesListInputV3' +import { MileageRegistrationHistory } from '../models/v3/mileageRegistrationHistory.model' +import { GetVehicleMileageInput } from '../dto/getVehicleMileageInput' + +@UseGuards(IdsUserGuard, ScopesGuard) +@Resolver(() => VehiclesCurrentListResponse) +@Audit({ namespace: '@island.is/api/vehicles' }) +export class VehiclesV3Resolver { + constructor(private readonly vehiclesService: VehiclesService) {} + + @Scopes(ApiScope.vehicles) + @Query(() => VehiclesCurrentListResponse, { + name: 'vehiclesListV3', + nullable: true, + }) + @Audit() + async getVehicleListV3( + @CurrentUser() user: User, + @Args('input', { nullable: true }) input: VehiclesListInputV3, + ) { + return this.vehiclesService.getVehiclesListV3(user, input) + } + @Scopes(ApiScope.vehicles) + @Query(() => MileageRegistrationHistory, { + name: 'vehiclesMileageRegistrationHistory', + nullable: true, + }) + @Audit() + async vehicleMileageRegistrations( + @CurrentUser() user: User, + @Args('input', { nullable: true }) input: GetVehicleMileageInput, + ) { + return this.vehiclesService.getVehicleMileageHistory(user, input) + } +} diff --git a/libs/api/domains/vehicles/src/lib/api-domains-vehicles.resolver.ts b/libs/api/domains/vehicles/src/lib/resolvers/vehicles.resolver.ts similarity index 98% rename from libs/api/domains/vehicles/src/lib/api-domains-vehicles.resolver.ts rename to libs/api/domains/vehicles/src/lib/resolvers/vehicles.resolver.ts index 2998706d46848..2a92af484ca2d 100644 --- a/libs/api/domains/vehicles/src/lib/api-domains-vehicles.resolver.ts +++ b/libs/api/domains/vehicles/src/lib/resolvers/vehicles.resolver.ts @@ -15,7 +15,6 @@ import { Audit } from '@island.is/nest/audit' import { DownloadServiceConfig } from '@island.is/nest/config' import type { ConfigType } from '@island.is/nest/config' import { VehiclesList, VehiclesListV2 } from '../models/usersVehicles.model' -import { VehiclesService } from './api-domains-vehicles.service' import { GetVehicleDetailInput } from '../dto/getVehicleDetailInput' import { VehiclesDetail, VehiclesExcel } from '../models/getVehicleDetail.model' import { VehiclesVehicleSearch } from '../models/getVehicleSearch.model' @@ -26,6 +25,7 @@ import { GetVehiclesListV2Input, } from '../dto/getVehiclesForUserInput' import { GetVehicleSearchInput } from '../dto/getVehicleSearchInput' +import { VehiclesService } from '../services/vehicles.service' const defaultCache: CacheControlOptions = { maxAge: CACHE_CONTROL_MAX_AGE } diff --git a/libs/api/domains/vehicles/src/lib/services/bulkMileage.service.ts b/libs/api/domains/vehicles/src/lib/services/bulkMileage.service.ts new file mode 100644 index 0000000000000..72743c79f0ad8 --- /dev/null +++ b/libs/api/domains/vehicles/src/lib/services/bulkMileage.service.ts @@ -0,0 +1,143 @@ +import { Inject, Injectable } from '@nestjs/common' +import { + BulkMileageReadingRequestResultDto, + GetbulkmileagereadingrequeststatusGuidGetRequest, + MileageReadingApi, +} from '@island.is/clients/vehicles-mileage' +import { AuthMiddleware } from '@island.is/auth-nest-tools' +import type { Auth, User } from '@island.is/auth-nest-tools' +import { PostVehicleBulkMileageInput } from '../dto/postBulkVehicleMileage.input' +import { isDefined } from '@island.is/shared/utils' +import { LOG_CATEGORY } from '../constants' +import { LOGGER_PROVIDER, type Logger } from '@island.is/logging' +import { VehiclesBulkMileageReadingResponse } from '../models/v3/bulkMileage/bulkMileageReadingResponse.model' +import { VehiclesBulkMileageRegistrationJobHistory } from '../models/v3/bulkMileage/bulkMileageRegistrationJobHistory.model' +import { VehiclesBulkMileageRegistrationRequestStatus } from '../models/v3/bulkMileage/bulkMileageRegistrationRequestStatus.model' +import { VehiclesBulkMileageRegistrationRequestOverview } from '../models/v3/bulkMileage/bulkMileageRegistrationRequestOverview.model' + +@Injectable() +export class BulkMileageService { + constructor( + private mileageReadingApi: MileageReadingApi, + @Inject(LOGGER_PROVIDER) private readonly logger: Logger, + ) {} + + private getMileageWithAuth(auth: Auth) { + return this.mileageReadingApi.withMiddleware(new AuthMiddleware(auth)) + } + + async postBulkMileageReading( + auth: User, + input: PostVehicleBulkMileageInput, + ): Promise { + if (!input) { + return null + } + + const res: BulkMileageReadingRequestResultDto = + await this.getMileageWithAuth(auth).requestbulkmileagereadingPost({ + postBulkMileageReadingModel: { + originCode: input.originCode, + mileageData: input.mileageData.map((m) => ({ + permno: m.vehicleId, + mileage: m.mileageNumber, + })), + }, + }) + + if (!res.guid) { + this.logger.warn( + 'Missing guid from bulk mileage reading registration response', + { + category: LOG_CATEGORY, + }, + ) + return null + } + + return { + requestId: res.guid, + errorMessage: res.errorMessage ?? undefined, + } + } + + async getBulkMileageRegistrationJobHistory( + auth: User, + ): Promise { + const res = await this.getMileageWithAuth( + auth, + ).getbulkmileagereadingrequestsGet({}) + + return { + history: res + .map((r) => { + if (!r.guid) { + return null + } + + return { + guid: r.guid, + reportingPersonNationalId: r.reportingPersidno ?? undefined, + reportingPersonName: r.reportingPersidnoName ?? undefined, + originCode: r.originCode ?? undefined, + originName: r.originName ?? undefined, + dateRequested: r.dateInserted ?? undefined, + dateStarted: r.dateStarted ?? undefined, + dateFinished: r.dateFinished ?? undefined, + } + }) + .filter(isDefined), + } + } + + async getBulkMileageRegistrationRequestStatus( + auth: User, + input: GetbulkmileagereadingrequeststatusGuidGetRequest['guid'], + ): Promise { + const data = await this.getMileageWithAuth( + auth, + ).getbulkmileagereadingrequeststatusGuidGet({ guid: input }) + + if (!data.guid) { + return null + } + + return { + requestId: data.guid, + jobsSubmitted: data.totalVehicles ?? undefined, + jobsFinished: data.done ?? undefined, + jobsRemaining: data.remaining ?? undefined, + jobsValid: data.processOk ?? undefined, + jobsErrored: data.processWithErrors ?? undefined, + } + } + + async getBulkMileageRegistrationRequestOverview( + auth: User, + input: GetbulkmileagereadingrequeststatusGuidGetRequest['guid'], + ): Promise { + const data = await this.getMileageWithAuth( + auth, + ).getbulkmileagereadingrequestdetailsGuidGet({ guid: input }) + + return { + requests: data + .map((d) => { + if (!d.guid || !d.permno) { + return null + } + return { + guid: d.guid, + vehicleId: d.permno, + mileage: d.mileage ?? undefined, + returnCode: d.returnCode ?? undefined, + errors: d.errors?.map((e) => ({ + code: e.errorCode ?? undefined, + message: e.errorText ?? undefined, + })), + } + }) + .filter(isDefined), + } + } +} diff --git a/libs/api/domains/vehicles/src/lib/api-domains-vehicles.service.ts b/libs/api/domains/vehicles/src/lib/services/vehicles.service.ts similarity index 75% rename from libs/api/domains/vehicles/src/lib/api-domains-vehicles.service.ts rename to libs/api/domains/vehicles/src/lib/services/vehicles.service.ts index cf7085d25cde6..96488fed7ad6d 100644 --- a/libs/api/domains/vehicles/src/lib/api-domains-vehicles.service.ts +++ b/libs/api/domains/vehicles/src/lib/services/vehicles.service.ts @@ -17,8 +17,8 @@ import { CanregistermileagePermnoGetRequest, GetMileageReadingRequest, MileageReadingApi, + MileageReadingDto, PostMileageReadingModel, - PutMileageReadingModel, RequiresmileageregistrationPermnoGetRequest, RootPostRequest, RootPutRequest, @@ -38,6 +38,11 @@ import { VehicleMileageOverview } from '../models/getVehicleMileage.model' import isSameDay from 'date-fns/isSameDay' import { mileageDetailConstructor } from '../utils/helpers' import { handle404 } from '@island.is/clients/middlewares' +import { VehiclesListInputV3 } from '../dto/vehiclesListInputV3' +import { VehiclesCurrentListResponse } from '../models/v3/currentVehicleListResponse.model' +import { isDefined } from '@island.is/shared/utils' +import { GetVehicleMileageInput } from '../dto/getVehicleMileageInput' +import { MileageRegistrationHistory } from '../models/v3/mileageRegistrationHistory.model' const ORIGIN_CODE = 'ISLAND.IS' const LOG_CATEGORY = 'vehicle-service' @@ -93,6 +98,61 @@ export class VehiclesService { }) } + async getVehiclesListV3( + auth: User, + input: VehiclesListInputV3, + ): Promise { + const res = await this.getVehiclesWithAuth( + auth, + ).currentvehicleswithmileageandinspGet({ + showCoowned: true, + showOperated: true, + showOwned: true, + page: input.page, + pageSize: input.pageSize, + }) + + if ( + !res.pageNumber || + !res.pageSize || + !res.totalPages || + !res.totalRecords + ) { + return null + } + + return { + pageNumber: res.pageNumber, + pageSize: res.pageSize, + totalPages: res.totalPages, + totalRecords: res.totalRecords, + data: + res.data + ?.map((d) => { + if ( + !d.permno || + !d.regno || + !d.canRegisterMilage || + !d.requiresMileageRegistration + ) { + return null + } + return { + vehicleId: d.permno, + registrationNumber: d.regno, + userRole: d.role ?? undefined, + type: d.make ?? undefined, + color: d.colorName ?? undefined, + mileageDetails: { + canRegisterMileage: d.canRegisterMilage, + requiresMileageRegistration: d.requiresMileageRegistration, + }, + } + }) + .filter(isDefined) ?? [], + } + } + async getVehiclesForUser( auth: User, input: GetVehiclesForUserInput, @@ -286,6 +346,52 @@ export class VehiclesService { } } + async getVehicleMileageHistory( + auth: User, + input: GetVehicleMileageInput, + ): Promise { + const res = await this.getMileageWithAuth(auth).getMileageReading({ + permno: input.permno, + }) + + const [lastRegistration, ...history] = res + + if (!lastRegistration.permno) { + return null + } + + return { + vehicleId: lastRegistration.permno, + lastMileageRegistration: + lastRegistration.originCode && + lastRegistration.readDate && + lastRegistration.mileage + ? { + originCode: lastRegistration.originCode, + mileage: lastRegistration.mileage, + date: lastRegistration.readDate, + } + : undefined, + mileageRegistrationHistory: history?.length + ? history + .map((h) => { + if (h.permno !== lastRegistration.permno) { + return null + } + if (!h.originCode || !h.mileage || !h.readDate) { + return null + } + return { + originCode: h.originCode, + mileage: h.mileage, + date: h.readDate, + } + }) + .filter(isDefined) + : undefined, + } + } + async postMileageReading( auth: User, input: RootPostRequest['postMileageReadingModel'], @@ -304,17 +410,24 @@ export class VehiclesService { throw new ForbiddenException(UNAUTHORIZED_OWNERSHIP_LOG) } - const res = await this.getMileageWithAuth(auth).rootPost({ + const res = await this.getMileageWithAuth(auth).rootPostRaw({ postMileageReadingModel: input, }) - return res + if (res.raw.status === 200) { + this.logger.info( + 'Tried to post already existing mileage reading. Should use PUT', + ) + return null + } + + return res.value() } async putMileageReading( auth: User, input: RootPutRequest['putMileageReadingModel'], - ): Promise { + ): Promise | null> { if (!input) return null const isAllowed = await this.isAllowedMileageRegistration( diff --git a/libs/api/domains/vehicles/src/utils/basicVehicleInformationMapper.ts b/libs/api/domains/vehicles/src/lib/utils/basicVehicleInformationMapper.ts similarity index 100% rename from libs/api/domains/vehicles/src/utils/basicVehicleInformationMapper.ts rename to libs/api/domains/vehicles/src/lib/utils/basicVehicleInformationMapper.ts diff --git a/libs/api/domains/vehicles/src/utils/helpers.ts b/libs/api/domains/vehicles/src/lib/utils/helpers.ts similarity index 100% rename from libs/api/domains/vehicles/src/utils/helpers.ts rename to libs/api/domains/vehicles/src/lib/utils/helpers.ts diff --git a/libs/api/domains/vehicles/src/lib/api-domains-vehicles.module.ts b/libs/api/domains/vehicles/src/lib/vehicles.module.ts similarity index 52% rename from libs/api/domains/vehicles/src/lib/api-domains-vehicles.module.ts rename to libs/api/domains/vehicles/src/lib/vehicles.module.ts index 49874c35352ac..3fcb00edafd3a 100644 --- a/libs/api/domains/vehicles/src/lib/api-domains-vehicles.module.ts +++ b/libs/api/domains/vehicles/src/lib/vehicles.module.ts @@ -2,19 +2,25 @@ import { Module } from '@nestjs/common' import { VehiclesClientModule } from '@island.is/clients/vehicles' import { VehiclesMileageClientModule } from '@island.is/clients/vehicles-mileage' -import { VehiclesResolver } from './api-domains-vehicles.resolver' -import { VehiclesMileageResolver } from './api-domains-vehicles-mileage.resolver' -import { VehiclesService } from './api-domains-vehicles.service' +import { VehiclesResolver } from './resolvers/vehicles.resolver' +import { VehiclesMileageResolver } from './resolvers/mileage.resolver' +import { VehiclesService } from './services/vehicles.service' import { AuthModule } from '@island.is/auth-nest-tools' -import { VehiclesSharedResolver } from './api-domains-vehicles-shared.resolver' +import { VehiclesSharedResolver } from './resolvers/shared.resolver' import { FeatureFlagModule } from '@island.is/nest/feature-flags' +import { BulkMileageService } from './services/bulkMileage.service' +import { VehiclesV3Resolver } from './resolvers/vehicleV3.resolver' +import { VehiclesBulkMileageResolver } from './resolvers/bulkMileage.resolver' @Module({ providers: [ VehiclesResolver, VehiclesSharedResolver, VehiclesMileageResolver, + VehiclesBulkMileageResolver, + VehiclesV3Resolver, VehiclesService, + BulkMileageService, ], imports: [ VehiclesClientModule, diff --git a/libs/clients/vehicles-mileage/project.json b/libs/clients/vehicles-mileage/project.json index ef161d547864e..5ae8a04b0eca8 100644 --- a/libs/clients/vehicles-mileage/project.json +++ b/libs/clients/vehicles-mileage/project.json @@ -21,10 +21,11 @@ "commands": [ "curl -H \"X-Road-Client: IS-DEV/GOV/10000/island-is-client\" http://localhost:8081/r1/IS-DEV/GOV/10017/Samgongustofa-Protected/getOpenAPI?serviceCode=Vehicle-Mileagereading-V1 > src/clientConfig.json", "jq '.components.schemas.ProblemDetails.additionalProperties = false' src/{args.apiVersion}/clientConfig.json > _.tmp && mv _.tmp src/{args.apiVersion}/clientConfig.json", + "cat <<< $(jq 'del(.paths.\"/\".post.responses.\"200\")' src/clientConfig.json) > src/clientConfig.json", "prettier --write src/clientConfig.json" ], "parallel": false, - "cwd": "libs/clients/vehicles" + "cwd": "libs/clients/vehicles-mileage" } }, "codegen/backend-client": { diff --git a/libs/clients/vehicles-mileage/src/clientConfig.json b/libs/clients/vehicles-mileage/src/clientConfig.json index 75a8201878cb4..24dee803a1d7e 100644 --- a/libs/clients/vehicles-mileage/src/clientConfig.json +++ b/libs/clients/vehicles-mileage/src/clientConfig.json @@ -2,14 +2,13 @@ "openapi": "3.0.1", "info": { "title": "SGS Rest API", - "description": "Mileage reading API developed in .Net6.0 - Release-5 : 20231122.2", + "description": "Mileage reading API developed in .Net8.0 - Release-6 : 20231122.2", "contact": { "name": "Samgöngustofa", "email": "tolvuhjalp@samgongustofa.is" }, "version": "1.0" }, - "servers": [{ "url": "/vehicle/mileagereading" }], "paths": { "/authenticate": { "post": { @@ -55,7 +54,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { "$ref": "#/components/schemas/User" } @@ -177,6 +176,14 @@ "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ProblemDetails" } + } + } } } }, @@ -237,11 +244,12 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PutMileageReadingModel" + "type": "array", + "items": { "$ref": "#/components/schemas/MileageReadingDto" } } } } @@ -291,7 +299,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -300,6 +308,22 @@ } } } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ProblemDetails" } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ProblemDetails" } + } + } } } } @@ -329,12 +353,27 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { "type": "string" } } } }, - "204": { "description": "No Content" } + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ProblemDetails" } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ProblemDetails" } + } + } + } } } }, @@ -363,10 +402,26 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { "type": "boolean" } } } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ProblemDetails" } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ProblemDetails" } + } + } } } } @@ -396,10 +451,290 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { "type": "boolean" } } } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ProblemDetails" } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ProblemDetails" } + } + } + } + } + } + }, + "/requestbulkmileagereading": { + "post": { + "tags": ["MileageReading"], + "parameters": [ + { + "name": "api-version", + "in": "header", + "description": "The requested API version", + "schema": { "type": "string", "default": "1.0" } + }, + { + "name": "api-version", + "in": "query", + "description": "The requested API version", + "schema": { "type": "string", "default": "1.0" } + } + ], + "requestBody": { + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/PostBulkMileageReadingModel" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/PostBulkMileageReadingModel" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/PostBulkMileageReadingModel" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/PostBulkMileageReadingModel" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/PostBulkMileageReadingModel" + } + }, + "text/xml": { + "schema": { + "$ref": "#/components/schemas/PostBulkMileageReadingModel" + } + }, + "application/*+xml": { + "schema": { + "$ref": "#/components/schemas/PostBulkMileageReadingModel" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkMileageReadingRequestResultDto" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkMileageReadingRequestResultDto" + } + } + } + } + } + } + }, + "/getbulkmileagereadingrequests": { + "get": { + "tags": ["MileageReading"], + "parameters": [ + { "name": "persidno", "in": "query", "schema": { "type": "string" } }, + { + "name": "api-version", + "in": "header", + "description": "The requested API version", + "schema": { "type": "string", "default": "1.0" } + }, + { + "name": "api-version", + "in": "query", + "description": "The requested API version", + "schema": { "type": "string", "default": "1.0" } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BulkMileageReadingRequestDto" + } + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ProblemDetails" } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ProblemDetails" } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ProblemDetails" } + } + } + } + } + } + }, + "/getbulkmileagereadingrequestdetails/{guid}": { + "get": { + "tags": ["MileageReading"], + "parameters": [ + { + "name": "guid", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "api-version", + "in": "header", + "description": "The requested API version", + "schema": { "type": "string", "default": "1.0" } + }, + { + "name": "api-version", + "in": "query", + "description": "The requested API version", + "schema": { "type": "string", "default": "1.0" } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BulkMileageReadingRequestDetailDto" + } + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ProblemDetails" } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ProblemDetails" } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ProblemDetails" } + } + } + } + } + } + }, + "/getbulkmileagereadingrequeststatus/{guid}": { + "get": { + "tags": ["MileageReading"], + "parameters": [ + { + "name": "guid", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "api-version", + "in": "header", + "description": "The requested API version", + "schema": { "type": "string", "default": "1.0" } + }, + { + "name": "api-version", + "in": "query", + "description": "The requested API version", + "schema": { "type": "string", "default": "1.0" } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkMileageReadingStatusDto" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ProblemDetails" } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ProblemDetails" } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ProblemDetails" } + } + } } } } @@ -416,6 +751,160 @@ }, "additionalProperties": false }, + "BulkMileageReadingRequestDetailDto": { + "type": "object", + "properties": { + "guid": { "type": "string", "description": "Guid", "nullable": true }, + "permno": { + "type": "string", + "description": "Vehicle permanent number", + "nullable": true + }, + "mileage": { + "type": "integer", + "description": "Mileage", + "format": "int32", + "nullable": true + }, + "returnCode": { + "type": "string", + "description": "Return code", + "nullable": true + }, + "errors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BulkMileageReadingRequestDetailErrors" + }, + "description": "List of errors if any", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Dto for bulk mileage reading request details" + }, + "BulkMileageReadingRequestDetailErrors": { + "type": "object", + "properties": { + "errorCode": { + "type": "string", + "description": "Error code", + "nullable": true + }, + "errorText": { + "type": "string", + "description": "Error text", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Class for errors on mileage reading in bulk" + }, + "BulkMileageReadingRequestDto": { + "type": "object", + "properties": { + "guid": { "type": "string", "description": "Guid", "nullable": true }, + "reportingPersidno": { + "type": "string", + "description": "Reporting person social security number", + "nullable": true + }, + "reportingPersidnoName": { + "type": "string", + "description": "Reporting person name", + "nullable": true + }, + "originCode": { + "type": "string", + "description": "Origin code", + "nullable": true + }, + "originName": { + "type": "string", + "description": "Origin name", + "nullable": true + }, + "dateInserted": { + "type": "string", + "description": "When was bulk request inserted", + "format": "date-time", + "nullable": true + }, + "dateStarted": { + "type": "string", + "description": "When did bulk request start working", + "format": "date-time", + "nullable": true + }, + "dateFinished": { + "type": "string", + "description": "When dd bulk request finish", + "format": "date-time", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Dto for bulk mileage reading requests" + }, + "BulkMileageReadingRequestResultDto": { + "type": "object", + "properties": { + "guid": { + "type": "string", + "description": "Guid to check for status and results", + "nullable": true + }, + "errorMessage": { + "type": "string", + "description": "Error message if any", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Dto for bulk mileage reading request" + }, + "BulkMileageReadingStatusDto": { + "type": "object", + "properties": { + "guid": { + "type": "string", + "description": "Guid of request", + "nullable": true + }, + "totalVehicles": { + "type": "integer", + "description": "Total vehicles in the request", + "format": "int32", + "nullable": true + }, + "done": { + "type": "integer", + "description": "Total done", + "format": "int32", + "nullable": true + }, + "remaining": { + "type": "integer", + "description": "Remaining vehicles", + "format": "int32", + "nullable": true + }, + "processOk": { + "type": "integer", + "description": "How many readings were ok", + "format": "int32", + "nullable": true + }, + "processWithErrors": { + "type": "integer", + "description": "How many readings with errors/warnings/locks", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Dto for status of bulk mileage reading request" + }, "MileageReadingDto": { "type": "object", "properties": { @@ -449,11 +938,57 @@ "type": "string", "description": "Operation (I,U,D)", "nullable": true + }, + "reportingPersidno": { + "type": "string", + "description": "If user registering this mileage is not owner/operator, the owner/operator social security number should be in here", + "nullable": true + }, + "transactionDate": { + "type": "string", + "description": "Date of transaction", + "format": "date-time", + "nullable": true } }, "additionalProperties": false, "description": "Mileage data transfer object" }, + "MileageReadingModel": { + "type": "object", + "properties": { + "permno": { + "type": "string", + "description": "Vehicle permno", + "nullable": true + }, + "mileage": { + "type": "integer", + "description": "Milegae", + "format": "int32" + } + }, + "additionalProperties": false, + "description": "One mileage entity" + }, + "PostBulkMileageReadingModel": { + "type": "object", + "properties": { + "originCode": { + "type": "string", + "description": "Origin code of reading", + "nullable": true + }, + "mileageData": { + "type": "array", + "items": { "$ref": "#/components/schemas/MileageReadingModel" }, + "description": "Mileage reading data", + "nullable": true + } + }, + "additionalProperties": false, + "description": "Bulk mileage reading model" + }, "PostMileageReadingModel": { "required": ["mileage", "originCode", "permno"], "type": "object", @@ -490,6 +1025,14 @@ "description": "Operation (I,U,D), is always I for Insert", "nullable": true, "readOnly": true + }, + "reportingPersidno": { + "maxLength": 10, + "minLength": 10, + "pattern": "[0-7]\\d[01]\\d{3}[-]*\\d{3}[09]", + "type": "string", + "description": "If user registering this mileage is not owner/operator, the owner/operator social security number should be in here", + "nullable": true } }, "additionalProperties": false, diff --git a/libs/feature-flags/src/lib/features.ts b/libs/feature-flags/src/lib/features.ts index fd67ac6db300a..acefa400d3865 100644 --- a/libs/feature-flags/src/lib/features.ts +++ b/libs/feature-flags/src/lib/features.ts @@ -50,6 +50,7 @@ export enum Features { servicePortalWorkMachinesModule = 'isServicePortalWorkMachinesPageEnabled', servicePortalSignatureCollection = 'isServicePortalSignatureCollectionEnabled', servicePortalVehicleMileagePageEnabled = 'isServicePortalVehicleMileagePageEnabled', + servicePortalVehicleBulkMileagePageEnabled = 'isServicePortalVehicleBulkMileagePageEnabled', servicePortalSocialInsurancePageEnabled = 'isServicePortalSocialInsurancePageEnabled', servicePortalSocialInsuranceIncomePlanPageEnabled = 'isServicePortalSocialInsuranceIncomePlanPageEnabled', diff --git a/libs/island-ui/core/src/lib/Icon/Icon.tsx b/libs/island-ui/core/src/lib/Icon/Icon.tsx index 2e2af21e865fe..e050b463c3f80 100644 --- a/libs/island-ui/core/src/lib/Icon/Icon.tsx +++ b/libs/island-ui/core/src/lib/Icon/Icon.tsx @@ -30,6 +30,7 @@ export type IconTypes = | 'user' | 'calendar' | 'download' + | 'upload' | 'toasterSuccess' | 'toasterInfo' | 'toasterError' @@ -242,6 +243,12 @@ const iconsConf: Icons = { width: 24, path: 'M21.3333 12V21.3333H2.66667V12H0V21.3333C0 22.8 1.2 24 2.66667 24H21.3333C22.8 24 24 22.8 24 21.3333V12H21.3333ZM13.3333 12.8933L16.7867 9.45333L18.6667 11.3333L12 18L5.33333 11.3333L7.21333 9.45333L10.6667 12.8933V0H13.3333V12.8933Z', }, + upload: { + viewBox: '0 0 24 24', + height: 24, + width: 24, + path: 'M7.5 14.5C7.5 14.7761 7.72386 15 8 15C8.27614 15 8.5 14.7761 8.5 14.5V6.20711L10.1464 7.85355C10.3417 8.04882 10.6583 8.04882 10.8536 7.85355C11.0488 7.65829 11.0488 7.34171 10.8536 7.14645L8.35355 4.64645C8.15829 4.45119 7.84171 4.45119 7.64645 4.64645L5.14645 7.14645C4.95118 7.34171 4.95118 7.65829 5.14645 7.85355C5.34171 8.04882 5.65829 8.04882 5.85355 7.85355L7.5 6.20711L7.5 14.5Z', + }, } const SvgPathContainer = ({ diff --git a/libs/island-ui/core/src/lib/IconRC/iconMap.ts b/libs/island-ui/core/src/lib/IconRC/iconMap.ts index e3e549ffa12bc..c562731e4731b 100644 --- a/libs/island-ui/core/src/lib/IconRC/iconMap.ts +++ b/libs/island-ui/core/src/lib/IconRC/iconMap.ts @@ -32,6 +32,7 @@ export type Icon = | 'documents' | 'dots' | 'download' + | 'upload' | 'ellipse' | 'ellipsisHorizontal' | 'ellipsisVertical' @@ -126,6 +127,7 @@ export default { documents: 'Documents', dots: 'Dots', download: 'Download', + upload: 'Upload', ellipse: 'Ellipse', ellipsisHorizontal: 'EllipsisHorizontal', ellipsisVertical: 'EllipsisVertical', @@ -219,6 +221,7 @@ export default { documents: 'DocumentsOutline', dots: 'Dots', download: 'DownloadOutline', + upload: 'UploadOutline', ellipse: 'EllipseOutline', ellipsisHorizontal: 'EllipsisHorizontalOutline', ellipsisVertical: 'EllipsisVerticalOutline', diff --git a/libs/island-ui/core/src/lib/IconRC/icons/Upload.tsx b/libs/island-ui/core/src/lib/IconRC/icons/Upload.tsx new file mode 100644 index 0000000000000..c45fa30f8181f --- /dev/null +++ b/libs/island-ui/core/src/lib/IconRC/icons/Upload.tsx @@ -0,0 +1,23 @@ +import * as React from 'react' +import type { SvgProps as SVGRProps } from '../types' + +const SvgUpload = ({ + title, + titleId, + ...props +}: React.SVGProps & SVGRProps) => { + return ( + + {title ? {title} : null} + + + ) +} + +export default SvgUpload diff --git a/libs/island-ui/core/src/lib/IconRC/icons/UploadOutline.tsx b/libs/island-ui/core/src/lib/IconRC/icons/UploadOutline.tsx new file mode 100644 index 0000000000000..cbc866c40a3f8 --- /dev/null +++ b/libs/island-ui/core/src/lib/IconRC/icons/UploadOutline.tsx @@ -0,0 +1,38 @@ +import * as React from 'react' +import type { SvgProps as SVGRProps } from '../types' + +const SvgUploadOutline = ({ + title, + titleId, + ...props +}: React.SVGProps & SVGRProps) => { + return ( + + {title ? {title} : null} + + + + ) +} + +export default SvgUploadOutline diff --git a/libs/service-portal/assets/src/lib/messages.ts b/libs/service-portal/assets/src/lib/messages.ts index ef01b45773f5a..5a9329ee69f58 100644 --- a/libs/service-portal/assets/src/lib/messages.ts +++ b/libs/service-portal/assets/src/lib/messages.ts @@ -283,6 +283,10 @@ export const vehicleMessage = defineMessages({ id: 'sp.vehicles:permno', defaultMessage: 'Fastanúmer', }, + lastRegistration: { + id: 'sp.vehicles:last-registration', + defaultMessage: 'Síðasta skráning', + }, verno: { id: 'sp.vehicles:verno', defaultMessage: 'Verksmiðjunúmer', @@ -391,6 +395,14 @@ export const vehicleMessage = defineMessages({ id: 'sp.vehicles:insp-type', defaultMessage: 'Tegund skoðunar', }, + registration: { + id: 'sp.vehicles:registration', + defaultMessage: 'Skráning', + }, + annualUsage: { + id: 'sp.vehicles:annual-usage', + defaultMessage: 'Ársnotkun', + }, date: { id: 'sp.vehicles:date', defaultMessage: 'Dagsetning', @@ -880,6 +892,163 @@ export const vehicleMessage = defineMessages({ id: 'sp.vehicles:mileage-external-link', defaultMessage: '/kilometragjald-a-vetnis-og-rafmagnsbila', }, + bulkMileageButton: { + id: 'sp.vehicles:bulk-mileage-btn', + defaultMessage: 'Senda inn gögn', + }, + bulkMileageUploadStatus: { + id: 'sp.vehicles:bulk-mileage-upload-status', + defaultMessage: 'Skoða má stöðu upphleðslu á magnskráningarsíðu', + }, + bulkPostMileage: { + id: 'sp.vehicles:bulk-post-mileage', + defaultMessage: 'Magnskrá kílómetrastöðu', + }, + jobOverview: { + id: 'sp.vehicles:job-overview', + defaultMessage: 'Yfirlit skráninga', + }, + jobsSubmitted: { + id: 'sp.vehicles:jobs-submitted', + defaultMessage: 'Innsendar kílómetrastöðuskráningar', + }, + jobSubmitted: { + id: 'sp.vehicles:job-submitted', + defaultMessage: 'Innsending', + }, + jobStarted: { + id: 'sp.vehicles:job-started', + defaultMessage: 'Verk hófst', + }, + jobFinished: { + id: 'sp.vehicles:job-finished', + defaultMessage: 'Verki lauk', + }, + jobNotStarted: { + id: 'sp.vehicles:job-not-started', + defaultMessage: 'Ekki hafið', + }, + openJob: { + id: 'sp.vehicles:open-job', + defaultMessage: 'Opna keyrslu', + }, + jobStatus: { + id: 'sp.vehicles:job-status', + defaultMessage: 'Staða keyrslu', + }, + jobInProgress: { + id: 'sp.vehicles:job-in-progress', + defaultMessage: 'Í vinnslu', + }, + goToJob: { + id: 'sp.vehicles:go-to-job', + defaultMessage: 'Skoða verk', + }, + noJobFound: { + id: 'sp.vehicles:no-job-found', + defaultMessage: 'Ekkert verk fannst', + }, + noJobsFound: { + id: 'sp.vehicles:no-jobs-found', + defaultMessage: 'Engin verk fundust', + }, + uploadFailed: { + id: 'sp.vehicles:upload-failed', + defaultMessage: 'Upphleðsla mistókst', + }, + errorWhileProcessing: { + id: 'sp.vehicles:error-while-processing', + defaultMessage: 'Villa við að meðhöndla skjal. Villur: ', + }, + downloadFailed: { + id: 'sp.vehicles:download-failed', + defaultMessage: 'Niðurhal mistókst', + }, + uploadSuccess: { + id: 'sp.vehicles:upload-success', + defaultMessage: 'Upphleðsla tókst', + }, + totalSubmitted: { + id: 'sp.vehicles:total-submitted', + defaultMessage: 'Fjöldi innsendra', + }, + totalFinished: { + id: 'sp.vehicles:total-finished', + defaultMessage: 'Fjöldi lokið', + }, + totalRemaining: { + id: 'sp.vehicles:total-remaining', + defaultMessage: 'Fjöldi eftir', + }, + healthyJobs: { + id: 'sp.vehicles:healthy-jobs', + defaultMessage: 'Heilbrigð verk', + }, + unhealthyJobs: { + id: 'sp.vehicles:unhealthy-jobs', + defaultMessage: 'Misheppnuð verk', + }, + noValidMileage: { + id: 'sp.vehicles:no-valid-mileage', + defaultMessage: 'Engin gild kílómetrastaða fannst í skjali', + }, + dragFileToUpload: { + id: 'sp.vehicles:drag-file-to-upload', + defaultMessage: 'Dragðu skjal hingað til að hlaða upp', + }, + errors: { + id: 'sp.vehicles:errors', + defaultMessage: 'Villur', + }, + noRegistrationsFound: { + id: 'sp.vehicles:no-registrations-found', + defaultMessage: 'Engar skráningar fundust', + }, + downloadErrors: { + id: 'sp.vehicles:download-errors', + defaultMessage: 'Hlaða niður villum (.csv)', + }, + fileUploadAcceptedTypes: { + id: 'sp.vehicles:file-upload-accepted-types', + defaultMessage: 'Tekið er við skjölum með endingu; .csv, .xls', + }, + dataAboutJob: { + id: 'sp.vehicles:data-about-job', + defaultMessage: 'Hér finnur þú upplýsingar um skráningu', + }, + refreshDataAboutJob: { + id: 'sp.vehicles:refresh-data-about-job', + defaultMessage: + 'Til að sækja nýjustu stöðu er hægt að smella á "Uppfæra stöðu"', + }, + refreshJob: { + id: 'sp.vehicles:refresh-job', + defaultMessage: 'Uppfæra stöðu', + }, + mileageHistoryFetchFailed: { + id: 'sp.vehicles:mileage-history-fetch-failed', + defaultMessage: 'Eitthvað fór úrskeiðis við að sækja fyrri skráningar', + }, + mileageHistoryNotFound: { + id: 'sp.vehicles:mileage-history-not-found', + defaultMessage: 'Engar fyrri skráningar fundust', + }, + selectFileToUpload: { + id: 'sp.vehicles:select-file-to-upload', + defaultMessage: 'Velja skjal til að hlaða upp', + }, + downloadTemplate: { + id: 'sp.vehicles:download-template', + defaultMessage: 'Hlaða niður sniðmáti', + }, + saveAllVisible: { + id: 'sp.vehicles:save-all-visible', + defaultMessage: 'Vista allar sýnilegar færslur', + }, + entriesPerPage: { + id: 'sp.vehicles:entries-per-page', + defaultMessage: 'Fj. á síðu:', + }, }) export const ipMessages = defineMessages({ diff --git a/libs/service-portal/assets/src/lib/navigation.ts b/libs/service-portal/assets/src/lib/navigation.ts index 4a91891aef71b..5c9e98a530b7e 100644 --- a/libs/service-portal/assets/src/lib/navigation.ts +++ b/libs/service-portal/assets/src/lib/navigation.ts @@ -48,6 +48,29 @@ export const assetsNavigation: PortalNavigationItem = { name: m.vehiclesLookup, path: AssetsPaths.AssetsVehiclesLookup, }, + { + name: m.vehiclesBulkMileage, + path: AssetsPaths.AssetsVehiclesBulkMileage, + children: [ + { + name: m.vehiclesBulkMileageUpload, + path: AssetsPaths.AssetsVehiclesBulkMileageUpload, + navHide: true, + }, + { + name: m.vehiclesBulkMileageJobOverview, + path: AssetsPaths.AssetsVehiclesBulkMileageJobOverview, + navHide: true, + children: [ + { + name: m.vehiclesBulkMileageJobDetail, + path: AssetsPaths.AssetsVehiclesBulkMileageJobDetail, + navHide: true, + }, + ], + }, + ], + }, { name: m.vehiclesHistory, path: AssetsPaths.AssetsVehiclesHistory, diff --git a/libs/service-portal/assets/src/lib/paths.ts b/libs/service-portal/assets/src/lib/paths.ts index cc56bd55eb1f7..f784fe122679a 100644 --- a/libs/service-portal/assets/src/lib/paths.ts +++ b/libs/service-portal/assets/src/lib/paths.ts @@ -6,6 +6,10 @@ export enum AssetsPaths { AssetsMyVehicles = '/eignir/okutaeki/min-okutaeki', AssetsVehiclesDetail = '/eignir/okutaeki/min-okutaeki/:id', AssetsVehiclesDetailMileage = '/eignir/okutaeki/min-okutaeki/:id/kilometrastada', + AssetsVehiclesBulkMileage = '/eignir/okutaeki/magnskraning-kilometrastodu', + AssetsVehiclesBulkMileageUpload = '/eignir/okutaeki/magnskraning-kilometrastodu/hlada-upp', + AssetsVehiclesBulkMileageJobOverview = '/eignir/okutaeki/magnskraning-kilometrastodu/runuverk', + AssetsVehiclesBulkMileageJobDetail = '/eignir/okutaeki/magnskraning-kilometrastodu/runuverk/:id', AssetsVehiclesLookup = '/eignir/okutaeki/leit', AssetsVehiclesHistory = '/eignir/okutaeki/okutaekjaferill', AssetsWorkMachines = '/eignir/vinnuvelar', diff --git a/libs/service-portal/assets/src/module.tsx b/libs/service-portal/assets/src/module.tsx index 633e5ac1b7cd8..6237c03c3fcaf 100644 --- a/libs/service-portal/assets/src/module.tsx +++ b/libs/service-portal/assets/src/module.tsx @@ -54,6 +54,23 @@ const VehicleMileage = lazy(() => import('./screens/VehicleMileage/VehicleMileage'), ) +const VehicleBulkMileage = lazy(() => + import('./screens/VehicleBulkMileage/VehicleBulkMileage'), +) + +const VehicleBulkMileageUpload = lazy(() => + import('./screens/VehicleBulkMileageUpload/VehicleBulkMileageUpload'), +) + +const VehicleBulkMileageJobOverview = lazy(() => + import( + './screens/VehicleBulkMileageJobOverview/VehicleBulkMileageJobOverview' + ), +) +const VehicleBulkMileageJobDetail = lazy(() => + import('./screens/VehicleBulkMileageJobDetail/VehicleBulkMileageJobDetail'), +) + export const assetsModule: PortalModule = { name: 'Fasteignir', routes: ({ userInfo, ...rest }) => { @@ -128,6 +145,34 @@ export const assetsModule: PortalModule = { key: 'VehicleMileage', element: , }, + { + name: m.vehiclesBulkMileage, + path: AssetsPaths.AssetsVehiclesBulkMileage, + enabled: userInfo.scopes.includes(ApiScope.vehicles), + key: 'VehicleBulkMileage', + element: , + }, + { + name: m.vehiclesBulkMileageUpload, + path: AssetsPaths.AssetsVehiclesBulkMileageUpload, + enabled: userInfo.scopes.includes(ApiScope.vehicles), + key: 'VehicleBulkMileage', + element: , + }, + { + name: m.vehiclesBulkMileageJobOverview, + path: AssetsPaths.AssetsVehiclesBulkMileageJobOverview, + enabled: userInfo.scopes.includes(ApiScope.vehicles), + key: 'VehicleBulkMileage', + element: , + }, + { + name: m.vehiclesBulkMileageJobDetail, + path: AssetsPaths.AssetsVehiclesBulkMileageJobDetail, + enabled: userInfo.scopes.includes(ApiScope.vehicles), + key: 'VehicleBulkMileage', + element: , + }, { name: m.vehiclesLookup, path: AssetsPaths.AssetsVehiclesLookup, diff --git a/libs/service-portal/assets/src/screens/IntellectualPropertiesPatentDetail/patentVariations/IS.tsx b/libs/service-portal/assets/src/screens/IntellectualPropertiesPatentDetail/patentVariations/IS.tsx index 533e79a08d45c..a4980f64b2c47 100644 --- a/libs/service-portal/assets/src/screens/IntellectualPropertiesPatentDetail/patentVariations/IS.tsx +++ b/libs/service-portal/assets/src/screens/IntellectualPropertiesPatentDetail/patentVariations/IS.tsx @@ -8,7 +8,6 @@ import { } from '@island.is/service-portal/core' import { IntellectualPropertiesPatentIs } from '@island.is/api/schema' import { Divider, Stack, Text } from '@island.is/island-ui/core' -import { Problem } from '@island.is/react-spa/shared' import { ipMessages } from '../../../lib/messages' import { useMemo } from 'react' import Timeline from '../../../components/Timeline/Timeline' diff --git a/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileage.css.ts b/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileage.css.ts new file mode 100644 index 0000000000000..81c4758f83c3c --- /dev/null +++ b/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileage.css.ts @@ -0,0 +1,11 @@ +import { theme } from '@island.is/island-ui/theme' +import { style } from '@vanilla-extract/css' + +export const link = style({ + color: theme.color.blue400, + textDecoration: 'underline', +}) + +export const mwInput = style({ + maxWidth: 150, +}) diff --git a/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileage.graphql b/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileage.graphql new file mode 100644 index 0000000000000..9b71ade607a2c --- /dev/null +++ b/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileage.graphql @@ -0,0 +1,37 @@ +query vehiclesList($input: VehiclesListInputV3!) { + vehiclesListV3(input: $input) { + pageNumber + pageSize + totalPages + totalRecords + data { + vehicleId + registrationNumber + userRole + type + color + mileageDetails { + canRegisterMileage + requiresMileageRegistration + } + } + } +} + +fragment mileageRegistration on VehiclesMileageRegistration { + originCode + mileage + date +} + +query vehicleMileageRegistrationHistory($input: GetVehicleMileageInput) { + vehiclesMileageRegistrationHistory(input: $input) { + vehicleId + lastMileageRegistration { + ...mileageRegistration + } + mileageRegistrationHistory { + ...mileageRegistration + } + } +} diff --git a/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileage.tsx b/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileage.tsx new file mode 100644 index 0000000000000..aa4984c4e2162 --- /dev/null +++ b/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileage.tsx @@ -0,0 +1,146 @@ +import { Stack, Pagination, Text, Inline } from '@island.is/island-ui/core' +import { useLocale, useNamespaces } from '@island.is/localization' +import { + m, + SAMGONGUSTOFA_SLUG, + IntroHeader, + LinkButton, +} from '@island.is/service-portal/core' +import { vehicleMessage as messages, vehicleMessage } from '../../lib/messages' +import * as styles from './VehicleBulkMileage.css' +import { useEffect, useState } from 'react' +import VehicleBulkMileageTable from './VehicleBulkMileageTable' +import { SubmissionState, VehicleType } from './types' +import { FormProvider, useForm } from 'react-hook-form' +import { useVehiclesListQuery } from './VehicleBulkMileage.generated' +import { isDefined } from '@island.is/shared/utils' +import { AssetsPaths } from '../../lib/paths' +import { Problem } from '@island.is/react-spa/shared' + +interface FormData { + [key: string]: number +} + +const VehicleBulkMileage = () => { + useNamespaces('sp.vehicles') + const { formatMessage } = useLocale() + const [vehicles, setVehicles] = useState>([]) + const [page, setPage] = useState(1) + const [totalPages, setTotalPages] = useState(1) + const [pageSize, setPageSize] = useState(10) + + const { data, loading, error, fetchMore } = useVehiclesListQuery({ + variables: { + input: { + page, + pageSize, + }, + }, + }) + + const methods = useForm() + + useEffect(() => { + if (data?.vehiclesListV3?.data) { + const vehicles: Array = data.vehiclesListV3?.data + .map((v) => { + if (!v.type) { + return null + } + return { + vehicleId: v.vehicleId, + vehicleType: v.type, + submissionStatus: 'idle' as const, + lastMileageRegistration: undefined, + } + }) + .filter(isDefined) + setVehicles(vehicles) + setTotalPages(data?.vehiclesListV3?.totalPages || 1) + } + }, [data?.vehiclesListV3]) + + const updateVehicleStatus = (status: SubmissionState, vehicleId: string) => { + const newVehicles = vehicles.map((v) => { + if (v.vehicleId === vehicleId) { + return { + ...v, + submissionStatus: status, + } + } else return v + }) + setVehicles(newVehicles) + } + + return ( + + + + {formatMessage(messages.vehicleMileageIntro, { + href: (str: React.ReactNode) => ( + + + {str} + + + ), + })} + + } + serviceProviderSlug={SAMGONGUSTOFA_SLUG} + serviceProviderTooltip={formatMessage(m.vehiclesTooltip)} + /> + + + + + + {error && !loading && } + {!error && ( + + )} + + {totalPages > 1 && ( + ( + + )} + /> + )} + + + + ) +} + +export default VehicleBulkMileage diff --git a/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileageFileDownloader.tsx b/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileageFileDownloader.tsx new file mode 100644 index 0000000000000..e9cb958cc1d3d --- /dev/null +++ b/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileageFileDownloader.tsx @@ -0,0 +1,46 @@ +import { Button } from '@island.is/island-ui/core' +import { useLocale } from '@island.is/localization' +import { vehicleMessage } from '../../lib/messages' +import { downloadFile } from '@island.is/service-portal/core' +import { useState } from 'react' + +interface Props { + onError: (error: string) => void +} + +const VehicleBulkMileageFileDownloader = ({ onError }: Props) => { + const { formatMessage } = useLocale() + const [isLoading, setIsLoading] = useState(false) + + const downloadExampleFile = async () => { + setIsLoading(true) + try { + downloadFile( + `magnskraning_kilometrastodu_example`, + ['Ökutæki', 'Kílómetrastaða'], + [['ABC001', 10000]], + 'csv', + ) + } catch (error) { + onError(error) + } finally { + setIsLoading(false) + } + } + + return ( + + ) +} + +export default VehicleBulkMileageFileDownloader diff --git a/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileageRow.tsx b/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileageRow.tsx new file mode 100644 index 0000000000000..8ca205d54aa4a --- /dev/null +++ b/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileageRow.tsx @@ -0,0 +1,160 @@ +import { AlertMessage, Box } from '@island.is/island-ui/core' +import { + ExpandRow, + NestedFullTable, + formatDate, +} from '@island.is/service-portal/core' +import * as styles from './VehicleBulkMileage.css' +import { VehicleBulkMileageSaveButton } from './VehicleBulkMileageSaveButton' +import { useLocale } from '@island.is/localization' +import { vehicleMessage } from '../../lib/messages' +import { InputController } from '@island.is/shared/form-fields' +import { useFormContext } from 'react-hook-form' +import { VehicleType } from './types' +import { isReadDateToday } from '../../utils/readDate' +import { useVehicleMileageRegistrationHistoryLazyQuery } from './VehicleBulkMileage.generated' +import { displayWithUnit } from '../../utils/displayWithUnit' + +interface Props { + vehicle: VehicleType + onSave: (vehicleId: string) => void +} + +export const VehicleBulkMileageRow = ({ vehicle, onSave }: Props) => { + const { formatMessage } = useLocale() + + const [executeRegistrationsQuery, { data, loading, error }] = + useVehicleMileageRegistrationHistoryLazyQuery({ + variables: { + input: { + permno: vehicle.vehicleId, + }, + }, + }) + + const { + control, + formState: { errors }, + } = useFormContext() + + const onSaveButtonClick = () => { + onSave(vehicle.vehicleId) + } + + return ( + + { + // Input number must be higher than the highest known mileage registration value + if (vehicle.registrationHistory) { + // If we're in editing mode, we want to find the highest confirmed registered number, ignoring all Island.is registrations from today. + const confirmedRegistrations = + vehicle.registrationHistory.filter((item) => { + if (item.date) { + const isIslandIsReadingToday = + item.origin === 'ISLAND-IS' && + isReadDateToday(new Date(item.date)) + return !isIslandIsReadingToday + } + return true + }) + + const detailArray = vehicle.isCurrentlyEditing + ? confirmedRegistrations + : [...vehicle.registrationHistory] + + const latestRegistration = + detailArray.length > 0 ? detailArray?.[0].mileage : 0 + if (latestRegistration > value) { + return formatMessage( + vehicleMessage.mileageInputTooLow, + ) + } + } + }, + }, + minLength: { + value: 1, + message: formatMessage( + vehicleMessage.mileageInputMinLength, + ), + }, + required: { + value: true, + message: formatMessage( + vehicleMessage.mileageInputMinLength, + ), + }, + }} + /> + + ), + }, + { + value: ( + + ), + }, + ]} + > + {error ? ( + + ) : ( + [ + formatDate(r.date), + r.originCode, + '-', + displayWithUnit(r.mileage, 'km', true), + ], + ) ?? [] + } + /> + )} + + ) +} diff --git a/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileageSaveButton.tsx b/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileageSaveButton.tsx new file mode 100644 index 0000000000000..f4184a1cd927c --- /dev/null +++ b/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileageSaveButton.tsx @@ -0,0 +1,56 @@ +import { Button } from '@island.is/island-ui/core' +import { useLocale } from '@island.is/localization' +import { m as coreMessages } from '@island.is/service-portal/core' +import { useMemo } from 'react' +import { type SubmissionState } from './types' + +interface Props { + submissionState: SubmissionState + onClick: () => void + disabled?: boolean +} + +export const VehicleBulkMileageSaveButton = ({ + submissionState, + onClick, + disabled, +}: Props) => { + const { formatMessage } = useLocale() + + const tag = useMemo(() => { + switch (submissionState) { + //case 'waiting-success': + case 'success': { + return { + text: formatMessage(coreMessages.saved), + icon: 'checkmarkCircle' as const, + } + } + //case 'waiting-failure': + case 'failure': { + return { + text: formatMessage(coreMessages.errorTitle), + icon: 'closeCircle' as const, + } + } + default: + return { + text: formatMessage(coreMessages.save), + icon: 'pencil' as const, + } + } + }, [formatMessage, submissionState]) + + return ( + + ) +} diff --git a/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileageTable.tsx b/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileageTable.tsx new file mode 100644 index 0000000000000..3d0bb6e88e179 --- /dev/null +++ b/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileageTable.tsx @@ -0,0 +1,88 @@ +import { Table as T, Box } from '@island.is/island-ui/core' +import { useLocale } from '@island.is/localization' +import { EmptyTable, ExpandHeader } from '@island.is/service-portal/core' +import { vehicleMessage } from '../../lib/messages' +import { useMemo } from 'react' +import { useFormContext } from 'react-hook-form' +import { SubmissionState, VehicleType } from './types' +import { VehicleBulkMileageRow } from './VehicleBulkMileageRow' + +interface Props { + vehicles: Array + loading: boolean + updateVehicleStatus: (status: SubmissionState, vehicleId: string) => void +} + +const VehicleBulkMileageTable = ({ + vehicles, + loading, + updateVehicleStatus, +}: Props) => { + const { formatMessage } = useLocale() + + const { getValues, trigger } = useFormContext() + + const getValueFromForm = async ( + formFieldId: string, + skipEmpty = false, + ): Promise => { + const value = getValues(formFieldId) + if (!value && skipEmpty) { + return + } + if (await trigger(formFieldId)) { + return value + } + return -1 + } + + const onRowPost = async (vehicleId: string) => { + const formValue = await getValueFromForm(vehicleId) + if (formValue && formValue > 0) { + //post stuff + updateVehicleStatus('success', vehicleId) + } else { + updateVehicleStatus('failure', vehicleId) + } + } + + const rows = useMemo(() => { + return vehicles.map((item) => ( + + )) + }, [formatMessage, vehicles]) + + return ( + +
+ {rows && ( + + + {rows} + + )} + {(!rows.length || loading) && ( + + )} + +
+ ) +} + +export default VehicleBulkMileageTable diff --git a/libs/service-portal/assets/src/screens/VehicleBulkMileage/mocks/propsDummy.ts b/libs/service-portal/assets/src/screens/VehicleBulkMileage/mocks/propsDummy.ts new file mode 100644 index 0000000000000..f91ce032f6b7a --- /dev/null +++ b/libs/service-portal/assets/src/screens/VehicleBulkMileage/mocks/propsDummy.ts @@ -0,0 +1,284 @@ +import { VehicleType } from '../types' + +export const dummy: Array = [ + { + vehicleId: 'AA001', + vehicleType: 'MAZDA 3', + submissionStatus: 'idle', + lastRegistrationDate: new Date('2021-06-11'), + isCurrentlyEditing: false, + registrationHistory: [ + { + date: new Date('2021-06-11'), + origin: 'ISLAND-IS', + mileage: 2, + }, + ], + }, + { + vehicleId: 'BCD89', + vehicleType: 'SUBARU IMPREZA', + submissionStatus: 'idle', + lastRegistrationDate: new Date(), + isCurrentlyEditing: false, + registrationHistory: [ + { + date: new Date(), + origin: 'ISLAND-IS', + mileage: 99, + }, + { + date: new Date('2021-06-11'), + origin: 'ISLAND-IS', + mileage: 3, + }, + ], + }, + { + vehicleId: 'BZX09', + vehicleType: 'MAZDA 3', + submissionStatus: 'idle', + lastRegistrationDate: new Date(), + isCurrentlyEditing: false, + registrationHistory: [ + { + date: new Date(), + origin: 'ISLAND-IS', + mileage: 99, + }, + { + date: new Date('2021-06-11'), + origin: 'ISLAND-IS', + mileage: 3, + }, + ], + }, + { + vehicleId: 'ED676', + vehicleType: 'SUBARU FORESTER', + submissionStatus: 'idle', + lastRegistrationDate: new Date(), + isCurrentlyEditing: false, + registrationHistory: [ + { + date: new Date(), + origin: 'ISLAND-IS', + mileage: 99, + }, + { + date: new Date('2021-06-11'), + origin: 'ISLAND-IS', + mileage: 3, + }, + ], + }, + { + vehicleId: 'HMV76', + vehicleType: 'VOLKSWAGEN, VW CADDY', + submissionStatus: 'idle', + lastRegistrationDate: new Date(), + isCurrentlyEditing: false, + registrationHistory: [ + { + date: new Date(), + origin: 'ISLAND-IS', + mileage: 99, + }, + { + date: new Date('2021-06-11'), + origin: 'ISLAND-IS', + mileage: 3, + }, + ], + }, + { + vehicleId: 'JZB99', + vehicleType: 'VOLVO V90', + submissionStatus: 'idle', + lastRegistrationDate: new Date(), + isCurrentlyEditing: false, + registrationHistory: [ + { + date: new Date(), + origin: 'ISLAND-IS', + mileage: 99, + }, + { + date: new Date('2021-06-11'), + origin: 'ISLAND-IS', + mileage: 3, + }, + ], + }, + { + vehicleId: 'KDE08', + vehicleType: 'FORD FOCUS', + submissionStatus: 'idle', + lastRegistrationDate: new Date(), + isCurrentlyEditing: false, + registrationHistory: [ + { + date: new Date(), + origin: 'ISLAND-IS', + mileage: 99, + }, + { + date: new Date('2021-06-11'), + origin: 'ISLAND-IS', + mileage: 3, + }, + ], + }, + { + vehicleId: 'PIU46', + vehicleType: 'FORD MUSTANG', + submissionStatus: 'idle', + lastRegistrationDate: new Date(), + isCurrentlyEditing: false, + registrationHistory: [ + { + date: new Date(), + origin: 'ISLAND-IS', + mileage: 99, + }, + { + date: new Date('2021-06-11'), + origin: 'ISLAND-IS', + mileage: 3, + }, + ], + }, + { + vehicleId: 'ZX716', + vehicleType: 'MAN H29', + submissionStatus: 'idle', + lastRegistrationDate: new Date(), + isCurrentlyEditing: false, + registrationHistory: [ + { + date: new Date(), + origin: 'ISLAND-IS', + mileage: 99, + }, + { + date: new Date('2021-06-11'), + origin: 'ISLAND-IS', + mileage: 3, + }, + ], + }, + { + vehicleId: 'ZAPPA', + vehicleType: 'HONDA COOL', + submissionStatus: 'idle', + lastRegistrationDate: new Date(), + isCurrentlyEditing: false, + registrationHistory: [ + { + date: new Date(), + origin: 'ISLAND-IS', + mileage: 99, + }, + { + date: new Date('2021-06-11'), + origin: 'ISLAND-IS', + mileage: 3, + }, + ], + }, + { + vehicleId: 'STAPPA', + vehicleType: 'HONDA COOL', + submissionStatus: 'idle', + lastRegistrationDate: new Date(), + isCurrentlyEditing: false, + registrationHistory: [ + { + date: new Date(), + origin: 'ISLAND-IS', + mileage: 99, + }, + { + date: new Date('2021-06-11'), + origin: 'ISLAND-IS', + mileage: 3, + }, + ], + }, + { + vehicleId: 'Parappa', + vehicleType: 'HONDA COOL', + submissionStatus: 'idle', + lastRegistrationDate: new Date(), + isCurrentlyEditing: false, + registrationHistory: [ + { + date: new Date(), + origin: 'ISLAND-IS', + mileage: 99, + }, + { + date: new Date('2021-06-11'), + origin: 'ISLAND-IS', + mileage: 3, + }, + ], + }, + { + vehicleId: 'zzzzzz', + vehicleType: 'HONDA COOL', + submissionStatus: 'idle', + lastRegistrationDate: new Date(), + isCurrentlyEditing: false, + registrationHistory: [ + { + date: new Date(), + origin: 'ISLAND-IS', + mileage: 99, + }, + { + date: new Date('2021-06-11'), + origin: 'ISLAND-IS', + mileage: 3, + }, + ], + }, + { + vehicleId: 'obboboj', + vehicleType: 'HONDA COOL', + submissionStatus: 'idle', + lastRegistrationDate: new Date(), + isCurrentlyEditing: false, + registrationHistory: [ + { + date: new Date(), + origin: 'ISLAND-IS', + mileage: 99, + }, + { + date: new Date('2021-06-11'), + origin: 'ISLAND-IS', + mileage: 3, + }, + ], + }, + { + vehicleId: 'Habbebbi', + vehicleType: 'HONDA COOL', + submissionStatus: 'idle', + lastRegistrationDate: new Date(), + isCurrentlyEditing: false, + registrationHistory: [ + { + date: new Date(), + origin: 'ISLAND-IS', + mileage: 99, + }, + { + date: new Date('2021-06-11'), + origin: 'ISLAND-IS', + mileage: 3, + }, + ], + }, +] diff --git a/libs/service-portal/assets/src/screens/VehicleBulkMileage/types.ts b/libs/service-portal/assets/src/screens/VehicleBulkMileage/types.ts new file mode 100644 index 0000000000000..236066dcb658d --- /dev/null +++ b/libs/service-portal/assets/src/screens/VehicleBulkMileage/types.ts @@ -0,0 +1,34 @@ +export interface VehicleProps { + vehicleId: string + vehicleType: string + lastMileageRegistration?: Date + submissionStatus: SubmissionState +} + +export type SubmissionState = + | 'idle' + | 'success' + | 'failure' + | 'submit' + | 'submit-all' + | 'waiting-success' + | 'waiting-failure' + | 'waiting-idle' + +export interface Props { + vehicles: Array +} + +export interface VehicleType extends VehicleProps { + mileageUploadedFromFile?: number + isCurrentlyEditing?: boolean + registrationHistory?: Array<{ + date: Date + origin: string + mileage: number + }> +} + +export interface VehicleList { + vehicles: Array +} diff --git a/libs/service-portal/assets/src/screens/VehicleBulkMileageJobDetail/VehicleBulkMileageJobDetail.graphql b/libs/service-portal/assets/src/screens/VehicleBulkMileageJobDetail/VehicleBulkMileageJobDetail.graphql new file mode 100644 index 0000000000000..41047a57ba5c1 --- /dev/null +++ b/libs/service-portal/assets/src/screens/VehicleBulkMileageJobDetail/VehicleBulkMileageJobDetail.graphql @@ -0,0 +1,24 @@ +query getJobsStatus($input: BulkVehicleMileageRequestStatusInput!) { + vehicleBulkMileageRegistrationRequestStatus(input: $input) { + jobsErrored + jobsFinished + jobsRemaining + jobsSubmitted + jobsValid + requestId + } +} +query getJobRegistrations($input: BulkVehicleMileageRequestOverviewInput!) { + vehicleBulkMileageRegistrationRequestOverview(input: $input) { + requests { + guid + vehicleId + mileage + returnCode + errors { + code + message + } + } + } +} diff --git a/libs/service-portal/assets/src/screens/VehicleBulkMileageJobDetail/VehicleBulkMileageJobDetail.tsx b/libs/service-portal/assets/src/screens/VehicleBulkMileageJobDetail/VehicleBulkMileageJobDetail.tsx new file mode 100644 index 0000000000000..d5a9a33168535 --- /dev/null +++ b/libs/service-portal/assets/src/screens/VehicleBulkMileageJobDetail/VehicleBulkMileageJobDetail.tsx @@ -0,0 +1,242 @@ +import { + Box, + Button, + Icon, + Stack, + Table as T, + Text, +} from '@island.is/island-ui/core' +import { useLocale, useNamespaces } from '@island.is/localization' +import { + IntroHeader, + SAMGONGUSTOFA_SLUG, + m, + EmptyTable, + TableGrid, + downloadFile, +} from '@island.is/service-portal/core' +import { Problem } from '@island.is/react-spa/shared' +import { VehiclesBulkMileageRegistrationRequestStatus } from '@island.is/api/schema' +import { useParams } from 'react-router-dom' +import { + useGetJobRegistrationsQuery, + useGetJobsStatusQuery, +} from './VehicleBulkMileageJobDetail.generated' +import { VehiclesBulkMileageRegistrationRequestOverview } from '@island.is/service-portal/graphql' +import { displayWithUnit } from '../../utils/displayWithUnit' +import { useMemo } from 'react' +import { isDefined } from '@island.is/shared/utils' +import { vehicleMessage } from '../../lib/messages' + +type UseParams = { + id: string +} + +const VehicleBulkMileageJobDetail = () => { + useNamespaces('sp.vehicles') + const { formatMessage } = useLocale() + const { id } = useParams() as UseParams + + const { data, loading, error, refetch } = useGetJobsStatusQuery({ + variables: { + input: { + requestId: id, + }, + }, + }) + + const { + data: registrationData, + loading: registrationLoading, + error: registrationError, + } = useGetJobRegistrationsQuery({ + variables: { + input: { + guid: id, + }, + }, + }) + + const jobsStatus: VehiclesBulkMileageRegistrationRequestStatus | undefined = + data?.vehicleBulkMileageRegistrationRequestStatus ?? undefined + + const registrations: + | VehiclesBulkMileageRegistrationRequestOverview + | undefined = + registrationData?.vehicleBulkMileageRegistrationRequestOverview ?? undefined + + const tableArray = useMemo(() => { + if (data?.vehicleBulkMileageRegistrationRequestStatus) { + return [ + [ + { + title: formatMessage(vehicleMessage.totalSubmitted), + value: jobsStatus?.jobsSubmitted + ? jobsStatus.jobsSubmitted.toString() + : '0', + }, + { title: '', value: '' }, + ], + [ + { + title: formatMessage(vehicleMessage.totalFinished), + value: jobsStatus?.jobsFinished + ? jobsStatus.jobsFinished.toString() + : '0', + }, + { + title: formatMessage(vehicleMessage.totalRemaining), + value: jobsStatus?.jobsRemaining + ? jobsStatus.jobsRemaining.toString() + : '0', + }, + ], + [ + { + title: formatMessage(vehicleMessage.healthyJobs), + value: jobsStatus?.jobsValid + ? jobsStatus.jobsValid.toString() + : '0', + }, + { + title: formatMessage(vehicleMessage.unhealthyJobs), + value: jobsStatus?.jobsErrored + ? jobsStatus.jobsErrored.toString() + : '0', + }, + ], + ] + } + }, [data?.vehicleBulkMileageRegistrationRequestStatus]) + + const handleFileDownload = async () => { + const requests = registrations?.requests ?? [] + if (!requests.length) { + return + } + + const data: Array> = requests + .filter((r) => !!r.errors?.length) + .map((erroredVehicle) => { + if (!erroredVehicle.errors?.length) { + return null + } + return [ + erroredVehicle.vehicleId, + erroredVehicle.errors.map((j) => j.message).join(', '), + ] + }) + .filter(isDefined) + + downloadFile(`magnskraning_villur`, ['Ökutæki', 'Villur'], data, 'csv') + } + + return ( + + + {formatMessage(vehicleMessage.dataAboutJob)} +
+ {formatMessage(vehicleMessage.refreshDataAboutJob)} + + } + serviceProviderSlug={SAMGONGUSTOFA_SLUG} + serviceProviderTooltip={formatMessage(m.vehiclesTooltip)} + > + + + +
+ {!error && !loading && !jobsStatus && ( + + )} + {!error && ( + + + + {registrationError && } + + + + {formatMessage(vehicleMessage.jobsSubmitted)} + + + + + + + + {formatMessage(vehicleMessage.permno)} + + + {formatMessage(vehicleMessage.odometer)} + + + {formatMessage(vehicleMessage.errors)} + + + + + + {!!registrations?.requests.length && + registrations?.requests.map((j) => ( + + + + + {j.vehicleId} + + + {displayWithUnit(j.mileage, 'km', true)} + + {(j.errors ?? []).map((j) => j.message).join(', ')} + + + ))} + + + {(!registrations || registrationLoading) && ( + + )} + + + )} +
+ ) +} + +export default VehicleBulkMileageJobDetail diff --git a/libs/service-portal/assets/src/screens/VehicleBulkMileageJobOverview/VehicleBulkMileageJobOverview.graphql b/libs/service-portal/assets/src/screens/VehicleBulkMileageJobOverview/VehicleBulkMileageJobOverview.graphql new file mode 100644 index 0000000000000..493fc8d248045 --- /dev/null +++ b/libs/service-portal/assets/src/screens/VehicleBulkMileageJobOverview/VehicleBulkMileageJobOverview.graphql @@ -0,0 +1,14 @@ +query getRequestsStatus { + vehicleBulkMileageRegistrationJobHistory { + history { + guid + reportingPersonNationalId + reportingPersonName + originCode + originName + dateRequested + dateStarted + dateFinished + } + } +} diff --git a/libs/service-portal/assets/src/screens/VehicleBulkMileageJobOverview/VehicleBulkMileageJobOverview.tsx b/libs/service-portal/assets/src/screens/VehicleBulkMileageJobOverview/VehicleBulkMileageJobOverview.tsx new file mode 100644 index 0000000000000..648ef2116b7c4 --- /dev/null +++ b/libs/service-portal/assets/src/screens/VehicleBulkMileageJobOverview/VehicleBulkMileageJobOverview.tsx @@ -0,0 +1,148 @@ +import { Box, Table as T, Tag } from '@island.is/island-ui/core' +import { useLocale, useNamespaces } from '@island.is/localization' +import { + IntroHeader, + SAMGONGUSTOFA_SLUG, + m, + LinkButton, + EmptyTable, + formatDateWithTime, +} from '@island.is/service-portal/core' +import { Problem } from '@island.is/react-spa/shared' +import { useGetRequestsStatusQuery } from './VehicleBulkMileageJobOverview.generated' +import { VehiclesBulkMileageRegistrationJob } from '@island.is/api/schema' +import { AssetsPaths } from '../../lib/paths' +import { vehicleMessage } from '../../lib/messages' +import compareDesc from 'date-fns/compareDesc' + +const sortDate = (dateOne?: Date, dateTwo?: Date) => { + if (!dateOne && !dateTwo) { + return 0 + } else if (!dateOne) { + return -1 + } else if (!dateTwo) { + return 1 + } + const l = compareDesc(dateOne, dateTwo) + return l +} + +const sortJobs = ( + jobOne: VehiclesBulkMileageRegistrationJob, + jobTwo: VehiclesBulkMileageRegistrationJob, +) => { + let sortedValue = 0 + sortedValue = sortDate( + jobOne.dateFinished ? new Date(jobOne.dateFinished) : undefined, + jobTwo.dateFinished ? new Date(jobTwo.dateFinished) : undefined, + ) + + if (sortedValue === 0) { + sortedValue = sortDate( + jobOne.dateStarted ? new Date(jobOne.dateStarted) : undefined, + jobTwo.dateStarted ? new Date(jobTwo.dateStarted) : undefined, + ) + } + + if (sortedValue === 0) { + sortedValue = sortDate( + jobOne.dateRequested ? new Date(jobOne.dateRequested) : undefined, + jobTwo.dateRequested ? new Date(jobTwo.dateRequested) : undefined, + ) + } + + return sortedValue +} + +const VehicleBulkMileageUploadJobOverview = () => { + useNamespaces('sp.vehicles') + const { formatMessage } = useLocale() + + const { data, loading, error } = useGetRequestsStatusQuery() + + const jobs: Array = + data?.vehicleBulkMileageRegistrationJobHistory?.history ?? [] + + const sortedJobs = jobs.length > 1 ? [...jobs] : [] + sortedJobs.sort((a, b) => sortJobs(a, b)) + return ( + + + {error && } + {!error && ( + + + + + {formatMessage(vehicleMessage.jobSubmitted)} + + + {formatMessage(vehicleMessage.jobStarted)} + + + {formatMessage(vehicleMessage.jobFinished)} + + + + + + {sortedJobs.map((j) => ( + + + {j.dateRequested ? formatDateWithTime(j.dateRequested) : '-'} + + + {j.dateStarted ? ( + formatDateWithTime(j.dateStarted) + ) : j.dateRequested ? ( + + {formatMessage(vehicleMessage.jobNotStarted)} + + ) : ( + '' + )} + + + {j.dateFinished ? ( + formatDateWithTime(j.dateFinished) + ) : j.dateStarted ? ( + + {formatMessage(vehicleMessage.jobInProgress)} + + ) : ( + '' + )} + + + + + + + ))} + + + )} + {!error && (loading || !jobs.length) && ( + + )} + + ) +} + +export default VehicleBulkMileageUploadJobOverview diff --git a/libs/service-portal/assets/src/screens/VehicleBulkMileageUpload/VehicleBulkMileageUpload.graphql b/libs/service-portal/assets/src/screens/VehicleBulkMileageUpload/VehicleBulkMileageUpload.graphql new file mode 100644 index 0000000000000..5a1a216ada0a1 --- /dev/null +++ b/libs/service-portal/assets/src/screens/VehicleBulkMileageUpload/VehicleBulkMileageUpload.graphql @@ -0,0 +1,6 @@ +mutation vehicleBulkMileagePost($input: PostVehicleBulkMileageInput!) { + vehicleBulkMileagePost(input: $input) { + requestId + errorMessage + } +} diff --git a/libs/service-portal/assets/src/screens/VehicleBulkMileageUpload/VehicleBulkMileageUpload.tsx b/libs/service-portal/assets/src/screens/VehicleBulkMileageUpload/VehicleBulkMileageUpload.tsx new file mode 100644 index 0000000000000..886234b4e80f0 --- /dev/null +++ b/libs/service-portal/assets/src/screens/VehicleBulkMileageUpload/VehicleBulkMileageUpload.tsx @@ -0,0 +1,174 @@ +import { + InputFileUpload, + Box, + Text, + fileToObject, + AlertMessage, + Stack, +} from '@island.is/island-ui/core' +import { useLocale, useNamespaces } from '@island.is/localization' +import { useEffect, useState } from 'react' +import { FileRejection } from 'react-dropzone' +import { + IntroHeader, + LinkButton, + SAMGONGUSTOFA_SLUG, + m, +} from '@island.is/service-portal/core' +import { + MileageRecord, + parseCsvToMileageRecord, +} from '../../utils/parseCsvToMileage' +import { Problem } from '@island.is/react-spa/shared' +import { AssetsPaths } from '../../lib/paths' +import { useVehicleBulkMileagePostMutation } from './VehicleBulkMileageUpload.generated' +import VehicleBulkMileageFileDownloader from '../VehicleBulkMileage/VehicleBulkMileageFileDownloader' +import { vehicleMessage } from '../../lib/messages' + +const VehicleBulkMileageUpload = () => { + useNamespaces('sp.vehicles') + const { formatMessage } = useLocale() + + const [vehicleBulkMileagePostMutation, { data, loading, error }] = + useVehicleBulkMileagePostMutation() + + const [uploadedFile, setUploadedFile] = useState() + const [uploadErrorMessage, setUploadErrorMessage] = useState( + null, + ) + const [downloadError, setDownloadError] = useState() + + const [requestGuid, setRequestGuid] = useState() + + useEffect(() => { + const id = data?.vehicleBulkMileagePost?.requestId + if (id && id !== requestGuid) { + setRequestGuid(id) + } + }, [data?.vehicleBulkMileagePost?.requestId]) + + const postMileage = async (file: File) => { + try { + const records = await parseCsvToMileageRecord(file) + if (!records.length) { + setUploadErrorMessage(formatMessage(vehicleMessage.uploadFailed)) + return + } + vehicleBulkMileagePostMutation({ + variables: { + input: { + mileageData: records.map((r) => ({ + mileageNumber: r.mileage, + vehicleId: r.vehicleId, + })), + originCode: 'ISLAND.IS', + }, + }, + }) + } catch (error) { + setUploadErrorMessage( + `${formatMessage(vehicleMessage.errorWhileProcessing) + error.message} + `, + ) + } + } + + const handleOnInputFileUploadError = (files: FileRejection[]) => + setUploadErrorMessage(files[0].errors[0].message) + + const handleOnInputFileUploadRemove = () => setUploadedFile(null) + + const handleOnInputFileUploadChange = (files: File[]) => { + const file = fileToObject(files[0]) + if (file.status === 'done' && file.originalFileObj instanceof File) { + postMileage(file.originalFileObj) + } + } + + const handleFileDownloadError = (error: string) => { + setDownloadError(error) + } + + return ( + + + + + + + + + {error && } + {data?.vehicleBulkMileagePost?.errorMessage && !loading && !error && ( + + )} + {downloadError && ( + + )} + {data?.vehicleBulkMileagePost?.errorMessage && !loading && !error && ( + + )} + {data?.vehicleBulkMileagePost?.requestId && + !data?.vehicleBulkMileagePost?.errorMessage && + !loading && + !error && ( + + + {formatMessage(vehicleMessage.bulkMileageUploadStatus)} + + + + + + } + /> + )} + + + + ) +} + +export default VehicleBulkMileageUpload diff --git a/libs/service-portal/assets/src/utils/makeArrayEven.ts b/libs/service-portal/assets/src/utils/makeArrayEven.ts index d17f09aa0fca3..f2d5549be999c 100644 --- a/libs/service-portal/assets/src/utils/makeArrayEven.ts +++ b/libs/service-portal/assets/src/utils/makeArrayEven.ts @@ -1,4 +1,4 @@ -export function makeArrayEven(data: Array, toAddIfOdd: T) { +export const makeArrayEven = (data: Array, toAddIfOdd: T) => { if (data.length % 2 !== 0) { data.push(toAddIfOdd) } diff --git a/libs/service-portal/assets/src/utils/parseCsvToMileage.ts b/libs/service-portal/assets/src/utils/parseCsvToMileage.ts new file mode 100644 index 0000000000000..8ab44d003c23a --- /dev/null +++ b/libs/service-portal/assets/src/utils/parseCsvToMileage.ts @@ -0,0 +1,58 @@ +import { isDefined } from '@island.is/shared/utils' + +export interface MileageRecord { + vehicleId: string + mileage: number +} + +const letters = + 'aábcdðeéfghiíjklmnoópqrstuúvwxyýzþæöAÁBCDÐEÉFGHIÍJKLMNOÓPQRSTUÚVWXYÝZÞÆÖ' +const newlines = '\\Q\\r\\n\\E|\\r|\\n' +const wordbreaks = '[;,]' + +export const parseCsvToMileageRecord = async (file: File) => { + const reader = file.stream().getReader() + + const parsedLines: Array> = [[]] + const parseChunk = async (res: ReadableStreamReadResult) => { + if (res.done) { + return + } + const chunk = Buffer.from(res.value).toString('utf8') + + let rowIndex = 0 + for (const cell of chunk.matchAll( + new RegExp(`([${letters}\\d]+)(${newlines}|${wordbreaks})?`, 'gi'), + )) { + const [_, trimmedValue, delimiter] = cell + const lineBreak = ['\r\n', '\n', '\r'].includes(delimiter) + + parsedLines[rowIndex].push(trimmedValue.trim()) + if (lineBreak) { + parsedLines.push([]) + rowIndex++ + } + } + } + + await reader.read().then(parseChunk) + const [header, ...values] = parsedLines + const vehicleIndex = header.findIndex((l) => l.toLowerCase() === 'ökutæki') + const mileageIndex = header.findIndex( + (l) => l.toLowerCase() === 'kílómetraskráning', + ) + const uploadedOdometerStatuses: Array = values + .map((row) => { + const mileage = parseInt(row[mileageIndex]) + if (Number.isNaN(mileage)) { + return undefined + } + return { + vehicleId: row[vehicleIndex], + mileage, + } + }) + .filter(isDefined) + + return uploadedOdometerStatuses +} diff --git a/libs/service-portal/assets/src/utils/vehicleOwnedMapper.ts b/libs/service-portal/assets/src/utils/vehicleOwnedMapper.ts index 9b91a8325efda..2c87bb23eed2d 100644 --- a/libs/service-portal/assets/src/utils/vehicleOwnedMapper.ts +++ b/libs/service-portal/assets/src/utils/vehicleOwnedMapper.ts @@ -5,11 +5,7 @@ import { } from './dataHeaders' import isValid from 'date-fns/isValid' import { LOCALE } from './constants' -import { - Query, - VehicleUserTypeEnum, - VehiclesDetail, -} from '@island.is/api/schema' +import { VehicleUserTypeEnum, VehiclesDetail } from '@island.is/api/schema' export const exportVehicleOwnedDocument = async ( data: Array, @@ -30,12 +26,12 @@ export const exportVehicleOwnedDocument = async ( ) } -function filterOwners( +const filterOwners = ( role: VehicleUserTypeEnum, vehicles: Array, nationalId: string, name: string, -) { +) => { let filteredVehicles switch (role) { diff --git a/libs/service-portal/core/src/components/LinkButton/LinkButton.tsx b/libs/service-portal/core/src/components/LinkButton/LinkButton.tsx index edf0aa6ffa821..229ecf58217ae 100644 --- a/libs/service-portal/core/src/components/LinkButton/LinkButton.tsx +++ b/libs/service-portal/core/src/components/LinkButton/LinkButton.tsx @@ -11,18 +11,10 @@ interface SharedProps { skipOutboundTrack?: boolean } -type Props = - | { - variant?: 'primary' | 'ghost' | 'utility' - icon?: ButtonProps['icon'] - } - | { - /** - * default variant is "text" - */ - variant?: 'text' - icon?: never - } +type Props = { + variant?: 'primary' | 'ghost' | 'utility' | 'text' + icon?: ButtonProps['icon'] +} type LinkButtonProps = SharedProps & Props diff --git a/libs/service-portal/core/src/components/NestedTable/NestedFullTable.tsx b/libs/service-portal/core/src/components/NestedTable/NestedFullTable.tsx new file mode 100644 index 0000000000000..4d61100c55de7 --- /dev/null +++ b/libs/service-portal/core/src/components/NestedTable/NestedFullTable.tsx @@ -0,0 +1,76 @@ +import { Box, Table as T, Text } from '@island.is/island-ui/core' +import { tableStyles } from '../../utils/utils' + +import * as styles from './NestedTable.css' +import { EmptyTable } from '../EmptyTable/EmptyTable' + +interface Props { + headerArray: string[] + data: Array + loading?: boolean + emptyMessage?: string +} + +export const NestedFullTable = ({ + headerArray, + data, + loading, + emptyMessage, +}: Props) => { + return ( + + {!loading && data.length && ( + + + + {headerArray.map((item, i) => ( + 1 ? 'right' : 'left', + paddingRight: 2, + paddingLeft: 2, + className: styles.noBorder, + }} + key={i} + text={{ truncate: true }} + style={tableStyles} + > + + {item} + + + ))} + + + + {data?.map((row, i) => ( + + {row.map((value, ii) => ( + 1 ? 'right' : 'left', + background: i % 2 === 0 ? 'white' : undefined, + className: styles.noBorder, + }} + key={ii} + style={tableStyles} + > + {value} + + ))} + + ))} + + + )} + {(loading || !data.length) && ( + + )} + + ) +} diff --git a/libs/service-portal/core/src/components/NestedTable/NestedTable.css.ts b/libs/service-portal/core/src/components/NestedTable/NestedTable.css.ts index a730679cba25c..c74de3a35e44b 100644 --- a/libs/service-portal/core/src/components/NestedTable/NestedTable.css.ts +++ b/libs/service-portal/core/src/components/NestedTable/NestedTable.css.ts @@ -16,3 +16,21 @@ export const white = style({ export const titleCol = style({ paddingLeft: theme.spacing[2], }) + +export const wrapper = style({ + display: 'grid', + background: theme.color.blue100, + padding: `0 ${theme.spacing[3]}px ${theme.spacing[3]}px ${theme.spacing[3]}px`, +}) + +export const td = style({ + width: 'max-content', +}) + +export const alignTd = style({ + marginLeft: 'auto', +}) + +export const noBorder = style({ + border: 'none', +}) diff --git a/libs/service-portal/core/src/index.ts b/libs/service-portal/core/src/index.ts index 9f528d7f1c1f5..25c5a74f64a00 100644 --- a/libs/service-portal/core/src/index.ts +++ b/libs/service-portal/core/src/index.ts @@ -38,6 +38,7 @@ export * from './components/AudioPlayer/AudioPlayer' export * from './components/VideoPlayer/VideoPlayer' export * from './components/DownloadFileButtons/DownloadFileButtons' export * from './components/NestedTable/NestedTable' +export * from './components/NestedTable/NestedFullTable' export * from './components/EmptyTable/EmptyTable' export * from './components/StackWithBottomDivider/StackWithBottomDivider' export * from './components/StackOrTableBlock/StackOrTableBlock' diff --git a/libs/service-portal/core/src/lib/messages.ts b/libs/service-portal/core/src/lib/messages.ts index 3a5bff6b5f84c..e522a617fe8a4 100644 --- a/libs/service-portal/core/src/lib/messages.ts +++ b/libs/service-portal/core/src/lib/messages.ts @@ -305,6 +305,35 @@ export const m = defineMessages({ id: 'service.portal:vehicles-lookup', defaultMessage: 'Uppfletting í ökutækjaskrá', }, + vehiclesBulkMileage: { + id: 'service.portal:vehicles-bulk-mileage', + defaultMessage: 'Magnskráning kílómetrastöðu', + }, + vehiclesBulkMileageUpload: { + id: 'service.portal:vehicles-bulk-mileage-upload', + defaultMessage: 'Magnskrá með skjali', + }, + vehiclesBulkMileageUploadDescription: { + id: 'service.portal:vehicles-bulk-mileage-upload-description', + defaultMessage: + 'Hér geturu hlaðið upp skjali til að magnskrá kílómetrastöður', + }, + vehiclesBulkMileageJobOverview: { + id: 'service.portal:vehicles-bulk-mileage-job-overview', + defaultMessage: 'Yfirlit skráninga', + }, + vehiclesBulkMileageJobOverviewDescription: { + id: 'service.portal:vehicles-bulk-mileage-job-overview', + defaultMessage: 'Yfirlit yfir skráða kílómetrastöðu', + }, + vehiclesBulkMileageJobDetail: { + id: 'service.portal:vehicles-bulk-mileage-job-detail', + defaultMessage: 'Magnskráning', + }, + vehiclesBulkMileageJobRegistration: { + id: 'service.portal:vehicles-bulk-mileage-job-registration', + defaultMessage: 'Hvaða skráningar eru höndlaðar í þessu verki', + }, vehiclesDrivingLessons: { id: 'service.portal:vehicles-driving-lessons', defaultMessage: 'Ökunám', @@ -1084,6 +1113,10 @@ export const m = defineMessages({ id: 'service.portal:save', defaultMessage: 'Vista', }, + saved: { + id: 'service.portal:saved', + defaultMessage: 'Vistað', + }, register: { id: 'service.portal:register', defaultMessage: 'Skrá',