From bced3115083ebd09937d370136a5074e36d38b94 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Mon, 20 Oct 2025 13:38:47 -0600 Subject: [PATCH 1/4] started thinking about the gate report pdf generator --- src/providers/pond/gateAPI.tsx | 1 + src/reports/MiPCA/GateReports.tsx | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 src/reports/MiPCA/GateReports.tsx diff --git a/src/providers/pond/gateAPI.tsx b/src/providers/pond/gateAPI.tsx index 5b07606..6f540dc 100644 --- a/src/providers/pond/gateAPI.tsx +++ b/src/providers/pond/gateAPI.tsx @@ -80,6 +80,7 @@ export interface IGateInterface { end: string, otherTeam?: string ) => Promise>; + //new call for getting report data for gates } export const GateAPIcontext = createContext({} as IGateInterface); diff --git a/src/reports/MiPCA/GateReports.tsx b/src/reports/MiPCA/GateReports.tsx new file mode 100644 index 0000000..8fa7f51 --- /dev/null +++ b/src/reports/MiPCA/GateReports.tsx @@ -0,0 +1,18 @@ +import React from "react" + +export default function GateReports(){ + + //load all of the gates for the user/team + + //selection table to select which gates to generate a report + + /** + * new api call for gates that takes in an array of gate keys to build the report data for + * this call will return an array/map of the gate report data + * in the return loop through the data building a pdf report for each one + */ + + return ( + + ) +} \ No newline at end of file From 6dbb7c3432feb97365a197a24de103aa0b979db9 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Tue, 21 Oct 2025 10:40:14 -0600 Subject: [PATCH 2/4] temp commit so i can change branches --- package-lock.json | 4 +- package.json | 2 +- src/gate/GateList.tsx | 13 +- src/maps/MapBase.tsx | 3 +- .../mapObjectControllers/AgMapController.tsx | 172 ++++++++++-------- src/providers/pond/gateAPI.tsx | 22 ++- src/reports/MiPCA/GateReports.tsx | 31 +++- 7 files changed, 160 insertions(+), 87 deletions(-) diff --git a/package-lock.json b/package-lock.json index dda1c90..b9275ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "mui-tel-input": "^7.0.0", "notistack": "^3.0.1", "openweathermap-ts": "^1.2.10", - "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#mipca_reports", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", @@ -10911,7 +10911,7 @@ }, "node_modules/protobuf-ts": { "version": "1.0.0", - "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#d0ac088df3822c10c0497f49173d1679b4e107b0", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#4aa7c4efefa38e86c21ec08cddcbd7f789b38464", "dependencies": { "protobufjs": "^6.8.8" } diff --git a/package.json b/package.json index ee0e9b7..a8e73f7 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "mui-tel-input": "^7.0.0", "notistack": "^3.0.1", "openweathermap-ts": "^1.2.10", - "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#mipca_reports", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", diff --git a/src/gate/GateList.tsx b/src/gate/GateList.tsx index 5c548e2..b590b2b 100644 --- a/src/gate/GateList.tsx +++ b/src/gate/GateList.tsx @@ -1,5 +1,5 @@ import { Gate } from "models/Gate"; -import { Box, Card, IconButton, List, ListItem, ListItemText, Theme, Typography } from "@mui/material"; +import { Box, Button, Card, IconButton, List, ListItem, ListItemText, Theme, Typography } from "@mui/material"; import React, { useCallback, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useMobile } from "hooks"; @@ -13,6 +13,7 @@ import GateSettings from "./GateSettings"; import { useGateAPI, useGlobalState } from "providers"; import { Settings } from "@mui/icons-material"; import { cloneDeep } from "lodash"; +import GateReports from "reports/MiPCA/GateReports"; interface Props { //gates: Gate[]; @@ -54,6 +55,7 @@ export default function GateList(props: Props) { const [gates, setGates] = useState([]) const [selectedGate, setSelectedGate] = useState() const [search, setSearch] = useState("") + const [openReports, setOpenReports] = useState(false) const goToGate = (gate: Gate) => { let path = "/terminals/" + gate.key; @@ -208,6 +210,8 @@ export default function GateList(props: Props) { const gateTable = () => { return ( + actions={} + title="Terminal Gates" page={tablePage} pageSize={pageSize} columns={isMobile || props.useMobile ? mobileCols() : desktopCols()} @@ -237,7 +241,8 @@ export default function GateList(props: Props) { ) }; - return + return ( + {gateTable()} - + {setOpenReports(false)}}/> + + ) } diff --git a/src/maps/MapBase.tsx b/src/maps/MapBase.tsx index b1dc432..59689fc 100644 --- a/src/maps/MapBase.tsx +++ b/src/maps/MapBase.tsx @@ -241,7 +241,7 @@ export default function MapBase(props: Props) { homeMarkerAPI .listHomeMarkers(1, 0, undefined, undefined, undefined, undefined, as) .then(resp => { - if (resp.data.homeMarker.length < 1) { + if (resp.data.homeMarker === undefined) { setHomePin(undefined); setHaveHome(false); setStartingView(props.currentView); @@ -271,6 +271,7 @@ export default function MapBase(props: Props) { } }) .catch(err => { + console.log(err) openSnack("Could not load Home Pin"); }); }, [as, homeMarkerAPI, openSnack, props.ignoreHomeLoad, user]); // eslint-disable-line react-hooks/exhaustive-deps diff --git a/src/maps/mapObjectControllers/AgMapController.tsx b/src/maps/mapObjectControllers/AgMapController.tsx index 7001f1a..14817ee 100644 --- a/src/maps/mapObjectControllers/AgMapController.tsx +++ b/src/maps/mapObjectControllers/AgMapController.tsx @@ -221,24 +221,26 @@ import { Result } from "@mapbox/mapbox-gl-geocoder"; .then(resp => { let fields: Map = new Map(); let fieldEntries: Result[] = []; - resp.data.fields.forEach(f => { - let field = Field.any(f); - fields.set(field.key(), field); - fieldEntries.push({ - id: field.key(), - place_name: field.name(), - place_type: ["field"], - center: [field.center().longitude, field.center().latitude], - address: "", - type: "Feature", - text: field.name(), - bbox: [0,0,0,0], - geometry: {type: "Point", coordinates: [field.center().longitude, field.center().latitude]}, - context: [], - properties: {}, - relevance: 0 + if(resp.data.fields){ + resp.data.fields.forEach(f => { + let field = Field.any(f); + fields.set(field.key(), field); + fieldEntries.push({ + id: field.key(), + place_name: field.name(), + place_type: ["field"], + center: [field.center().longitude, field.center().latitude], + address: "", + type: "Feature", + text: field.name(), + bbox: [0,0,0,0], + geometry: {type: "Point", coordinates: [field.center().longitude, field.center().latitude]}, + context: [], + properties: {}, + relevance: 0 + }); }); - }); + } setFields(fields); setFieldSearchEntries(fieldEntries); }) @@ -314,53 +316,56 @@ import { Result } from "@mapbox/mapbox-gl-geocoder"; let bagOp: BagModel[] = []; let bagMarkerData: Map = new Map(); let bagEntries: Result[] = []; - resp.data.grainBags.forEach(bag => { - let b = BagModel.create(bag); - bags.set(b.key(), b); - if (bag.settings) { - if (!b.settings.startLocation?.longitude && !b.settings.endLocation?.longitude) { - bagOp.push(b); - } else { - //build the data for the marker - //TODO-CS: think about making the creation of marker data a function in the object model - let mData: MarkerData = { - title: b.name(), - longitude: b.centerLocation().longitude, - latitude: b.centerLocation().latitude, - visibleLevels: { max: 13 }, - colour: b.settings.theme?.color ?? "white", - clickFunc: (e, i, isMobile) => { - clickDelay(); - closeDrawers(); - setBagDrawer(true); - setIgnoreFeatures(true); - setObjectKey(b.key()); - moveMap( - b.centerLocation().latitude, - b.centerLocation().longitude, - zoomLevels.far, - isMobile - ); - } - }; - bagMarkerData.set(b.key(), mData); - bagEntries.push({ - center: [b.centerLocation().longitude, b.centerLocation().latitude], - id: b.key(), - place_name: b.name(), - place_type: ["grainBag"], - address: "", - bbox: [0,0,0,0], - context: [], - geometry: {type: "Point", coordinates: [b.centerLocation().longitude, b.centerLocation().latitude]}, - properties: {}, - relevance: 0, - text: "", - type: "Feature" - }); + if(resp.data.grainBags){ + + resp.data.grainBags.forEach(bag => { + let b = BagModel.create(bag); + bags.set(b.key(), b); + if (bag.settings) { + if (!b.settings.startLocation?.longitude && !b.settings.endLocation?.longitude) { + bagOp.push(b); + } else { + //build the data for the marker + //TODO-CS: think about making the creation of marker data a function in the object model + let mData: MarkerData = { + title: b.name(), + longitude: b.centerLocation().longitude, + latitude: b.centerLocation().latitude, + visibleLevels: { max: 13 }, + colour: b.settings.theme?.color ?? "white", + clickFunc: (e, i, isMobile) => { + clickDelay(); + closeDrawers(); + setBagDrawer(true); + setIgnoreFeatures(true); + setObjectKey(b.key()); + moveMap( + b.centerLocation().latitude, + b.centerLocation().longitude, + zoomLevels.far, + isMobile + ); + } + }; + bagMarkerData.set(b.key(), mData); + bagEntries.push({ + center: [b.centerLocation().longitude, b.centerLocation().latitude], + id: b.key(), + place_name: b.name(), + place_type: ["grainBag"], + address: "", + bbox: [0,0,0,0], + context: [], + geometry: {type: "Point", coordinates: [b.centerLocation().longitude, b.centerLocation().latitude]}, + properties: {}, + relevance: 0, + text: "", + type: "Feature" + }); + } } - } - }); + }); + } setBagOptions(bagOp); setGrainBags(bags); grainBagsRef.current = bags; @@ -382,19 +387,21 @@ import { Result } from "@mapbox/mapbox-gl-geocoder"; binYardAPI .listBinYards(400, 0, "asc", undefined, undefined, undefined, as) .then(resp => { - resp.data.yard.forEach(yard => { - if (yard.settings) { - yardMap.set(yard.settings.key, yard.settings); - if ( - (!yard.settings.latitude && !yard.settings.longitude) || - (yard.settings.latitude === 0 && yard.settings.longitude === 0) - ) { - yardOps.push(yard.settings); - } else { - let mData: MarkerData = { - title: yard.settings.name, - longitude: yard.settings.longitude, - latitude: yard.settings.latitude, + if(resp.data.yard){ + + resp.data.yard.forEach(yard => { + if (yard.settings) { + yardMap.set(yard.settings.key, yard.settings); + if ( + (!yard.settings.latitude && !yard.settings.longitude) || + (yard.settings.latitude === 0 && yard.settings.longitude === 0) + ) { + yardOps.push(yard.settings); + } else { + let mData: MarkerData = { + title: yard.settings.name, + longitude: yard.settings.longitude, + latitude: yard.settings.latitude, colour: yard.settings.theme?.color ?? "green", markerIcon: , visibleLevels: { max: 17 }, @@ -429,6 +436,7 @@ import { Result } from "@mapbox/mapbox-gl-geocoder"; } } }); + } setYardMarkerData(newYardMarkers); setYardSearchEntries(yardEntries); setBinYards(yardMap); @@ -447,6 +455,9 @@ import { Result } from "@mapbox/mapbox-gl-geocoder"; let markerData: Map = new Map(); let searchEntries: Result[] = []; let binOp: IBin[] = []; + if(resp.data.bins){ + + resp.data.bins.forEach(bin => { let b = IBin.create(bin); binMap.set(b.key(), b); @@ -495,6 +506,7 @@ import { Result } from "@mapbox/mapbox-gl-geocoder"; } } }); + } setBinMarkerData(markerData); setBinSearchEntries(searchEntries); setBins(binMap); @@ -514,6 +526,9 @@ import { Result } from "@mapbox/mapbox-gl-geocoder"; let searchEntries: Result[] = []; let options: DeviceModel[] = []; + if(resp.data.devices){ + + resp.data.devices.forEach((device, i) => { //add device to the main map let d = DeviceModel.any(device); @@ -553,6 +568,7 @@ import { Result } from "@mapbox/mapbox-gl-geocoder"; } } }); + } setDevices(map); setDeviceOptions(options); setDeviceMarkerData(newDevMarkers); @@ -725,6 +741,9 @@ import { Result } from "@mapbox/mapbox-gl-geocoder"; let fmMap: Map = new Map(); let markerData: Map = new Map(); let searchEntries: Result[] = []; + if(resp.data.fieldMarker){ + + resp.data.fieldMarker.forEach(fieldMarker => { let fm = FieldMarker.create(fieldMarker); fmMap.set(fm.key(), fm); @@ -760,6 +779,7 @@ import { Result } from "@mapbox/mapbox-gl-geocoder"; place_type: ["fieldMarker"] } as Result); }); + } setFieldMarkers(fmMap); setFieldMarkerData(markerData); setFieldMarkerSearchEntries(searchEntries); diff --git a/src/providers/pond/gateAPI.tsx b/src/providers/pond/gateAPI.tsx index 6f540dc..b9ab561 100644 --- a/src/providers/pond/gateAPI.tsx +++ b/src/providers/pond/gateAPI.tsx @@ -81,6 +81,7 @@ export interface IGateInterface { otherTeam?: string ) => Promise>; //new call for getting report data for gates + gateReportData: (gates: string[], start: string, end: string, otherTeam?: string) => Promise> } export const GateAPIcontext = createContext({} as IGateInterface); @@ -348,6 +349,24 @@ export default function GateProvider(props: PropsWithChildren) { ); }; + const gateReportData = ( + gates: string[], + start: string, + end: string, + otherTeam?: string + ) => { + const view = otherTeam ? otherTeam : as + let url = pondURL("reports/gates?keys=" + gates.toString() + "start=" + start + "&end=" + end + "&as=" + view) + return new Promise>((resolve, reject) => { + get(url).then(resp => { + resp.data = pond.GateReportDataResponse.fromObject(resp.data) + return resolve(resp) + }).catch(err => { + return reject(err) + }) + }) + } + return ( ) { updatePrefs, listGateAirflow, listGateMeasurements, - listGateFlowEvents + listGateFlowEvents, + gateReportData }}> {children} diff --git a/src/reports/MiPCA/GateReports.tsx b/src/reports/MiPCA/GateReports.tsx index 8fa7f51..73bd5c9 100644 --- a/src/reports/MiPCA/GateReports.tsx +++ b/src/reports/MiPCA/GateReports.tsx @@ -1,8 +1,27 @@ -import React from "react" +import { DialogActions, DialogContent, DialogTitle } from "@mui/material" +import ResponsiveDialog from "common/ResponsiveDialog" +import { useGateAPI } from "providers" +import React, { useEffect } from "react" -export default function GateReports(){ +interface Props { + open: boolean + onClose: () => void + gate?: string //if the gate key is passed in just generate for that gate, otherwise give the user a way of selecting which gates +} + +export default function GateReports(props: Props){ + const {gate, open, onClose} = props + const gateAPI = useGateAPI() //load all of the gates for the user/team + useEffect(()=>{ + if(gate) return //if there was a gate passed in we dont need to load them + gateAPI.listGates(0, 0).then(resp => { + console.log(resp.data) + }).catch(err => { + + }) + },[gate]) //selection table to select which gates to generate a report @@ -13,6 +32,12 @@ export default function GateReports(){ */ return ( - + + Gate Reports + + + + + ) } \ No newline at end of file From eb01964622787604f293516aff5c0fab149cc838 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Tue, 21 Oct 2025 16:29:43 -0600 Subject: [PATCH 3/4] built out the functionality for the table --- src/reports/MiPCA/GateReports.tsx | 108 +++++++++++++++++++++++++++--- 1 file changed, 98 insertions(+), 10 deletions(-) diff --git a/src/reports/MiPCA/GateReports.tsx b/src/reports/MiPCA/GateReports.tsx index 1d9eac5..56cd883 100644 --- a/src/reports/MiPCA/GateReports.tsx +++ b/src/reports/MiPCA/GateReports.tsx @@ -1,6 +1,11 @@ -import { Button, DialogActions, DialogContent, DialogTitle } from "@mui/material" +import { Button, DialogActions, DialogContent, DialogTitle, Typography } from "@mui/material" import ResponsiveDialog from "common/ResponsiveDialog" +import ResponsiveTable from "common/ResponsiveTable" +import { GetDefaultDateRange } from "common/time/DateRange" +import DateSelect from "common/time/DateSelect" +import { cloneDeep } from "lodash" import { Gate } from "models/Gate" +import { Moment } from "moment" import { pond } from "protobuf-ts/pond" import { useGateAPI } from "providers" import React, { useEffect, useState } from "react" @@ -15,40 +20,123 @@ export default function GateReports(props: Props){ const {gate, open, onClose} = props const gateAPI = useGateAPI() const [allGates, setAllGates] = useState([]) + const [displayedGates, setDisplayedGates] = useState([]) + const [pagesize, setPageSize] = useState(10) + const [page, setPage] = useState(0) + const [gateKeys, setGateKeys] = useState([]) + const [selectedTableRows,setSelectedTableRows] = useState([]) const [reportData, setReportData] = useState([]) + const [loadingReportData, setLoadingReportData] = useState(false) + const defaultDateRange = GetDefaultDateRange(); + const [startDate, setStartDate] = useState(defaultDateRange.start); + const [endDate, setEndDate] = useState(defaultDateRange.end); //load all of the gates for the user/team useEffect(()=>{ - if(gate) return //if there was a gate passed in we dont need to load them if(open){ - gateAPI.listGates(0, 0).then(resp => { - console.log(resp.data) - }).catch(err => { - - }) + if(gate) { + setGateKeys([gate]) + return + } else { + gateAPI.listGates(0, 0).then(resp => { + if(resp.data.gates){ + setAllGates(resp.data.gates.map(g => Gate.create(g))) + } + }).catch(err => { + console.log(err) + }) + } + } },[gate, open]) + useEffect(()=>{ + let start = page*pagesize + let offset = pagesize + setDisplayedGates(allGates.slice(start, start+offset)) + },[allGates, page, pagesize]) + //selection table to select which gates to generate a report const gateTable = () => { + return ( + + columns={[ + { + title: "Gate", + render: row => {row.name} + } + ]} + selectedRows={selectedTableRows} + rowSelect={(gate, tableIndex, checked) => { + let rowIndexes = cloneDeep(selectedTableRows) + let keys = cloneDeep(gateKeys) + if(checked){ + if(!selectedTableRows.includes(tableIndex)) rowIndexes.push(tableIndex) + if(!gateKeys.includes(gate.key)) keys.push(gate.key) + }else{ + if(rowIndexes.includes(tableIndex)) rowIndexes.splice(rowIndexes.indexOf(tableIndex), 1) + if(keys.includes(gate.key)) keys.splice(keys.indexOf(gate.key), 1) + } + setSelectedTableRows(rowIndexes) + setGateKeys(keys) + }} + rows={displayedGates} + total={allGates.length} + pageSize={pagesize} + page={page} + setPage={(page) => { + setPage(page) + }} + handleRowsPerPageChange={(event) => { + setPageSize(event.target.value) + }} /> + ) + } + //two text field select boxes to select the start and end dates + const dateSelect = () => { + return ( + { + setStartDate(start) + setEndDate(end) + }} + label="Build Reports For" + /> + ) } /** * new api call for gates that takes in an array of gate keys to build the report data for - * this call will return an array/map of the gate report data + * this call will return an array of the gate report data * in the return loop through the data building a pdf report for each one */ + const generateReports = () => { + setLoadingReportData(true) + gateAPI.gateReportData(gateKeys, startDate.toISOString(), endDate.toISOString()).then(resp => { + //in the response use the data to build the pdf's + }).catch(err => { + + }).finally(()=>{ + //in the finally set the loading to false + setLoadingReportData(false) + }) + } return ( Gate Reports - + {dateSelect()} + {!gate && gateTable()} - + ) From 9de2f1285fbea0b144282e849e9d4649c7391959 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 23 Oct 2025 11:26:33 -0600 Subject: [PATCH 4/4] finished building out the basice infrasturcture for the MiPCA reports, just need to get the actual content of the PDF --- src/cableEstimator/cableQuote.tsx | 2 +- .../Agriculture/agPDFTemplate.tsx | 8 ++- .../pdfTemplates/MiPCA/components/footer.tsx | 31 ++++++++++ .../pdfTemplates/MiPCA/components/header.tsx | 32 ++++++++++ .../pdfTemplates/MiPCA/mipcaPDFTemplate.tsx | 43 +++++++++++++ src/providers/pond/gateAPI.tsx | 2 +- src/reports/MiPCA/GateReports.tsx | 62 ++++++++++++++++--- .../MiPCA/pdfComponents.tsx/GateReportPDF.tsx | 29 +++++++++ 8 files changed, 197 insertions(+), 12 deletions(-) create mode 100644 src/common/pdfTemplates/MiPCA/components/footer.tsx create mode 100644 src/common/pdfTemplates/MiPCA/components/header.tsx create mode 100644 src/common/pdfTemplates/MiPCA/mipcaPDFTemplate.tsx create mode 100644 src/reports/MiPCA/pdfComponents.tsx/GateReportPDF.tsx diff --git a/src/cableEstimator/cableQuote.tsx b/src/cableEstimator/cableQuote.tsx index ae37870..af6ccee 100644 --- a/src/cableEstimator/cableQuote.tsx +++ b/src/cableEstimator/cableQuote.tsx @@ -27,7 +27,7 @@ export default function CableQuote(props: Props) { }); const pdfDoc = () => { - return } />; + return ]} />; }; return ( diff --git a/src/common/pdfTemplates/Agriculture/agPDFTemplate.tsx b/src/common/pdfTemplates/Agriculture/agPDFTemplate.tsx index 5b1285f..9a79783 100644 --- a/src/common/pdfTemplates/Agriculture/agPDFTemplate.tsx +++ b/src/common/pdfTemplates/Agriculture/agPDFTemplate.tsx @@ -3,7 +3,7 @@ import Header from "./components/header"; import Footer from "./components/footer"; interface Props { - content: JSX.Element; + content: JSX.Element[]; } export default function AgPDFTemplate(props: Props) { @@ -27,15 +27,17 @@ export default function AgPDFTemplate(props: Props) { }); return ( - + {props.content.map(page => ( +
- {props.content} + {page}