Parcourir la source

Merge branch 'controles-dashboard' into develop

wilitp il y a 4 ans
Parent
commit
5d0520c973

+ 15 - 12
app/src/App.tsx

@@ -3,21 +3,24 @@ import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
 import Home from "./components/pages/Home";
 import Login from "./components/pages/Login";
 import AuthProvider from "./context/auth/AuthProvider";
+import ErrorBoundary from "./components/hoc/ErrorBoundary";
 
 function App() {
   return (
-    <Router>
-      <AuthProvider>
-        <Switch>
-          <Route exact path="/">
-            <Home />
-          </Route>
-          <Route exact path="/login">
-            <Login />
-          </Route>
-        </Switch>
-      </AuthProvider>
-    </Router>
+    <ErrorBoundary>
+      <Router>
+        <AuthProvider>
+          <Switch>
+            <Route exact path="/">
+              <Home />
+            </Route>
+            <Route exact path="/login">
+              <Login />
+            </Route>
+          </Switch>
+        </AuthProvider>
+      </Router>
+    </ErrorBoundary>
   );
 }
 

+ 24 - 24
app/src/api/tables.ts

@@ -1,31 +1,12 @@
 import { apiURL } from "../config";
-import { Summary } from "../types/summary";
+import { Summary, Station } from "../types";
 import { mockPayloads, mockRequest } from "./mocks";
 // Quizas llamarlos table no hacia falta,
 // pero es preferible antes de que a alguien se le olvide usar un alias cuando importe estas funciones.
 
