Procházet zdrojové kódy

Merge branch 'cambios-graficos' into develop

wilitp před 4 roky
rodič
revize
7d8f03cab3

+ 51 - 0
app/package-lock.json

@@ -13,6 +13,7 @@
         "@testing-library/react": "^11.1.0",
         "@testing-library/user-event": "^12.1.10",
         "@types/bootstrap": "^5.1.6",
+        "@types/chroma-js": "^2.1.3",
         "@types/jest": "^26.0.15",
         "@types/node": "^12.0.0",
         "@types/react": "^17.0.0",
@@ -20,6 +21,7 @@
         "@types/react-router-dom": "^5.3.1",
         "bootstrap": "^5.1.1",
         "bootstrap-icons": "^1.5.0",
+        "chroma-js": "^2.1.2",
         "react": "^17.0.2",
         "react-dom": "^17.0.2",
         "react-router-dom": "^5.3.0",
@@ -2513,6 +2515,11 @@
         "@types/jquery": "*"
       }
     },
+    "node_modules/@types/chroma-js": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz",
+      "integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g=="
+    },
     "node_modules/@types/eslint": {
       "version": "7.2.6",
       "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.6.tgz",
@@ -4658,6 +4665,14 @@
         "node": ">=10"
       }
     },
+    "node_modules/chroma-js": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.1.2.tgz",
+      "integrity": "sha512-ri/ouYDWuxfus3UcaMxC1Tfp3IE9K5iQzxc2hSxbBRVNQFut1UuGAsZmiAf2mOUubzGJwgMSv9lHg+XqLaz1QQ==",
+      "dependencies": {
+        "cross-env": "^6.0.3"
+      }
+    },
     "node_modules/chrome-trace-event": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz",
@@ -5297,6 +5312,21 @@
         "sha.js": "^2.4.8"
       }
     },
+    "node_modules/cross-env": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-6.0.3.tgz",
+      "integrity": "sha512-+KqxF6LCvfhWvADcDPqo64yVIB31gv/jQulX2NGzKS/g3GEVz6/pt4wjHFtFWsHMddebWD/sDthJemzM4MaAag==",
+      "dependencies": {
+        "cross-spawn": "^7.0.0"
+      },
+      "bin": {
+        "cross-env": "src/bin/cross-env.js",
+        "cross-env-shell": "src/bin/cross-env-shell.js"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
     "node_modules/cross-spawn": {
       "version": "7.0.3",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -21903,6 +21933,11 @@
         "@types/jquery": "*"
       }
     },
+    "@types/chroma-js": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz",
+      "integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g=="
+    },
     "@types/eslint": {
       "version": "7.2.6",
       "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.6.tgz",
@@ -23645,6 +23680,14 @@
       "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
       "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="
     },
+    "chroma-js": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.1.2.tgz",
+      "integrity": "sha512-ri/ouYDWuxfus3UcaMxC1Tfp3IE9K5iQzxc2hSxbBRVNQFut1UuGAsZmiAf2mOUubzGJwgMSv9lHg+XqLaz1QQ==",
+      "requires": {
+        "cross-env": "^6.0.3"
+      }
+    },
     "chrome-trace-event": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz",
@@ -24186,6 +24229,14 @@
         "sha.js": "^2.4.8"
       }
     },
