Compare commits

...
Sign in to create a new pull request.

5 commits

11 changed files with 367 additions and 11 deletions

4
package-lock.json generated
View file

@ -42,7 +42,7 @@
"mui-tel-input": "^7.0.0", "mui-tel-input": "^7.0.0",
"notistack": "^3.0.1", "notistack": "^3.0.1",
"openweathermap-ts": "^1.2.10", "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", "query-string": "^9.2.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",
@ -10911,7 +10911,7 @@
}, },
"node_modules/protobuf-ts": { "node_modules/protobuf-ts": {
"version": "1.0.0", "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#90d01d04404d6b38d3200634e194c17bae8a0737",
"dependencies": { "dependencies": {
"protobufjs": "^6.8.8" "protobufjs": "^6.8.8"
} }

View file

@ -54,7 +54,7 @@
"mui-tel-input": "^7.0.0", "mui-tel-input": "^7.0.0",
"notistack": "^3.0.1", "notistack": "^3.0.1",
"openweathermap-ts": "^1.2.10", "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", "query-string": "^9.2.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",

View file

@ -27,7 +27,7 @@ export default function CableQuote(props: Props) {
}); });
const pdfDoc = () => { const pdfDoc = () => {
return <AgPDFTemplate content={<PdfContent bin={bin} />} />; return <AgPDFTemplate content={[<PdfContent bin={bin} />]} />;
}; };
return ( return (

View file

@ -3,7 +3,7 @@ import Header from "./components/header";
import Footer from "./components/footer"; import Footer from "./components/footer";
interface Props { interface Props {
content: JSX.Element; content: JSX.Element[];
} }
export default function AgPDFTemplate(props: Props) { export default function AgPDFTemplate(props: Props) {
@ -27,15 +27,17 @@ export default function AgPDFTemplate(props: Props) {
}); });
return ( return (
<Document> <Document>
<Page size={"A4"} style={pdfStyle.page}> {props.content.map(page => (
<Page size={"A4"} style={pdfStyle.page}>
<View style={pdfStyle.header}> <View style={pdfStyle.header}>
<Header /> <Header />
</View> </View>
<View style={pdfStyle.content}>{props.content}</View> <View style={pdfStyle.content}>{page}</View>
<View style={pdfStyle.footer}> <View style={pdfStyle.footer}>
<Footer /> <Footer />
</View> </View>
</Page> </Page>
))}
</Document> </Document>
); );
} }

View file

@ -0,0 +1,31 @@
import { Image, StyleSheet, View } from "@react-pdf/renderer";
import agFooter from "common/pdfTemplates/Agriculture/components/footerImages/agFooter.png";
import React from "react";
export default function Footer() {
const styles = StyleSheet.create({
view: {
flexDirection: "row",
fontStyle: "normal",
fontWeight: 650,
borderTop: "1px solid black",
padding: 10
},
label: {
width: "20%"
},
email: {
width: "40%"
},
phone: {
width: "40%"
}
});
return (
<React.Fragment>
<View style={styles.view}>
<Image src={agFooter} />
</View>
</React.Fragment>
);
}

View file

@ -0,0 +1,32 @@
import { Image, StyleSheet, Text, View } from "@react-pdf/renderer";
import moment from "moment";
import React from "react";
import logo from "assets/whitelabels/AdaptiveAgriculture/AgLogoText.png";
export default function Header() {
const styles = StyleSheet.create({
view: {
flexDirection: "row",
fontStyle: "normal",
fontWeight: 650,
borderBottom: "1px solid black"
},
logo: {
width: "55%",
textAlign: "left",
paddingBottom: 15
},
estimate: {
width: "45%",
textAlign: "right"
}
});
return (
<React.Fragment>
<View style={styles.view}>
<Image style={styles.logo} src={logo} />
<Text style={styles.estimate}>{moment().format("YYYY-MM-DD")}</Text>
</View>
</React.Fragment>
);
}

View file

@ -0,0 +1,43 @@
import { Document, Page, StyleSheet, View } from "@react-pdf/renderer";
import Header from "./components/header";
import Footer from "./components/footer";
interface Props {
content: JSX.Element[];
}
export default function MiPCAPDFTemplate(props: Props) {
const pdfStyle = StyleSheet.create({
page: {
fontSize: 11,
padding: "5%"
},
header: {
flexDirection: "row",
height: "8%"
},
content: {
flexDirection: "row",
height: "87%"
},
footer: {
flexDirection: "row",
height: "5%"
}
});
return (
<Document>
{props.content.map(page => (
<Page size={"A4"} style={pdfStyle.page}>
<View style={pdfStyle.header}>
<Header />
</View>
<View style={pdfStyle.content}>{page}</View>
<View style={pdfStyle.footer}>
<Footer />
</View>
</Page>
))}
</Document>
);
}

View file

@ -1,5 +1,5 @@
import { Gate } from "models/Gate"; 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 React, { useCallback, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useMobile } from "hooks"; import { useMobile } from "hooks";
@ -13,6 +13,7 @@ import GateSettings from "./GateSettings";
import { useGateAPI, useGlobalState } from "providers"; import { useGateAPI, useGlobalState } from "providers";
import { Settings } from "@mui/icons-material"; import { Settings } from "@mui/icons-material";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import GateReports from "reports/MiPCA/GateReports";
interface Props { interface Props {
//gates: Gate[]; //gates: Gate[];
@ -54,6 +55,7 @@ export default function GateList(props: Props) {
const [gates, setGates] = useState<Gate[]>([]) const [gates, setGates] = useState<Gate[]>([])
const [selectedGate, setSelectedGate] = useState<Gate | undefined>() const [selectedGate, setSelectedGate] = useState<Gate | undefined>()
const [search, setSearch] = useState("") const [search, setSearch] = useState("")
const [openReports, setOpenReports] = useState(false)
const goToGate = (gate: Gate) => { const goToGate = (gate: Gate) => {
let path = "/terminals/" + gate.key; let path = "/terminals/" + gate.key;
@ -208,6 +210,8 @@ export default function GateList(props: Props) {
const gateTable = () => { const gateTable = () => {
return ( return (
<ResponsiveTable<Gate> <ResponsiveTable<Gate>
actions={<Button onClick={()=>{setOpenReports(true)}}>Generate Reports</Button>}
title="Terminal Gates"
page={tablePage} page={tablePage}
pageSize={pageSize} pageSize={pageSize}
columns={isMobile || props.useMobile ? mobileCols() : desktopCols()} columns={isMobile || props.useMobile ? mobileCols() : desktopCols()}
@ -237,7 +241,8 @@ export default function GateList(props: Props) {
) )
}; };
return <Box> return (
<React.Fragment>
<GateSettings <GateSettings
open={gateDialog} open={gateDialog}
gate={selectedGate} gate={selectedGate}
@ -256,5 +261,7 @@ export default function GateList(props: Props) {
pulse={gates.length < 1} pulse={gates.length < 1}
/> />
{gateTable()} {gateTable()}
</Box> <GateReports open={openReports} onClose={()=>{setOpenReports(false)}}/>
</React.Fragment>
)
} }

View file

@ -80,6 +80,8 @@ export interface IGateInterface {
end: string, end: string,
otherTeam?: string otherTeam?: string
) => Promise<AxiosResponse<pond.ListGateMeasurementsResponse>>; ) => Promise<AxiosResponse<pond.ListGateMeasurementsResponse>>;
//new call for getting report data for gates
gateReportData: (gates: string[], start: string, end: string, otherTeam?: string) => Promise<AxiosResponse<pond.GateReportDataResponse>>
} }
export const GateAPIcontext = createContext<IGateInterface>({} as IGateInterface); export const GateAPIcontext = createContext<IGateInterface>({} as IGateInterface);
@ -347,6 +349,24 @@ export default function GateProvider(props: PropsWithChildren<Props>) {
); );
}; };
const gateReportData = (
gates: string[],
start: string,
end: string,
otherTeam?: string
) => {
const view = otherTeam ? otherTeam : as
let url = pondURL("/reports/gates?gateKeys=" + gates.toString() + "&start=" + start + "&end=" + end + "&as=" + view)
return new Promise<AxiosResponse<pond.GateReportDataResponse>>((resolve, reject) => {
get<pond.GateReportDataResponse>(url).then(resp => {
resp.data = pond.GateReportDataResponse.fromObject(resp.data)
return resolve(resp)
}).catch(err => {
return reject(err)
})
})
}
return ( return (
<GateAPIcontext.Provider <GateAPIcontext.Provider
value={{ value={{
@ -360,7 +380,8 @@ export default function GateProvider(props: PropsWithChildren<Props>) {
updatePrefs, updatePrefs,
listGateAirflow, listGateAirflow,
listGateMeasurements, listGateMeasurements,
listGateFlowEvents listGateFlowEvents,
gateReportData
}}> }}>
{children} {children}
</GateAPIcontext.Provider> </GateAPIcontext.Provider>

View file

@ -0,0 +1,191 @@
import { Box, Button, CircularProgress, DialogActions, DialogContent, DialogTitle, Typography } from "@mui/material"
import MiPCAPDFTemplate from "common/pdfTemplates/MiPCA/mipcaPDFTemplate"
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"
import GateReportPDF from "./pdfComponents.tsx/GateReportPDF"
import { PDFViewer, StyleSheet } from "@react-pdf/renderer"
import { useMobile } from "hooks"
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()
const [allGates, setAllGates] = useState<Gate[]>([])
const [displayedGates, setDisplayedGates] = useState<Gate[]>([])
const [pagesize, setPageSize] = useState(10)
const [page, setPage] = useState(0)
const [gateKeys, setGateKeys] = useState<string[]>([])
const [selectedTableRows,setSelectedTableRows] = useState<number[]>([])
const [reportPages, setReportPages] = useState<JSX.Element[]>([])
const [loadingReportData, setLoadingReportData] = useState(false)
const defaultDateRange = GetDefaultDateRange();
const [startDate, setStartDate] = useState<Moment>(defaultDateRange.start);
const [endDate, setEndDate] = useState<Moment>(defaultDateRange.end);
const isMobile = useMobile()
const pdfStyle = StyleSheet.create({
viewerDesktop: {
height: 500 * 1.44,
width: 500
},
viewerMobile: {
height: "100%",
width: "100%"
}
});
const closeDialog = () => {
setGateKeys([])
setSelectedTableRows([])
setReportPages([])
onClose()
}
//load all of the gates for the user/team
useEffect(()=>{
if(open){
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 (
<ResponsiveTable<Gate>
columns={[
{
title: "Gate",
render: row => <Typography>{row.name}</Typography>
}
]}
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 (
<DateSelect
startDate={startDate}
endDate={endDate}
updateDateRange={(start, end) => {
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 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 => {
console.log(resp)
let pdfPages: JSX.Element[] = []
if(resp.data.data){
resp.data.data.forEach(reportData => {
pdfPages.push(<GateReportPDF reportData={reportData}/>)
})
}
setReportPages(pdfPages)
}).catch(err => {
}).finally(()=>{
//in the finally set the loading to false
setLoadingReportData(false)
})
}
return (
<ResponsiveDialog open={open} onClose={() => {closeDialog()}}>
<DialogTitle>Gate Reports</DialogTitle>
<DialogContent>
{loadingReportData ?
<Box display="flex" justifyContent="center">
<CircularProgress color="primary" size={100}/>
</Box>
:
<React.Fragment>
{reportPages.length > 0 ?
<Box>
<PDFViewer style={isMobile ? pdfStyle.viewerMobile : pdfStyle.viewerDesktop}>
<MiPCAPDFTemplate content={reportPages}/>
</PDFViewer>
</Box> :
<Box>
{dateSelect()}
{!gate && gateTable()}
</Box>
}
</React.Fragment>
}
</DialogContent>
<DialogActions>
<Button onClick={()=>{
closeDialog()
}}>Close</Button>
<Button variant="contained" color="primary" disabled={loadingReportData} onClick={()=>{
generateReports()
}}>Generate Reports</Button>
</DialogActions>
</ResponsiveDialog>
)
}

View file

@ -0,0 +1,29 @@
import { View, StyleSheet } from "@react-pdf/renderer";
import { pond } from "protobuf-ts/pond";
interface Props {
reportData: pond.GateReportData
}
export default function GateReportPDF(props: Props){
const styles = StyleSheet.create({
general: {
marginTop: 2,
flexDirection: "row",
borderTop: "1px solid black",
borderBottom: "1px solid black",
paddingTop: 12,
paddingBottom: 12,
fontFamily: "Helvetica-Bold",
fontSize: 20,
textAlign: "center"
}
});
return (
<View>
<View style={styles.general}>
</View>
</View>
)
}