diff --git a/app/client/src/App.jsx b/app/client/src/App.jsx index 5447cf48..2c50d05c 100644 --- a/app/client/src/App.jsx +++ b/app/client/src/App.jsx @@ -42,6 +42,11 @@ import AdminConfig from "./features/admin/AdminConfig"; import AdminDairyTestResults from "./features/admin/AdminDairyTestResults"; import AdminPremisesId from "./features/admin/AdminPremisesId"; +import ViewTrailerPage from "./features/trailers/ViewTrailerPage"; + +import ViewTrailerInspectionPage from "./features/trailerinspections/ViewTrailerInspectionPage"; +import CreateTrailerInspectionPage from "./features/trailerinspections/CreateTrailerInspectionPage"; + import ModalComponent from "./components/ModalComponent"; import keycloak from "./app/keycloak"; import { fetchCurrentUser } from "./app/appSlice"; @@ -97,6 +102,9 @@ function App() { + + + @@ -111,6 +119,16 @@ function App() { + + + + + + diff --git a/app/client/src/app/store.js b/app/client/src/app/store.js index 7b13d9ae..19592606 100644 --- a/app/client/src/app/store.js +++ b/app/client/src/app/store.js @@ -14,6 +14,8 @@ import dairyTankNoticesReducer from "../features/documents/dairyTankNoticesSlice import adminReducer from "../features/admin/adminSlice"; import reportsReducer from "../features/reports/reportsSlice"; import inspectionsReducer from "../features/inspections/inspectionsSlice"; +import trailersReducer from "../features/trailers/trailersSlice"; +import trailerInspectionsReducer from "../features/trailerinspections/trailerInspectionsSlice"; const reducer = { admin: adminReducer, @@ -24,21 +26,24 @@ const reducer = { dairyTankNotices: dairyTankNoticesReducer, comments: commentsReducer, inspections: inspectionsReducer, + trailerinspections: trailerInspectionsReducer, licences: licencesReducer, lookups: lookupsReducer, reports: reportsReducer, search: searchReducer, sites: sitesReducer, + trailers: trailersReducer, config: configReducer, }; export default configureStore({ reducer, - middleware: (getDefaultMiddleware) => getDefaultMiddleware({ - serializableCheck: { - // Ignore these action types - ignoredActions: ["app/SHOW_MODAL"], - }, - }), + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: { + // Ignore these action types + ignoredActions: ["app/SHOW_MODAL"], + }, + }), devTools: process.env.NODE_ENV !== "production", }); diff --git a/app/client/src/features/documents/SelectApiaryRenewalsPage.jsx b/app/client/src/features/documents/SelectApiaryRenewalsPage.jsx index e32ee03e..d43f8c29 100644 --- a/app/client/src/features/documents/SelectApiaryRenewalsPage.jsx +++ b/app/client/src/features/documents/SelectApiaryRenewalsPage.jsx @@ -75,12 +75,12 @@ export default function SelectApiaryRenewalsPage() { useEffect(() => { licences = queuedRenewals.data ? queuedRenewals.data.map((licence) => ({ - ...licence, - licenceId: licence.licenceId, - issuedOnDate: formatDateString(licence.issuedOnDate), - expiryDate: formatDateString(licence.expiryDate), - selected: "true", - })) + ...licence, + licenceId: licence.licenceId, + issuedOnDate: formatDateString(licence.issuedOnDate), + expiryDate: formatDateString(licence.expiryDate), + selected: "true", + })) : []; setValue("licences", licences); }, [queuedRenewals.data]); @@ -95,7 +95,9 @@ export default function SelectApiaryRenewalsPage() { }, [watchStartDate, watchEndDate]); const onSubmit = (data) => { - const checked = isCheckAll ? watchLicences.map((x) => x.licenceId) : isChecked; + const checked = isCheckAll + ? watchLicences.map((x) => x.licenceId) + : isChecked; dispatch(startRenewalJob(checked)); history.push(DOWNLOAD_RENEWALS_PATHNAME); }; @@ -139,7 +141,8 @@ export default function SelectApiaryRenewalsPage() { variant="primary" type="submit" disabled={ - !isCheckAll && (isChecked.length === 0 || renewalJob.status !== REQUEST_STATUS.IDLE) + !isCheckAll && + (isChecked.length === 0 || renewalJob.status !== REQUEST_STATUS.IDLE) } > Generate @@ -184,8 +187,12 @@ export default function SelectApiaryRenewalsPage() {
- {isCheckAll ? licences.length : isChecked.length} {pluralize(isCheckAll ? licences.length : isChecked.length, "renewal")} selected - for generation. + {isCheckAll ? licences.length : isChecked.length}{" "} + {pluralize( + isCheckAll ? licences.length : isChecked.length, + "renewal" + )}{" "} + selected for generation. diff --git a/app/client/src/features/inspections/ApiaryInspectionDetailsEdit.jsx b/app/client/src/features/inspections/ApiaryInspectionDetailsEdit.jsx index 7600607d..7f29b30a 100644 --- a/app/client/src/features/inspections/ApiaryInspectionDetailsEdit.jsx +++ b/app/client/src/features/inspections/ApiaryInspectionDetailsEdit.jsx @@ -10,6 +10,7 @@ export default function ApiaryInspectionDetailsEdit({ initialValues, site, }) { + console.log("ApiaryInspectionDetailsEdit"); const { setValue, register, diff --git a/app/client/src/features/inspections/CreateInspectionPage.jsx b/app/client/src/features/inspections/CreateInspectionPage.jsx index 68e62586..6599821a 100644 --- a/app/client/src/features/inspections/CreateInspectionPage.jsx +++ b/app/client/src/features/inspections/CreateInspectionPage.jsx @@ -137,6 +137,8 @@ export default function CreateInspectionPage() { const submissionLabel = submitting ? "Submitting..." : "Create"; + console.log(inspection.status); + if (inspection.status === REQUEST_STATUS.FULFILLED) { return ; } diff --git a/app/client/src/features/inspections/InspectionDetailsViewEdit.jsx b/app/client/src/features/inspections/InspectionDetailsViewEdit.jsx index ec6d4a9c..b487fae8 100644 --- a/app/client/src/features/inspections/InspectionDetailsViewEdit.jsx +++ b/app/client/src/features/inspections/InspectionDetailsViewEdit.jsx @@ -35,6 +35,7 @@ export default function InspectionDetailsViewEdit({ site, licence, }) { + console.log("InspectionDetailsViewEdit"); const { status, error, mode } = inspection; const dispatch = useDispatch(); @@ -71,7 +72,7 @@ export default function InspectionDetailsViewEdit({ inspectionComment: null, }; - useEffect(() => { }, [dispatch]); + useEffect(() => {}, [dispatch]); useEffect(() => { setValue("inspectionDate", new Date(inspection.data.inspectionDate)); @@ -184,7 +185,9 @@ export default function InspectionDetailsViewEdit({ supersInspected: parseAsInt(data.supersInspected), supersDestroyed: parseAsInt(data.supersDestroyed), inspectionComment: - data.inspectionComment?.length === 0 ? null : data.inspectionComment, + data.inspectionComment?.length === 0 + ? null + : data.inspectionComment, }; dispatch( diff --git a/app/client/src/features/licences/AssociatedLicences.jsx b/app/client/src/features/licences/AssociatedLicences.jsx index c2427db6..21b2614b 100644 --- a/app/client/src/features/licences/AssociatedLicences.jsx +++ b/app/client/src/features/licences/AssociatedLicences.jsx @@ -315,7 +315,6 @@ export default function AssociatedLicences({ licence }) { ); } else if (results.status === REQUEST_STATUS.FULFILLED && results.count > 0) { - console.log(licence.data); control = ( <>
diff --git a/app/client/src/features/licences/GenerateDairyTrailerInspection.jsx b/app/client/src/features/licences/GenerateDairyTrailerInspection.jsx new file mode 100644 index 00000000..cbf9a93f --- /dev/null +++ b/app/client/src/features/licences/GenerateDairyTrailerInspection.jsx @@ -0,0 +1,66 @@ +import React, { useEffect } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { Row, Col, Form, Button } from "react-bootstrap"; + +import DocGenDownloadBar from "../../components/DocGenDownloadBar"; + +import { + startDairyTrailerInspectionJob, + generateReport, + fetchReportJob, + selectReportsJob, + completeReportJob, +} from "../reports/reportsSlice"; + +import { REPORTS } from "../../utilities/constants"; + +export default function GenerateDairyTrailerInspection({ licenceNumber }) { + const dispatch = useDispatch(); + + const job = useSelector(selectReportsJob); + const { pendingDocuments } = job; + + useEffect(() => { + if (job.id && job.type === REPORTS.DAIRY_TRAILER_INSPECTION) { + dispatch(fetchReportJob()); + + if (pendingDocuments?.length > 0) { + dispatch(generateReport(pendingDocuments[0].documentId)); + } else { + dispatch(completeReportJob(job.id)); + } + } + }, [pendingDocuments]); // eslint-disable-line react-hooks/exhaustive-deps + + const onGenerateReport = () => { + dispatch( + startDairyTrailerInspectionJob({ + licenceNumber: licenceNumber, + }) + ); + }; + + return ( + <> + + +   + + + +
+ +
+ + ); +} + +GenerateDairyTrailerInspection.propTypes = {}; diff --git a/app/client/src/features/licences/LicenceSites.jsx b/app/client/src/features/licences/LicenceSites.jsx index a9efc26f..e10a88dd 100644 --- a/app/client/src/features/licences/LicenceSites.jsx +++ b/app/client/src/features/licences/LicenceSites.jsx @@ -25,7 +25,7 @@ import { selectSiteResults, setSiteSearchPage, setSiteFilterText, - clearSiteFilterText + clearSiteFilterText, } from "../search/searchSlice"; import { selectLicenceStatuses } from "../lookups/licenceStatusesSlice"; @@ -96,7 +96,6 @@ export default function LicenceSites({ licence }) { dispatch(setSiteSearchPage(1)); dispatch(setSiteFilterText(newFilterText)); dispatch(fetchSiteResults()); - }, 700); setDebouncedActionTimeout(newTimeout); @@ -113,7 +112,7 @@ export default function LicenceSites({ licence }) { province: PROVINCES.BC, region: null, regionalDistrict: null, - registrationDate: startOfToday() + registrationDate: startOfToday(), }; dispatch(createSite(payload)); } @@ -173,7 +172,7 @@ export default function LicenceSites({ licence }) { errorMessage={"There are no sites associated with this licence."} /> {currentUser.data.roleId !== SYSTEM_ROLES.READ_ONLY && - currentUser.data.roleId !== SYSTEM_ROLES.INSPECTOR ? ( + currentUser.data.roleId !== SYSTEM_ROLES.INSPECTOR ? ( {addSiteButton} @@ -199,7 +198,7 @@ export default function LicenceSites({ licence }) {
{currentUser.data.roleId !== SYSTEM_ROLES.READ_ONLY && - currentUser.data.roleId !== SYSTEM_ROLES.INSPECTOR ? ( + currentUser.data.roleId !== SYSTEM_ROLES.INSPECTOR ? ( {addSiteButton} ) : null} @@ -235,7 +234,7 @@ export default function LicenceSites({ licence }) { <> Sites -
+
{control} - + ); } diff --git a/app/client/src/features/licences/LicenceTrailers.jsx b/app/client/src/features/licences/LicenceTrailers.jsx new file mode 100644 index 00000000..3f7af85a --- /dev/null +++ b/app/client/src/features/licences/LicenceTrailers.jsx @@ -0,0 +1,253 @@ +/* eslint-disable */ +import React, { useEffect, useState } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import PropTypes from "prop-types"; +import { Link } from "react-router-dom"; +import { + Alert, + Container, + Spinner, + Table, + Row, + Col, + Button, + ButtonGroup, +} from "react-bootstrap"; +import { startOfToday } from "date-fns"; + +import SectionHeading from "../../components/SectionHeading"; + +import { selectCreatedTrailer, createTrailer } from "../trailers/trailersSlice"; +import { + clearTrailerParameters, + setTrailerParameters, + fetchTrailerResults, + selectTrailerResults, + setTrailerSearchPage, + setTrailerFilterText, + clearTrailerFilterText, +} from "../search/searchSlice"; + +import { selectLicenceStatuses } from "../lookups/licenceStatusesSlice"; + +import { + REQUEST_STATUS, + LICENCE_STATUS_TYPES, + COUNTRIES, + PROVINCES, + SYSTEM_ROLES, + TRAILERS_PATHNAME, +} from "../../utilities/constants"; + +import ErrorMessageRow from "../../components/ErrorMessageRow"; + +import { selectCurrentUser } from "../../app/appSlice"; +import GenerateDairyTrailerInspection from "./GenerateDairyTrailerInspection"; + +function formatResultRow(result) { + const url = `${TRAILERS_PATHNAME}/${result.dairyFarmTrailerId}`; + return ( + + + + {`${result.licenceNumber}-${result.licenceTrailerSeq}`} + + + {result.licenceStatus} + {result.registrantLastFirst} + {result.geographicalDivision} + + ); +} + +function navigateToSearchPage(dispatch, page) { + dispatch(setTrailerSearchPage(page)); + dispatch(fetchTrailerResults()); +} + +export default function LicenceTrailers({ licence }) { + const dispatch = useDispatch(); + const licenceStatuses = useSelector(selectLicenceStatuses); + const results = useSelector(selectTrailerResults); + const createdTrailer = useSelector(selectCreatedTrailer); + const currentUser = useSelector(selectCurrentUser); + + const [debouncedActionTimeout, setDebouncedActionTimeout] = useState(null); + + useEffect(() => { + dispatch(clearTrailerParameters()); + dispatch(clearTrailerFilterText()); + dispatch( + setTrailerParameters({ licenceNumber: licence.data.licenceNumber }) + ); + dispatch(fetchTrailerResults()); + }, [dispatch]); + + const handleFilterTextChange = (e) => { + const newFilterText = e.target.value; + + if (debouncedActionTimeout) { + clearTimeout(debouncedActionTimeout); + } + + const newTimeout = setTimeout(() => { + dispatch(setTrailerSearchPage(1)); + dispatch(setTrailerFilterText(newFilterText)); + dispatch(fetchTrailerResults()); + }, 700); + + setDebouncedActionTimeout(newTimeout); + }; + + function addTrailerOnClick() { + const payload = { + licenceId: licence.data.id, + licenceTypeId: licence.data.licenceTypeId, + licenceStatus: licenceStatuses.data.find( + (x) => x.code_description === LICENCE_STATUS_TYPES.ACTIVE + ).id, + country: COUNTRIES.CANADA, + province: PROVINCES.BC, + region: null, + regionalDistrict: null, + registrationDate: startOfToday(), + }; + dispatch(createTrailer(payload)); + } + + const addTrailerButton = ( + + ); + + let control = null; + if (results.status === REQUEST_STATUS.PENDING) { + control = ( +
+ + Searching... + +
+ ); + } else if (results.status === REQUEST_STATUS.REJECTED) { + control = ( + + + An error was encountered while retrieving results. + +

+ {results.error.code}: {results.error.description} +

+
+ ); + } else if (createdTrailer.status === REQUEST_STATUS.REJECTED) { + control = ( + + + An error was encountered while creating a trailer. + +

+ {createdTrailer.error.code}: {createdTrailer.error.description} +

+
+ ); + } else if ( + results.status === REQUEST_STATUS.FULFILLED && + results.count === 0 + ) { + control = ( + <> + + {currentUser.data.roleId !== SYSTEM_ROLES.READ_ONLY && + currentUser.data.roleId !== SYSTEM_ROLES.INSPECTOR ? ( + + {addTrailerButton} + + ) : null} + + ); + } else if (results.status === REQUEST_STATUS.FULFILLED && results.count > 0) { + control = ( + <> + + + + + + + + + + {results.data.map((result) => formatResultRow(result))} +
Trailer IDTrailer StatusNameDivision
+ + {currentUser.data.roleId !== SYSTEM_ROLES.READ_ONLY && + currentUser.data.roleId !== SYSTEM_ROLES.INSPECTOR ? ( + {addTrailerButton} + ) : null} + + Showing {results.data.length} of {results.count} entries + + + + + + + + + + + ); + } + + return ( + <> + Trailers + +
+ +
+ {control} +
+ Inspections Report + + + + + ); +} + +LicenceTrailers.propTypes = { + licence: PropTypes.object.isRequired, +}; diff --git a/app/client/src/features/licences/ViewLicencePage.jsx b/app/client/src/features/licences/ViewLicencePage.jsx index bc33b694..eeea0d30 100644 --- a/app/client/src/features/licences/ViewLicencePage.jsx +++ b/app/client/src/features/licences/ViewLicencePage.jsx @@ -3,7 +3,11 @@ import { useSelector, useDispatch } from "react-redux"; import { useParams, Redirect } from "react-router-dom"; import { Spinner, Alert } from "react-bootstrap"; -import { REQUEST_STATUS, SITES_PATHNAME } from "../../utilities/constants"; +import { + REQUEST_STATUS, + SITES_PATHNAME, + TRAILERS_PATHNAME, +} from "../../utilities/constants"; import PageHeading from "../../components/PageHeading"; import RegistrantsViewEdit from "../registrants/RegistrantsViewEdit"; @@ -16,6 +20,7 @@ import { selectCreatedSite, clearCurrentSite } from "../sites/sitesSlice"; import LicenceDetailsViewEdit from "./LicenceDetailsViewEdit"; import LicenceHeader from "./LicenceHeader"; import LicenceSites from "./LicenceSites"; +import LicenceTrailers from "./LicenceTrailers"; import LicenceInventory from "./LicenceInventory"; import LicenceInventoryHistory from "./LicenceInventoryHistory"; import LicenceDairyTestInventory from "./LicenceDairyTestInventory"; @@ -30,11 +35,16 @@ import { LICENCE_TYPE_ID_VETERINARY_DRUG, LICENCE_TYPE_ID_MEDICATED_FEED, LICENCE_TYPE_ID_DAIRY_FARM, + LICENCE_TYPE_ID_DAIRY_TANK_TRUCK, } from "./constants"; import Comments from "../comments/Comments"; import "./ViewLicencePage.scss"; +import { + clearCurrentTrailer, + selectCreatedTrailer, +} from "../trailers/trailersSlice"; export default function ViewLicencePage() { const dispatch = useDispatch(); @@ -42,16 +52,21 @@ export default function ViewLicencePage() { const licence = useSelector(selectCurrentLicence); const createdSite = useSelector(selectCreatedSite); + const createdTrailer = useSelector(selectCreatedTrailer); useEffect(() => { dispatch(clearCurrentLicence()); dispatch(clearCurrentSite()); + dispatch(clearCurrentTrailer()); dispatch(fetchLicence(id)); }, [dispatch, id]); if (createdSite.status === REQUEST_STATUS.FULFILLED) { return ; } + if (createdTrailer.status === REQUEST_STATUS.FULFILLED) { + return ; + } const associatedLicenceTypes = [ LICENCE_TYPE_ID_LIVESTOCK_DEALER, @@ -65,7 +80,7 @@ export default function ViewLicencePage() { const showAssociatedLicence = licence.data && associatedLicenceTypes.find((x) => x === licence.data.licenceTypeId) !== - undefined; + undefined; let content; if (licence.data) { @@ -74,12 +89,17 @@ export default function ViewLicencePage() { - + + {licence.data.licenceTypeId === LICENCE_TYPE_ID_DAIRY_TANK_TRUCK ? ( + + ) : ( + + )} {licence.data.licenceTypeId === LICENCE_TYPE_ID_DAIRY_FARM ? ( ) : null} {licence.data.licenceTypeId === LICENCE_TYPE_ID_GAME_FARM || - licence.data.licenceTypeId === LICENCE_TYPE_ID_FUR_FARM ? ( + licence.data.licenceTypeId === LICENCE_TYPE_ID_FUR_FARM ? ( <> diff --git a/app/client/src/features/reports/ReportDairyTrailerInspection.jsx b/app/client/src/features/reports/ReportDairyTrailerInspection.jsx new file mode 100644 index 00000000..ce05a85d --- /dev/null +++ b/app/client/src/features/reports/ReportDairyTrailerInspection.jsx @@ -0,0 +1,83 @@ +import React, { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { useSelector, useDispatch } from "react-redux"; +import { Row, Col, Form, Button } from "react-bootstrap"; + +import DocGenDownloadBar from "../../components/DocGenDownloadBar"; + +import { + startDairyTrailerInspectionJob, + generateReport, + fetchReportJob, + selectReportsJob, + completeReportJob, +} from "./reportsSlice"; + +import { REPORTS } from "../../utilities/constants"; + +export default function ReportDairyTrailerInspection() { + const dispatch = useDispatch(); + + const job = useSelector(selectReportsJob); + const { pendingDocuments } = job; + + const form = useForm({ + reValidateMode: "onBlur", + }); + const { register, watch } = form; + + const watchLicenceNumber = watch("licenceNumber", null); + + useEffect(() => { + if (job.id && job.type === REPORTS.DAIRY_TRAILER_INSPECTION) { + dispatch(fetchReportJob()); + + if (pendingDocuments?.length > 0) { + dispatch(generateReport(pendingDocuments[0].documentId)); + } else { + dispatch(completeReportJob(job.id)); + } + } + }, [pendingDocuments]); // eslint-disable-line react-hooks/exhaustive-deps + + const onGenerateReport = () => { + dispatch( + startDairyTrailerInspectionJob({ + licenceNumber: watchLicenceNumber, + }) + ); + }; + + return ( + <> + + + Licence Number + + + +   + + + +
+ +
+ + ); +} + +ReportDairyTrailerInspection.propTypes = {}; diff --git a/app/client/src/features/reports/Reports.jsx b/app/client/src/features/reports/Reports.jsx index e5532bf8..e810b1bd 100644 --- a/app/client/src/features/reports/Reports.jsx +++ b/app/client/src/features/reports/Reports.jsx @@ -22,6 +22,7 @@ import ReportLicenceExpiry from "./ReportLicenceExpiry"; import { clearReportsJob } from "./reportsSlice"; import RenderOnRole from "../../components/RenderOnRole"; +import ReportDairyTrailerInspection from "./ReportDairyTrailerInspection"; export default function Reports() { const dispatch = useDispatch(); @@ -75,6 +76,8 @@ export default function Reports() { case REPORTS.LICENCE_EXPIRY: control = ; break; + case REPORTS.DAIRY_TRAILER_INSPECTION: + control = ; default: break; } @@ -177,6 +180,19 @@ export default function Reports() { + + + + { + try { + if (payload && payload.licenceNumber) { + const licenceDetails = await Api.get( + `licences/number/${payload.licenceNumber}` + ); + if ( + licenceDetails && + licenceDetails.data && + licenceDetails.data.licenceType && + licenceDetails.data.licenceType === "DAIRY TANK TRUCK" + ) { + const response = await Api.post( + `documents/reports/startJob/dairyTrailerInspection`, + payload + ); + return response.data; + } else { + throw new Error( + "Invalid licence type for dairy trailer inspection report." + ); + } + } + } catch (error) { + if (error instanceof ApiError) { + return thunkApi.rejectWithValue(error.serialize()); + } + return thunkApi.rejectWithValue({ code: -1, description: error.message }); + } + } +); + export const startProducersAnalysisRegionJob = createAsyncThunk( "reports/startProducersAnalysisRegionJob", async (_, thunkApi) => { @@ -312,6 +346,9 @@ export const reportsSlice = createSlice({ [startApiaryHiveInspectionJob.pending]: pendingStartJobReducer, [startApiaryHiveInspectionJob.fulfilled]: fulfilledStartJobReducer, [startApiaryHiveInspectionJob.rejected]: rejectionStartJobReducer, + [startDairyTrailerInspectionJob.pending]: pendingStartJobReducer, + [startDairyTrailerInspectionJob.fulfilled]: fulfilledStartJobReducer, + [startDairyTrailerInspectionJob.rejected]: rejectionStartJobReducer, [startProducersAnalysisRegionJob.pending]: pendingStartJobReducer, [startProducersAnalysisRegionJob.fulfilled]: fulfilledStartJobReducer, [startProducersAnalysisRegionJob.rejected]: rejectionStartJobReducer, diff --git a/app/client/src/features/search/searchSlice.js b/app/client/src/features/search/searchSlice.js index c2f4f59e..589136a4 100644 --- a/app/client/src/features/search/searchSlice.js +++ b/app/client/src/features/search/searchSlice.js @@ -15,6 +15,13 @@ export const selectSiteParameters = (state) => state.search.sites.parameters; export const selectSiteResults = (state) => state.search.sites.results; export const selectSiteFilterText = (state) => state.search.sites.filter; +export const selectTrailerSearchType = (state) => + state.search.trailers.searchType; +export const selectTrailerParameters = (state) => + state.search.trailers.parameters; +export const selectTrailerResults = (state) => state.search.trailers.results; +export const selectTrailerFilterText = (state) => state.search.trailers.filter; + export const selectInventoryHistorySearchType = (state) => state.search.inventoryHistory.searchType; export const selectInventoryHistoryParameters = (state) => @@ -107,6 +114,27 @@ export const fetchSiteResults = createAsyncThunk( } ); +export const fetchTrailerResults = createAsyncThunk( + "search/fetchTrailerResults", + async (_, thunkApi) => { + try { + const parameters = selectTrailerParameters(thunkApi.getState()); + + const parsedParameters = { + ...parameters, + }; + + const response = await Api.get(`trailers/search`, parsedParameters); + return response.data; + } catch (error) { + if (error instanceof ApiError) { + return thunkApi.rejectWithValue(error.serialize()); + } + return thunkApi.rejectWithValue({ code: -1, description: error.message }); + } + } +); + export const fetchInventoryHistoryResults = createAsyncThunk( "search/fetchInventoryHistoryResults", async (_, thunkApi) => { @@ -203,6 +231,17 @@ export const searchSlice = createSlice({ status: REQUEST_STATUS.IDLE, }, }, + trailers: { + searchType: SEARCH_TYPE.SIMPLE, + parameters: {}, + results: { + data: undefined, + page: undefined, + count: undefined, + error: undefined, + status: REQUEST_STATUS.IDLE, + }, + }, inventoryHistory: { searchType: SEARCH_TYPE.SIMPLE, parameters: {}, @@ -294,6 +333,37 @@ export const searchSlice = createSlice({ state.sites.parameters.filterText = undefined; }, + // Trailers + clearTrailerParameters: (state) => { + state.trailers.parameters = {}; + state.trailers.searchType = SEARCH_TYPE.SIMPLE; + }, + clearTrailerResults: (state) => { + state.trailers.results.data = undefined; + state.trailers.results.error = undefined; + state.trailers.results.status = REQUEST_STATUS.IDLE; + }, + toggleTrailerSearchType: (state) => { + const currentSearchType = state.trailers.searchType; + state.trailers.searchType = + currentSearchType === SEARCH_TYPE.SIMPLE + ? SEARCH_TYPE.ADVANCED + : SEARCH_TYPE.SIMPLE; + }, + setTrailerParameters: (state, action) => { + state.trailers.parameters = action.payload; + state.trailers.results.status = REQUEST_STATUS.IDLE; + }, + setTrailerSearchPage: (state, action) => { + state.trailers.parameters.page = action.payload; + }, + setTrailerFilterText: (state, action) => { + state.trailers.parameters.filterText = action.payload; + }, + clearTrailerFilterText: (state) => { + state.trailers.parameters.filterText = undefined; + }, + // Inventory History clearInventoryHistoryParameters: (state) => { state.inventoryHistory.parameters = {}; @@ -417,6 +487,24 @@ export const searchSlice = createSlice({ state.sites.results.status = REQUEST_STATUS.REJECTED; }, + // Trailers + [fetchTrailerResults.pending]: (state) => { + state.trailers.results.error = undefined; + state.trailers.results.status = REQUEST_STATUS.PENDING; + }, + [fetchTrailerResults.fulfilled]: (state, action) => { + state.trailers.results.data = action.payload.results; + state.trailers.results.page = action.payload.page; + state.trailers.results.count = action.payload.count; + state.trailers.results.error = undefined; + state.trailers.results.status = REQUEST_STATUS.FULFILLED; + }, + [fetchTrailerResults.rejected]: (state, action) => { + state.trailers.results.data = undefined; + state.trailers.results.error = action.payload; + state.trailers.results.status = REQUEST_STATUS.REJECTED; + }, + // Inventory History [fetchInventoryHistoryResults.pending]: (state) => { state.inventoryHistory.results.error = undefined; @@ -490,6 +578,14 @@ export const { setSiteFilterText, clearSiteFilterText, + clearTrailerParameters, + clearTrailerResults, + toggleTrailerSearchType, + setTrailerParameters, + setTrailerSearchPage, + setTrailerFilterText, + clearTrailerFilterText, + clearInventoryHistoryParameters, clearInventoryHistoryResults, toggleInventoryHistorySearchType, diff --git a/app/client/src/features/sites/ViewSitePage.jsx b/app/client/src/features/sites/ViewSitePage.jsx index f09d585f..1e80be8f 100644 --- a/app/client/src/features/sites/ViewSitePage.jsx +++ b/app/client/src/features/sites/ViewSitePage.jsx @@ -22,8 +22,17 @@ import RenderOnRole from "../../components/RenderOnRole"; import PageHeading from "../../components/PageHeading"; import SectionHeading from "../../components/SectionHeading"; -import { fetchSite, selectCurrentSite, clearCreatedSite, clearCurrentSite } from "./sitesSlice"; -import { fetchLicence, selectCurrentLicence, clearCurrentLicence } from "../licences/licencesSlice"; +import { + fetchSite, + selectCurrentSite, + clearCreatedSite, + clearCurrentSite, +} from "./sitesSlice"; +import { + fetchLicence, + selectCurrentLicence, + clearCurrentLicence, +} from "../licences/licencesSlice"; import SiteHeader from "./SiteHeader"; import LicenceDetailsView from "../licences/LicenceDetailsView"; @@ -33,6 +42,7 @@ import Comments from "../comments/Comments"; import "./ViewSitePage.scss"; import DairyTanksViewEdit from "./dairytanks/DairyTanksViewEdit"; + import { LICENCE_TYPE_ID_DAIRY_FARM, LICENCE_TYPE_ID_APIARY, diff --git a/app/client/src/features/trailerinspections/CreateTrailerInspectionPage.jsx b/app/client/src/features/trailerinspections/CreateTrailerInspectionPage.jsx new file mode 100644 index 00000000..26880aee --- /dev/null +++ b/app/client/src/features/trailerinspections/CreateTrailerInspectionPage.jsx @@ -0,0 +1,167 @@ +import React, { useEffect } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { Redirect, useHistory, useParams } from "react-router-dom"; +import { useForm } from "react-hook-form"; +import { Container, Form } from "react-bootstrap"; +import { startOfToday } from "date-fns"; + +import { REQUEST_STATUS, TRAILERS_PATHNAME } from "../../utilities/constants"; + +import ErrorMessageRow from "../../components/ErrorMessageRow"; +import PageHeading from "../../components/PageHeading"; +import SectionHeading from "../../components/SectionHeading"; +import SubmissionButtons from "../../components/SubmissionButtons"; + +import { + selectCreatedInspection, + createTrailerInspection, + clearCreatedInspection, +} from "./trailerInspectionsSlice"; +import { fetchTrailer, selectCurrentTrailer } from "../trailers/trailersSlice"; +import { fetchLicence, selectCurrentLicence } from "../licences/licencesSlice"; +import * as LicenceTypeConstants from "../licences/constants"; + +import TrailerHeader from "../trailers/TrailerHeader"; +import TrailerDetailsView from "../trailers/TrailerDetailsView"; +import LicenceDetailsView from "../licences/LicenceDetailsView"; +import TrailerInspectionDetailsEdit from "./TrailerInspectionDetailsEdit"; + +function submissionController( + licence, + trailer, + setError, + clearErrors, + dispatch +) { + console.log("CreateTrailerInspectionPage"); + const onSubmit = async (data) => { + switch (licence.data.licenceTypeId) { + case LicenceTypeConstants.LICENCE_TYPE_ID_DAIRY_TANK_TRUCK: { + const payload = { + ...data, + trailerId: trailer.data.id, + trailerNumber: trailer.data.trailerNumber, + inspectorId: data.inspectorId.length === 0 ? null : data.inspectorId, + inspectionComment: + data.inspectionComment.length === 0 ? null : data.inspectionComment, + }; + dispatch(createTrailerInspection(payload)); + break; + } + default: + break; + } + }; + + return { onSubmit }; +} + +const today = startOfToday(); +const initialFormValues = { + inspectionDate: today, + inspectorId: null, + inspectionComment: null, +}; + +export default function CreateTrailerInspectionPage() { + const history = useHistory(); + const dispatch = useDispatch(); + + const { id } = useParams(); + + const trailer = useSelector(selectCurrentTrailer); + const licence = useSelector(selectCurrentLicence); + const inspection = useSelector(selectCreatedInspection); + + const form = useForm({ + reValidateMode: "onBlur", + }); + const { handleSubmit, setValue, setError, clearErrors } = form; + + useEffect(() => { + setValue("inspectionDate", today); + + dispatch(clearCreatedInspection()); + + dispatch(fetchTrailer(id)).then((s) => { + dispatch(fetchLicence(s.payload.licenceId)); + }); + }, [dispatch]); + + const onCancel = () => { + history.push(`${TRAILERS_PATHNAME}/${id}`); + }; + + const { onSubmit } = submissionController( + licence, + trailer, + setError, + clearErrors, + dispatch + ); + + const submitting = inspection.status === REQUEST_STATUS.PENDING; + + let errorMessage = null; + if (inspection.status === REQUEST_STATUS.REJECTED) { + errorMessage = `${inspection.error.code}: ${inspection.error.description}`; + } + + const submissionLabel = submitting ? "Submitting..." : "Create"; + + console.log(inspection); + if (inspection.status === REQUEST_STATUS.FULFILLED) { + return ; + } + + let content; + if (trailer.data && licence.data) { + content = ( + <> + +
+ License Details + + + +
+
+ Trailer Details + + + +
+ + ); + } + + return ( +
+ Create Inspection + {content} + {trailer.data ? ( +
+ Inspection Details + + + +
+ + +
+ +
+
+ ) : null} +
+ ); +} diff --git a/app/client/src/features/trailerinspections/TrailerInspectionDetailsEdit.jsx b/app/client/src/features/trailerinspections/TrailerInspectionDetailsEdit.jsx new file mode 100644 index 00000000..ecc67125 --- /dev/null +++ b/app/client/src/features/trailerinspections/TrailerInspectionDetailsEdit.jsx @@ -0,0 +1,84 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { Form, Row, Col } from "react-bootstrap"; + +import CustomDatePicker from "../../components/CustomDatePicker"; +import SectionHeading from "../../components/SectionHeading"; + +export default function TrailerInspectionDetailsEdit({ + form, + initialValues, + trailer, +}) { + console.log("TrailerInspectionDetailsEdit"); + const { + setValue, + register, + formState: { errors }, + } = form; + + const handleFieldChange = (field) => { + return (value) => { + setValue(field, value); + }; + }; + + return ( + <> + + + + Dairy Trailer ID + + + + + + + + + Inspector ID + + + + + Comments + + + + + + + ); +} + +TrailerInspectionDetailsEdit.propTypes = { + form: PropTypes.object.isRequired, + initialValues: PropTypes.object.isRequired, + trailer: PropTypes.object.isRequired, +}; diff --git a/app/client/src/features/trailerinspections/TrailerInspectionDetailsView.jsx b/app/client/src/features/trailerinspections/TrailerInspectionDetailsView.jsx new file mode 100644 index 00000000..80825e80 --- /dev/null +++ b/app/client/src/features/trailerinspections/TrailerInspectionDetailsView.jsx @@ -0,0 +1,49 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { Form, Row, Col } from "react-bootstrap"; + +import SectionHeading from "../../components/SectionHeading"; +import VerticalField from "../../components/VerticalField"; + +export default function TrailerInspectionDetailsView({ inspection, trailer }) { + return ( + <> + + + + + + + + + + + + Comments + + + + + + + ); +} + +TrailerInspectionDetailsView.propTypes = { + inspection: PropTypes.object.isRequired, + trailer: PropTypes.object.isRequired, +}; diff --git a/app/client/src/features/trailerinspections/TrailerInspectionDetailsViewEdit.jsx b/app/client/src/features/trailerinspections/TrailerInspectionDetailsViewEdit.jsx new file mode 100644 index 00000000..8d525d98 --- /dev/null +++ b/app/client/src/features/trailerinspections/TrailerInspectionDetailsViewEdit.jsx @@ -0,0 +1,161 @@ +import React, { useEffect } from "react"; +import PropTypes from "prop-types"; +import { useDispatch, useSelector } from "react-redux"; +import { useForm } from "react-hook-form"; +import { Container, Form } from "react-bootstrap"; +import { startOfToday } from "date-fns"; + +import { + LICENCE_MODE, + REQUEST_STATUS, + SYSTEM_ROLES, +} from "../../utilities/constants.js"; +import { formatNumber } from "../../utilities/formatting.ts"; +import { parseAsDate } from "../../utilities/parsing.js"; + +import ErrorMessageRow from "../../components/ErrorMessageRow.jsx"; +import SectionHeading from "../../components/SectionHeading.jsx"; +import SubmissionButtons from "../../components/SubmissionButtons.jsx"; + +import { + updateTrailerInspection, + setCurrentInspectionModeToEdit, + setCurrentInspectionModeToView, +} from "./trailerInspectionsSlice.js"; + +import TrailerInspectionDetailsEdit from "./TrailerInspectionDetailsEdit.jsx"; +import TrailerInspectionDetailsView from "./TrailerInspectionDetailsView.jsx"; + +import * as LicenceTypeConstants from "../licences/constants.js"; + +import { selectCurrentUser } from "../../app/appSlice.js"; + +export default function TrailerInspectionDetailsViewEdit({ + inspection, + trailer, + licence, +}) { + const { status, error, mode } = inspection; + + const dispatch = useDispatch(); + + const currentUser = useSelector(selectCurrentUser); + + const today = startOfToday(); + + const form = useForm({ + reValidateMode: "onBlur", + }); + const { handleSubmit, setValue } = form; + + const initialFormValues = { + inspectionDate: inspection.data + ? parseAsDate(inspection.data.inspectionDate) + : today, + inspectorId: null, + inspectionComment: null, + }; + + useEffect(() => {}, [dispatch]); + + useEffect(() => { + setValue("inspectionDate", new Date(inspection.data.inspectionDate)); + setValue("inspectorId", formatNumber(inspection.data.inspectorId)); + setValue("inspectionComment", inspection.data.inspectionComment); + }, [ + setValue, + inspection.data.inspectionDate, + inspection.data.inspectorId, + inspection.data.inspectionComment, + mode, + ]); + + if (mode === LICENCE_MODE.VIEW) { + const onEdit = () => { + dispatch(setCurrentInspectionModeToEdit()); + }; + return ( +
+ + Inspection Details + + + + +
+ ); + } + + const submitting = status === REQUEST_STATUS.PENDING; + + let errorMessage = null; + if (status === REQUEST_STATUS.REJECTED) { + errorMessage = `${error.code}: ${error.description}`; + } + + const submissionLabel = submitting ? "Saving..." : "Save"; + + const onSubmit = async (data) => { + switch (licence.data.licenceTypeId) { + case LicenceTypeConstants.LICENCE_TYPE_ID_DAIRY_TANK_TRUCK: { + const payload = { + ...data, + trailerId: trailer.data.id, + inspectorId: data.inspectorId.length === 0 ? null : data.inspectorId, + inspectionComment: + data.inspectionComment?.length === 0 + ? null + : data.inspectionComment, + }; + + dispatch( + updateTrailerInspection({ + inspection: payload, + id: inspection.data.id, + }) + ); + break; + } + default: + break; + } + }; + + const onCancel = () => { + dispatch(setCurrentInspectionModeToView()); + }; + + return ( +
+
+ Inspection Details + + + + + +
+
+ ); +} + +TrailerInspectionDetailsViewEdit.propTypes = { + inspection: PropTypes.object.isRequired, + trailer: PropTypes.object.isRequired, + licence: PropTypes.object.isRequired, +}; diff --git a/app/client/src/features/trailerinspections/ViewTrailerInspectionPage.jsx b/app/client/src/features/trailerinspections/ViewTrailerInspectionPage.jsx new file mode 100644 index 00000000..963b8a9e --- /dev/null +++ b/app/client/src/features/trailerinspections/ViewTrailerInspectionPage.jsx @@ -0,0 +1,109 @@ +import React, { useEffect } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { useParams } from "react-router-dom"; +import { Spinner, Alert, Container } from "react-bootstrap"; + +import { REQUEST_STATUS } from "../../utilities/constants"; + +import PageHeading from "../../components/PageHeading"; +import SectionHeading from "../../components/SectionHeading"; + +import { + fetchTrailerInspection, + selectCurrentInspection, + clearCreatedInspection, +} from "./trailerInspectionsSlice"; +import { fetchTrailer, selectCurrentTrailer } from "../trailers/trailersSlice"; +import { fetchLicence, selectCurrentLicence } from "../licences/licencesSlice"; + +import TrailerHeader from "../trailers/TrailerHeader"; +import LicenceDetailsView from "../licences/LicenceDetailsView"; +import TrailerDetailsView from "../trailers/TrailerDetailsView"; +import TrailerInspectionDetailsViewEdit from "./TrailerInspectionDetailsViewEdit"; + +export default function ViewTrailerInspectionPage() { + const dispatch = useDispatch(); + const { id } = useParams(); + const inspection = useSelector(selectCurrentInspection); + const trailer = useSelector(selectCurrentTrailer); + const licence = useSelector(selectCurrentLicence); + + useEffect(() => { + dispatch(clearCreatedInspection()); + + dispatch(fetchTrailerInspection(id)).then((inspectionRecord) => + dispatch(fetchTrailer(inspectionRecord.payload.trailerId)).then( + (trailerRecord) => + dispatch(fetchLicence(trailerRecord.payload.licenceId)) + ) + ); + }, [dispatch, id]); + + let content; + if (inspection.data && trailer.data && licence.data) { + content = ( + <> + +
+ License Details + + + +
+
+ Trailer Details + + + +
+
+ +
+ + ); + } else if ( + inspection.status === REQUEST_STATUS.IDLE || + inspection.status === REQUEST_STATUS.PENDING || + trailer.status === REQUEST_STATUS.IDLE || + trailer.status === REQUEST_STATUS.PENDING || + licence.status === REQUEST_STATUS.IDLE || + licence.status === REQUEST_STATUS.PENDING + ) { + content = ( + + Loading... + + ); + } else { + content = ( + + + An error was encountered while loading the inspection. + + {inspection.error && ( +

{`${inspection.error.code}: ${inspection.error.description}`}

+ )} + {trailer.error && ( +

{`${trailer.error.code}: ${trailer.error.description}`}

+ )} + {licence.error && ( +

{`${licence.error.code}: ${licence.error.description}`}

+ )} +
+ ); + } + + return ( +
+ View Inspection + {content} +
+ ); +} diff --git a/app/client/src/features/trailerinspections/trailerInspectionsSlice.js b/app/client/src/features/trailerinspections/trailerInspectionsSlice.js new file mode 100644 index 00000000..03b48a27 --- /dev/null +++ b/app/client/src/features/trailerinspections/trailerInspectionsSlice.js @@ -0,0 +1,145 @@ +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; + +import Api, { ApiError } from "../../utilities/api.ts"; +import { REQUEST_STATUS, LICENCE_MODE } from "../../utilities/constants.js"; + +export const fetchTrailerInspection = createAsyncThunk( + "trailerinspections/fetchTrailerInspection", + async (id, thunkApi) => { + try { + const response = await Api.get(`inspections/trailer/${id}`); + return response.data; + } catch (error) { + if (error instanceof ApiError) { + return thunkApi.rejectWithValue(error.serialize()); + } + return thunkApi.rejectWithValue({ code: -1, description: error.message }); + } + } +); + +export const createTrailerInspection = createAsyncThunk( + "trailerinspections/createTrailerInspection", + async (inspection, thunkApi) => { + try { + const response = await Api.post("inspections/trailer", inspection); + return response.data; + } catch (error) { + if (error instanceof ApiError) { + return thunkApi.rejectWithValue(error.serialize()); + } + return thunkApi.rejectWithValue({ code: -1, description: error.message }); + } + } +); + +export const updateTrailerInspection = createAsyncThunk( + "trailerinspections/updateTrailerInspection", + async ({ inspection, id }, thunkApi) => { + try { + const response = await Api.put(`inspections/trailer/${id}`, inspection); + return response.data; + } catch (error) { + if (error instanceof ApiError) { + return thunkApi.rejectWithValue(error.serialize()); + } + return thunkApi.rejectWithValue({ code: -1, description: error.message }); + } + } +); + +export const trailerInspectionsSlice = createSlice({ + name: "trailerinspections", + initialState: { + createdInspection: { + data: undefined, + error: undefined, + status: REQUEST_STATUS.IDLE, + }, + currentInspection: { + data: undefined, + error: undefined, + status: REQUEST_STATUS.IDLE, + mode: LICENCE_MODE.VIEW, + }, + }, + reducers: { + clearCreatedInspection: (state) => { + state.createdInspection.data = undefined; + state.createdInspection.error = undefined; + state.createdInspection.status = REQUEST_STATUS.IDLE; + }, + clearCurrentInspection: (state) => { + state.currentInspection.data = undefined; + state.currentInspection.error = undefined; + state.currentInspection.status = REQUEST_STATUS.IDLE; + }, + setCurrentInspectionModeToEdit: (state) => { + state.currentInspection.mode = LICENCE_MODE.EDIT; + }, + setCurrentInspectionModeToView: (state) => { + state.currentInspection.mode = LICENCE_MODE.VIEW; + }, + }, + extraReducers: { + [createTrailerInspection.pending]: (state) => { + state.createdInspection.error = undefined; + state.createdInspection.status = REQUEST_STATUS.PENDING; + }, + [createTrailerInspection.fulfilled]: (state, action) => { + state.createdInspection.data = action.payload; + state.createdInspection.error = undefined; + state.createdInspection.status = REQUEST_STATUS.FULFILLED; + }, + [createTrailerInspection.rejected]: (state, action) => { + state.createdInspection.data = undefined; + state.createdInspection.error = action.payload; + state.createdInspection.status = REQUEST_STATUS.REJECTED; + }, + [fetchTrailerInspection.pending]: (state) => { + state.currentInspection.error = undefined; + state.currentInspection.status = REQUEST_STATUS.PENDING; + }, + [fetchTrailerInspection.fulfilled]: (state, action) => { + state.currentInspection.data = action.payload; + state.currentInspection.error = undefined; + state.currentInspection.status = REQUEST_STATUS.FULFILLED; + state.currentInspection.mode = LICENCE_MODE.VIEW; + }, + [fetchTrailerInspection.rejected]: (state, action) => { + state.currentInspection.data = undefined; + state.currentInspection.error = action.payload; + state.currentInspection.status = REQUEST_STATUS.REJECTED; + }, + [updateTrailerInspection.pending]: (state) => { + state.currentInspection.error = undefined; + state.currentInspection.status = REQUEST_STATUS.PENDING; + }, + [updateTrailerInspection.fulfilled]: (state, action) => { + state.currentInspection.data = action.payload; + state.currentInspection.error = undefined; + state.currentInspection.status = REQUEST_STATUS.FULFILLED; + state.currentInspection.mode = LICENCE_MODE.VIEW; + }, + [updateTrailerInspection.rejected]: (state, action) => { + state.currentInspection.error = action.payload; + state.currentInspection.status = REQUEST_STATUS.REJECTED; + }, + }, +}); + +export const selectCreatedInspection = (state) => + state.trailerinspections.createdInspection; +export const selectCurrentInspection = (state) => + state.trailerinspections.currentInspection; + +const { actions, reducer } = trailerInspectionsSlice; + +export const { + clearCreatedInspection, + clearCurrentInspection, + setCurrentInspectionModeToEdit, + setCurrentInspectionModeToView, +} = actions; + +export default reducer; diff --git a/app/client/src/features/trailers/TrailerDetailsEdit.jsx b/app/client/src/features/trailers/TrailerDetailsEdit.jsx new file mode 100644 index 00000000..9fb388d8 --- /dev/null +++ b/app/client/src/features/trailers/TrailerDetailsEdit.jsx @@ -0,0 +1,157 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { Form, Row, Col } from "react-bootstrap"; +import LicenceStatuses from "../lookups/LicenceStatuses"; +import CustomDatePicker from "../../components/CustomDatePicker"; + +export default function TrailerDetailsEdit({ form, initialValues }) { + const { + setValue, + register, + formState: { errors }, + } = form; + + const handleFieldChange = (field) => { + return (value) => { + setValue(field, value); + }; + }; + + return ( + <> + + + + + + + + + + + + Trailer # + + + + + + + + Division + + + + + + Serial No / VIN + + + + + + Licence Plate # + + + + + + + + Year + + + + + + Make + + + + + + Trailer Type + + + + + + + + Capacity (L) + + + + + + Compartments + + + + + + + ); +} + +TrailerDetailsEdit.propTypes = { + form: PropTypes.object.isRequired, + initialValues: PropTypes.object.isRequired, + licence: PropTypes.object, + mode: PropTypes.string.isRequired, +}; diff --git a/app/client/src/features/trailers/TrailerDetailsView.jsx b/app/client/src/features/trailers/TrailerDetailsView.jsx new file mode 100644 index 00000000..0c58d561 --- /dev/null +++ b/app/client/src/features/trailers/TrailerDetailsView.jsx @@ -0,0 +1,74 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { Row, Col } from "react-bootstrap"; +import { PatternFormat } from "react-number-format"; + +import VerticalField from "../../components/VerticalField"; +import SectionHeading from "../../components/SectionHeading"; + +export default function TrailerDetailsView({ trailer }) { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +TrailerDetailsView.propTypes = { + trailer: PropTypes.object.isRequired, + licenceTypeId: PropTypes.number, +}; + +TrailerDetailsView.defaultProps = { + licenceTypeId: null, +}; diff --git a/app/client/src/features/trailers/TrailerDetailsViewEdit.jsx b/app/client/src/features/trailers/TrailerDetailsViewEdit.jsx new file mode 100644 index 00000000..ff80721f --- /dev/null +++ b/app/client/src/features/trailers/TrailerDetailsViewEdit.jsx @@ -0,0 +1,182 @@ +import React, { useEffect } from "react"; +import PropTypes from "prop-types"; +import { useDispatch, useSelector } from "react-redux"; +import { useForm } from "react-hook-form"; +import { Container, Form } from "react-bootstrap"; + +import { + LICENCE_MODE, + REQUEST_STATUS, + SYSTEM_ROLES, +} from "../../utilities/constants"; +import { formatNumber } from "../../utilities/formatting"; +import { parseAsDate, parseAsInt } from "../../utilities/parsing"; + +import ErrorMessageRow from "../../components/ErrorMessageRow"; +import SectionHeading from "../../components/SectionHeading"; +import SubmissionButtons from "../../components/SubmissionButtons"; + +import { fetchLicenceStatuses } from "../lookups/licenceStatusesSlice"; +import { + setCurrentTrailerModeToEdit, + setCurrentTrailerModeToView, +} from "../trailers/trailersSlice"; + +import TrailerDetailsEdit from "./TrailerDetailsEdit"; +import TrailerDetailsView from "./TrailerDetailsView"; + +import { selectCurrentUser } from "../../app/appSlice"; +import { formatDateString } from "../../utilities/formatting"; + +import { updateTrailer } from "../trailers/trailersSlice"; + +export default function TrailerDetailsViewEdit({ trailer, licence }) { + const { status, error, mode } = trailer; + + const dispatch = useDispatch(); + + const currentUser = useSelector(selectCurrentUser); + + useEffect(() => { + dispatch(fetchLicenceStatuses()); + }, [dispatch]); + + const form = useForm({ + reValidateMode: "onBlur", + }); + const { handleSubmit, setValue, setError } = form; + + const initialFormValues = { + licenceStatus: null, + dateIssued: trailer.data ? parseAsDate(trailer.data.dateIssued) : null, + trailerNumber: null, + geographicalDivision: null, + serialNumberVIN: null, + licensePlate: null, + trailerYear: null, + trailerMake: null, + trailerType: null, + trailerCapacity: null, + trailerCompartments: null, + }; + + useEffect(() => { + setValue("licenceStatus", trailer.data.licenceStatusId); + setValue("dateIssued", formatDateString(trailer.data.dateIssued)); + setValue("trailerNumber", trailer.data.trailerNumber); + setValue("geographicalDivision", trailer.data.geographicalDivision); + setValue("serialNumberVIN", trailer.data.serialNumberVIN); + setValue("licensePlate", trailer.data.licensePlate); + setValue("trailerYear", formatNumber(trailer.data.trailerYear)); + setValue("trailerMake", trailer.data.trailerMake); + setValue("trailerType", trailer.data.trailerType); + setValue("trailerCapacity", formatNumber(trailer.data.trailerCapacity)); + setValue( + "trailerCompartments", + formatNumber(trailer.data.trailerCompartments) + ); + }, [ + setValue, + trailer.data.licenceStatusId, + trailer.data.dateIssued, + trailer.data.trailerNumber, + trailer.data.geographicalDivision, + trailer.data.serialNumberVIN, + trailer.data.licensePlate, + trailer.data.trailerYear, + trailer.data.trailerMake, + trailer.data.trailerType, + trailer.data.trailerCapacity, + trailer.data.trailerCompartments, + mode, + ]); + + if (mode === LICENCE_MODE.VIEW) { + const onEdit = () => { + dispatch(setCurrentTrailerModeToEdit()); + }; + return ( +
+ + Trailer Details + + + + +
+ ); + } + + const submitting = status === REQUEST_STATUS.PENDING; + + let errorMessage = null; + if (status === REQUEST_STATUS.REJECTED) { + errorMessage = `${error.code}: ${error.description}`; + } + + const submissionLabel = submitting ? "Saving..." : "Save"; + + const onSubmit = async (data) => { + let errorCount = 0; + + // error checks + + if (errorCount > 0) { + return; + } + + const payload = { + ...data, + licenceId: parseAsInt(trailer.data.licenceId), + licenceStatus: parseAsInt(data.licenceStatus), + dateIssued: data.dateIssued ? data.dateIssued : null, + trailerNumber: data.trailerNumber, + geographicalDivision: data.geographicalDivision, + serialNumberVIN: data.serialNumberVIN, + licensePlate: data.licensePlate, + trailerYear: parseAsInt(data.trailerYear), + trailerMake: data.trailerMake, + trailerType: data.trailerType, + trailerCapacity: parseAsInt(data.trailerCapacity), + trailerCompartments: parseAsInt(data.trailerCompartments), + }; + + dispatch(updateTrailer({ trailer: payload, id: trailer.data.id })); + }; + + const onCancel = () => { + dispatch(setCurrentTrailerModeToView()); + }; + + return ( +
+
+ Trailer Details + + + + + +
+
+ ); +} + +TrailerDetailsViewEdit.propTypes = { + trailer: PropTypes.object.isRequired, +}; diff --git a/app/client/src/features/trailers/TrailerHeader.jsx b/app/client/src/features/trailers/TrailerHeader.jsx new file mode 100644 index 00000000..047c64d3 --- /dev/null +++ b/app/client/src/features/trailers/TrailerHeader.jsx @@ -0,0 +1,56 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { Container, Row } from "react-bootstrap"; +import { Link } from "react-router-dom"; +import { formatDateTimeString } from "../../utilities/formatting.ts"; + +import HorizontalField from "../../components/HorizontalField"; + +import { LICENSES_PATHNAME } from "../../utilities/constants"; + +export default function TrailerHeader({ trailer, licence }) { + const url = `${LICENSES_PATHNAME}/${licence.id}`; + + return ( +
+ + + {licence.licenceNumber}} + /> +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + + +
+ ); +} + +TrailerHeader.propTypes = { + trailer: PropTypes.object.isRequired, + licence: PropTypes.object.isRequired, +}; diff --git a/app/client/src/features/trailers/ViewTrailerPage.jsx b/app/client/src/features/trailers/ViewTrailerPage.jsx new file mode 100644 index 00000000..19ff8a27 --- /dev/null +++ b/app/client/src/features/trailers/ViewTrailerPage.jsx @@ -0,0 +1,205 @@ +import React, { useEffect } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { Link, useParams, useHistory } from "react-router-dom"; +import { + Spinner, + Alert, + Container, + Table, + Row, + Col, + Button, +} from "react-bootstrap"; + +import { + REQUEST_STATUS, + SYSTEM_ROLES, + CREATE_TRAILER_INSPECTIONS_PATHNAME, + TRAILER_INSPECTIONS_PATHNAME, +} from "../../utilities/constants"; + +import RenderOnRole from "../../components/RenderOnRole"; +import PageHeading from "../../components/PageHeading"; +import SectionHeading from "../../components/SectionHeading"; + +import { + fetchTrailer, + selectCurrentTrailer, + clearCreatedTrailer, + clearCurrentTrailer, +} from "./trailersSlice"; +import { + fetchLicence, + selectCurrentLicence, + clearCurrentLicence, +} from "../licences/licencesSlice"; + +import TrailerHeader from "./TrailerHeader"; +import LicenceDetailsView from "../licences/LicenceDetailsView"; +import TrailerDetailsViewEdit from "./TrailerDetailsViewEdit"; + +import Comments from "../comments/Comments"; + +import "./ViewTrailerPage.scss"; + +export default function ViewTrailerPage() { + const history = useHistory(); + const dispatch = useDispatch(); + const { id } = useParams(); + const trailer = useSelector(selectCurrentTrailer); + const licence = useSelector(selectCurrentLicence); + useEffect(() => { + dispatch(clearCreatedTrailer()); + dispatch(clearCurrentLicence()); + dispatch(clearCurrentTrailer()); + + dispatch(fetchTrailer(id)).then((record) => { + dispatch(fetchLicence(record.payload.licenceId)); + }); + }, [dispatch, id]); + + function formatInspectionsResultRow(result) { + const url = `${TRAILER_INSPECTIONS_PATHNAME}/${result.id}`; + const comment = + result?.inspectionComment?.length > 50 + ? result.inspectionComment.substring(0, 50) + "..." + : result.inspectionComment; + + return ( + + {result.inspectionDate} + {result.inspectorId} + {comment} + + View + + + ); + } + + function addInspectionOnClick() { + history.push(`${CREATE_TRAILER_INSPECTIONS_PATHNAME}/${id}`); + } + + const addInspectionButton = ( + + ); + + let content; + if (trailer.data && licence.data) { + content = ( + <> + +
+ License Details + + + +
+ + + +
+ Inspections + + {trailer.data.inspections?.length > 0 ? ( + <> + + + + + + + + + + {trailer.data.inspections.map((result) => + formatInspectionsResultRow(result) + )} + +
Inspection DateInspector IDComments +
+ + + {addInspectionButton} + + + + ) : ( + <> + + + +
+ There are no inspections associated with this trailer. +
+
+ +
+ + + {addInspectionButton} + + + + )} +
+
+ + + + ); + } else if ( + trailer.status === REQUEST_STATUS.IDLE || + trailer.status === REQUEST_STATUS.PENDING || + licence.status === REQUEST_STATUS.IDLE || + licence.status === REQUEST_STATUS.PENDING + ) { + content = ( + + Loading... + + ); + } else { + content = ( + + + An error was encountered while loading the trailer. + + {trailer.error && ( +

{`${trailer.error.code}: ${trailer.error.description}`}

+ )} + {licence.error && ( +

{`${licence.error.code}: ${licence.error.description}`}

+ )} +
+ ); + } + + return ( +
+ View a Trailer Record + {content} +
+ ); +} diff --git a/app/client/src/features/trailers/ViewTrailerPage.scss b/app/client/src/features/trailers/ViewTrailerPage.scss new file mode 100644 index 00000000..77181576 --- /dev/null +++ b/app/client/src/features/trailers/ViewTrailerPage.scss @@ -0,0 +1,3 @@ +header { + font-size: 95%; +} diff --git a/app/client/src/features/trailers/trailersSlice.js b/app/client/src/features/trailers/trailersSlice.js new file mode 100644 index 00000000..3542b5eb --- /dev/null +++ b/app/client/src/features/trailers/trailersSlice.js @@ -0,0 +1,143 @@ +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; + +import Api, { ApiError } from "../../utilities/api.ts"; +import { REQUEST_STATUS, LICENCE_MODE } from "../../utilities/constants"; + +export const createTrailer = createAsyncThunk( + "trailers/createTrailer", + async (trailer, thunkApi) => { + try { + const response = await Api.post("trailers", trailer); + return response.data; + } catch (error) { + if (error instanceof ApiError) { + return thunkApi.rejectWithValue(error.serialize()); + } + return thunkApi.rejectWithValue({ code: -1, description: error.message }); + } + } +); + +export const updateTrailer = createAsyncThunk( + "trailers/updateTrailer", + async ({ trailer, id }, thunkApi) => { + try { + const response = await Api.put(`trailers/${id}`, trailer); + return response.data; + } catch (error) { + if (error instanceof ApiError) { + return thunkApi.rejectWithValue(error.serialize()); + } + return thunkApi.rejectWithValue({ code: -1, description: error.message }); + } + } +); + +export const fetchTrailer = createAsyncThunk( + "trailers/fetchTrailer", + async (id, thunkApi) => { + try { + const response = await Api.get(`trailers/${id}`); + return response.data; + } catch (error) { + if (error instanceof ApiError) { + return thunkApi.rejectWithValue(error.serialize()); + } + return thunkApi.rejectWithValue({ code: -1, description: error.message }); + } + } +); + +export const trailersSlice = createSlice({ + name: "trailers", + initialState: { + createdTrailer: { + data: undefined, + error: undefined, + status: REQUEST_STATUS.IDLE, + }, + currentTrailer: { + data: undefined, + error: undefined, + status: REQUEST_STATUS.IDLE, + mode: LICENCE_MODE.VIEW, + }, + }, + reducers: { + clearCreatedTrailer: (state) => { + state.createdTrailer.data = undefined; + state.createdTrailer.error = undefined; + state.createdTrailer.status = REQUEST_STATUS.IDLE; + }, + clearCurrentTrailer: (state) => { + state.currentTrailer.data = undefined; + state.currentTrailer.error = undefined; + state.currentTrailer.status = REQUEST_STATUS.IDLE; + }, + setCurrentTrailerModeToEdit: (state) => { + state.currentTrailer.mode = LICENCE_MODE.EDIT; + }, + setCurrentTrailerModeToView: (state) => { + state.currentTrailer.mode = LICENCE_MODE.VIEW; + }, + }, + extraReducers: { + [createTrailer.pending]: (state) => { + state.createdTrailer.error = undefined; + state.createdTrailer.status = REQUEST_STATUS.PENDING; + }, + [createTrailer.fulfilled]: (state, action) => { + state.createdTrailer.data = action.payload; + state.createdTrailer.error = undefined; + state.createdTrailer.status = REQUEST_STATUS.FULFILLED; + }, + [createTrailer.rejected]: (state, action) => { + state.createdTrailer.data = undefined; + state.createdTrailer.error = action.payload; + state.createdTrailer.status = REQUEST_STATUS.REJECTED; + }, + [fetchTrailer.pending]: (state) => { + state.currentTrailer.error = undefined; + state.currentTrailer.status = REQUEST_STATUS.PENDING; + }, + [fetchTrailer.fulfilled]: (state, action) => { + state.currentTrailer.data = action.payload; + state.currentTrailer.error = undefined; + state.currentTrailer.status = REQUEST_STATUS.FULFILLED; + state.currentTrailer.mode = LICENCE_MODE.VIEW; + }, + [fetchTrailer.rejected]: (state, action) => { + state.currentTrailer.data = undefined; + state.currentTrailer.error = action.payload; + state.currentTrailer.status = REQUEST_STATUS.REJECTED; + }, + [updateTrailer.pending]: (state) => { + state.currentTrailer.error = undefined; + state.currentTrailer.status = REQUEST_STATUS.PENDING; + }, + [updateTrailer.fulfilled]: (state, action) => { + state.currentTrailer.data = action.payload; + state.currentTrailer.error = undefined; + state.currentTrailer.status = REQUEST_STATUS.FULFILLED; + state.currentTrailer.mode = LICENCE_MODE.VIEW; + }, + [updateTrailer.rejected]: (state, action) => { + state.currentTrailer.error = action.payload; + state.currentTrailer.status = REQUEST_STATUS.REJECTED; + }, + }, +}); + +export const selectCreatedTrailer = (state) => state.trailers.createdTrailer; +export const selectCurrentTrailer = (state) => state.trailers.currentTrailer; + +const { actions, reducer } = trailersSlice; + +export const { + clearCreatedTrailer, + clearCurrentTrailer, + setCurrentTrailerModeToEdit, + setCurrentTrailerModeToView, +} = actions; + +export default reducer; diff --git a/app/client/src/utilities/constants.js b/app/client/src/utilities/constants.js index 6ed6970d..087078ca 100644 --- a/app/client/src/utilities/constants.js +++ b/app/client/src/utilities/constants.js @@ -15,6 +15,11 @@ export const CREATE_CONTACTS_PATHNAME = "/contacts/create"; export const INSPECTIONS_PATHNAME = "/inspections"; export const SEARCH_INSPECTIONS_PATHNAME = "/inspections/search"; export const CREATE_INSPECTIONS_PATHNAME = "/inspections/create"; + +export const TRAILER_INSPECTIONS_PATHNAME = "/trailerinspections"; +// export const SEARCH_TRAILER_INSPECTIONS_PATHNAME = "/trailerinspections/search"; // unused +export const CREATE_TRAILER_INSPECTIONS_PATHNAME = "/trailerinspections/create"; + export const DOCUMENT_GENERATION_PATHNAME = "/documents"; export const REPORTS_PATHNAME = "/documents/reports"; export const SELECT_CERTIFICATES_PATHNAME = "/documents/certificates"; @@ -38,6 +43,9 @@ export const LICENSE_TYPES_ADMIN_PATHNAME = "/admin/license-types"; export const SITES_ADMIN_PATHNAME = "/admin/sites"; export const INSPECTIONS_ADMIN_PATHNAME = "/admin/inspections"; +export const TRAILERS_PATHNAME = "/trailers"; +export const CREATE_TRAILERS_PATHNAME = "/trailers/create"; // not used? + export const REQUEST_STATUS = { IDLE: "idle", PENDING: "pending", @@ -144,6 +152,7 @@ export const REPORTS = { DAIRY_TEST_THRESHOLD: "DAIRY_TEST_THRESHOLD", LICENCE_LOCATION: "LICENCE_LOCATION", LICENCE_EXPIRY: "LICENCE_EXPIRY", + DAIRY_TRAILER_INSPECTION: "TRAILER INSPECTION", }; export const SYSTEM_ROLES = { diff --git a/app/prisma/schema.prisma b/app/prisma/schema.prisma index 76b01c81..a774ec77 100644 --- a/app/prisma/schema.prisma +++ b/app/prisma/schema.prisma @@ -374,6 +374,7 @@ model mal_licence { mal_registrant mal_registrant? @relation(fields: [primary_registrant_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "lic_rgst_fk") mal_status_code_lu mal_status_code_lu @relation(fields: [status_code_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "lic_stat_fk") mal_dairy_farm_test_result mal_dairy_farm_test_result[] + mal_dairy_farm_trailer mal_dairy_farm_trailer[] mal_fur_farm_inventory mal_fur_farm_inventory[] mal_game_farm_inventory mal_game_farm_inventory[] mal_licence_comment mal_licence_comment[] @@ -762,16 +763,56 @@ model mal_site { } model mal_status_code_lu { - id Int @id @default(autoincrement()) - code_name String @unique(map: "mal_statcd_code_name_uk") @db.VarChar(50) - code_description String? @db.VarChar(120) - active_flag Boolean - create_userid String @db.VarChar(63) - create_timestamp DateTime @db.Timestamp(6) - update_userid String @db.VarChar(63) - update_timestamp DateTime @db.Timestamp(6) - mal_licence mal_licence[] - mal_site mal_site[] + id Int @id @default(autoincrement()) + code_name String @unique(map: "mal_statcd_code_name_uk") @db.VarChar(50) + code_description String? @db.VarChar(120) + active_flag Boolean + create_userid String @db.VarChar(63) + create_timestamp DateTime @db.Timestamp(6) + update_userid String @db.VarChar(63) + update_timestamp DateTime @db.Timestamp(6) + mal_dairy_farm_trailer mal_dairy_farm_trailer[] + mal_licence mal_licence[] + mal_site mal_site[] +} + +model mal_dairy_farm_trailer { + id Int @id @default(autoincrement()) + licence_id Int + status_code_id Int + licence_trailer_seq Int? + date_issued DateTime? @db.Timestamp(6) + trailer_number String? @db.VarChar(50) + geographical_division String? @db.VarChar(50) + serial_number_vin String? @db.VarChar(50) + license_plate String? @db.VarChar(10) + trailer_year Int? @db.SmallInt + trailer_make String? @db.VarChar(50) + trailer_type String? @db.VarChar(50) + trailer_capacity Int? + trailer_compartments Int? @db.SmallInt + create_userid String @db.VarChar(63) + create_timestamp DateTime @db.Timestamp(6) + update_userid String @db.VarChar(63) + update_timestamp DateTime @db.Timestamp(6) + mal_licence mal_licence @relation(fields: [licence_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "dftr_licence_fk") + mal_status_code_lu mal_status_code_lu @relation(fields: [status_code_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "dftr_status_code_id_fk") + mal_dairy_farm_trailer_inspection mal_dairy_farm_trailer_inspection[] + + @@unique([licence_id, licence_trailer_seq], map: "mal_dairy_farm_trailer_ukey") +} + +model mal_dairy_farm_trailer_inspection { + id Int @id @default(autoincrement()) + trailer_id Int + inspection_date DateTime? @db.Timestamp(6) + inspector_id String? @db.VarChar(128) + inspection_comments String? @db.VarChar(4000) + create_userid String @db.VarChar(63) + create_timestamp DateTime @db.Timestamp(6) + update_userid String @db.VarChar(63) + update_timestamp DateTime @db.Timestamp(6) + mal_dairy_farm_trailer mal_dairy_farm_trailer @relation(fields: [trailer_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "dftri_trailer_fk") } model mal_licence_summary_vw { @@ -861,3 +902,59 @@ model mal_print_dairy_farm_tank_recheck_vw { print_recheck_notice Boolean recheck_notice_json Json } + +model mal_dairy_farm_trailer_vw { + dairy_farm_trailer_id Int @unique + licence_id Int? + licence_number Int? + irma_number String? @db.VarChar(10) + licence_status String? @db.VarChar(50) + company_name String? @db.VarChar(200) + derived_licence_holder_name String? + registrant_last_first String? @db.VarChar + address String? + city String? @db.VarChar(35) + province String? @db.VarChar(4) + postal_code String? @db.VarChar(6) + registrant_primary_phone String? @db.VarChar(10) + registrant_secondary_phone String? @db.VarChar(10) + registrant_fax_number String? @db.VarChar(10) + registrant_email_address String? @db.VarChar(128) + issue_date DateTime? @db.Date + issue_date_display String? + licence_trailer_seq Int? + trailer_number String? @db.VarChar(50) + geographical_division String? @db.VarChar(50) + serial_number_vin String? @db.VarChar(50) + license_plate String? @db.VarChar(10) + trailer_year Int? @db.SmallInt + trailer_make String? @db.VarChar(50) + trailer_type String? @db.VarChar(50) + trailer_capacity Int? + trailer_compartments Int? @db.SmallInt +} + +model mal_dairy_farm_trailer_inspection_vw { + dairy_farm_trailer_inspection_id Int @unique + dairy_farm_trailer_id Int + licence_id Int? + licence_number Int? + irma_number String? @db.VarChar(10) + licence_type String? + licence_status String? + company_name String? + licence_trailer_seq Int? + trailer_number String? + trailer_status String? + geographical_division String? + serial_number_vin String? + license_plate String? + trailer_year Int? @db.SmallInt + trailer_make String? + trailer_type String? + trailer_capacity Int? + trailer_compartments Int? @db.SmallInt + inspection_date DateTime? + inspector_id String? + inspection_comments String? +} \ No newline at end of file diff --git a/app/prisma/views.prisma b/app/prisma/views.prisma index dbe88b91..995d8ddd 100644 --- a/app/prisma/views.prisma +++ b/app/prisma/views.prisma @@ -86,3 +86,35 @@ model mal_print_dairy_farm_tank_recheck_vw { print_recheck_notice Boolean recheck_notice_json Json } + +model mal_dairy_farm_trailer_vw { + dairy_farm_trailer_id Int @unique + licence_id Int? + licence_number Int? + irma_number String? @db.VarChar(10) + licence_status String? @db.VarChar(50) + company_name String? @db.VarChar(200) + derived_licence_holder_name String? + registrant_last_first String? @db.VarChar + address String? + city String? @db.VarChar(35) + province String? @db.VarChar(4) + postal_code String? @db.VarChar(6) + registrant_primary_phone String? @db.VarChar(10) + registrant_secondary_phone String? @db.VarChar(10) + registrant_fax_number String? @db.VarChar(10) + registrant_email_address String? @db.VarChar(128) + issue_date DateTime? @db.Date + issue_date_display String? + licence_trailer_seq Int? + trailer_number String? @db.VarChar(50) + geographical_division String? @db.VarChar(50) + serial_number_vin String? @db.VarChar(50) + license_plate String? @db.VarChar(10) + trailer_year Int? @db.SmallInt + trailer_make String? @db.VarChar(50) + trailer_type String? @db.VarChar(50) + trailer_capacity Int? + trailer_compartments Int? @db.SmallInt + trailer_active_flag Boolean? +} \ No newline at end of file diff --git a/app/server/models/inspection.js b/app/server/models/inspection.js index eaad1aa6..fa6b36c9 100644 --- a/app/server/models/inspection.js +++ b/app/server/models/inspection.js @@ -59,7 +59,35 @@ function convertApiaryInspectionToPhysicalModel(input) { return output; } +function convertTrailerInspectionToLogicalModel(input) { + const output = { + id: input.id, + trailerId: input.trailer_id, + inspectionDate: formatDate(input.inspection_date), + inspectorId: input.inspector_id, + inspectionComment: input.inspection_comments, + }; + + return output; +} + +function convertTrailerInspectionToPhysicalModel(input) { + const output = { + trailer_id: input.trailerId, + inspection_date: input.inspectionDate, + inspector_id: input.inspectorId, + inspection_comments: input.inspectionComment, + create_userid: input.createdBy, + create_timestamp: input.createdOn, + update_userid: input.updatedBy, + update_timestamp: input.updatedOn, + }; + return output; +} + module.exports = { convertApiaryInspectionToLogicalModel, convertApiaryInspectionToPhysicalModel, + convertTrailerInspectionToLogicalModel, + convertTrailerInspectionToPhysicalModel, }; diff --git a/app/server/models/site.js b/app/server/models/site.js index 3bcc1425..257ee8c0 100644 --- a/app/server/models/site.js +++ b/app/server/models/site.js @@ -60,9 +60,9 @@ function convertToLogicalModel(input) { dairyTanks: input.mal_dairy_farm_tank ? input.mal_dairy_farm_tank.map((xref, index) => ({ - ...dairyTank.convertToLogicalModel(xref), - key: index, - })) + ...dairyTank.convertToLogicalModel(xref), + key: index, + })) : null, inspections: [], @@ -152,8 +152,8 @@ function convertToPhysicalModel(input, update) { input.region === null ? emptyRegion : { - connect: { id: input.region }, - }, + connect: { id: input.region }, + }, mal_status_code_lu: { connect: { id: input.siteStatus }, }, @@ -161,8 +161,8 @@ function convertToPhysicalModel(input, update) { input.regionalDistrict === null ? emptyRegionalDistrict : { - connect: { id: input.regionalDistrict }, - }, + connect: { id: input.regionalDistrict }, + }, address_line_1: input.addressLine1, address_line_2: input.addressLine2, city: input.city, diff --git a/app/server/models/trailer.js b/app/server/models/trailer.js new file mode 100644 index 00000000..14b55a64 --- /dev/null +++ b/app/server/models/trailer.js @@ -0,0 +1,119 @@ +const { formatDate } = require("../utilities/formatting"); +const { parseAsInt, parseAsDate } = require("../utilities/parsing"); + +function convertToLogicalModel(input) { + const output = { + id: input.id, + licenceId: input.licence_id, + + licenceNumber: input.licence_number, + irmaNumber: input.irma_number, + licenceStatus: + input.mal_status_code_lu == null + ? null + : input.mal_status_code_lu.code_description, + licenceStatusId: input.status_code_id, + companyName: input.company_name, + derivedLicenceHolderName: input.derived_licence_holder_name, + registrantLastFirst: input.registrant_last_first, + address: input.address, + city: input.city, + province: input.province, + postalCode: input.postal_code, + registrantPrimaryPhone: input.registrant_primary_phone, + registrantSecondaryPhone: input.registrant_secondary_phone, + registrantFaxNumber: input.registrant_fax_number, + registrantEmailAddress: input.registrant_email_address, + dateIssued: input.date_issued ? formatDate(input.date_issued) : "", + issueDateDisplay: input.issue_date_display, + trailerNumber: input.trailer_number, + licenceTrailerSeq: input.licence_trailer_seq, + geographicalDivision: input.geographical_division, + serialNumberVIN: input.serial_number_vin, + licensePlate: input.license_plate, + trailerYear: input.trailer_year, + trailerMake: input.trailer_make, + trailerType: input.trailer_type, + trailerCapacity: input.trailer_capacity, + trailerCompartments: input.trailer_compartments, + createdBy: input.create_userid, + createdOn: input.create_timestamp, + updatedBy: input.update_userid, + updatedOn: input.update_timestamp, + + inspections: [], + }; + + return output; +} + +function convertSearchResultToLogicalModel(input) { + const output = { + dairyFarmTrailerId: input.dairy_farm_trailer_id, + licenceId: input.licence_id, + licenceNumber: input.licence_number, + irmaNumber: input.irma_number, + licenceStatus: input.licence_status, + licenceStatusId: input.licence_status_id, + companyName: input.company_name, + derivedLicenceHolderName: input.derived_licence_holder_name, + registrantLastFirst: input.registrant_last_first, + address: input.address, + city: input.city, + province: input.province, + postalCode: input.postal_code, + registrantPrimaryPhone: input.registrant_primary_phone, + registrantSecondaryPhone: input.registrant_secondary_phone, + registrantFaxNumber: input.registrant_fax_number, + registrantEmailAddress: input.registrant_email_address, + dateIssued: input.date_issued, + dateIssuedDisplay: input.date_issued_display, // ? + licenceTrailerSeq: input.licence_trailer_seq, + trailerNumber: input.trailer_number, + licenceTrailerSeq: input.licence_trailer_seq, + geographicalDivision: input.geographical_division, + serialNumberVin: input.serial_number_vin, + licensePlate: input.license_plate, + trailerYear: input.trailer_year, + trailerMake: input.trailer_make, + trailerType: input.trailer_type, + trailerCapacity: input.trailer_capacity, + trailerCompartments: input.trailer_compartments, + }; + + return output; +} + +function convertToPhysicalModel(input, update) { + const output = { + mal_licence: { + connect: { id: input.licenceId }, + }, + mal_status_code_lu: { + connect: { id: input.licenceStatus }, + }, + trailer_number: input.trailerNumber, + licence_trailer_seq: input.licenceTrailerSeq, + date_issued: parseAsDate(input.dateIssued), + geographical_division: input.geographicalDivision, + serial_number_vin: input.serialNumberVIN, + license_plate: input.licensePlate, + trailer_year: input.trailerYear, + trailer_make: input.trailerMake, + trailer_type: input.trailerType, + trailer_capacity: input.trailerCapacity, + trailer_compartments: input.trailerCompartments, + create_userid: input.createdBy, + create_timestamp: input.createdOn, + update_userid: input.updatedBy, + update_timestamp: input.updatedOn, + }; + + return output; +} + +module.exports = { + convertToPhysicalModel, + convertSearchResultToLogicalModel, + convertToLogicalModel, +}; diff --git a/app/server/routes/v1/documents.js b/app/server/routes/v1/documents.js index 668b81e4..21006263 100644 --- a/app/server/routes/v1/documents.js +++ b/app/server/routes/v1/documents.js @@ -31,12 +31,18 @@ const certificateTemplateDir = path.join( __dirname, "../../static/templates/certificates" ); -const renewalsTemplateDir = path.join(__dirname, "../../static/templates/notices"); +const renewalsTemplateDir = path.join( + __dirname, + "../../static/templates/notices" +); const dairyNoticeTemplateDir = path.join( __dirname, "../../static/templates/notices/dairy" ); -const reportsTemplateDir = path.join(__dirname, "../../static/templates/reports"); +const reportsTemplateDir = path.join( + __dirname, + "../../static/templates/reports" +); // As templates are converted to base 64 for the first time they will be pushed to this for reuse const templateBuffers = []; @@ -61,7 +67,6 @@ cdogs.interceptors.request.use( ) ); - async function getPendingDocuments(jobId) { const documents = await prisma.mal_print_job_output.findMany({ where: { document_binary: null, print_job_id: jobId }, @@ -286,6 +291,99 @@ async function generateCertificate(documentId) { const template = templateBuffers.find( (x) => x.templateFileName === templateFileName ); + // check if certificate is CARD (3 templates), if so, split into two arrays + if ( + document.document_type === "CARD" && + (document.licence_type === "BULK TANK MILK GRADER" || + document.licence_type === "LIVESTOCK DEALER" || + document.licence_type === "LIVESTOCK DEALER AGENT") + ) { + /** + * The Card templates have a table that is 2 columns by N rows, this function effectively combines + * those two columns into one so that we don't have to deal with bi-directional looping + * (which cdogs doesn't support yet) */ + + function combineEntries(licenceType, documentJson) { + let combinedJson = []; + switch (licenceType) { + case "BULK TANK MILK GRADER": + for (let i = 0; i < documentJson.length; i += 2) { + let combinedEntry = { ...documentJson[i] }; + if (documentJson[i + 1]) { + combinedEntry.LicenceHolderName2 = + documentJson[i + 1].LicenceHolderName; + combinedEntry.LicenceHolderCompany2 = + documentJson[i + 1].LicenceHolderCompany; + combinedEntry.LicenceNumber2 = documentJson[i + 1].LicenceNumber; + combinedEntry.ExpiryDate2 = documentJson[i + 1].ExpiryDate; + combinedEntry.CardLabel2 = documentJson[i + 1].CardLabel; + } else { + combinedEntry.LicenceHolderName2 = null; + combinedEntry.LicenceHolderCompany2 = null; + combinedEntry.LicenceNumber2 = null; + combinedEntry.ExpiryDate2 = null; + combinedEntry.CardLabel2 = null; + } + combinedJson.push(combinedEntry); + } + return combinedJson; + case "LIVESTOCK DEALER AGENT": + for (let i = 0; i < documentJson.length; i += 2) { + let combinedEntry = { ...documentJson[i] }; + if (documentJson[i + 1]) { + combinedEntry.CardType2 = documentJson[i + 1].CardType; + combinedEntry.LicenceHolderName2 = + documentJson[i + 1].LicenceHolderName; + combinedEntry.LastFirstName2 = documentJson[i + 1].LastFirstName; + combinedEntry.AgentFor2 = documentJson[i + 1].AgentFor; + combinedEntry.LicenceNumber2 = documentJson[i + 1].LicenceNumber; + combinedEntry.StartDate2 = documentJson[i + 1].StartDate; + combinedEntry.ExpiryDate2 = documentJson[i + 1].ExpiryDate; + } else { + combinedEntry.CardType2 = null; + combinedEntry.LicenceHolderName2 = null; + combinedEntry.LastFirstName2 = null; + combinedEntry.AgentFor2 = null; + combinedEntry.LicenceNumber2 = null; + combinedEntry.StartDate2 = null; + combinedEntry.ExpiryDate2 = null; + } + combinedJson.push(combinedEntry); + } + return combinedJson; + case "LIVESTOCK DEALER": + for (let i = 0; i < documentJson.length; i += 2) { + let combinedEntry = { ...documentJson[i] }; + if (documentJson[i + 1]) { + combinedEntry.CardType2 = documentJson[i + 1].CardType; + combinedEntry.LicenceHolderCompany2 = + documentJson[i + 1].LicenceHolderCompany; + combinedEntry.LicenceNumber2 = documentJson[i + 1].LicenceNumber; + combinedEntry.StartDate2 = documentJson[i + 1].StartDate; + combinedEntry.ExpiryDate2 = documentJson[i + 1].ExpiryDate; + } else { + combinedEntry.CardType2 = null; + combinedEntry.LicenceHolderCompany2 = null; + combinedEntry.LicenceNumber2 = null; + combinedEntry.StartDate2 = null; + combinedEntry.ExpiryDate2 = null; + } + combinedJson.push(combinedEntry); + } + return combinedJson; + default: + return null; + } + } + + let updatedJson = combineEntries( + document.licence_type, + document.document_json + ); + + document.document_json = updatedJson; + } + const generate = async () => { const { data, status } = await cdogs.post( "template/render", @@ -363,7 +461,7 @@ async function startRenewalJob(licenceIds) { }, }; - const [, , procedureResult,] = await prisma.$transaction([ + const [, , procedureResult] = await prisma.$transaction([ // ensure selected licences have print_renewal set to true prisma.mal_licence.updateMany({ where: licenceFilterCriteria, @@ -463,7 +561,6 @@ async function generateRenewal(documentId) { } async function getQueuedDairyNotices(startDate, endDate) { - const andArray = []; andArray.push({ recorded_date: { gte: new Date(startDate) } }); andArray.push({ recorded_date: { lte: new Date(endDate) } }); @@ -586,7 +683,6 @@ async function generateDairyNotice(documentId) { return result; } - async function getQueuedDairyTankNotices() { return prisma.mal_print_dairy_farm_tank_recheck_vw.findMany({ where: { @@ -730,6 +826,18 @@ async function startApiaryHiveInspectionJob(startDate, endDate) { return { jobId, documents }; } +async function startDairyTrailerInspectionJob(licenceNumber) { + const [procedureResult] = await prisma.$transaction([ + prisma.$queryRawUnsafe( + `CALL mals_app.pr_generate_print_json_dairy_farm_trailer_inspection('${licenceNumber}', NULL)` + ), + ]); + + const jobId = procedureResult[0].iop_print_job_id; + const documents = await getPendingDocuments(jobId); + return { jobId, documents }; +} + async function startProducersAnalysisRegionJob() { const [procedureResult] = await prisma.$transaction([ prisma.$queryRawUnsafe( @@ -862,7 +970,6 @@ async function startLicenceExpiryJob(startDate, endDate) { async function generateReport(documentId) { const document = await getDocument(documentId); - const templateFileName = getReportsTemplateName(document.document_type); if (templateFileName === undefined) { @@ -1208,6 +1315,24 @@ router.post( } ); +router.post( + "/reports/startJob/dairyTrailerInspection", + async (req, res, next) => { + const licenceNumber = parseAsInt(req.body.licenceNumber); + + await startDairyTrailerInspectionJob(licenceNumber) + .then(({ jobId, documents }) => { + return res.send({ + jobId, + documents, + type: REPORTS.DAIRY_TRAILER_INSPECTION, + }); + }) + .catch(next) + .finally(async () => prisma.$disconnect()); + } +); + router.post( "/reports/startJob/producersAnalysisRegion", async (req, res, next) => { diff --git a/app/server/routes/v1/index.js b/app/server/routes/v1/index.js index daceb284..dd86ebfb 100644 --- a/app/server/routes/v1/index.js +++ b/app/server/routes/v1/index.js @@ -1,13 +1,13 @@ -const router = require('express').Router(); +const router = require("express").Router(); const httpContext = require("express-http-context"); -const { currentUser } = require('../../middleware/authentication'); - +const { currentUser } = require("../../middleware/authentication"); const userRouter = require("./user"); const licenceTypesRouter = require("./licenceTypes"); const licenceStatusesRouter = require("./licenceStatuses"); const licencesRouter = require("./licences"); const sitesRouter = require("./sites"); +const trailersRouter = require("./trailers"); const regionalDistrictsRouter = require("./regionalDistricts"); const regionsRouter = require("./regions"); const commentsRouter = require("./comments"); @@ -24,171 +24,182 @@ const roleValidation = require("../../middleware/roleValidation"); router.use(currentUser); router.use((req, res, next) => { - if (req.currentUser) { - httpContext.set("currentUser", req.currentUser); - } - next(); + if (req.currentUser) { + httpContext.set("currentUser", req.currentUser); + } + next(); }); // Base v1 Responder -router.get('/', (_req, res) => { - res.status(200).json({ - endpoints: [ - '/user', - '/licence-types', - '/licence-statuses', - '/licences', - '/sites', - '/regional-districts', - '/regions', - '/config', - '/comments', - '/licence-species', - '/slaughterhouse-species', - '/documents', - '/cities', - '/dairyfarmtestthresholds', - '/inspections', - '/admin' - ] - }); +router.get("/", (_req, res) => { + res.status(200).json({ + endpoints: [ + "/user", + "/licence-types", + "/licence-statuses", + "/licences", + "/sites", + "/trailers", + "/regional-districts", + "/regions", + "/config", + "/comments", + "/licence-species", + "/slaughterhouse-species", + "/documents", + "/cities", + "/dairyfarmtestthresholds", + "/inspections", + "/admin", + ], + }); }); router.use("/user", userRouter); router.use( - "/licence-types", - roleValidation([ - constants.SYSTEM_ROLES.READ_ONLY, - constants.SYSTEM_ROLES.USER, - constants.SYSTEM_ROLES.INSPECTOR, - constants.SYSTEM_ROLES.SYSTEM_ADMIN, - ]), - licenceTypesRouter + "/licence-types", + roleValidation([ + constants.SYSTEM_ROLES.READ_ONLY, + constants.SYSTEM_ROLES.USER, + constants.SYSTEM_ROLES.INSPECTOR, + constants.SYSTEM_ROLES.SYSTEM_ADMIN, + ]), + licenceTypesRouter +); +router.use( + "/licence-statuses", + roleValidation([ + constants.SYSTEM_ROLES.READ_ONLY, + constants.SYSTEM_ROLES.USER, + constants.SYSTEM_ROLES.INSPECTOR, + constants.SYSTEM_ROLES.SYSTEM_ADMIN, + ]), + licenceStatusesRouter ); router.use( - "/licence-statuses", - roleValidation([ - constants.SYSTEM_ROLES.READ_ONLY, - constants.SYSTEM_ROLES.USER, - constants.SYSTEM_ROLES.INSPECTOR, - constants.SYSTEM_ROLES.SYSTEM_ADMIN, - ]), - licenceStatusesRouter + "/licences", + roleValidation([ + constants.SYSTEM_ROLES.READ_ONLY, + constants.SYSTEM_ROLES.USER, + constants.SYSTEM_ROLES.INSPECTOR, + constants.SYSTEM_ROLES.SYSTEM_ADMIN, + ]), + licencesRouter ); router.use( - "/licences", - roleValidation([ - constants.SYSTEM_ROLES.READ_ONLY, - constants.SYSTEM_ROLES.USER, - constants.SYSTEM_ROLES.INSPECTOR, - constants.SYSTEM_ROLES.SYSTEM_ADMIN, - ]), - licencesRouter + "/sites", + roleValidation([ + constants.SYSTEM_ROLES.READ_ONLY, + constants.SYSTEM_ROLES.USER, + constants.SYSTEM_ROLES.INSPECTOR, + constants.SYSTEM_ROLES.SYSTEM_ADMIN, + ]), + sitesRouter ); router.use( - "/sites", - roleValidation([ - constants.SYSTEM_ROLES.READ_ONLY, - constants.SYSTEM_ROLES.USER, - constants.SYSTEM_ROLES.INSPECTOR, - constants.SYSTEM_ROLES.SYSTEM_ADMIN, - ]), - sitesRouter + "/trailers", + roleValidation([ + constants.SYSTEM_ROLES.READ_ONLY, + constants.SYSTEM_ROLES.USER, + constants.SYSTEM_ROLES.INSPECTOR, + constants.SYSTEM_ROLES.SYSTEM_ADMIN, + ]), + trailersRouter ); router.use( - "/regional-districts", - roleValidation([ - constants.SYSTEM_ROLES.READ_ONLY, - constants.SYSTEM_ROLES.USER, - constants.SYSTEM_ROLES.INSPECTOR, - constants.SYSTEM_ROLES.SYSTEM_ADMIN, - ]), - regionalDistrictsRouter + "/regional-districts", + roleValidation([ + constants.SYSTEM_ROLES.READ_ONLY, + constants.SYSTEM_ROLES.USER, + constants.SYSTEM_ROLES.INSPECTOR, + constants.SYSTEM_ROLES.SYSTEM_ADMIN, + ]), + regionalDistrictsRouter ); router.use( - "/regions", - roleValidation([ - constants.SYSTEM_ROLES.READ_ONLY, - constants.SYSTEM_ROLES.USER, - constants.SYSTEM_ROLES.INSPECTOR, - constants.SYSTEM_ROLES.SYSTEM_ADMIN, - ]), - regionsRouter + "/regions", + roleValidation([ + constants.SYSTEM_ROLES.READ_ONLY, + constants.SYSTEM_ROLES.USER, + constants.SYSTEM_ROLES.INSPECTOR, + constants.SYSTEM_ROLES.SYSTEM_ADMIN, + ]), + regionsRouter ); router.use( - "/comments", - roleValidation([ - constants.SYSTEM_ROLES.READ_ONLY, - constants.SYSTEM_ROLES.USER, - constants.SYSTEM_ROLES.INSPECTOR, - constants.SYSTEM_ROLES.SYSTEM_ADMIN, - ]), - commentsRouter.router + "/comments", + roleValidation([ + constants.SYSTEM_ROLES.READ_ONLY, + constants.SYSTEM_ROLES.USER, + constants.SYSTEM_ROLES.INSPECTOR, + constants.SYSTEM_ROLES.SYSTEM_ADMIN, + ]), + commentsRouter.router ); router.use( - "/licence-species", - roleValidation([ - constants.SYSTEM_ROLES.READ_ONLY, - constants.SYSTEM_ROLES.USER, - constants.SYSTEM_ROLES.INSPECTOR, - constants.SYSTEM_ROLES.SYSTEM_ADMIN, - ]), - licenceSpeciesRouter + "/licence-species", + roleValidation([ + constants.SYSTEM_ROLES.READ_ONLY, + constants.SYSTEM_ROLES.USER, + constants.SYSTEM_ROLES.INSPECTOR, + constants.SYSTEM_ROLES.SYSTEM_ADMIN, + ]), + licenceSpeciesRouter ); router.use( - "/slaughterhouse-species", - roleValidation([ - constants.SYSTEM_ROLES.READ_ONLY, - constants.SYSTEM_ROLES.USER, - constants.SYSTEM_ROLES.INSPECTOR, - constants.SYSTEM_ROLES.SYSTEM_ADMIN, - ]), - slaughterhouseSpeciesRouter + "/slaughterhouse-species", + roleValidation([ + constants.SYSTEM_ROLES.READ_ONLY, + constants.SYSTEM_ROLES.USER, + constants.SYSTEM_ROLES.INSPECTOR, + constants.SYSTEM_ROLES.SYSTEM_ADMIN, + ]), + slaughterhouseSpeciesRouter ); router.use( - "/documents", - roleValidation([ - constants.SYSTEM_ROLES.READ_ONLY, - constants.SYSTEM_ROLES.USER, - constants.SYSTEM_ROLES.INSPECTOR, - constants.SYSTEM_ROLES.SYSTEM_ADMIN, - ]), - documentsRouter + "/documents", + roleValidation([ + constants.SYSTEM_ROLES.READ_ONLY, + constants.SYSTEM_ROLES.USER, + constants.SYSTEM_ROLES.INSPECTOR, + constants.SYSTEM_ROLES.SYSTEM_ADMIN, + ]), + documentsRouter ); router.use( - "/cities", - roleValidation([ - constants.SYSTEM_ROLES.READ_ONLY, - constants.SYSTEM_ROLES.USER, - constants.SYSTEM_ROLES.INSPECTOR, - constants.SYSTEM_ROLES.SYSTEM_ADMIN, - ]), - citiesRouter + "/cities", + roleValidation([ + constants.SYSTEM_ROLES.READ_ONLY, + constants.SYSTEM_ROLES.USER, + constants.SYSTEM_ROLES.INSPECTOR, + constants.SYSTEM_ROLES.SYSTEM_ADMIN, + ]), + citiesRouter ); router.use( - "/dairyfarmtestthresholds", - roleValidation([ - constants.SYSTEM_ROLES.READ_ONLY, - constants.SYSTEM_ROLES.USER, - constants.SYSTEM_ROLES.INSPECTOR, - constants.SYSTEM_ROLES.SYSTEM_ADMIN, - ]), - dairyFarmTestThresholdsRouter + "/dairyfarmtestthresholds", + roleValidation([ + constants.SYSTEM_ROLES.READ_ONLY, + constants.SYSTEM_ROLES.USER, + constants.SYSTEM_ROLES.INSPECTOR, + constants.SYSTEM_ROLES.SYSTEM_ADMIN, + ]), + dairyFarmTestThresholdsRouter ); router.use( - "/inspections", - roleValidation([ - constants.SYSTEM_ROLES.READ_ONLY, - constants.SYSTEM_ROLES.USER, - constants.SYSTEM_ROLES.INSPECTOR, - constants.SYSTEM_ROLES.SYSTEM_ADMIN, - ]), - inspectionsRouter + "/inspections", + roleValidation([ + constants.SYSTEM_ROLES.READ_ONLY, + constants.SYSTEM_ROLES.USER, + constants.SYSTEM_ROLES.INSPECTOR, + constants.SYSTEM_ROLES.SYSTEM_ADMIN, + ]), + inspectionsRouter ); router.use( - "/admin", - roleValidation([constants.SYSTEM_ROLES.SYSTEM_ADMIN]), - adminRouter + "/admin", + roleValidation([constants.SYSTEM_ROLES.SYSTEM_ADMIN]), + adminRouter ); module.exports = router; diff --git a/app/server/routes/v1/inspections.js b/app/server/routes/v1/inspections.js index ef79ded5..ea9967e8 100644 --- a/app/server/routes/v1/inspections.js +++ b/app/server/routes/v1/inspections.js @@ -9,6 +9,7 @@ const inspection = require("../../models/inspection"); const router = express.Router(); const prisma = new PrismaClient(); +/*** Apiary Inspections ***/ async function findApiaryInspection(inspectionId) { return prisma.mal_apiary_inspection.findUnique({ where: { @@ -99,4 +100,96 @@ router.put("/apiary/:inspectionId(\\d+)", async (req, res, next) => { .finally(async () => prisma.$disconnect()); }); +/*** Dairy Trailer Inspections ***/ +async function findTrailerInspection(inspectionId) { + return prisma.mal_dairy_farm_trailer_inspection.findUnique({ + where: { + id: inspectionId, + }, + }); +} + +async function createTrailerInspection(payload) { + return prisma.mal_dairy_farm_trailer_inspection.create({ + data: payload, + }); +} + +async function updateTrailerInspection(inspectionId, payload) { + return prisma.mal_dairy_farm_trailer_inspection.update({ + data: payload, + where: { + id: inspectionId, + }, + }); +} + +router.get("/trailer/:inspectionId(\\d+)", async (req, res, next) => { + const inspectionId = parseInt(req.params.inspectionId, 10); + + await findTrailerInspection(inspectionId) + .then((record) => { + if (record === null) { + return res.status(404).send({ + code: 404, + description: "The requested trailer inspection could not be found.", + }); + } + + const payload = inspection.convertTrailerInspectionToLogicalModel(record); + return res.send(payload); + }) + .catch(next) + .finally(async () => prisma.$disconnect()); +}); + +router.post("/trailer", async (req, res, next) => { + console.log("create trailer route"); + const now = new Date(); + + const payload = inspection.convertTrailerInspectionToPhysicalModel( + populateAuditColumnsCreate(req.body, now, now) + ); + + await createTrailerInspection(payload) + .then((record) => { + if (record === null) { + return res.status(404).send({ + code: 404, + description: "Error while creating trailer inspection.", + }); + } + + const payload = inspection.convertTrailerInspectionToLogicalModel(record); + return res.send(payload); + }) + .catch(next) + .finally(async () => prisma.$disconnect()); +}); + +router.put("/trailer/:inspectionId(\\d+)", async (req, res, next) => { + const inspectionId = parseInt(req.params.inspectionId, 10); + + const now = new Date(); + + const payload = inspection.convertTrailerInspectionToPhysicalModel( + populateAuditColumnsUpdate(req.body, now) + ); + + await updateTrailerInspection(inspectionId, payload) + .then((record) => { + if (record === null) { + return res.status(404).send({ + code: 404, + description: "Error while updating inspection.", + }); + } + + const payload = inspection.convertTrailerInspectionToLogicalModel(record); + return res.send(payload); + }) + .catch(next) + .finally(async () => prisma.$disconnect()); +}); + module.exports = router; diff --git a/app/server/routes/v1/licences.js b/app/server/routes/v1/licences.js index 7e796439..8ae0a2b2 100644 --- a/app/server/routes/v1/licences.js +++ b/app/server/routes/v1/licences.js @@ -64,6 +64,43 @@ async function findLicence(licenceId) { }); } +async function findLicenceByNumber(licenceNumber) { + return prisma.mal_licence.findFirst({ + where: { + licence_number: licenceNumber, + }, + include: { + mal_licence_type_lu: true, + mal_region_lu: true, + mal_regional_district_lu: true, + mal_status_code_lu: true, + mal_licence_registrant_xref: { + select: { + mal_registrant: true, + }, + }, + mal_licence_parent_child_xref_mal_licenceTomal_licence_parent_child_xref_parent_licence_id: { + select: { + create_timestamp: true, + mal_licence_mal_licenceTomal_licence_parent_child_xref_child_licence_id: { + include: { + mal_licence_type_lu: true, + mal_status_code_lu: true, + mal_licence_registrant_xref: { + select: { + mal_registrant: true, + }, + }, + }, + }, + }, + }, + mal_game_farm_inventory: true, + mal_fur_farm_inventory: true, + }, + }); +} + async function findLicenceRegistrantXref(licenceId, registrantId) { if (licenceId === undefined || registrantId === undefined) { return null; @@ -565,6 +602,25 @@ router.get("/:licenceId(\\d+)", async (req, res, next) => { .finally(async () => prisma.$disconnect()); }); +router.get("/number/:licenceNumber(\\d+)", async (req, res, next) => { + const licenceNumber = parseInt(req.params.licenceNumber, 10); + + await findLicenceByNumber(licenceNumber) + .then((record) => { + if (record === null) { + return res.status(404).send({ + code: 404, + description: "The requested licence could not be found.", + }); + } + + const payload = licence.convertToLogicalModel(record); + return res.send(payload); + }) + .catch(next) + .finally(async () => prisma.$disconnect()); +}); + router.put("/:licenceId(\\d+)/associated", async (req, res, next) => { const licenceId = parseInt(req.params.licenceId, 10); @@ -948,8 +1004,10 @@ router.put("/dairyactions/:licenceId(\\d+)", async (req, res, next) => { where: { id: req.body.thresholdId }, }); - if (req.body.thresholdId !== constants.DAIRY_TEST_THRESHOLD_IDS.FFA && // No FFA infractions - req.body.value > threshold.upper_limit.toFixed(2)) { + if ( + req.body.thresholdId !== constants.DAIRY_TEST_THRESHOLD_IDS.FFA && // No FFA infractions + req.body.value > threshold.upper_limit.toFixed(2) + ) { const infractions = await prisma.mal_dairy_farm_test_infraction_lu.findMany( { where: { test_threshold_id: req.body.thresholdId }, @@ -983,13 +1041,17 @@ router.put("/dairyactions/:licenceId(\\d+)", async (req, res, next) => { .length, 3 ); - } else if (req.body.thresholdId === constants.DAIRY_TEST_THRESHOLD_IDS.SCC) { + } else if ( + req.body.thresholdId === constants.DAIRY_TEST_THRESHOLD_IDS.SCC + ) { infractionCount = Math.min( filteredResults.filter((x) => x.sccCorrespondenceDescription !== null) .length, 3 ); - } else if (req.body.thresholdId === constants.DAIRY_TEST_THRESHOLD_IDS.CRY) { + } else if ( + req.body.thresholdId === constants.DAIRY_TEST_THRESHOLD_IDS.CRY + ) { infractionCount = Math.min( filteredResults.filter((x) => x.cryCorrespondenceDescription !== null) .length, @@ -1172,8 +1234,8 @@ router.put("/:licenceId(\\d+)/registrants", async (req, res, next) => { a.createTimestamp > b.createTimestamp ? 1 : b.createTimestamp > a.createTimestamp - ? -1 - : 0 + ? -1 + : 0 ); let newPrimaryRegistrant = undefined; @@ -1301,8 +1363,8 @@ router.post("/", async (req, res, next) => { const newRegistrants = req.body.registrants ? req.body.registrants.filter( - (r) => r && r.status === REGISTRANT_STATUS.NEW - ) + (r) => r && r.status === REGISTRANT_STATUS.NEW + ) : []; req.body.companyName = @@ -1346,8 +1408,8 @@ router.post("/", async (req, res, next) => { a.createTimestamp > b.createTimestamp ? 1 : b.createTimestamp > a.createTimestamp - ? -1 - : 0 + ? -1 + : 0 ); updatedRecordLogical.primaryRegistrantId = registrants[0].id; diff --git a/app/server/routes/v1/sites.js b/app/server/routes/v1/sites.js index 20a3026d..52936dc6 100644 --- a/app/server/routes/v1/sites.js +++ b/app/server/routes/v1/sites.js @@ -10,7 +10,6 @@ const dairyTank = require("../../models/dairyTank"); const inspection = require("../../models/inspection"); const constants = require("../../utilities/constants"); - const router = express.Router(); const prisma = new PrismaClient(); @@ -93,23 +92,59 @@ function getSearchFilter(params) { } if (params.filterText) { - if (params.filterText === 'ACT' || params.filterText === 'INA') { + if (params.filterText === "ACT" || params.filterText === "INA") { andArray.push({ site_status: { contains: params.filterText, - mode: "insensitive" - } - }) + mode: "insensitive", + }, + }); } else { andArray.push({ OR: [ - { site_id_pk: { equals: isNaN(parseInt(params.filterText, 10)) ? 0 : parseInt(params.filterText, 10) } }, // unfortunately site id has to be an exact match - { apiary_site_id_display: { contains: params.filterText, mode: "insensitive" } }, - { registrant_last_name: { contains: params.filterText, mode: "insensitive" } }, - { registrant_first_name: { contains: params.filterText, mode: "insensitive" } }, - { site_address_line_1: { contains: params.filterText, mode: "insensitive" } }, - { licence_region_name: { contains: params.filterText, mode: "insensitive" } }, - { licence_regional_district_name: { contains: params.filterText, mode: "insensitive" } }, + { + site_id_pk: { + equals: isNaN(parseInt(params.filterText, 10)) + ? 0 + : parseInt(params.filterText, 10), + }, + }, // unfortunately site id has to be an exact match + { + apiary_site_id_display: { + contains: params.filterText, + mode: "insensitive", + }, + }, + { + registrant_last_name: { + contains: params.filterText, + mode: "insensitive", + }, + }, + { + registrant_first_name: { + contains: params.filterText, + mode: "insensitive", + }, + }, + { + site_address_line_1: { + contains: params.filterText, + mode: "insensitive", + }, + }, + { + licence_region_name: { + contains: params.filterText, + mode: "insensitive", + }, + }, + { + licence_regional_district_name: { + contains: params.filterText, + mode: "insensitive", + }, + }, ], }); } @@ -299,14 +334,15 @@ router.post("/search/export", async (req, res, next) => { "Site ID,Registrant Name,Company Name,Licence Number,City,Region,District,Next Inspection Date\n"; const values = results .map((x) => { - return `${x.apiarySiteIdDisplay ? x.apiarySiteIdDisplay : x.siteId - },${formatValue(x.registrantLastName)},${formatValue( - x.registrantCompanyName - )},${formatValue(x.licenceNumber)},${formatValue( - x.licenceCity - )},${formatValue(x.licenceRegion)},${formatValue( - x.licenceDistrict - )},${formatValue(x.nextInspectionDate)}`; + return `${ + x.apiarySiteIdDisplay ? x.apiarySiteIdDisplay : x.siteId + },${formatValue(x.registrantLastName)},${formatValue( + x.registrantCompanyName + )},${formatValue(x.licenceNumber)},${formatValue( + x.licenceCity + )},${formatValue(x.licenceRegion)},${formatValue( + x.licenceDistrict + )},${formatValue(x.nextInspectionDate)}`; }) .join("\n"); const payload = columnHeaders.concat(values); diff --git a/app/server/routes/v1/trailers.js b/app/server/routes/v1/trailers.js new file mode 100644 index 00000000..12a89a5c --- /dev/null +++ b/app/server/routes/v1/trailers.js @@ -0,0 +1,231 @@ +const express = require("express"); +const { PrismaClient } = require("@prisma/client"); + +const { + populateAuditColumnsCreate, + populateAuditColumnsUpdate, +} = require("../../utilities/auditing"); +const trailer = require("../../models/trailer"); +const inspection = require("../../models/inspection"); +const constants = require("../../utilities/constants"); + +const router = express.Router(); +const prisma = new PrismaClient(); + +function getSearchFilter(params) { + let filter = {}; + + const andArray = []; + const licenceTypeId = parseInt(params.licenceType, 10); + const licenceNumber = parseInt(params.licenceNumber, 10); + + if (!Number.isNaN(licenceTypeId)) { + andArray.push({ licence_type_id: licenceTypeId }); + } + + if (!Number.isNaN(licenceNumber)) { + andArray.push({ licence_number: licenceNumber }); + } + + filter = { + AND: andArray, + }; + + return filter; +} + +async function countTrailers(params) { + const filter = getSearchFilter(params); + return prisma.mal_dairy_farm_trailer_vw.count({ + where: filter, + }); +} + +async function searchTrailers(params, skip, take) { + const filter = getSearchFilter(params); + return prisma.mal_dairy_farm_trailer_vw.findMany({ + where: filter, + skip, + take, + }); +} + +async function findTrailersByLicenceId(licenceId) { + return prisma.mal_dairy_farm_trailer.findMany({ + where: { + licence_id: licenceId, + }, + include: { + mal_status_code_lu: true, + mal_licence: true, + }, + }); +} + +async function findTrailer(trailerId) { + return prisma.mal_dairy_farm_trailer.findUnique({ + where: { + id: trailerId, + }, + include: { + mal_status_code_lu: true, + }, + }); +} + +async function updateTrailer(trailerId, payload) { + return prisma.mal_dairy_farm_trailer.update({ + data: payload, + where: { + id: trailerId, + }, + include: { + mal_status_code_lu: true, + }, + }); +} + +async function createTrailer(payload) { + return prisma.mal_dairy_farm_trailer.create({ + data: payload, + }); +} + +// Search for trailers (will always be by licence id) +router.get("/search", async (req, res, next) => { + let { page } = req.query; + if (page) { + page = parseInt(page, 10); + } else { + page = 1; + } + + const size = 20; + const skip = (page - 1) * size; + + const params = req.query; + + await searchTrailers(params, skip, size) + .then(async (records) => { + if (records === null) { + return res.status(404).send({ + code: 404, + description: "The requested trailer could not be found.", + }); + } + + const results = records.map((record) => + trailer.convertSearchResultToLogicalModel(record) + ); + + const count = await countTrailers(params); + + const payload = { + results, + page, + count, + }; + + return res.send(payload); + }) + .catch(next) + .finally(async () => prisma.$disconnect()); +}); + +// Get trailer by id +router.get("/:trailerId(\\d+)", async (req, res, next) => { + const trailerId = parseInt(req.params.trailerId, 10); + + await findTrailer(trailerId) + .then(async (record) => { + if (record === null) { + return res.status(404).send({ + code: 404, + description: "The requested trailer could not be found.", + }); + } + + const payload = trailer.convertToLogicalModel(record); + + // Grab inspections since they aren't linked by FKs + if (payload.licenceTrailerSeq !== null) { + const trailerInspections = await prisma.mal_dairy_farm_trailer_inspection.findMany( + { + where: { + trailer_id: payload.id, + }, + } + ); + + payload.inspections = trailerInspections.map((xref, index) => ({ + ...inspection.convertTrailerInspectionToLogicalModel(xref), + key: index, + })); + } + + return res.send(payload); + }) + .catch(next) + .finally(async () => prisma.$disconnect()); +}); + +// Update Trailer +router.put("/:trailerId(\\d+)", async (req, res, next) => { + const trailerId = parseInt(req.params.trailerId, 10); + const now = new Date(); + const trailerPayload = trailer.convertToPhysicalModel( + populateAuditColumnsUpdate(req.body, now), + true + ); + + await updateTrailer(trailerId, trailerPayload) + .then(async (record) => { + if (record === null) { + return res.status(404).send({ + code: 404, + description: "The requested trailer could not be found.", + }); + } + + const payload = trailer.convertToLogicalModel(record); + return res.send(payload); + }) + .catch(next) + .finally(async () => prisma.$disconnect()); +}); + +// Create Trailer +router.post("/", async (req, res, next) => { + const now = new Date(); + const data = req.body; + + if (data.licenceTypeId === constants.LICENCE_TYPE_ID_DAIRY_TANK_TRUCK) { + const trailers = await findTrailersByLicenceId(data.licenceId); + if (trailers === null || trailers === undefined || trailers.length === 0) { + data.licenceTrailerSeq = 100; + } else { + const high = Math.max.apply( + Math, + trailers.map(function (o) { + return o.licence_trailer_seq; + }) + ); + const next = high + 1; + data.licenceTrailerSeq = next; + } + } + + const trailerPayload = trailer.convertToPhysicalModel( + populateAuditColumnsCreate(data, now, now), + false + ); + + await createTrailer(trailerPayload) + .then(async (record) => { + return res.send({ id: record.id }); + }) + .catch(next) + .finally(async () => prisma.$disconnect()); +}); + +module.exports = router; diff --git a/app/server/server.js b/app/server/server.js index 1d57586c..de2e013c 100644 --- a/app/server/server.js +++ b/app/server/server.js @@ -1,7 +1,7 @@ require("dotenv").config(); const express = require("express"); -const Problem = require('api-problem'); +const Problem = require("api-problem"); const httpContext = require("express-http-context"); const path = require("path"); const cookieParser = require("cookie-parser"); @@ -9,14 +9,14 @@ const logger = require("morgan"); const helmet = require("helmet"); const cors = require("cors"); -const appRouter = require('./routes/v1'); -const { Error, Log, getGitRevision } = require('./utilities/util'); +const appRouter = require("./routes/v1"); +const { Error, Log, getGitRevision } = require("./utilities/util"); const apiRouter = express.Router(); const state = { gitRev: getGitRevision(), ready: false, - shutdown: false + shutdown: false, }; const app = express(); @@ -43,7 +43,7 @@ app.use( "*.oidc.gov.bc.ca", "oidc.gov.bc.ca", "loginproxy.gov.bc.ca", - "*.loginproxy.gov.bc.ca/" + "*.loginproxy.gov.bc.ca/", ], }, }, @@ -56,9 +56,11 @@ app.use(function (req, res, next) { next(); }); -app.use(cors({ - origin: true // Set true to dynamically set Access-Control-Allow-Origin based on Origin -})); +app.use( + cors({ + origin: true, // Set true to dynamically set Access-Control-Allow-Origin based on Origin + }) +); app.use(logger("dev")); app.use(express.json({ limit: "50mb" })); app.use(express.urlencoded({ extended: false, limit: "50mb" })); @@ -66,7 +68,7 @@ app.use(cookieParser()); app.use(httpContext.middleware); // Skip if running tests -if (process.env.NODE_ENV !== 'test') { +if (process.env.NODE_ENV !== "test") { // Initialize connections and exit if unsuccessful initializeConnections(); } @@ -74,9 +76,9 @@ if (process.env.NODE_ENV !== 'test') { // Block requests until service is ready app.use((_req, res, next) => { if (state.shutdown) { - new Problem(503, { details: 'Server is shutting down' }).send(res); + new Problem(503, { details: "Server is shutting down" }).send(res); } else if (!state.ready) { - new Problem(503, { details: 'Server is not ready' }).send(res); + new Problem(503, { details: "Server is not ready" }).send(res); } else { next(); } @@ -92,11 +94,11 @@ apiRouter.use("/v1/config", (req, res) => { const response = { environment: process.env.ENVIRONMENT_LABEL || "dev", nodeVersion: process.version, - version: process.env.npm_package_version + version: process.env.npm_package_version, }; return res.send(response); }); -apiRouter.use('/v1', appRouter); +apiRouter.use("/v1", appRouter); // Root level Router app.use(/(\/api)?/, apiRouter); @@ -139,12 +141,12 @@ app.use(function handleError(error, req, res, next) { }); // Graceful shutdown support -process.on('SIGTERM', shutdown); -process.on('SIGINT', shutdown); -process.on('SIGUSR1', shutdown); -process.on('SIGUSR2', shutdown); -process.on('exit', () => { - Log('Exiting...'); +process.on("SIGTERM", shutdown); +process.on("SIGINT", shutdown); +process.on("SIGUSR1", shutdown); +process.on("SIGUSR2", shutdown); +process.on("exit", () => { + Log("Exiting..."); }); /** @@ -152,10 +154,10 @@ process.on('exit', () => { * Cleans up connections in this application. */ function cleanup() { - Log('Service no longer accepting traffic'); + Log("Service no longer accepting traffic"); state.shutdown = true; - Log('Cleaning up...'); + Log("Cleaning up..."); // Wait 10 seconds max before hard exiting setTimeout(() => process.exit(), 10000); @@ -166,7 +168,7 @@ function cleanup() { * Shuts down this application after at least 3 seconds. */ function shutdown() { - Log('Received kill signal. Shutting down...'); + Log("Received kill signal. Shutting down..."); // Wait 3 seconds before starting cleanup if (!state.shutdown) setTimeout(cleanup, 3000); } @@ -179,19 +181,17 @@ function shutdown() { function initializeConnections() { try { // Empty block - } - catch (error) { - Error('Connection initialization failure'); + } catch (error) { + Error("Connection initialization failure"); Error(error.message); if (!state.ready) { process.exitCode = 1; shutdown(); } - } - finally { + } finally { state.ready = true; if (state.ready) { - Log('Service ready to accept traffic'); + Log("Service ready to accept traffic"); } } } diff --git a/app/server/static/templates/certificates/Bulk-Tank-Milk-Grader-Card.docx b/app/server/static/templates/certificates/Bulk-Tank-Milk-Grader-Card.docx index cc21831b..3a16c908 100644 Binary files a/app/server/static/templates/certificates/Bulk-Tank-Milk-Grader-Card.docx and b/app/server/static/templates/certificates/Bulk-Tank-Milk-Grader-Card.docx differ diff --git a/app/server/static/templates/certificates/Dairy-Tank-Truck.docx b/app/server/static/templates/certificates/Dairy-Tank-Truck.docx new file mode 100644 index 00000000..750c2dd4 Binary files /dev/null and b/app/server/static/templates/certificates/Dairy-Tank-Truck.docx differ diff --git a/app/server/static/templates/certificates/Livestock-Dealer-Agent-Card.docx b/app/server/static/templates/certificates/Livestock-Dealer-Agent-Card.docx index d812fae9..dbda3b0f 100644 Binary files a/app/server/static/templates/certificates/Livestock-Dealer-Agent-Card.docx and b/app/server/static/templates/certificates/Livestock-Dealer-Agent-Card.docx differ diff --git a/app/server/static/templates/certificates/Livestock-Dealer-Card.docx b/app/server/static/templates/certificates/Livestock-Dealer-Card.docx index 27821519..78807f81 100644 Binary files a/app/server/static/templates/certificates/Livestock-Dealer-Card.docx and b/app/server/static/templates/certificates/Livestock-Dealer-Card.docx differ diff --git a/app/server/static/templates/notices/Renewal_DairyTankTruck_Template.docx b/app/server/static/templates/notices/Renewal_DairyTankTruck_Template.docx new file mode 100644 index 00000000..7e3b8c95 Binary files /dev/null and b/app/server/static/templates/notices/Renewal_DairyTankTruck_Template.docx differ diff --git a/app/server/static/templates/reports/Dairy_Trailer_Inspection_Template.xlsx b/app/server/static/templates/reports/Dairy_Trailer_Inspection_Template.xlsx new file mode 100644 index 00000000..6b6c2ca8 Binary files /dev/null and b/app/server/static/templates/reports/Dairy_Trailer_Inspection_Template.xlsx differ diff --git a/app/server/utilities/constants.js b/app/server/utilities/constants.js index 804fc50d..bac122b7 100644 --- a/app/server/utilities/constants.js +++ b/app/server/utilities/constants.js @@ -99,6 +99,7 @@ module.exports = Object.freeze({ DAIRY_FARM_QUALITY: "DAIRY_FARM_QUALITY", DAIRY_FARM_TANK: "DAIRY_FARM_TANK", DAIRY_TEST_THRESHOLD: "DAIRY_TEST_THRESHOLD", + DAIRY_TRAILER_INSPECTION: "TRAILER INSPECTION", LICENCE_LOCATION: "LICENCE_LOCATION", LICENCE_EXPIRY: "LICENCE_EXPIRY", }, diff --git a/app/server/utilities/documents.js b/app/server/utilities/documents.js index 6bac2233..5ad59215 100644 --- a/app/server/utilities/documents.js +++ b/app/server/utilities/documents.js @@ -71,6 +71,8 @@ function getCertificateTemplateName(documentType, licenceType) { return "Veterinary-Drug-Outlet"; case "DISPENSER": return "Veterinary-Drug-Dispenser"; + case "DAIRY TANK TRUCK": + return "Dairy-Tank-Truck"; default: return undefined; } @@ -110,6 +112,8 @@ function getRenewalTemplateName(documentType, licenceType) { return "Renewal_VetDrugLicence_Template"; case "DISPENSER": return "Renewal_VetDrugDispenser_Template"; + case "DAIRY TANK TRUCK": + return "Renewal_DairyTankTruck_Template"; // not implemented default: return undefined; } @@ -192,6 +196,8 @@ function getReportsTemplateName(documentType) { return "LicenceType_Location_Template"; case constants.REPORTS.LICENCE_EXPIRY: return "Licence_Expiry_Species_NoSpecies_Template"; + case constants.REPORTS.DAIRY_TRAILER_INSPECTION: + return "Dairy_Trailer_Inspection_Template"; default: return null; }