diff --git a/webapp/public/locales/en/main.json b/webapp/public/locales/en/main.json index 78d1ae6300..144538d16c 100644 --- a/webapp/public/locales/en/main.json +++ b/webapp/public/locales/en/main.json @@ -126,6 +126,7 @@ "form.field.minLength": "{{0}} character(s) minimum", "form.field.minValue": "The minimum value is {{0}}", "form.field.maxValue": "The maximum value is {{0}}", + "form.field.invalidNumber": "Invalid number", "form.field.notAllowedValue": "Not allowed value", "form.field.specialChars": "Special characters allowed: {{0}}", "form.field.specialCharsNotAllowed": "Special characters are not allowed", @@ -202,7 +203,7 @@ "settings.error.groupRolesSave": "Role(s) for group '{{0}}' not saved", "settings.error.tokenSave": "'{{0}}' token not saved", "settings.error.updateMaintenance": "Maintenance mode not updated", - "settings.user.form.confirmPassword":"Confirm password", + "settings.user.form.confirmPassword": "Confirm password", "settings.user.form.error.passwordMismatch": "Passwords do not match", "launcher.additionalModes": "Additional modes", "launcher.autoUnzip": "Automatically unzip", @@ -487,10 +488,13 @@ "study.modelization.clusters.thermal.op5": "Other pollutant 5 (t/MWh)", "study.modelization.clusters.operatingCosts": "Operating costs", "study.modelization.clusters.marginalCost": "Marginal cost (€/MWh)", - "study.modelization.clusters.fixedCost": "Fixed costs (€/h)", + "study.modelization.clusters.fixedCost": "Fixed O&M costs (€/h)", "study.modelization.clusters.startupCost": "Startup cost (€)", "study.modelization.clusters.marketBidCost": "Market bid cost (€/MWh)", - "study.modelization.clusters.spreadCost": "Spread cost (€/MWh)", + "study.modelization.clusters.spreadCost": "Random spread (€/MWh)", + "study.modelization.clusters.variableOMCost": "Variable O&M cost (€/MWh)", + "study.modelization.clusters.costGeneration": "TS Cost", + "study.modelization.clusters.efficiency": "Efficiency (%)", "study.modelization.clusters.timeSeriesGen": "Time-Series generation", "study.modelization.clusters.genTs": "Generate Time-Series", "study.modelization.clusters.volatilityForced": "Volatility forced", @@ -500,6 +504,8 @@ "study.modelization.clusters.matrix.common": "Common", "study.modelization.clusters.matrix.tsGen": "TS generator", "study.modelization.clusters.matrix.timeSeries": "Time-Series", + "study.modelization.clusters.matrix.fuelCost": "Fuel Cost", + "study.modelization.clusters.matrix.co2Cost": "CO2 Cost", "study.modelization.clusters.backClusterList": "Back to cluster list", "study.modelization.clusters.tsInterpretation": "TS interpretation", "studies.modelization.clusters.question.delete_one": "Are you sure you want to delete this cluster?", diff --git a/webapp/public/locales/fr/main.json b/webapp/public/locales/fr/main.json index b96a4d1735..7210751d43 100644 --- a/webapp/public/locales/fr/main.json +++ b/webapp/public/locales/fr/main.json @@ -126,6 +126,7 @@ "form.field.minLength": "{{0}} caractère(s) minimum", "form.field.minValue": "La valeur minimum est {{0}}", "form.field.maxValue": "La valeur maximum est {{0}}", + "form.field.invalidNumber": "Nombre invalide", "form.field.notAllowedValue": "Valeur non autorisée", "form.field.specialChars": "Caractères spéciaux autorisés: {{0}}", "form.field.specialCharsNotAllowed": "Les caractères spéciaux ne sont pas autorisés", @@ -486,11 +487,14 @@ "study.modelization.clusters.thermal.op4": "Autre polluant 4 (t/MWh)", "study.modelization.clusters.thermal.op5": "Autre polluant 5 (t/MWh)", "study.modelization.clusters.operatingCosts": "Coûts d'exploitation", - "study.modelization.clusters.marginalCost": "Coûts marginaux (€/MWh)", - "study.modelization.clusters.fixedCost": "Coûts fixes (€/h)", + "study.modelization.clusters.marginalCost": "Coût marginal (€/MWh)", + "study.modelization.clusters.fixedCost": "Coûts fixes O&M (€/h)", "study.modelization.clusters.startupCost": "Coûts de démarrage (€)", "study.modelization.clusters.marketBidCost": "Offre de marché (€/MWh)", - "study.modelization.clusters.spreadCost": "Spread (€/MWh)", + "study.modelization.clusters.spreadCost": "Random Spread (€/MWh)", + "study.modelization.clusters.variableOMCost": "Coût variable O&M (€/MWh)", + "study.modelization.clusters.costGeneration": "TS Cost", + "study.modelization.clusters.efficiency": "Rendement (%)", "study.modelization.clusters.timeSeriesGen": "Génération des Séries temporelles", "study.modelization.clusters.genTs": "Générer des Séries temporelles", "study.modelization.clusters.volatilityForced": "Volatilité forcée", @@ -500,6 +504,8 @@ "study.modelization.clusters.matrix.common": "Common", "study.modelization.clusters.matrix.tsGen": "TS generator", "study.modelization.clusters.matrix.timeSeries": "Séries temporelles", + "study.modelization.clusters.matrix.fuelCost": "Fuel Cost", + "study.modelization.clusters.matrix.co2Cost": "CO2 Cost", "study.modelization.clusters.backClusterList": "Retour à la liste des clusters", "study.modelization.clusters.tsInterpretation": "TS interpretation", "studies.modelization.clusters.question.delete_one": "Êtes-vous sûr de vouloir supprimer ce cluster ?", diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/Fields.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/Fields.tsx index ec5d6fc632..15f67cdb41 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/Fields.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/Fields.tsx @@ -1,6 +1,7 @@ import { useTranslation } from "react-i18next"; import { useOutletContext } from "react-router"; import { StudyMetadata } from "../../../../../../../common/types"; +import Box from "@mui/material/Box"; import NumberFE from "../../../../../../common/fieldEditors/NumberFE"; import SelectFE from "../../../../../../common/fieldEditors/SelectFE"; import StringFE from "../../../../../../common/fieldEditors/StringFE"; @@ -8,25 +9,29 @@ import SwitchFE from "../../../../../../common/fieldEditors/SwitchFE"; import Fieldset from "../../../../../../common/Fieldset"; import { useFormContextPlus } from "../../../../../../common/Form"; import { + COST_GENERATION_OPTIONS, THERMAL_GROUPS, THERMAL_POLLUTANTS, ThermalCluster, TS_GENERATION_OPTIONS, TS_LAW_OPTIONS, } from "./utils"; +import { validateNumber } from "../../../../../../../utils/validationUtils"; function Fields() { const [t] = useTranslation(); - const { control } = useFormContextPlus(); + const { control, watch } = useFormContextPlus(); const { study } = useOutletContext<{ study: StudyMetadata }>(); const studyVersion = Number(study.version); + const isTSCost = watch("costGeneration") === "useCostTimeseries"; //////////////////////////////////////////////////////////////// // JSX //////////////////////////////////////////////////////////////// return ( - <> + // TODO: remove the margin reset after updating MUI Theme. +
validateNumber(v, { min: 1 }), setValueAs: Math.floor, }} /> @@ -80,10 +83,7 @@ function Fields() { name="nominalCapacity" control={control} rules={{ - min: { - value: 0, - message: t("form.field.minValue", { 0: 0 }), - }, + validate: (v) => validateNumber(v, { min: 0 }), }} /> validateNumber(v, { min: 0, max: 100 }), }} /> validateNumber(v, { min: 1, max: 168 }), setValueAs: Math.floor, }} /> @@ -127,50 +113,46 @@ function Fields() { name="minDownTime" control={control} rules={{ - min: { - value: 1, - message: t("form.field.minValue", { 0: 1 }), - }, - max: { - value: 168, - message: t("form.field.maxValue", { 0: 168 }), - }, + validate: (v) => validateNumber(v, { min: 1, max: 168 }), setValueAs: Math.floor, }} />
+ validateNumber(v, { min: 0 }), }} + disabled={!isTSCost} /> validateNumber(v, { min: 0 }), }} /> + validateNumber(v, { min: 0 }), }} /> validateNumber(v, { min: 0 }), + }} + /> + validateNumber(v, { min: 0 }), + }} + /> + validateNumber(v, { min: 0 }), }} + disabled={!isTSCost} /> validateNumber(v, { min: 0 }), }} /> ), @@ -211,6 +204,7 @@ function Fields() {
validateNumber(v, { min: 0, max: 1 }), }} inputProps={{ step: 0.1 }} /> @@ -240,18 +227,12 @@ function Fields() { name="volatilityPlanned" control={control} rules={{ - min: { - value: 0, - message: t("form.field.minValue", { 0: 0 }), - }, - max: { - value: 1, - message: t("form.field.maxValue", { 0: 1 }), - }, + validate: (v) => validateNumber(v, { min: 0, max: 1 }), }} inputProps={{ step: 0.1 }} />
- +
); } diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/Matrix.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/Matrix.tsx index 10801c59a4..4e4652f8a4 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/Matrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/Matrix.tsx @@ -1,5 +1,4 @@ -import * as React from "react"; -import * as R from "ramda"; +import { useState } from "react"; import Tabs from "@mui/material/Tabs"; import Tab from "@mui/material/Tab"; import Box from "@mui/material/Box"; @@ -10,6 +9,7 @@ import { StudyMetadata, } from "../../../../../../../common/types"; import MatrixInput from "../../../../../../common/MatrixInput"; +import { COMMON_MATRIX_COLS, TS_GEN_MATRIX_COLS } from "./utils"; interface Props { study: StudyMetadata; @@ -19,91 +19,87 @@ interface Props { function Matrix({ study, areaId, clusterId }: Props) { const [t] = useTranslation(); - const [value, setValue] = React.useState(0); + const [value, setValue] = useState(0); - const commonNames = [ - "Marginal cost modulation", - "Market bid modulation", - "Capacity modulation", - "Min gen modulation", - ]; - - const tsGenNames = [ - "FO Duration", - "PO Duration", - "FO Rate", - "PO Rate", - "NPO Min", - "NPO Max", - ]; + //////////////////////////////////////////////////////////////// + // Event Handlers + //////////////////////////////////////////////////////////////// const handleChange = (event: React.SyntheticEvent, newValue: number) => { setValue(newValue); }; + //////////////////////////////////////////////////////////////// + // JSX + //////////////////////////////////////////////////////////////// + return ( - + + + - {R.cond([ - [ - () => value === 0, - () => ( - - ), - ], - [ - () => value === 1, - () => ( - - ), - ], - [ - R.T, - () => ( - - ), - ], - ])()} + {value === 0 && ( + + )} + {value === 1 && ( + + )} + {value === 2 && ( + + )} + {value === 3 && ( + + )} + {value === 4 && ( + + )} ); diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/utils.ts index 8d5836a4e0..b5b0526ded 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/utils.ts @@ -11,6 +11,22 @@ import type { ClusterWithCapacity } from "../common/clustersUtils"; // Constants //////////////////////////////////////////////////////////////// +export const COMMON_MATRIX_COLS = [ + "Marginal cost modulation", + "Market bid modulation", + "Capacity modulation", + "Min gen modulation", +] as const; + +export const TS_GEN_MATRIX_COLS = [ + "FO Duration", + "PO Duration", + "FO Rate", + "PO Rate", + "NPO Min", + "NPO Max", +] as const; + export const THERMAL_GROUPS = [ "Gas", "Hard Coal", @@ -25,8 +41,8 @@ export const THERMAL_GROUPS = [ ] as const; export const THERMAL_POLLUTANTS = [ - // For study versions >= 860 "co2", + // Since v8.6 "so2", "nh3", "nox", @@ -49,6 +65,11 @@ export const TS_GENERATION_OPTIONS = [ export const TS_LAW_OPTIONS = ["geometric", "uniform"] as const; +export const COST_GENERATION_OPTIONS = [ + "SetManually", + "useCostTimeseries", +] as const; + //////////////////////////////////////////////////////////////// // Types //////////////////////////////////////////////////////////////// @@ -57,7 +78,7 @@ export type ThermalGroup = (typeof THERMAL_GROUPS)[number]; type LocalTSGenerationBehavior = (typeof TS_GENERATION_OPTIONS)[number]; type TimeSeriesLawOption = (typeof TS_LAW_OPTIONS)[number]; - +type CostGeneration = (typeof COST_GENERATION_OPTIONS)[number]; type ThermalPollutants = { [K in (typeof THERMAL_POLLUTANTS)[number]]: number; }; @@ -84,6 +105,10 @@ export interface ThermalCluster extends ThermalPollutants { volatilityPlanned: number; lawForced: TimeSeriesLawOption; lawPlanned: TimeSeriesLawOption; + // Since v8.7 + costGeneration: CostGeneration; + efficiency: number; + variableOMCost: number; } export type ThermalClusterWithCapacity = ClusterWithCapacity; diff --git a/webapp/src/components/common/EditableMatrix/index.tsx b/webapp/src/components/common/EditableMatrix/index.tsx index 3e17ed5aaa..7916fa2a1f 100644 --- a/webapp/src/components/common/EditableMatrix/index.tsx +++ b/webapp/src/components/common/EditableMatrix/index.tsx @@ -24,7 +24,7 @@ interface PropTypes { matrixTime: boolean; readOnly: boolean; onUpdate?: (change: MatrixEditDTO[], source: string) => void; - columnsNames?: string[]; + columnsNames?: string[] | readonly string[]; rowNames?: string[]; computStats?: MatrixStats; isPercentDisplayEnabled?: boolean; diff --git a/webapp/src/components/common/MatrixInput/index.tsx b/webapp/src/components/common/MatrixInput/index.tsx index fdeb92749f..d5c9a71f2a 100644 --- a/webapp/src/components/common/MatrixInput/index.tsx +++ b/webapp/src/components/common/MatrixInput/index.tsx @@ -29,7 +29,7 @@ const logErr = debug("antares:createimportform:error"); interface Props { study: StudyMetadata; url: string; - columnsNames?: string[]; + columnsNames?: string[] | readonly string[]; rowNames?: string[]; title?: string; computStats: MatrixStats; diff --git a/webapp/src/utils/validationUtils.ts b/webapp/src/utils/validationUtils.ts index 9af316cbba..5b3f60091d 100644 --- a/webapp/src/utils/validationUtils.ts +++ b/webapp/src/utils/validationUtils.ts @@ -150,6 +150,37 @@ export function validatePassword(password: string): string | true { return true; } +/** + * Validates a number against specified numerical limits. + * + * @param value - The number to validate. + * @param options - Configuration options for validation including min and max values. (Optional) + * @param [options.min=Number.MIN_SAFE_INTEGER] - Minimum allowed value for the number. + * @param [options.max=Number.MAX_SAFE_INTEGER] - Maximum allowed value for the number. + * @returns True if validation is successful, or a localized error message if it fails. + */ +export function validateNumber( + value: number, + options?: ValidationOptions, +): string | true { + if (typeof value !== "number" || isNaN(value) || !isFinite(value)) { + return t("form.field.invalidNumber", { value }); + } + + const { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = + options || {}; + + if (value < min) { + return t("form.field.minValue", { 0: min }); + } + + if (value > max) { + return t("form.field.maxValue", { 0: max }); + } + + return true; +} + //////////////////////////////////////////////////////////////// // Utils ////////////////////////////////////////////////////////////////