+    "cross-env": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-6.0.3.tgz",
+      "integrity": "sha512-+KqxF6LCvfhWvADcDPqo64yVIB31gv/jQulX2NGzKS/g3GEVz6/pt4wjHFtFWsHMddebWD/sDthJemzM4MaAag==",
+      "requires": {
+        "cross-spawn": "^7.0.0"
+      }
+    },
     "cross-spawn": {
       "version": "7.0.3",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",

+ 2 - 0
app/package.json

@@ -8,6 +8,7 @@
     "@testing-library/react": "^11.1.0",
     "@testing-library/user-event": "^12.1.10",
     "@types/bootstrap": "^5.1.6",
+    "@types/chroma-js": "^2.1.3",
     "@types/jest": "^26.0.15",
     "@types/node": "^12.0.0",
     "@types/react": "^17.0.0",
@@ -15,6 +16,7 @@
     "@types/react-router-dom": "^5.3.1",
     "bootstrap": "^5.1.1",
     "bootstrap-icons": "^1.5.0",
+    "chroma-js": "^2.1.2",
     "react": "^17.0.2",
     "react-dom": "^17.0.2",
     "react-router-dom": "^5.3.0",

+ 45 - 26
app/src/components/UI/dashboard/checkboxTables/index.tsx

@@ -1,35 +1,54 @@
 import React, { FC } from "react";
 
 interface checkProps {
-    changeSeason:any;
-    changeDegree:any;
-    changePrep:any;
-
+  changeSeason: any;
+  changeDegree: any;
+  changePrep: any;
 }
 
-
-const CheckboxTables: FC<checkProps> = ({changeSeason,changeDegree,changePrep,...props }) => {
-    return (    
-        <section className="row p-lg-4 p-md-3 p-2">
-            <div className="btn-group col-8" role="group" aria-label="Basic checkbox toggle button group">
-                {/* 
-                {list.map((tittle:any) =>
-                    <input type="checkbox" className="btn-check" id="btncheck1" autoComplete="off"/>
-                    <label className="btn btn-outline-primary" htmlFor="btncheck1">{tittle}</label>
-                ): null} */}
-                <input type="checkbox" className="btn-check" onClick={changeSeason}  id="btncheckSeason" autoComplete="off"/>
-                <label className="btn btn-outline-primary" htmlFor="btncheckSeason">GeneralPerSeason</label>
-
-                <input type="checkbox" className="btn-check" onClick={changeDegree} id="btncheckDegree" autoComplete="off"/>
-                <label className="btn btn-outline-primary" htmlFor="btncheckDegree">DegreeDay</label>
-
-                <input type="checkbox" className="btn-check" onClick={changePrep} id="btncheckPre" autoComplete="off"/>
-                <label className="btn btn-outline-primary" htmlFor="btncheckPre">Precipitation</label>
-
-            </div>
-        </section>     
+const CheckboxTables: FC<checkProps> = ({
+  changeSeason,
+  changeDegree,
+  changePrep,
+}) => {
+  return (
+    <section className="row p-lg-4 p-md-3 p-2">
+      <div className="col-8" role="group">
+        <input
+          type="checkbox"
+          className="form-check-input me-2"
+          onClick={changeSeason}
+          id="btncheckSeason"
+          autoComplete="off"
+        />
+        <label className="form-check-label me-3" htmlFor="btncheckSeason">
+          General
+        </label>
+
+        <input
+          type="checkbox"
+          className="form-check-input me-2"
+          onClick={changePrep}
+          id="btncheckPre"
+          autoComplete="off"
+        />
+        <label className="form-check-label me-3" htmlFor="btncheckPre">
+          Precipitaciones
+        </label>
+
+        <input
+          type="checkbox"
+          className="form-check-input me-2"
+          onClick={changeDegree}
+          id="btncheckDegree"
+          autoComplete="off"
+        />
+        <label className="form-check-label me-3" htmlFor="btncheckDegree">
+          Grados día
+        </label>
+      </div>
+    </section>
   );
 };
 
 export default CheckboxTables;
-

+ 13 - 6
app/src/components/UI/dashboard/cockpit/index.tsx

@@ -16,7 +16,7 @@ import * as actions from "../../../../context/dashboard/actions";
 import { sectors } from "../../../../api";
 import { UserStateContext } from "../../../../context/auth/AuthProvider";
 import { Station } from "../../../../types";
-import { mustCheckDateOrder } from "../../../../utils";
+import { campString, mustCheckDateOrder } from "../../../../utils";
 import Modal from "../../../portals/modal";
 
 const Cockpit: FC = () => {
@@ -98,15 +98,16 @@ const Cockpit: FC = () => {
           <p style={{ color: "red" }}>{error.toString()}</p>
         </Modal>
       )}
-      <section className="row p-lg-4 p-md-3 p-2">
+      <section className="row mb-3">
         {/* Finca */}
         <div className="col-12 col-lg-4 mb-2 col-xl-3 mb-xl-0">
           <Select
+            labelTitle="Finca"
             list={sectorChoices}
             onChange={(e: ChangeEvent<HTMLInputElement>) =>
               dashboardDispatch(actions.setSector(e.target.value))
             }
-            name="Comparación"
+            name="finca"
             placeholder="Fincas"
           />
         </div>
@@ -117,7 +118,8 @@ const Cockpit: FC = () => {
             list={campaignList}
             onChange={handleCampChange}
             value={selectedCampaignYear}
-            name="Comparación"
+            name="campania"
+            labelTitle="Campaña"
             placeholder="Camapaña"
           />
         </div>
@@ -127,9 +129,10 @@ const Cockpit: FC = () => {
           <CalendarInput
             onChange={handleFromChange}
             value={dashboardState.from}
-            name="Comparación"
             min={dashboardState.minDate}
             max={dashboardState.maxDate}
+            name="desde"
+            labelTitle="Fecha desde"
           />
         </div>
 
@@ -138,12 +141,16 @@ const Cockpit: FC = () => {
           <CalendarInput
             onChange={handleToChange}
             value={dashboardState.to}
-            name="Comparación"
             min={dashboardState.minDate}
             max={dashboardState.maxDate}
+            name="hasta"
+            labelTitle="Fecha hasta"
           />
         </div>
       </section>
+      <h5>
+        Temporada {campString(dashboardState.from, dashboardState.to, true)}
+      </h5>
     </>
   );
 };

+ 52 - 0
app/src/components/UI/dashboard/detail/index.tsx

@@ -0,0 +1,52 @@
+import React, { FC, useState, useContext } from "react";
+import { StateContext as DashboardContext } from "../../../../context/dashboard/Provider";
+import CheckboxTables from "../checkboxTables";
+import GeneralPerSeason from "../../../data/GeneralPerSeason";
+import Precipitation from "../../../data/Precipitation";
+import DegreeDay from "../../../data/DegreeDay";
+
+const Detail: FC = () => {
+  const [viewSeasonState, setViewSeasonState] = useState<Boolean>(false);
+  const [viewDegreeState, setViewDegreeState] = useState<Boolean>(false);
+  const [viewPrepState, setViewPrepState] = useState<Boolean>(false);
+  const toggleSeason = () => setViewSeasonState(!viewSeasonState);
+  const toggleDegree = () => setViewDegreeState(!viewDegreeState);
+  const togglePrep = () => setViewPrepState(!viewPrepState);
+  const dashboardState = useContext(DashboardContext);
+
+  return (
+    <>
+      <h4>Gráficos históricos de finca</h4>
+
+      <CheckboxTables
+        changeSeason={toggleSeason}
+        changeDegree={toggleDegree}
+        changePrep={togglePrep}
+      />
+
+      {viewSeasonState ? (
+        <section className="row my-4">
+          <div className="col-xl-8">
+            <GeneralPerSeason />
+          </div>
+        </section>
+      ) : null}
+      {viewPrepState ? (
+        <section className="row my-4">
+          <div className="col-xl-8">
+            <Precipitation />
+          </div>
+        </section>
+      ) : null}
+      {viewDegreeState ? (
+        <section className="row my-4">
+          <div className="col-xl-8">
+            <DegreeDay />
+          </div>
+        </section>
+      ) : null}
+    </>
+  );
+};
+
+export default Detail;

+ 12 - 7
app/src/components/UI/forms/calendarInput.tsx

@@ -11,13 +11,18 @@ interface calendarInputProps {
 
 const CalendarInput: FC<calendarInputProps> = ({ className, ...props }) => {
   return (
-    <input
-      {...(props as any)}
-      // defaultValue={defaultValue}
-      className={`form-control ${className}`}
-      type="date"
-      id="start"
-    />
+    <>
+      <label htmlFor={props.name} className="form-label">
+        {props.labelTitle}
+      </label>
+      <input
+        {...(props as any)}
+        // defaultValue={defaultValue}
+        className={`form-control ${className}`}
+        type="date"
+        id="start"
+      />
+    </>
   );
 };
 

+ 17 - 12
app/src/components/UI/forms/select.tsx

@@ -11,19 +11,24 @@ interface selectProps {
 // En las props hago un "rest", que me da el resto del objeto que estoy desestructurando
 const Select: FC<selectProps> = ({ list, className, ...props }) => {
   return (
-    <select {...(props as any)} className={`form-select ${className}`}>
-      {props.placeholder && !list.length ? (
-        <option key="placeholder" value="">
-          {props.placeholder}
-        </option>
-      ) : null}
+    <>
+      <label htmlFor={props.name} className="form-label">
+        {props.labelTitle}
+      </label>
+      <select {...(props as any)} className={`form-select ${className}`}>
+        {props.placeholder && !list.length ? (
+          <option key="placeholder" value="">
+            {props.placeholder}
+          </option>
+        ) : null}
 
-      {list?.map((item: any) => (
-        <option value={item.value} key={item.value}>
-          {item.title}
-        </option>
-      ))}
-    </select>
+        {list?.map((item: any) => (
+          <option value={item.value} key={item.value}>
+            {item.title}
+          </option>
+        ))}
+      </select>
+    </>
   );
 };
 

+ 20 - 24
app/src/components/data/DegreeDay/index.tsx

@@ -4,22 +4,21 @@ import { UserStateContext } from "../../../context/auth/AuthProvider";
 import { StateContext } from "../../../context/dashboard/Provider";
 import { DegreeDays } from "../../../types";
 import * as classes from "../tables.module.css";
+import { campString, greenScale } from "../../../utils";
 
-interface DegreesProps {
-  title: String;
-  periodString: String;
-}
+interface DegreesProps {}
 
-// TODO: Computar el string de temporada
-// Consigue un string para que el usuario identifique la temporada que esta viendo
-const campString = (ISOFrom: string, ISOTo: string) => {
-  const from = new Date(Date.parse(ISOFrom));
-  const to = new Date(Date.parse(ISOTo));
+const colorScale = greenScale([0, 60]);
+const ResultTd = ({ value }: any) => (
+  <td
+    className={classes.cell}
+    style={{ backgroundColor: colorScale(value).css() as any }}
+  >
+    {value ?? "-"}
+  </td>
+);
 
-  return `${ISOFrom.slice(0, 10)} / ${ISOTo.slice(0, 10)}`;
-};
-
-const DegreeDay: FC<DegreesProps> = ({ title, periodString }) => {
+const DegreeDay: FC<DegreesProps> = () => {
   const [data, setData] = useState<DegreeDays[] | null>(null);
   const { userToken } = useContext(UserStateContext);
   const { from, to, sector } = useContext(StateContext);
@@ -29,30 +28,27 @@ const DegreeDay: FC<DegreesProps> = ({ title, periodString }) => {
     monthlyDegrees(from, to, sector, userToken).then(setData);
   }, [from, to, userToken, sector]);
 
-  const suffix = "Grados días promedio mensuales";
-
   const rows = data?.map((x) => (
     <tr key={x.initial_date}>
       <th className={classes.cell}>
         {campString(x.initial_date, x.final_date)}
       </th>
-      <td className={classes.cell}>{x.months[10] ?? "-"}</td>
-      <td className={classes.cell}>{x.months[11] ?? "-"}</td>
-      <td className={classes.cell}>{x.months[12] ?? "-"}</td>
-      <td className={classes.cell}>{x.months[1] ?? "-"}</td>
-      <td className={classes.cell}>{x.months[2] ?? "-"}</td>
-      <td className={classes.cell}>{x.months[3] ?? "-"}</td>
+      <ResultTd value={x.months[10]} />
+      <ResultTd value={x.months[11]} />
+      <ResultTd value={x.months[12]} />
+      <ResultTd value={x.months[1]} />
+      <ResultTd value={x.months[2]} />
+      <ResultTd value={x.months[3]} />
     </tr>
   ));
 
   return (
     <section>
-      <h3>{`${title} - ${suffix}`}</h3>
-
+      <h5>Grados días promedio mensuales</h5>
       <table className={classes.table}>
         <thead>
           <tr>
-            <th className={classes.cell}>{periodString}</th>
+            <th className={classes.cell}>Temporada</th>
             <td className={classes.cell}>Octubre</td>
             <td className={classes.cell}>Noviembre</td>
             <td className={classes.cell}>Diciembre</td>

+ 44 - 7
app/src/components/data/GeneralPerSeason/index.tsx

@@ -5,6 +5,8 @@ import { TableHeader as Header, TdGroup } from "../shared";
 import * as classes from "../tables.module.css";
 import { StateContext as DashboardContext } from "../../../context/dashboard/Provider";
 import { UserStateContext } from "../../../context/auth/AuthProvider";
+import { campString, grayScale, greenScale, yellowScale } from "../../../utils";
+import { redScale, orangeScale, blueScale } from "../../../utils";
 
 const TemperaturePerSeason: FC = () => {
   const [data, setData] = useState<Summary[] | null>(null);
@@ -19,21 +21,56 @@ const TemperaturePerSeason: FC = () => {
 
   const rows = data?.map((x) => (
     <tr key={x.initial_date}>
-      <th className={classes.cell}>Temporada 2015-2016 - Vendimia 2016</th>
+      <th className={classes.cell}>
+        {campString(x.initial_date, x.final_date)}
+      </th>
       <TdGroup>
-        <td>{x.lt10 ?? "-"}%</td>
-        <td>{x.gt30 ?? "-"}%</td>
-        <td>{x.gt33 ?? "-"}%</td>
+        <td
+          style={{ backgroundColor: blueScale([0, 50])(x.lt10).css() as any }}
+        >
+          {x.lt10 ?? "-"}%
+        </td>
+        <td
+          style={{ backgroundColor: orangeScale([0, 50])(x.gt33).css() as any }}
+        >
+          {x.gt30 ?? "-"}%
+        </td>
+        <td style={{ backgroundColor: redScale([0, 50])(x.gt33).css() as any }}>
+          {x.gt33 ?? "-"}%
+        </td>
       </TdGroup>
-      <td className={classes.cell}>{x.grados_acumulados}</td>
+
+      <td
+        className={classes.cell}
+        style={{
+          backgroundColor: greenScale([0, 3000])(x.grados_acumulados).css(),
+        }}
+      >
+        {x.grados_acumulados ?? "-"}
+      </td>
       <td className={classes.cell}>{x.dias_igualar_temporada}</td>
-      <td className={classes.cell}>{x.amplitud_termica}</td>
-      <td className={classes.cell}>{x.precip_acumulada}</td>
+      <td
+        className={classes.cell}
+        style={{
+          backgroundColor: yellowScale([0, 50])(x.amplitud_termica).css(),
+        }}
+      >
+        {x.amplitud_termica ?? "-"}
+      </td>
+      <td
+        className={classes.cell}
+        style={{
+          backgroundColor: grayScale([0, 300])(x.precip_acumulada).css(),
+        }}
+      >
+        {x.precip_acumulada ?? "-"}
+      </td>
     </tr>
   ));
 
   return (
     <>
+      <h5>General</h5>
       <table className={classes.table}>
         <Header daysToMatchCurrentTemperature={true} />
         <tbody>{data && rows}</tbody>

+ 46 - 6
app/src/components/data/GeneralPerSector/index.tsx

@@ -5,6 +5,14 @@ import { StateContext } from "../../../context/dashboard/Provider";
 import { Summary } from "../../../types/summary";
 import { TableHeader as Header, TdGroup } from "../shared";
 import * as classes from "../tables.module.css";
+import {
+  redScale,
+  orangeScale,
+  blueScale,
+  greenScale,
+  yellowScale,
+  grayScale,
+} from "../../../utils";
 
 const GeneralPerSector: FC = () => {
   const [data, setData] = useState<Summary[] | null>(null);
@@ -26,13 +34,45 @@ const GeneralPerSector: FC = () => {
         {x.station.title}
       </th>
       <TdGroup>
-        <td>{x.lt10 ?? "-"}%</td>
-        <td>{x.gt30 ?? "-"}%</td>
-        <td>{x.gt33 ?? "-"}%</td>
+        <td
+          style={{ backgroundColor: blueScale([0, 50])(x.lt10).css() as any }}
+        >
+          {x.lt10 ?? "-"}%
+        </td>
+        <td
+          style={{ backgroundColor: orangeScale([0, 50])(x.gt33).css() as any }}
+        >
+          {x.gt30 ?? "-"}%
+        </td>
+        <td style={{ backgroundColor: redScale([0, 50])(x.gt33).css() as any }}>
+          {x.gt33 ?? "-"}%
+        </td>
       </TdGroup>
-      <td className={classes.cell}>{x.grados_acumulados ?? "-"}</td>
-      <td className={classes.cell}>{x.amplitud_termica ?? "-"}</td>
-      <td className={classes.cell}>{x.precip_acumulada ?? "-"}</td>
+
+      <td
+        className={classes.cell}
+        style={{
+          backgroundColor: greenScale([0, 3000])(x.grados_acumulados).css(),
+        }}
+      >
+        {x.grados_acumulados ?? "-"}
+      </td>
+      <td
+        className={classes.cell}
+        style={{
+          backgroundColor: yellowScale([0, 50])(x.amplitud_termica).css(),
+        }}
+      >
+        {x.amplitud_termica ?? "-"}
+      </td>
+      <td
+        className={classes.cell}
+        style={{
+          backgroundColor: grayScale([0, 300])(x.precip_acumulada).css(),
+        }}
+      >
+        {x.precip_acumulada ?? "-"}
+      </td>
     </tr>
   ));
 

+ 20 - 20
app/src/components/data/Precipitation/index.tsx

@@ -4,20 +4,22 @@ import { UserStateContext } from "../../../context/auth/AuthProvider";
 import { StateContext } from "../../../context/dashboard/Provider";
 import { Precipitations } from "../../../types";
 import * as classes from "../tables.module.css";
+import { blueScale, campString } from "../../../utils";
 
-interface PrecipitationProps {
-  title: String;
-  periodString: String;
-}
+interface PrecipitationProps {}
 
-const campString = (ISOFrom: string, ISOTo: string) => {
-  const from = new Date(Date.parse(ISOFrom));
-  const to = new Date(Date.parse(ISOTo));
+const colorScale = blueScale([0, 300]);
 
-  return `${ISOFrom.slice(0, 10)} / ${ISOTo.slice(0, 10)}`;
-};
+const ResultTd = ({ value }: any) => (
+  <td
+    className={classes.cell}
+    style={{ backgroundColor: colorScale(value).css() as any }}
+  >
+    {value ?? "-"}
+  </td>
+);
 
-const Precipitation: FC<PrecipitationProps> = ({ title, periodString }) => {
+const Precipitation: FC<PrecipitationProps> = () => {
   const [data, setData] = useState<Precipitations[] | null>(null);
   const { userToken } = useContext(UserStateContext);
   const { from, to, sector } = useContext(StateContext);
@@ -27,30 +29,28 @@ const Precipitation: FC<PrecipitationProps> = ({ title, periodString }) => {
     monthlyPrecip(from, to, sector, userToken).then(setData);
   }, [from, to, userToken, sector]);
 
-  const suffix = "Precipitaciones mensuales";
-
   const rows = data?.map((x) => (
     <tr key={x.initial_date}>
       <th className={classes.cell}>
         {campString(x.initial_date, x.final_date)}
       </th>
-      <td className={classes.cell}>{x.months[10] ?? "-"}</td>
-      <td className={classes.cell}>{x.months[11] ?? "-"}</td>
-      <td className={classes.cell}>{x.months[12] ?? "-"}</td>
-      <td className={classes.cell}>{x.months[1] ?? "-"}</td>
-      <td className={classes.cell}>{x.months[2] ?? "-"}</td>
-      <td className={classes.cell}>{x.months[3] ?? "-"}</td>
+      <ResultTd value={x.months[10]} />
+      <ResultTd value={x.months[11]} />
+      <ResultTd value={x.months[12]} />
+      <ResultTd value={x.months[1]} />
+      <ResultTd value={x.months[2]} />
+      <ResultTd value={x.months[3]} />
     </tr>
   ));
 
   return (
     <section>
-      <h3>{`${title} - ${suffix}`}</h3>
+      <h5>Precipitaciones mensuales</h5>
 
       <table className={classes.table}>
         <thead>
           <tr>
-            <th className={classes.cell}>{periodString}</th>
+            <th className={classes.cell}>Temporada</th>
             <td className={classes.cell}>Octubre</td>
             <td className={classes.cell}>Noviembre</td>
             <td className={classes.cell}>Diciembre</td>

+ 53 - 13
app/src/components/data/shared.tsx

@@ -1,37 +1,64 @@
 import React, { FC } from "react";
 import * as classes from "./tables.module.css";
+import {
+  redScale,
+  blueScale,
+  orangeScale,
+  greenScale,
+  yellowScale,
+  grayScale,
+} from "../../utils";
 
 interface tableHeaderProps {
   //Change this name
   daysToMatchCurrentTemperature: boolean;
 }
 
-export const TableHeader: FC<tableHeaderProps> = ({ daysToMatchCurrentTemperature }) => {
+export const TableHeader: FC<tableHeaderProps> = ({
+  daysToMatchCurrentTemperature,
+}) => {
   return (
     <thead>
-      <th className={classes.cell}>Octubre - Marzo</th>
+      <th className={classes.cell}>Temporada</th>
       <td style={{ padding: "0" }} className={classes.cell}>
         <table style={{ width: "100%" }}>
           <tr>
             <td colSpan={100}>% Horas segun temperatura</td>
           </tr>
           <tr>
-            <td>&lt;10%</td>
-            <td>&gt;30%</td>
-            <td>&gt;33%</td>
+            <td style={{ backgroundColor: blueScale([])(1).css() }}>&lt;10%</td>
+            <td style={{ backgroundColor: orangeScale([])(1).css() }}>
+              &gt;30%
+            </td>
+            <td style={{ backgroundColor: redScale([])(1).css() }}>&gt;33%</td>
           </tr>
         </table>
       </td>
 
-      <td className={classes.cell}>Grados días acumulados</td>
+      <td
+        className={classes.cell}
+        style={{ backgroundColor: greenScale([])(1).css() }}
+      >
+        Grados días acumulados
+      </td>
 
-      {daysToMatchCurrentTemperature &&
+      {daysToMatchCurrentTemperature && (
         <td className={classes.cell}>Días para igualar temperatura actual</td>
-      }
+      )}
 
-      <td className={classes.cell}>Amplitud térmica</td>
+      <td
+        className={classes.cell}
+        style={{ backgroundColor: yellowScale([])(1).css() }}
+      >
+        Amplitud térmica
+      </td>
 
-      <td className={classes.cell}>Precipitaciones acumuladas [mm]</td>
+      <td
+        className={classes.cell}
+        style={{ backgroundColor: grayScale([])(1).css() }}
+      >
+        Precipitaciones acumuladas [mm]
+      </td>
     </thead>
   );
 };
@@ -39,9 +66,22 @@ export const TableHeader: FC<tableHeaderProps> = ({ daysToMatchCurrentTemperatur
 export const TdGroup: FC = ({ children }) => {
   return (
     <td className={classes.tdGroup}>
-      <table>
-        <tr>{children}</tr>
+      <table style={{ position: "relative" }}>
+        <tr
+          style={{
+            display: "grid",
+            gridTemplateRows: "100%",
+            gridTemplateColumns: "repeat(3, 1fr)",
+            position: "absolute",
+            top: 0,
+            left: 0,
+            bottom: 0,
+            right: 0,
+          }}
+        >
+          {children}
+        </tr>
       </table>
     </td>
   );
-};
+};

+ 9 - 0
app/src/components/data/tables.module.css

@@ -3,6 +3,7 @@
    * No tengo idea de por que funciona
    * */
   height: 1px;
+  width: 100%;
 }
 
 .table * {
@@ -10,8 +11,16 @@
 }
 
 .tdGroup {
+  padding: 0;
   border: 1px solid rgba(0, 0, 0, 0.05);
 }
+
+.tdGroup td {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
 .tdGroup table {
   height: 100%;
   width: 100%;

+ 2 - 4
app/src/components/layout/index.tsx

@@ -1,6 +1,5 @@
 import React, { useState } from "react";
 import Sidebar from "./sidebar";
-import Header from "./header";
 import "bootstrap-icons/font/bootstrap-icons.css";
 
 const Layout = ({ children }: any) => {
@@ -13,14 +12,13 @@ const Layout = ({ children }: any) => {
 
   return (
     <>
-      <Header toggle={toggle} />
       <div className="container-fluid mx-auto px-0 row flex-nowrap">
         <section
-          className={`sidebar bg-light col-auto min-vh-100 col-md-3 col-xl-2 ${sbClass}`}
+          className={`sidebar bg-light col-auto vh-100 sticky-top col-1 ${sbClass}`}
         >
           <Sidebar />
         </section>
-        <main className="col content">{children}</main>
+        <main className="col p-3">{children}</main>
       </div>
     </>
   );

+ 8 - 13
app/src/components/layout/sidebar.tsx

@@ -13,23 +13,18 @@ const Sidebar = () => {
   const dispatch = useContext(UserDispatchContext);
 
   return (
-    <aside className="pt-5">
-      <ul className="nav nav-pills flex-column">
+    <aside className="py-5 d-flex flex-column h-100">
+      <ul className="nav nav-pills text-center flex-column mb-auto">
         <li>
           <NavLink to="/">Inicio</NavLink>
         </li>
-        <li>
-          <NavLink to="/comparativa">Comparativa</NavLink>
-        </li>
-        <li>
-          <NavLink to="/mapa-calor">Mapa de Calor</NavLink>
-        </li>
-        <li>
-          <button className="nav-link" onClick={() => dispatch(logout())}>
-            Logout
-          </button>
-        </li>
       </ul>
+      <button
+        className="nav-link btn btn-link"
+        onClick={() => dispatch(logout())}
+      >
+        Logout
+      </button>
     </aside>
   );
 };

+ 9 - 38
app/src/components/pages/Home.tsx

@@ -8,16 +8,10 @@ import DashboardProvider from "../../context/dashboard/Provider";
 import { Redirect } from "react-router-dom";
 import Layout from "../layout";
 import Cockpit from "../UI/dashboard/cockpit";
-import CheckboxTables from "../UI/dashboard/checkboxTables";
+import Detail from "../UI/dashboard/detail";
 
 const Home: FC = () => {
   const userState = useContext(UserStateContext);
-  const [viewSeasonState, setViewSeasonState] = useState<Boolean>(false);
-  const [viewDegreeState, setViewDegreeState] = useState<Boolean>(false);
-  const [viewPrepState, setViewPrepState] = useState<Boolean>(false);
-  const toggleSeason = () => setViewSeasonState(!viewSeasonState);
-  const toggleDegree = () => setViewDegreeState(!viewDegreeState);
-  const togglePrep = () => setViewPrepState(!viewPrepState);
 
   if (!userState.loggedIn) {
     return <Redirect to="/login" />;
@@ -27,40 +21,17 @@ const Home: FC = () => {
     <>
       <Layout>
         <DashboardProvider>
-          <Cockpit />
-          <section className="row">
-            <div className="col-xl-8">
-              <GeneralPerSector />
-            </div>
-          </section>
-
-          <CheckboxTables
-            changeSeason={toggleSeason}
-            changeDegree={toggleDegree}
-            changePrep={togglePrep}
-          />
-
-          {viewSeasonState ? (
-            <section className="row">
-              <div className="col-xl-8">
-                <GeneralPerSeason />
-              </div>
-            </section>
-          ) : null}
-          {viewDegreeState ? (
-            <section className="row">
+          <h4>Tabla comparativa entre fincas</h4>
+          <div className="p-2">
+            <Cockpit />
+            <section className="row mb-5">
               <div className="col-xl-8">
-                <DegreeDay title="hola" periodString="0/0/0" />
+                <GeneralPerSector />
               </div>
             </section>
-          ) : null}
-          {viewPrepState ? (
-            <section className="row">
-              <div className="col-xl-8">
-                <Precipitation title="holalol" periodString="0/0/0" />
-              </div>
-            </section>
-          ) : null}
+
+            <Detail />
+          </div>
         </DashboardProvider>
       </Layout>
     </>

+ 24 - 0
app/src/components/pages/Login.module.css

@@ -3,3 +3,27 @@
   max-width: 400px;
   padding: 0 5px;
 }
+
+.poweredBy {
+  display: flex;
+  gap: 10px;
+  position: absolute;
+  right: 40px;
+  bottom: 40px;
+}
+
+.passWrapper {
+  position: relative;
+}
+
+.vis {
+  opacity: 0.5;
+  position: absolute;
+  right: 10px;
+  top: 50%;
+  transform: translateY(-50%);
+}
+
+.vis.active {
+  opacity: 1;
+}

+ 67 - 29
app/src/components/pages/Login.tsx

@@ -13,9 +13,13 @@ import { Redirect } from "react-router-dom";
 import * as classes from "./Login.module.css";
 import { failureDismiss, login } from "../../context/auth/actions";
 import Modal from "../portals/modal";
+import bg from "../../images/background.jpg";
+import omx from "../../images/omx-logo.png";
+import vis from "../../images/visibility.png";
 
 const Login = () => {
   const [formState, setFormState] = useState<any>({});
+  const [passVisible, setPassVisible] = useState(false);
   const dispatch = useContext(UserDispatchContext);
   const userState = useContext(UserStateContext);
 
@@ -71,37 +75,71 @@ const Login = () => {
   }
 
   return (
-    <main className="d-flex justify-content-center align-items-center min-vh-100">
+    <main
+      style={{ background: `url(${bg}) no-repeat`, backgroundSize: "cover" }}
+      className="d-flex justify-content-center align-items-center min-vh-100"
+    >
       {modal}
-      <form
-        onSubmit={handleFormSubmit}
-        className={`text-center d-flex flex-column gap-2  ${classes.loginForm}`}
-      >
-        <h3>Ingresar</h3>
-        <input
-          onChange={handleFormChange}
-          id="username"
-          className="form-control"
-          type="text"
-          name="username"
-          placeholder="Usuario"
-        />
-        <input
-          onChange={handleFormChange}
-          id="password"
-          className="form-control"
-          type="password"
-          name="password"
-          placeholder="Contraseña"
-        />
-        <button
-          className={`btn btn-primary ${!isValid && "disabled"}`}
-          disabled={!isValid}
-          type="submit"
+      <div className="w-50">
+        <form
+          onSubmit={handleFormSubmit}
+          className={`text-center d-flex flex-column gap-2 mx-auto  ${classes.loginForm}`}
         >
-          Continuar
-        </button>
-      </form>
+          <h3 className="text-light">Inicio de sesión</h3>
+          <input
+            onChange={handleFormChange}
+            id="username"
+            className="form-control"
+            type="text"
+            name="username"
+            placeholder="Usuario"
+          />
+          <div className={classes.passWrapper}>
+            <input
+              onChange={handleFormChange}
+              id="password"
+              className="form-control"
+              type={passVisible ? "text" : "password"}
+              name="password"
+              placeholder="Contraseña"
+            />
+            <img
+              src={vis}
+              className={`${classes.vis} ${passVisible ? classes.active : ""}`}
+              onClick={() => setPassVisible(!passVisible)}
+              alt="Ver contraseña"
+            />
+          </div>
+          <button
+            className={`btn btn-primary ${!isValid && "disabled"}`}
+            disabled={!isValid}
+            type="submit"
+          >
+            Continuar
+          </button>
+          <hr className="text-light border" />
+          <a
+            href="https://new.omixom.com/accounts/login/?next=/"
+            target="_blank"
+            className="text-start text-light text-decoration-none"
+          >
+            Crear una cuenta
+          </a>
+          <a
+            href="https://new.omixom.com/accounts/login/?next=/"
+            target="_blank"
+            className="text-start text-light text-decoration-none"
+          >
+            Olvide mi contraseña
+          </a>
+        </form>
+      </div>
+      <div className={classes.poweredBy}>
+        <span className="text-light">Powered by</span>
+        <a href="https://www.omixom.com" target="_blank">
+          <img src={omx} alt="Logo Omixom" />
+        </a>
+      </div>
     </main>
   );
 };

binární
app/src/images/background.jpg


binární
app/src/images/omx-logo.png


binární
app/src/images/visibility.png


+ 41 - 0
app/src/utils.ts

@@ -1,3 +1,4 @@
+import { scale } from "chroma-js";
 export function checkDateOrder(first: string, second: string) {
   const firstTs = Date.parse(first);
   const secondTs = Date.parse(second);
@@ -10,3 +11,43 @@ export function mustCheckDateOrder(first: string, second: string) {
     );
   }
 }
+
+function campYear(ISOFrom: string): number {
+  // El año de la temporada 2021 - 2022 es 2022
+  // "El año empieza el 1 de Octubre"
+  // Offset desde el 1 de Octubre hasta el 1 de Enero
+  const offset = 7948800000;
+  const offsetDate = new Date(Date.parse(ISOFrom) + offset);
+
+  return offsetDate.getFullYear();
+}
+
+// Computa un string para que el usuario identifique la temporada que esta viendo
+export function campString(
+  ISOFrom: string,
+  ISOTo: string,
+  withVintage?: boolean
+) {
+  const yr = campYear(ISOFrom);
+
+  // Con vendimia
+  if (withVintage) {
+    return `${yr - 1} - ${yr} - Vendimia ${yr} `;
+  }
+  // Sin vendimia
+  return `${yr - 1} - ${yr}`;
+}
+
+// Escalas de colores
+export const orangeScale = (domain: number[]) =>
+  scale(["white", "orange"]).domain(domain);
+export const redScale = (domain: number[]) =>
+  scale(["white", "rgb(192, 0, 0)"]).domain(domain);
+export const blueScale = (domain: number[]) =>
+  scale(["white", "rgb(58, 145, 207)"]).domain(domain);
+export const greenScale = (domain: number[]) =>
+  scale(["white", "rgb(169, 208, 142)"]).domain(domain);
+export const yellowScale = (domain: number[]) =>
+  scale(["white", "rgb(255, 255, 0)"]).domain(domain);
+export const grayScale = (domain: number[]) =>
+  scale(["white", "rgb(169, 169, 169)"]).domain(domain);

+ 19 - 0
app/yarn.lock

@@ -1662,6 +1662,11 @@
     "@popperjs/core" "^2.9.2"
     "@types/jquery" "*"
 
+"@types/chroma-js@^2.1.3":
+  "integrity" "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g=="
+  "resolved" "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz"
+  "version" "2.1.3"
+
 "@types/eslint@^7.2.6":
   "integrity" "sha512-I+1sYH+NPQ3/tVqCeUSBwTE/0heyvtXqpIopUUArlBm0Kpocb8FbMa3AZ/ASKIFpN3rnEx932TTXDbt9OXsNDw=="
   "resolved" "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.6.tgz"
@@ -3274,6 +3279,13 @@
   "resolved" "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz"
   "version" "2.0.0"
 
+"chroma-js@^2.1.2":
+  "integrity" "sha512-ri/ouYDWuxfus3UcaMxC1Tfp3IE9K5iQzxc2hSxbBRVNQFut1UuGAsZmiAf2mOUubzGJwgMSv9lHg+XqLaz1QQ=="
+  "resolved" "https://registry.npmjs.org/chroma-js/-/chroma-js-2.1.2.tgz"
+  "version" "2.1.2"
+  dependencies:
+    "cross-env" "^6.0.3"
+
 "chrome-trace-event@^1.0.2":
   "integrity" "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ=="
   "resolved" "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz"
@@ -3659,6 +3671,13 @@
     "safe-buffer" "^5.0.1"
     "sha.js" "^2.4.8"
 
+"cross-env@^6.0.3":
+  "integrity" "sha512-+KqxF6LCvfhWvADcDPqo64yVIB31gv/jQulX2NGzKS/g3GEVz6/pt4wjHFtFWsHMddebWD/sDthJemzM4MaAag=="
+  "resolved" "https://registry.npmjs.org/cross-env/-/cross-env-6.0.3.tgz"
+  "version" "6.0.3"
+  dependencies:
+    "cross-spawn" "^7.0.0"
+
 "cross-spawn@^6.0.0":
   "integrity" "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ=="
   "resolved" "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz"