From 2ff24d8d8e5ff6e64a718293f30b6f7dfe60e1d6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9s=20Gonz=C3=A1lez=20Mu=C3=B1oz?=
 <andres.gonzalez@vizzuality.com>
Date: Tue, 17 Oct 2023 12:45:55 +0200
Subject: [PATCH] WIP

---
 .../cost-surfaces/bulk-action-menu/utils.ts   |   2 +-
 .../grid-setup/cost-surface/index.tsx         | 126 +++++++++++++++++-
 app/layout/scenarios/edit/map/component.tsx   |  21 ++-
 app/types/api/scenario.ts                     |   1 +
 4 files changed, 140 insertions(+), 10 deletions(-)

diff --git a/app/layout/project/sidebar/project/inventory-panel/cost-surfaces/bulk-action-menu/utils.ts b/app/layout/project/sidebar/project/inventory-panel/cost-surfaces/bulk-action-menu/utils.ts
index 15dc04b69c..d458ead34d 100644
--- a/app/layout/project/sidebar/project/inventory-panel/cost-surfaces/bulk-action-menu/utils.ts
+++ b/app/layout/project/sidebar/project/inventory-panel/cost-surfaces/bulk-action-menu/utils.ts
@@ -17,7 +17,7 @@ export function bulkDeleteCostSurfaceFromProject(
     pid: Project['id'];
     csid: CostSurface['id'];
   }) => {
-    return PROJECTS.delete(`/${pid}/cost-surface/${csid}`, {
+    return PROJECTS.delete(`/${pid}/cost-surfaces/${csid}`, {
       headers: {
         Authorization: `Bearer ${session.accessToken}`,
       },
diff --git a/app/layout/project/sidebar/scenario/grid-setup/cost-surface/index.tsx b/app/layout/project/sidebar/scenario/grid-setup/cost-surface/index.tsx
index ea16954a94..d2066e6f4a 100644
--- a/app/layout/project/sidebar/scenario/grid-setup/cost-surface/index.tsx
+++ b/app/layout/project/sidebar/scenario/grid-setup/cost-surface/index.tsx
@@ -1,7 +1,8 @@
-import { useCallback, useState } from 'react';
+import { ComponentProps, useCallback, useRef, useState } from 'react';
 
 import { useDropzone, DropzoneProps } from 'react-dropzone';
-import { Form, Field } from 'react-final-form';
+import { Form, Field, FormProps } from 'react-final-form';
+import { Form as FormRFF } from 'react-final-form';
 import { useDispatch } from 'react-redux';
 
 import { useRouter } from 'next/router';
@@ -9,15 +10,18 @@ import { useRouter } from 'next/router';
 import { getScenarioEditSlice } from 'store/slices/scenarios/edit';
 
 import { motion } from 'framer-motion';
+import { sortBy } from 'lodash';
 import { usePlausible } from 'next-plausible';
 
+import { useProjectCostSurfaces } from 'hooks/cost-surface';
 import { useMe } from 'hooks/me';
 import { useCanEditScenario } from 'hooks/permissions';
 import { useDownloadShapefileTemplate } from 'hooks/projects';
-import { useUploadCostSurface } from 'hooks/scenarios';
+import { useSaveScenario, useScenario, useUploadCostSurface } from 'hooks/scenarios';
 import { useToasts } from 'hooks/toast';
 
 import Button from 'components/button';
+import Select from 'components/forms/select';
 import { composeValidators } from 'components/forms/validations';
 import Icon from 'components/icon';
 import InfoButton from 'components/info-button';
@@ -25,6 +29,7 @@ import Loading from 'components/loading';
 import Uploader from 'components/uploader';
 import { COST_SURFACE_UPLOADER_MAX_SIZE } from 'constants/file-uploader-size-limits';
 import Section from 'layout/section';
+import { Scenario } from 'types/api/scenario';
 import { cn } from 'utils/cn';
 import { bytesToMegabytes } from 'utils/units';
 
@@ -33,10 +38,15 @@ import COST_SEA_IMG from 'images/info-buttons/img_cost_surface_terrestrial.png';
 
 import CLOSE_SVG from 'svgs/ui/close.svg?sprite';
 
+export type FormFields = {
+  costSurfaceId: Scenario['costSurfaceId'];
+};
+
 export const GridSetupCostSurface = (): JSX.Element => {
   const [opened, setOpened] = useState(false);
   const [loading, setLoading] = useState(false);
   const [successFile, setSuccessFile] = useState<{ name: string }>(null);
+  const formRef = useRef<FormProps<FormFields>['form']>(null);
 
   const { addToast } = useToasts();
   const plausible = usePlausible();
@@ -45,10 +55,28 @@ export const GridSetupCostSurface = (): JSX.Element => {
 
   const dispatch = useDispatch();
   const scenarioSlice = getScenarioEditSlice(sid);
-  const { setJob } = scenarioSlice.actions;
+  const { setJob, setSelectedCostSurface, setLayerSettings } = scenarioSlice.actions;
 
   const { data: user } = useMe();
   const editable = useCanEditScenario(pid, sid);
+  const costSurfaceQuery = useProjectCostSurfaces(
+    pid,
+    {},
+    {
+      select: (data) =>
+        sortBy(
+          data.filter(({ isDefault }) => !isDefault),
+          'name'
+        )?.map(({ id, name }) => ({ value: id, label: name })),
+    }
+  );
+  const scenarioQuery = useScenario(sid);
+  const scenarioQueryMutation = useSaveScenario({
+    requestConfig: {
+      method: 'PATCH',
+    },
+  });
+
   const downloadShapefileTemplateMutation = useDownloadShapefileTemplate();
   const uploadMutation = useUploadCostSurface({
     requestConfig: {
@@ -60,7 +88,6 @@ export const GridSetupCostSurface = (): JSX.Element => {
     downloadShapefileTemplateMutation.mutate(
       { pid },
       {
-        onSuccess: () => {},
         onError: () => {
           addToast(
             'download-error',
@@ -89,7 +116,7 @@ export const GridSetupCostSurface = (): JSX.Element => {
     uploadMutation.mutate(
       { id: `${sid}`, data },
       {
-        onSuccess: ({ data: { data: g, meta } }) => {
+        onSuccess: ({ data: { meta } }) => {
           dispatch(setJob(new Date(meta.isoDate).getTime()));
           setLoading(false);
           setSuccessFile({ name: f.name });
@@ -187,6 +214,58 @@ export const GridSetupCostSurface = (): JSX.Element => {
     // resetCustomArea();
   }, []);
 
+  const onChangeCostSurface = useCallback(
+    (value: string) => {
+      formRef.current.change('costSurfaceId', value);
+
+      dispatch(setSelectedCostSurface(value));
+      dispatch(
+        setLayerSettings({
+          id: value,
+          settings: {
+            visibility: true,
+          },
+        })
+      );
+    },
+    [dispatch, setSelectedCostSurface, setLayerSettings]
+  );
+
+  const handleCostSurfaceChange = useCallback(
+    (data: Parameters<ComponentProps<typeof FormRFF<FormFields>>['onSubmit']>[0]) => {
+      scenarioQueryMutation.mutate(
+        { id: sid, data },
+        {
+          onSuccess: () => {
+            addToast(
+              'scenario-cost-surface-success',
+              <>
+                <h2 className="font-medium">Cost surface updated</h2>
+                {/* <ul className="text-sm">Template not downloaded</ul> */}
+              </>,
+              {
+                level: 'success',
+              }
+            );
+          },
+          onError: () => {
+            addToast(
+              'scenario-cost-surface-error',
+              <>
+                <h2 className="font-medium">Something went wrong</h2>
+                <ul className="text-sm">Cost surface could not be updated.</ul>
+              </>,
+              {
+                level: 'error',
+              }
+            );
+          },
+        }
+      );
+    },
+    [scenarioQueryMutation, sid, addToast, dispatch, setSelectedCostSurface, setLayerSettings]
+  );
+
   return (
     <motion.div
       key="cost-surface"
@@ -226,8 +305,41 @@ export const GridSetupCostSurface = (): JSX.Element => {
           </div>
         </div>
 
+        <FormRFF<FormFields>
+          onSubmit={handleCostSurfaceChange}
+          initialValues={{
+            costSurfaceId: scenarioQuery.data?.costSurfaceId || null,
+          }}
+        >
+          {(fprops) => {
+            formRef.current = fprops.form;
+
+            return (
+              <form
+                id="form-cost-surface-scenario"
+                onSubmit={fprops.handleSubmit}
+                autoComplete="off"
+                className="space-y-3"
+              >
+                <Select
+                  maxHeight={300}
+                  size="base"
+                  theme="dark"
+                  selected={fprops.values.costSurfaceId}
+                  options={costSurfaceQuery.data}
+                  clearSelectionActive
+                  onChange={onChangeCostSurface}
+                />
+                <Button type="submit" theme="primary-alt" size="base" className="w-full">
+                  Apply cost surface
+                </Button>
+              </form>
+            );
+          }}
+        </FormRFF>
+
         <div className="relative mt-1 flex min-h-0 w-full flex-grow flex-col overflow-hidden text-sm">
-          <p className="pt-2">
+          <p className="mt-2 text-xs">
             By default all projects have an equal area cost surface which means that planning units
             with the same area have the same cost
           </p>
diff --git a/app/layout/scenarios/edit/map/component.tsx b/app/layout/scenarios/edit/map/component.tsx
index 973a015f42..ce0f44b540 100644
--- a/app/layout/scenarios/edit/map/component.tsx
+++ b/app/layout/scenarios/edit/map/component.tsx
@@ -11,6 +11,7 @@ import { sortBy } from 'lodash';
 import { FiLayers } from 'react-icons/fi';
 
 import { useAccessToken } from 'hooks/auth';
+import { useProjectCostSurface } from 'hooks/cost-surface';
 import { useSelectedFeatures, useTargetedFeatures } from 'hooks/features';
 import { useAllGapAnalysis } from 'hooks/gap-analysis';
 import {
@@ -22,6 +23,7 @@ import {
   // useLegend,
   useBBOX,
   useTargetedPreviewLayers,
+  useCostSurfaceLayer,
 } from 'hooks/map';
 import { useProject } from 'hooks/projects';
 import { useCostSurfaceRange, useScenario, useScenarioPU } from 'hooks/scenarios';
@@ -50,6 +52,9 @@ import { centerMap } from 'utils/map';
 
 import { useScenarioLegend } from './legend/hooks';
 
+const minZoom = 2;
+const maxZoom = 20;
+
 export const ScenariosEditMap = (): JSX.Element => {
   const [open, setOpen] = useState(true);
   const [mapInteractive, setMapInteractive] = useState(false);
@@ -200,8 +205,8 @@ export const ScenariosEditMap = (): JSX.Element => {
   });
   const bestSolution = bestSolutionData;
 
-  const minZoom = 2;
-  const maxZoom = 20;
+  const costSurfaceQuery = useProjectCostSurface(pid, selectedCostSurface);
+
   const [viewport, setViewport] = useState({});
   const [bounds, setBounds] = useState<MapProps['bounds']>(null);
 
@@ -307,10 +312,22 @@ export const ScenariosEditMap = (): JSX.Element => {
     },
   });
 
+  const costSurfaceLayer = useCostSurfaceLayer({
+    active: Boolean(selectedCostSurface) && costSurfaceQuery.isSuccess,
+    pid,
+    costSurfaceId: selectedCostSurface,
+    layerSettings: {
+      ...layerSettings[selectedCostSurface],
+      min: costSurfaceQuery.data?.min,
+      max: costSurfaceQuery.data?.max,
+    } as Parameters<typeof useCostSurfaceLayer>[0]['layerSettings'],
+  });
+
   const LAYERS = [
     // PUGridPreviewLayer,
     // AdminPreviewLayer,
     PUGridLayer,
+    costSurfaceLayer,
     WDPApreviewLayer,
     ...FeaturePreviewLayers,
     ...TargetedPreviewLayers,
diff --git a/app/types/api/scenario.ts b/app/types/api/scenario.ts
index 0d7ea3bb4e..24c72b437d 100644
--- a/app/types/api/scenario.ts
+++ b/app/types/api/scenario.ts
@@ -9,6 +9,7 @@ export interface Scenario {
   lastUpdate: string;
   lock?: Record<string, any>;
   lastUpdateDistance: string;
+  costSurfaceId: string;
   name: string;
   numberOfRuns: number;
   metadata: {