-export const generalTable = (from: string, to: string, token: string) => {
-  const headers = new Headers();
-  headers.append("Access-Control-Allow-Origin", "*");
-  headers.append("Authorization", `Bearer ${token}`);
-
-  const config: RequestInit = {
-    method: "GET",
-    mode: "cors",
-    headers,
-  };
-
-  const qParams = new URLSearchParams();
-  qParams.append("start_datetime", from);
-  qParams.append("end_datetime", to);
-
-  console.log({ from, to });
-
-  return new Promise<Summary[]>(async (resolve, reject) => {
-    const res = await fetch(
-      `${apiURL}summary/all_stations?${qParams.toString()}`,
-      config
-    );
+const mustFetchJson = <T>(url: string, config: RequestInit) =>
+  new Promise<T>(async (resolve, reject) => {
+    const res = await fetch(url, config);
     if (!res.ok) {
       const reason = await res.text();
       reject(reason);
@@ -38,6 +19,25 @@ export const generalTable = (from: string, to: string, token: string) => {
       reject(err);
     }
   });
+
+export const generalTable = (from: string, to: string, token: string) => {
+  const headers = new Headers();
+  headers.append("Access-Control-Allow-Origin", "*");
+  headers.append("Authorization", `Bearer ${token}`);
+
+  const config: RequestInit = {
+    method: "GET",
+    mode: "cors",
+    headers,
+  };
+
+  const qParams = new URLSearchParams();
+  qParams.append("start_datetime", `${from}T00:00`);
+  qParams.append("end_datetime", `${to}T00:00`);
+  return mustFetchJson<Summary[]>(
+    `${apiURL}station/all/summary?${qParams.toString()}`,
+    config
+  );
 };
 
 export const seasonsSummariesTable = (
@@ -58,5 +58,5 @@ export const sectors = (token: string) => {
     mode: "cors",
     headers,
   };
-  return fetch(`${apiURL}fincas`, config);
+  return mustFetchJson<Station[]>(`${apiURL}fincas`, config);
 };

+ 154 - 77
app/src/components/UI/dashboard/cockpit/index.tsx

@@ -1,4 +1,11 @@
-import React, { useContext, useState, useEffect, ChangeEvent, FC } from "react";
+import React, {
+  useContext,
+  useState,
+  useEffect,
+  ChangeEvent,
+  FC,
+  useMemo,
+} from "react";
 import Select from "../../forms/select";
 import CalendarInput from "../../forms/calendarInput";
 import {
@@ -8,91 +15,161 @@ import {
 import * as actions from "../../../../context/dashboard/actions";
 import { sectors } from "../../../../api";
 import { UserStateContext } from "../../../../context/auth/AuthProvider";
-
-const fincaList: string[] | number[] = ["a", "b", "c", "d"];
-const campaignList: any[] = ["2018", "2019", "2020", "2021"].map((c) => ({
-  value: c,
-  title: c,
-}));
+import { Station } from "../../../../types";
+import { mustCheckDateOrder } from "../../../../utils";
+import Modal from "../../../portals/modal";
 
 const Cockpit: FC = () => {
+  const [error, setError] = useState<string | null>(null);
+
   const dashboardState = useContext(StateContext);
   const dashboardDispatch = useContext(DispatchContext);
   const userState = useContext(UserStateContext);
-  const [sectorList, setSectorList] = useState<any[]>([]);
+  const [sectorList, setSectorList] = useState<Station[]>([]);
+
+  // Opciones para selector de campania
+  const selectedCampaignYear = dashboardState.maxDate.slice(0, 4);
+  const lastCampaignYear = useMemo(
+    () => dashboardState.maxDate.slice(0, 4),
+    []
+  );
+  const campaignListLength = 4;
+  const campaignList = useMemo(
+    () => campList(lastCampaignYear, campaignListLength),
+    []
+  );
 
   // Inicializacion del selector de fincas
-  // useEffect(() => {
-  //   const token = userState.userToken;
-  //   if (!token) return;
-  //   sectors(token)
-  //     .then((res: Response) => res.json())
-  //     .then((body: any[]) => {
-  //       debugger;
-  //       const options = body.map((s: any) => ({
-  //         title: s.title,
-  //         value: s.station_code,
-  //       }));
-
-  //       if (options.length) {
-  //         dashboardDispatch(actions.setSector(options[0].value));
-  //       }
-  //       setSectorList(options);
-  //     });
-  // }, []);
+  useEffect(() => {
+    const token = userState.userToken;
+    if (!token) return;
+    sectors(token).then((list) => {
+      setSectorList(list);
+      if (list.length > 0) {
+        dashboardDispatch(actions.setSector(list[0].station_code));
+      }
+    });
+  }, []);
+
+  const sectorChoices = useMemo(
+    () =>
+      sectorList.map(({ title, station_code }) => ({
+        title,
+        value: station_code,
+      })),
+    [sectorList]
+  );
+
+  const handleCampChange = (e: ChangeEvent<HTMLInputElement>) => {
+    const camp = campMaxMin(e.target.value);
+    dashboardDispatch(actions.setMinMaxDate(camp.min, camp.max));
+  };
+
+  const handleFromChange = (e: ChangeEvent<HTMLInputElement>) => {
+    try {
+      mustCheckDateOrder(e.target.value, dashboardState.to);
+    } catch (e) {
+      setError(e as any);
+      return;
+    }
+    return dashboardDispatch(actions.setFromControl(e.target.value));
+  };
+
+  const handleToChange = (e: ChangeEvent<HTMLInputElement>) => {
+    try {
+      mustCheckDateOrder(e.target.value, dashboardState.to);
+    } catch (e) {
+      setError(e as any);
+      return;
+    }
+    mustCheckDateOrder(e.target.value, dashboardState.to);
+    return dashboardDispatch(actions.setToControl(e.target.value));
+  };
 
   return (
-    <section className="row p-lg-4 p-md-3 p-2">
-      <div className="col-12 col-lg-4 mb-2 col-xl-3 mb-xl-0">
-        <Select
-          list={sectorList}
-          onChange={(e: ChangeEvent<HTMLInputElement>) =>
-            dashboardDispatch(actions.setSector(e.target.value))
-          }
-          name="Comparación"
-          placeholder="Fincas"
-        />
-      </div>
-      <div className="col-6 col-lg-4 mb-2 col-xl-auto mb-xl-0">
-        <CalendarInput
-          onChange={(e: ChangeEvent<HTMLInputElement>) =>
-            dashboardDispatch(
-              actions.setFromControl(e.target.valueAsDate?.toISOString() ?? "")
-            )
-          }
-          value={dashboardState.from.slice(0, 10)}
-          name="Comparación"
-        />
-      </div>
-      <div className="col-6 col-lg-4 mb-2 col-xl-auto mb-xl-0">
-        <CalendarInput
-          onChange={(e: ChangeEvent<HTMLInputElement>) =>
-            dashboardDispatch(
-              actions.setToControl(e.target.valueAsDate?.toISOString() ?? "")
-            )
-          }
-          value={dashboardState.to.slice(0, 10)}
-          name="Comparación"
-        />
-      </div>
-      <div className="col-6 col-lg-4 mb-2 col-xl-auto mb-xl-0">
-        <Select
-          list={campaignList}
-          onChange={(e: ChangeEvent<HTMLInputElement>) =>
-            dashboardDispatch(actions.setYearControl(e.target.value))
-          }
-          value={dashboardState.year}
-          name="Comparación"
-          placeholder="Año historicos"
-        />
-      </div>
-
-      {/* <div className="col-auto"> */}
-      {/*   <button type="button" className="btn btn-primary"> */}
-      {/*     Aplicar */}
-      {/*   </button> */}
-      {/* </div> */}
-    </section>
+    <>
+      {error && (
+        <Modal
+          key="Error"
+          title={"Error"}
+          staticBackdrop
+          accept={() => setError(null)}
+        >
+          <p style={{ color: "red" }}>{error.toString()}</p>
+        </Modal>
+      )}
+      <section className="row p-lg-4 p-md-3 p-2">
+        {/* Finca */}
+        <div className="col-12 col-lg-4 mb-2 col-xl-3 mb-xl-0">
+          <Select
+            list={sectorChoices}
+            onChange={(e: ChangeEvent<HTMLInputElement>) =>
+              dashboardDispatch(actions.setSector(e.target.value))
+            }
+            name="Comparación"
+            placeholder="Fincas"
+          />
+        </div>
+
+        {/* Campania */}
+        <div className="col-6 col-lg-4 mb-2 col-xl-auto mb-xl-0">
+          <Select
+            list={campaignList}
+            onChange={handleCampChange}
+            value={selectedCampaignYear}
+            name="Comparación"
+            placeholder="Camapaña"
+          />
+        </div>
+
+        {/* Desde */}
+        <div className="col-6 col-lg-4 mb-2 col-xl-auto mb-xl-0">
+          <CalendarInput
+            onChange={handleFromChange}
+            value={dashboardState.from}
+            name="Comparación"
+            min={dashboardState.minDate}
+            max={dashboardState.maxDate}
+          />
+        </div>
+
+        {/* Hasta */}
+        <div className="col-6 col-lg-4 mb-2 col-xl-auto mb-xl-0">
+          <CalendarInput
+            onChange={handleToChange}
+            value={dashboardState.to}
+            name="Comparación"
+            min={dashboardState.minDate}
+            max={dashboardState.maxDate}
+          />
+        </div>
+      </section>
+    </>
   );
 };
+
+const campMaxMin = (year: string) => {
+  // Las fechas extremas de la temporada de este anio
+  let maxDate = new Date(parseInt(year), 2, 31);
+  let minDate = new Date(parseInt(year) - 1, 9, 1);
+
+  // Conseguimos el string yyyy-mm-dd
+  const max = maxDate.toISOString().slice(0, 10);
+  const min = minDate.toISOString().slice(0, 10);
+
+  return { min, max };
+};
+
+// Computa las campanias para seleccionar
+// a partir de la ultima campania y la cantidad de anios para atras
+const campList = (lastCampaignYear: string, length: number) =>
+  Array.from({
+    length,
+  }).map((_, i) => {
+    const year = parseInt(lastCampaignYear) - length + 1 + i;
+    return {
+      value: year,
+      title: year,
+    };
+  });
 export default Cockpit;

+ 42 - 0
app/src/components/hoc/ErrorBoundary.tsx

@@ -0,0 +1,42 @@
+import React, { ClassicComponent, ErrorInfo } from "react";
+import Modal from "../portals/modal";
+
+export class ErrorBoundary extends React.Component<any, any> {
+  constructor(props: any) {
+    super(props);
+  }
+
+  state = {
+    currentError: null,
+    errorInfo: null,
+  };
+
+  componentDidCatch(error: Error, info: ErrorInfo) {
+    this.setState({
+      currentError: error,
+      errorInfo: info,
+    });
+  }
+
+  render() {
+    return (
+      <>
+        {this.props.children}
+        {this.state.currentError && (
+          <Modal
+            key="Error"
+            title={"Error"}
+            staticBackdrop
+            accept={() =>
+              this.setState({ currentError: null, errorInfo: null })
+            }
+          >
+            <p style={{ color: "red" }}>{this.state.errorInfo}</p>
+          </Modal>
+        )}
+      </>
+    );
+  }
+}
+
+export default ErrorBoundary;

+ 2 - 0
app/src/context/dashboard/Provider.tsx

@@ -37,6 +37,8 @@ export const StateContext = createContext<State>({
   year: "",
   from: "",
   to: "",
+  maxDate: "",
+  minDate: "",
 });
 export const DispatchContext = createContext<
   (action: Action | ((...args: any) => Promise<any>)) => void

+ 6 - 0
app/src/context/dashboard/actionTypes.ts

@@ -2,6 +2,9 @@ export type ActionType =
   | "SET_FROM_CONTROL"
   | "SET_TO_CONTROL"
   | "SET_YEAR_CONTROL"
+  | "SET_MIN_DATE"
+  | "SET_MAX_DATE"
+  | "SET_MIN_MAX_DATE"
   | "SET_SECTOR";
 
 export type Action = {
@@ -10,4 +13,7 @@ export type Action = {
   to?: string | null;
   from?: string | null;
   year?: string | null;
+  date?: string;
+  minDate?: string;
+  maxDate?: string;
 };

+ 21 - 0
app/src/context/dashboard/actions.ts

@@ -1,4 +1,5 @@
 import { Action } from "./actionTypes";
+import { mustCheckDateOrder } from "../../utils";
 
 export const setYearControl = (year: string): Action => ({
   type: "SET_YEAR_CONTROL",
@@ -20,4 +21,24 @@ export const setSector = (sector: string): Action => ({
   sector,
 });
 
+export const setMaxDate = (date: string): Action => ({
+  type: "SET_MAX_DATE",
+  date,
+});
+
+export const setMinDate = (date: string): Action => ({
+  type: "SET_MIN_DATE",
+  date,
+});
+
+export const setMinMaxDate = (min: string, max: string): Action => {
+  mustCheckDateOrder(min, max);
+
+  return {
+    type: "SET_MIN_MAX_DATE",
+    minDate: min,
+    maxDate: max,
+  };
+};
+
 export default {};

+ 43 - 10
app/src/context/dashboard/reducer.ts

@@ -9,12 +9,28 @@ export const defaultState = {
 
 export const getInitialState = (): State => {
   const now = new Date();
-  const day = now.toISOString();
+
+  // Las fechas extremas de la temporada de este anio
+  let maxDate = new Date(now.getFullYear(), 2, 31);
+  let minDate = new Date(now.getFullYear() - 1, 9, 1);
+
+  // Si ya empezo la temporada del anio siguiente, movemos las fechas por defecto
+  if (now.getTime() > maxDate.getTime()) {
+    maxDate.setFullYear(maxDate.getFullYear() + 1);
+    minDate.setFullYear(minDate.getFullYear() + 1);
+  }
+
+  // Conseguimos el string yyyy-mm-dd
+  const maxDateString = maxDate.toISOString().slice(0, 10);
+  const minDateString = minDate.toISOString().slice(0, 10);
+
   return {
     sector: null,
     year: now.getFullYear().toString(),
-    from: day,
-    to: day,
+    from: minDateString,
+    to: maxDateString,
+    minDate: minDateString,
+    maxDate: maxDateString,
   };
 };
 
@@ -24,11 +40,10 @@ export type State = {
   to: string;
   from: string;
   year: string;
+  maxDate: string;
+  minDate: string;
 };
 
-const fixYear = (year: string, dateString: string) =>
-  `${year}-${dateString.slice(5)}`;
-
 const reducer: Reducer<State, Action> = (state, action) => {
   switch (action.type) {
     case "SET_SECTOR":
@@ -36,22 +51,40 @@ const reducer: Reducer<State, Action> = (state, action) => {
         ...state,
         sector: action.sector!,
       };
+    case "SET_MIN_DATE":
+      return {
+        ...state,
+        from: action.date ?? "",
+        minDate: action.date ?? "",
+      };
+    case "SET_MAX_DATE":
+      return {
+        ...state,
+        to: action.date ?? "",
+        maxDate: action.date ?? "",
+      };
+    case "SET_MIN_MAX_DATE":
+      return {
+        ...state,
+        to: action.maxDate ?? "",
+        maxDate: action.maxDate ?? "",
+        from: action.minDate ?? "",
+        minDate: action.minDate ?? "",
+      };
     case "SET_TO_CONTROL":
       return {
         ...state,
-        to: fixYear(state.year, action.to!),
+        to: action.to!,
       };
     case "SET_FROM_CONTROL":
       return {
         ...state,
-        from: fixYear(state.year, action.from!),
+        from: action.from!,
       };
     case "SET_YEAR_CONTROL":
       return {
         ...state,
         year: action.year!,
-        to: fixYear(action.year!, state.to),
-        from: fixYear(action.year!, state.from),
       };
     default:
       return state;

+ 2 - 0
app/src/types/index.ts

@@ -0,0 +1,2 @@
+export * from "./station";
+export * from "./summary";

+ 5 - 0
app/src/types/station.ts

@@ -0,0 +1,5 @@
+export interface Station {
+  title: string;
+  station_code: string;
+  last_synced: "string";
+}

+ 12 - 0
app/src/utils.ts

@@ -0,0 +1,12 @@
+export function checkDateOrder(first: string, second: string) {
+  const firstTs = Date.parse(first);
+  const secondTs = Date.parse(second);
+  return firstTs <= secondTs;
+}
+export function mustCheckDateOrder(first: string, second: string) {
+  if (!checkDateOrder(first, second)) {
+    throw new Error(
+      "La fecha inicial no puede ser posterior a la fecha final."
+    );
+  }
+}