diff --git a/.vscode/settings.json b/.vscode/settings.json index 9d8aad38..7623d951 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,7 @@ "autogen", "autoheight", "autoplay", + "BHBK", "BWID", "canoebarge", "CARTOCODE", @@ -19,6 +20,7 @@ "fishsample", "flowtype", "fullscreen", + "Fultons", "geodef", "geodefs", "GNIS", diff --git a/_src/app/SummaryReport.js b/_src/app/SummaryReport.js deleted file mode 100644 index d6701d76..00000000 --- a/_src/app/SummaryReport.js +++ /dev/null @@ -1,174 +0,0 @@ -define([ - 'react-app/config', - - 'dijit/_TemplatedMixin', - 'dijit/_WidgetBase', - - 'dojo/Deferred', - 'dojo/text!app/templates/SummaryReport.html', - 'dojo/text!app/templates/SummaryReportTemplate.html', - 'dojo/_base/declare', - - 'handlebars/handlebars', - - 'bootstrap', -], function ( - config, - - _TemplatedMixin, - _WidgetBase, - - Deferred, - template, - reportTemplate, - declare, - - Handlebars -) { - // TODO: remove once this module is converted to a component - config = config.default; - - return declare([_WidgetBase, _TemplatedMixin], { - // description: - // Shows a summary of the report that is to be submitted - templateString: template, - baseClass: 'summary-report', - - // Properties to be sent into constructor - - postCreate: function () { - // summary: - // Overrides method of same name in dijit._Widget. - console.log('app/SummaryReport:postCreate', arguments); - - var that = this; - $(this.modal).on('hidden.bs.modal', function () { - if (that.promise) { - // user has cancelled or closed the dialog without confirming - that.promise.reject(); - } - }); - - this.inherited(arguments); - }, - verify: function (data) { - // summary: - // description - // data: Object - console.log('app/SummaryReport:verify', arguments); - - this.promise = new Deferred(); - - this.displayReport(data); - - return this.promise; - }, - displayReport: function (reportData) { - // summary: - // parses the report submission data and displays a summary - // reportData - console.log('app/SummaryReport:displayReport', arguments); - - var passes = {}; - // this builds something like this - // { - // 1: { - // BH: 15, - // CD: 2 - // }, - // 2: { - // AC: 55, - // FLS: 234 - // } - // } - reportData[config.tableNames.fish].forEach(function filterFish(fish) { - var passName = fish[config.fieldNames.fish.PASS_NUM]; - if (!passes[passName]) { - passes[passName] = {}; - } - var pass = passes[passName]; - - var speciesName = fish[config.fieldNames.fish.SPECIES_CODE]; - if (pass[speciesName]) { - pass[speciesName]++; - } else { - pass[speciesName] = 1; - } - }); - - // convert to something like this: - // { - // passes: [{ - // name: 1, - // species: [{ - // name: "BH", - // count: 15 - // }, { - // name: "CD", - // count: 2 - // }] - // }, { - // name: 2, - // species: [{ - // name: "AC", - // count: 55 - // }, { - // name: "FLS", - // count: 245 - // }, { - // name: "HSD", - // count: 235 - // }] - // }] - // } - var summaryReport = { - passes: [], - }; - for (var passName in passes) { - if (passes.hasOwnProperty(passName)) { - var speciesReport = []; - var pass = passes[passName]; - for (var speciesName in pass) { - if (pass.hasOwnProperty(speciesName)) { - speciesReport.push({ - name: speciesName, - count: pass[speciesName], - }); - } - } - summaryReport.passes.push({ - name: passName, - species: speciesReport, - }); - } - } - - this.report.innerHTML = Handlebars.compile(reportTemplate)(summaryReport); - - $(this.modal).modal('show'); - - return summaryReport; - }, - onConfirm: function () { - // summary: - // user has confirmed the report - // param or return - console.log('app/SummaryReport:onConfirm', arguments); - - this.promise.resolve(); - - this.promise = null; - - $(this.modal).modal('hide'); - }, - destroyRecursive: function () { - // summary: - // close the modal to clean up tests - console.log('app/SummaryReport:destroyRecursive', arguments); - - $(this.modal).modal('hide'); - - this.inherited(arguments); - }, - }); -}); diff --git a/_src/app/resources/SummaryReport.styl b/_src/app/resources/SummaryReport.styl deleted file mode 100644 index c9ebffc9..00000000 --- a/_src/app/resources/SummaryReport.styl +++ /dev/null @@ -1 +0,0 @@ -.summary-report {} diff --git a/_src/app/templates/SummaryReport.html b/_src/app/templates/SummaryReport.html deleted file mode 100644 index 2629c30c..00000000 --- a/_src/app/templates/SummaryReport.html +++ /dev/null @@ -1,28 +0,0 @@ -
- -
diff --git a/_src/app/templates/SummaryReportTemplate.html b/_src/app/templates/SummaryReportTemplate.html deleted file mode 100644 index 5dab49c6..00000000 --- a/_src/app/templates/SummaryReportTemplate.html +++ /dev/null @@ -1,21 +0,0 @@ -

- {{#if passes}} - {{#each passes}} -

Pass #{{name}}

- - - - - - {{#each species}} - - - - - {{/each}} -
SpeciesCount
{{name}}{{count}}
- {{/each}} - {{else}} - No fish found. - {{/if}} -

diff --git a/_src/app/tests/SummaryReportTemplateTests.html b/_src/app/tests/SummaryReportTemplateTests.html deleted file mode 100644 index b6c3669a..00000000 --- a/_src/app/tests/SummaryReportTemplateTests.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - SummaryReportTemplateTests - - - - - - - - - - - diff --git a/_src/app/tests/SummaryReportTests.html b/_src/app/tests/SummaryReportTests.html deleted file mode 100644 index 7e588b0f..00000000 --- a/_src/app/tests/SummaryReportTests.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - SummaryReport Tests - - - - - - - - - - - - - - - -
- - diff --git a/_src/app/tests/data/SummaryReport.json b/_src/app/tests/data/SummaryReport.json deleted file mode 100644 index f51d63d4..00000000 --- a/_src/app/tests/data/SummaryReport.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "passes": [{ - "name": 1, - "species": [{ - "name": "BH", - "count": 15 - }, { - "name": "CD", - "count": 2 - }] - }, { - "name": 2, - "species": [{ - "name": "AC", - "count": 55 - }, { - "name": "FLS", - "count": 245 - }, { - "name": "HSD", - "count": 235 - }] - }] -} diff --git a/_src/app/tests/spec/SpecSummaryReport.js b/_src/app/tests/spec/SpecSummaryReport.js deleted file mode 100644 index d2c09f6b..00000000 --- a/_src/app/tests/spec/SpecSummaryReport.js +++ /dev/null @@ -1,49 +0,0 @@ -require([ - 'app/SummaryReport', - - 'dojo/dom-construct', - 'dojo/text!../../../../scripts/Scripts/TestData/NewCollectionEventData.json', -], function ( - WidgetUnderTest, - - domConstruct, - reportDataJSON -) { - describe('app/SummaryReport', function () { - var widget; - var destroy = function (destroyWidget) { - destroyWidget.destroyRecursive(); - destroyWidget = null; - }; - - beforeEach(function () { - widget = new WidgetUnderTest(null, domConstruct.create('div', null, document.body)); - widget.startup(); - }); - - afterEach(function () { - if (widget) { - destroy(widget); - } - }); - - describe('Sanity', function () { - it('should create a SummaryReport', function () { - expect(widget).toEqual(jasmine.any(WidgetUnderTest)); - }); - }); - - describe('displayReport', function () { - it('generates the correct summary object', function () { - var data = widget.displayReport(JSON.parse(reportDataJSON)); - - expect(data.passes.length).toBe(2); - expect(data.passes[0].name).toBe('1'); - expect(data.passes[0].species.length).toBe(2); - expect(data.passes[0].species[0].name).toBe('BS'); - expect(data.passes[0].species[0].count).toBe(2); - expect(data.passes[0].species[1].count).toBe(1); - }); - }); - }); -}); diff --git a/_src/react-app/components/NewCollectionEvent.jsx b/_src/react-app/components/NewCollectionEvent.jsx index 76c40ea2..5f2bf333 100644 --- a/_src/react-app/components/NewCollectionEvent.jsx +++ b/_src/react-app/components/NewCollectionEvent.jsx @@ -6,7 +6,6 @@ import Location from './location/Location'; import Method from './method/Method'; import Catch from 'app/catch/Catch'; import Habitat from 'app/habitat/Habitat'; -import SummaryReport from 'app/SummaryReport'; import submitJob from '../helpers/submitJob'; import toastify from 'react-toastify'; import useDojoWidget from '../hooks/useDojoWidget'; @@ -14,6 +13,7 @@ import { AppContext, actionTypes as appActionTypes } from '../App'; import { useImmerReducer } from 'use-immer'; import NumericInputValidator from 'ijit/modules/NumericInputValidator'; import getGUID from '../helpers/getGUID'; +import SummaryReport from './SummaryReport'; export const EventContext = React.createContext(); export const actionTypes = { @@ -159,7 +159,7 @@ const reducer = (draft, action) => { const cancelConfirmMsg = 'Are you sure? This will clear all values in the report.'; // submitErrMsg: String -const submitErrMsg = 'There was an error submitting the report!'; +const submitErrMsg = 'There was an error submitting the report'; // invalidInputMsg: String const invalidInputMsg = 'Invalid value for '; @@ -239,10 +239,8 @@ const NewCollectionEvent = () => { // dojo widgets const catchTbDiv = React.useRef(); const habitatTbDiv = React.useRef(); - const reportSummaryDiv = React.useRef(); const catchTb = useDojoWidget(catchTbDiv, Catch); const habitatTb = useDojoWidget(habitatTbDiv, Habitat); - const reportSummary = useDojoWidget(reportSummaryDiv, SummaryReport); const showTab = (tabID) => { // summary: @@ -366,27 +364,17 @@ const NewCollectionEvent = () => { behavior: 'smooth', }); }, 500); + }, [clearReport]); - appDispatch({ - type: appActionTypes.SUBMIT_LOADING, - payload: false, - }); - }, [appDispatch, clearReport]); - - const onError = React.useCallback( - (message) => { - console.log('app/NewCollectionEvent:onError'); + const onError = (message) => { + console.log('app/NewCollectionEvent:onError'); - setValidateMsg(`${submitErrMsg}: ${typeof message === 'string' ? message : message.message || message}`); - window.scrollTo(0, 0); - appDispatch({ - type: appActionTypes.SUBMIT_LOADING, - payload: false, - }); - }, - [appDispatch] - ); + setValidateMsg(`${submitErrMsg}: ${typeof message === 'string' ? message : message.message || message}`); + window.scrollTo(0, 0); + }; + const [showSummary, setShowSummary] = React.useState(false); + const [submitData, setSubmitData] = React.useState(null); // this could be replaced by eventState once everything is moved to React const onSubmit = React.useCallback(() => { console.log('NewCollectionEvent:onSubmit'); @@ -425,32 +413,33 @@ const NewCollectionEvent = () => { data[config.tableNames.transect] = habitatTb.getTransectData(); data[config.tableNames.transectMeasurements] = habitatTb.getTransectMeasurementData(); - const data_txt = JSON.stringify(data); + setSubmitData(data); + setShowSummary(true); + }, [appDispatch, catchTb, eventState, habitatTb, validateReport]); - return reportSummary.verify(data).then( - async () => { - try { - await submitJob({ data: data_txt }, config.urls.newCollectionEvent); + const onConfirmSubmit = async () => { + const data_txt = JSON.stringify(submitData); - onSuccessfulSubmit(); - } catch (error) { - onError(error.message); - } + try { + await submitJob({ data: data_txt }, config.urls.newCollectionEvent); - // stringify, parse is so that we have a clean object to store in localforage - return archivesLocalForage.current.setItem( - data[config.tableNames.samplingEvents].attributes[config.fieldNames.samplingEvents.EVENT_ID], - JSON.parse(JSON.stringify(data)) - ); - }, - () => { - appDispatch({ - type: appActionTypes.SUBMIT_LOADING, - payload: false, - }); - } + onSuccessfulSubmit(); + } catch (error) { + onError(error.message); + } + + setShowSummary(false); + appDispatch({ + type: appActionTypes.SUBMIT_LOADING, + payload: false, + }); + + // stringify, parse is so that we have a clean object to store in localforage + await archivesLocalForage.current.setItem( + submitData[config.tableNames.samplingEvents].attributes[config.fieldNames.samplingEvents.EVENT_ID], + JSON.parse(JSON.stringify(submitData)) ); - }, [appDispatch, catchTb, eventState, habitatTb, onError, onSuccessfulSubmit, reportSummary, validateReport]); + }; const onCancel = React.useCallback(() => { console.log('NewCollectionEvent:onCancel'); @@ -501,7 +490,18 @@ const NewCollectionEvent = () => {
-
+ { + setShowSummary(false); + appDispatch({ + type: appActionTypes.SUBMIT_LOADING, + payload: false, + }); + }} + eventData={submitData} + onConfirm={onConfirmSubmit} + /> ); diff --git a/_src/react-app/components/SummaryReport.jsx b/_src/react-app/components/SummaryReport.jsx new file mode 100644 index 00000000..fc6c1083 --- /dev/null +++ b/_src/react-app/components/SummaryReport.jsx @@ -0,0 +1,208 @@ +import React from 'react'; +import config from '../config'; + +const DECIMAL_PLACES = 2; + +function round(value) { + return Number.parseFloat(value.toFixed(DECIMAL_PLACES)); +} +export function getStats(values) { + values = values.filter((x) => x !== null); + + if (values.length === 0) { + return { max: null, min: null, avg: null }; + } + + const max = round(Math.max(...values)); + const min = round(Math.min(...values)); + const avg = values.reduce((a, b) => a + b, 0) / values.length; + + return { max, min, avg: round(avg) }; +} + +export function getFultons(weights, lengths) { + return Array(Math.max(weights.length, lengths.length)) + .fill() + .map((_, i) => { + const weight = Number.parseFloat(weights[i]); + const length = Number.parseFloat(lengths[i]); + + if (Number.isNaN(weight) || Number.isNaN(length)) { + return null; + } + + const value = (weight / Math.pow(length, 3)) * 100000; + + return round(value); + }); +} + +export function getSummaryData(eventData) { + const speciesContainer = {}; + let numPasses = 0; + + eventData[config.tableNames.fish].forEach((fish) => { + const species = fish[config.fieldNames.fish.SPECIES_CODE]; + const pass = fish[config.fieldNames.fish.PASS_NUM]; + + if (pass > numPasses) { + numPasses = pass; + } + + if (!speciesContainer[species]) { + speciesContainer[species] = { + name: species, + counts: { + [pass]: 1, + }, + weights: [fish[config.fieldNames.fish.WEIGHT]], + lengths: [fish[config.fieldNames.fish.LENGTH]], + }; + } else { + if (!speciesContainer[species].counts[pass]) { + speciesContainer[species].counts[pass] = 1; + } else { + speciesContainer[species].counts[pass] += 1; + } + + speciesContainer[species].weights.push(fish[config.fieldNames.fish.WEIGHT]); + speciesContainer[species].lengths.push(fish[config.fieldNames.fish.LENGTH]); + } + }); + + return { + species: Object.values(speciesContainer).map((species) => { + return { + name: species.name, + counts: species.counts, + weight: getStats(species.weights), + length: getStats(species.lengths), + fulton: getStats(getFultons(species.weights, species.lengths)), + }; + }), + numPasses, + }; +} + +function SummaryReport({ show, onHide, eventData, onConfirm }) { + const modalRef = React.useRef(null); + const [summaryData, setSummaryData] = React.useState(null); + + React.useEffect(() => { + $(modalRef.current).modal({ backdrop: 'static', keyboard: false }); + }, []); + + React.useEffect(() => { + if (show) { + $(modalRef.current).modal('show'); + } else { + $(modalRef.current).modal('hide'); + } + }, [show]); + + React.useEffect(() => { + if (eventData && show) { + setSummaryData(getSummaryData(eventData)); + } + }, [eventData, show]); + + return ( +
+
+
+
+
+ +

Report Summary

+
+
+ {summaryData ? ( + + + + + + + + + + + + + + + + + + + + + + + {Array.from({ length: summaryData.numPasses }).map((_, i) => ( + + ))} + + + + {summaryData.species.map((s) => ( + + + + {/* Fulton's */} + + + {/* Weight */} + + + {/* Length */} + + + {/* Count by Pass */} + {Array(summaryData.numPasses) + .fill() + .map((_, i) => ( + + ))} + + ))} + +
+ Fulton's Condition Factor + + Weight + + Length + + Counts by Pass +
SpeciesMaxMinAvgMaxMinAvgMaxMinAvg#{i + 1}
{s.name}{s.counts[i + 1]}
+ ) : null} +
+
+ + +
+
+
+
+
+ ); +} + +function Stats({ data }) { + return ( + <> + {data.max} + {data.min} + {data.avg} + + ); +} + +export default SummaryReport; diff --git a/_src/react-app/components/SummaryReport.scss b/_src/react-app/components/SummaryReport.scss new file mode 100644 index 00000000..724c2ce6 --- /dev/null +++ b/_src/react-app/components/SummaryReport.scss @@ -0,0 +1,5 @@ +table th { + &.centered { + text-align: center; + } +} diff --git a/_src/react-app/components/SummaryReport.stories.jsx b/_src/react-app/components/SummaryReport.stories.jsx new file mode 100644 index 00000000..2e3eddeb --- /dev/null +++ b/_src/react-app/components/SummaryReport.stories.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import SummaryReport from './SummaryReport'; +import eventData from '../../../tests/data/NewCollectionEventData.json'; + +const story = { + title: 'SummaryReport', + component: SummaryReport, +}; + +export default story; + +export const Default = () => { + const [show, setShow] = React.useState(true); + + return ; +}; diff --git a/_src/react-app/components/SummaryReport.test.js b/_src/react-app/components/SummaryReport.test.js new file mode 100644 index 00000000..632e84e1 --- /dev/null +++ b/_src/react-app/components/SummaryReport.test.js @@ -0,0 +1,91 @@ +import { describe, it, expect } from 'vitest'; +import eventData from '../../../tests/data/NewCollectionEventData.json'; +import { getSummaryData, getStats, getFultons } from './SummaryReport'; + +describe('getSummaryData', () => { + it('should return an object with the correct keys', () => { + const result = getSummaryData(eventData); + expect(result).toHaveProperty('species'); + expect(result).toHaveProperty('numPasses'); + }); + + it('should return the correct species count', () => { + const result = getSummaryData(eventData); + expect(result.species.length).toBe(3); + }); + + it('should return the correct number of passes', () => { + const result = getSummaryData(eventData); + expect(result.numPasses).toBe(2); + }); + + it('should return the correct species names', () => { + const result = getSummaryData(eventData); + const speciesNames = result.species.map((s) => s.name); + expect(speciesNames).toEqual(['BHBK', 'CB', 'BS']); + }); + + it('should return the correct species counts', () => { + const result = getSummaryData(eventData); + // BHBK + expect(result.species[0].counts).toEqual({ + 1: 11, + 2: 9, + }); + }); + + it('should return the correct weight stats', () => { + const result = getSummaryData(eventData); + // BS + expect(result.species[2].weight).toEqual({ + min: 1, + max: 5, + avg: 3.67, + }); + }); +}); + +describe('getStats', () => { + it('should return the correct stats', () => { + const result = getStats([1, 2, 3.1235, 4, 5]); + expect(result).toEqual({ + min: 1, + max: 5, + avg: 3.02, + }); + }); + + it('can handle null values', () => { + const result = getStats([1, 2, null, 4, 5]); + expect(result).toEqual({ + min: 1, + max: 5, + avg: 3, + }); + }); + + it('can handle all nulls', () => { + const result = getStats([null, null, null, null, null]); + expect(result).toEqual({ + min: null, + max: null, + avg: null, + }); + }); + + it('rounds all values', () => { + const result = getStats([1.1234, 2.1234, 3.1234, 4.1234, 5.1234]); + expect(result).toEqual({ + min: 1.12, + max: 5.12, + avg: 3.12, + }); + }); +}); + +describe('getFultonStats', () => { + it('should return the correct stats', () => { + const result = getFultons([1, 2], [6, 7]); + expect(result).toEqual([462.96, 583.09]); + }); +}); diff --git a/_src/react-app/index.scss b/_src/react-app/index.scss index 8d0c3273..6948ee5c 100644 --- a/_src/react-app/index.scss +++ b/_src/react-app/index.scss @@ -1,5 +1,6 @@ @use 'App'; @use 'components/NewCollectionEvent'; +@use 'components/SummaryReport'; @use 'components/ComboBox'; @use 'components/Header'; @use 'components/OtherOptionHandler'; @@ -9,5 +10,6 @@ @use 'components/location/StreamSearch'; @use 'components/method/Method'; @use 'components/DataGrid'; + @use '../../node_modules/react-toastify/scss/main.scss'; @use '../../node_modules/leaflet/dist/leaflet.css'; diff --git a/tests/data/NewCollectionEventData.json b/tests/data/NewCollectionEventData.json new file mode 100644 index 00000000..0a42946a --- /dev/null +++ b/tests/data/NewCollectionEventData.json @@ -0,0 +1,370 @@ +{ + "SamplingEvents": { + "geometry": { + "paths": [ + [ + [ + 463502.7472000001, + 4493214.657199999 + ], + [ + 463497.3099999996, + 4493209.220000001 + ], + [ + 463455.7792999996, + 4493201.133300001 + ] + ] + ], + "spatialReference": { + "wkid": 26912 + } + }, + "attributes": { + "EVENT_ID": "{031dca27-b37e-4d2d-847f-5d5b9e0fe093}", + "GEO_DEF": "start:{\"x\":463455.55925876985,\"y\":4493200.233695918}|dist:50|dir:up", + "LOCATION_NOTES": "", + "EVENT_DATE": "2018-06-08", + "EVENT_TIME": "", + "OBSERVERS": "hi", + "WEATHER": "", + "STATION_ID": "{e9d9745d-505c-415e-8029-4c807fd1e9a2}", + "SEGMENT_LENGTH": 50, + "NUM_PASSES": 1 + } + }, + "Equipment": [ + { + "MODEL": "", + "ANODE_SHAPE": "", + "ARRAY_TYPE": "", + "NUM_NETTERS": null, + "CATHODE_TYPE": "", + "NUM_ANODES": 1, + "CATHODE_LEN": null, + "CATHODE_DIAMETER": null, + "MACHINE_RES": null, + "WAVEFORM": "", + "VOLTAGE": null, + "DUTY_CYCLE": null, + "FREQUENCY": null, + "AMPS": null, + "PEDAL_TIME": null, + "EVENT_ID": "{031dca27-b37e-4d2d-847f-5d5b9e0fe093}", + "TYPE": "raftboat", + "EQUIPMENT_ID": "{4d83f676-9ecd-4c51-86c8-0ea5c0aa714b}" + } + ], + "Anodes": [], + "Fish": [ + { + "FISH_ID": "{5c764b14-404b-45d1-8e36-dab0814eb662}", + "EVENT_ID": "{2cbee7f1-d2d7-4e90-82d9-5f8119e283f7}", + "PASS_NUM": 1, + "CATCH_ID": 1, + "SPECIES_CODE": "BHBK", + "LENGTH_TYPE": "TOT", + "LENGTH": 4, + "WEIGHT": 0.5714285714285714, + "NOTES": "" + }, + { + "FISH_ID": "{84b7a5e0-fc21-400c-83db-a2ef0aad4fdb}", + "EVENT_ID": "{2cbee7f1-d2d7-4e90-82d9-5f8119e283f7}", + "PASS_NUM": 1, + "CATCH_ID": 1, + "SPECIES_CODE": "BHBK", + "LENGTH_TYPE": "TOT", + "LENGTH": 4, + "WEIGHT": 0.5714285714285714, + "NOTES": "" + }, + { + "FISH_ID": "{54be933d-d912-431d-8a01-cf08702ab566}", + "EVENT_ID": "{2cbee7f1-d2d7-4e90-82d9-5f8119e283f7}", + "PASS_NUM": 1, + "CATCH_ID": 1, + "SPECIES_CODE": "BHBK", + "LENGTH_TYPE": "TOT", + "LENGTH": 4, + "WEIGHT": 0.5714285714285714, + "NOTES": "" + }, + { + "FISH_ID": "{0a8e72d3-beac-4e4b-806d-3ccf42a08462}", + "EVENT_ID": "{2cbee7f1-d2d7-4e90-82d9-5f8119e283f7}", + "PASS_NUM": 1, + "CATCH_ID": 1, + "SPECIES_CODE": "BHBK", + "LENGTH_TYPE": "TOT", + "LENGTH": 4, + "WEIGHT": 0.5714285714285714, + "NOTES": "" + }, + { + "FISH_ID": "{1f8e469f-26a1-4967-80cb-2bd2f3629776}", + "EVENT_ID": "{2cbee7f1-d2d7-4e90-82d9-5f8119e283f7}", + "PASS_NUM": 1, + "CATCH_ID": 1, + "SPECIES_CODE": "BHBK", + "LENGTH_TYPE": "TOT", + "LENGTH": 4, + "WEIGHT": 0.5714285714285714, + "NOTES": "" + }, + { + "FISH_ID": "{340435ca-fbe6-4666-83c4-be5b9a7891f6}", + "EVENT_ID": "{2cbee7f1-d2d7-4e90-82d9-5f8119e283f7}", + "PASS_NUM": 1, + "CATCH_ID": 1, + "SPECIES_CODE": "BHBK", + "LENGTH_TYPE": "TOT", + "LENGTH": 4, + "WEIGHT": 0.5714285714285714, + "NOTES": "" + }, + { + "FISH_ID": "{0964fa00-a36d-41b7-8142-37aca997047d}", + "EVENT_ID": "{2cbee7f1-d2d7-4e90-82d9-5f8119e283f7}", + "PASS_NUM": 1, + "CATCH_ID": 1, + "SPECIES_CODE": "BHBK", + "LENGTH_TYPE": "TOT", + "LENGTH": 4, + "WEIGHT": 0.5714285714285714, + "NOTES": "" + }, + { + "FISH_ID": "{0495d01b-3a96-401b-8f9a-07edaeef444b}", + "EVENT_ID": "{2cbee7f1-d2d7-4e90-82d9-5f8119e283f7}", + "PASS_NUM": 1, + "CATCH_ID": 2, + "SPECIES_CODE": "BHBK", + "LENGTH_TYPE": "TOT", + "LENGTH": null, + "WEIGHT": null, + "NOTES": "", + "isTrusted": true + }, + { + "FISH_ID": "{9be3bb87-02f0-4d6e-8fdb-82762882d00d}", + "EVENT_ID": "{2cbee7f1-d2d7-4e90-82d9-5f8119e283f7}", + "PASS_NUM": 1, + "CATCH_ID": 3, + "SPECIES_CODE": "BHBK", + "LENGTH_TYPE": "TOT", + "LENGTH": null, + "WEIGHT": null, + "NOTES": "" + }, + { + "FISH_ID": "{92dea6fd-278b-407c-86c5-f32d401d6af6}", + "EVENT_ID": "{2cbee7f1-d2d7-4e90-82d9-5f8119e283f7}", + "PASS_NUM": 1, + "CATCH_ID": 4, + "SPECIES_CODE": "BHBK", + "LENGTH_TYPE": "TOT", + "LENGTH": null, + "WEIGHT": 1.5, + "NOTES": "" + }, + { + "FISH_ID": "{8d7a8299-acee-4b36-8662-7c33f060c34b}", + "EVENT_ID": "{2cbee7f1-d2d7-4e90-82d9-5f8119e283f7}", + "PASS_NUM": 1, + "CATCH_ID": 4, + "SPECIES_CODE": "BHBK", + "LENGTH_TYPE": "TOT", + "LENGTH": null, + "WEIGHT": 1.5, + "NOTES": "" + }, + { + "FISH_ID": "{ff0211a1-cf0e-466e-8599-f00a5dc499d0}", + "EVENT_ID": "{2cbee7f1-d2d7-4e90-82d9-5f8119e283f7}", + "PASS_NUM": 2, + "CATCH_ID": 5, + "SPECIES_CODE": "BHBK", + "LENGTH_TYPE": "TOT", + "LENGTH": null, + "WEIGHT": 3, + "NOTES": "" + }, + { + "FISH_ID": "{b9f65703-8dd4-40ac-8c8a-e0261bfabfdd}", + "EVENT_ID": "{2cbee7f1-d2d7-4e90-82d9-5f8119e283f7}", + "PASS_NUM": 2, + "CATCH_ID": 6, + "SPECIES_CODE": "BHBK", + "LENGTH_TYPE": "TOT", + "LENGTH": null, + "WEIGHT": 3, + "NOTES": "" + }, + { + "FISH_ID": "{46e59a95-c40c-4d42-83d9-f8f4f687ee5b}", + "EVENT_ID": "{2cbee7f1-d2d7-4e90-82d9-5f8119e283f7}", + "PASS_NUM": 2, + "CATCH_ID": 7, + "SPECIES_CODE": "BHBK", + "LENGTH_TYPE": "TOT", + "LENGTH": null, + "WEIGHT": 24.6, + "NOTES": "" + }, + { + "FISH_ID": "{cf5c7063-0794-4467-89dd-7d2ba38e3a0a}", + "EVENT_ID": "{2cbee7f1-d2d7-4e90-82d9-5f8119e283f7}", + "PASS_NUM": 2, + "CATCH_ID": 8, + "SPECIES_CODE": "BHBK", + "LENGTH_TYPE": "TOT", + "LENGTH": null, + "WEIGHT": 24.6, + "NOTES": "" + }, + { + "FISH_ID": "{ad1d151e-81db-4ee1-8784-f15804fa0eac}", + "EVENT_ID": "{2cbee7f1-d2d7-4e90-82d9-5f8119e283f7}", + "PASS_NUM": 2, + "CATCH_ID": 9, + "SPECIES_CODE": "BHBK", + "LENGTH_TYPE": "TOT", + "LENGTH": null, + "WEIGHT": 24.6, + "NOTES": "" + }, + { + "FISH_ID": "{f5293fc5-90cd-4ebe-80bf-b8dd1dc6673b}", + "EVENT_ID": "{2cbee7f1-d2d7-4e90-82d9-5f8119e283f7}", + "PASS_NUM": 2, + "CATCH_ID": 10, + "SPECIES_CODE": "BHBK", + "LENGTH_TYPE": "TOT", + "LENGTH": null, + "WEIGHT": 24.6, + "NOTES": "" + }, + { + "FISH_ID": "{41632394-d805-4160-80dc-7f3b64e7027a}", + "EVENT_ID": "{2cbee7f1-d2d7-4e90-82d9-5f8119e283f7}", + "PASS_NUM": 2, + "CATCH_ID": 11, + "SPECIES_CODE": "BHBK", + "LENGTH_TYPE": "TOT", + "LENGTH": null, + "WEIGHT": 24.6, + "NOTES": "" + }, + { + "FISH_ID": "{79b4549f-b68b-426d-81d7-8d67875d234c}", + "EVENT_ID": "{2cbee7f1-d2d7-4e90-82d9-5f8119e283f7}", + "PASS_NUM": 2, + "CATCH_ID": 12, + "SPECIES_CODE": "BHBK", + "LENGTH_TYPE": "TOT", + "LENGTH": null, + "WEIGHT": 5, + "NOTES": "" + }, + { + "FISH_ID": "{e06c5aec-dd31-46ec-8c88-4c5d3bc8ff24}", + "EVENT_ID": "{2cbee7f1-d2d7-4e90-82d9-5f8119e283f7}", + "PASS_NUM": 2, + "CATCH_ID": 13, + "SPECIES_CODE": "CB", + "LENGTH_TYPE": "TOT", + "LENGTH": null, + "WEIGHT": 5, + "NOTES": "" + }, + { + "FISH_ID": "{afc6e542-f312-4f15-8c27-6831a329f286}", + "EVENT_ID": "{2cbee7f1-d2d7-4e90-82d9-5f8119e283f7}", + "PASS_NUM": 2, + "CATCH_ID": 14, + "SPECIES_CODE": "BHBK", + "LENGTH_TYPE": "TOT", + "LENGTH": null, + "WEIGHT": 5, + "NOTES": "" + }, + { + "FISH_ID": "{45aaa105-d286-4066-8d91-56156e0a2885}", + "EVENT_ID": "{2cbee7f1-d2d7-4e90-82d9-5f8119e283f7}", + "PASS_NUM": 2, + "CATCH_ID": 15, + "SPECIES_CODE": "BS", + "LENGTH_TYPE": "TOT", + "LENGTH": null, + "WEIGHT": 1, + "NOTES": "" + }, + { + "FISH_ID": "{21e8d36a-4112-46de-844c-8bd3dfb9c1d9}", + "EVENT_ID": "{2cbee7f1-d2d7-4e90-82d9-5f8119e283f7}", + "PASS_NUM": 2, + "CATCH_ID": 16, + "SPECIES_CODE": "BS", + "LENGTH_TYPE": "TOT", + "LENGTH": null, + "WEIGHT": 5, + "NOTES": "" + }, + { + "FISH_ID": "{21e8d36a-4112-46de-844c-8bd3dfb9c1d9}", + "EVENT_ID": "{2cbee7f1-d2d7-4e90-82d9-5f8119e283f7}", + "PASS_NUM": 2, + "CATCH_ID": 16, + "SPECIES_CODE": "BS", + "LENGTH_TYPE": "TOT", + "LENGTH": null, + "WEIGHT": 5, + "NOTES": "" + } + ], + "Diet": [], + "Tags": [], + "Health": [], + "Habitat": [ + { + "EVENT_ID": "{031dca27-b37e-4d2d-847f-5d5b9e0fe093}", + "BANKVEG": null, + "DOVR": "", + "DUND": "", + "LGWD": null, + "POOL": null, + "SPNG": "", + "RIFF": null, + "RUNA": null, + "SUB_FINES": null, + "SUB_SAND": null, + "SUB_GRAV": null, + "SUB_COBB": null, + "SUB_RUBB": null, + "SUB_BOUL": null, + "SUB_BEDR": null, + "SIN": null, + "EROS": null, + "TEMP_": null, + "PH": null, + "CON": null, + "OXYGEN": null, + "SOLIDS": null, + "TURBIDITY": null, + "ALKALINITY": null, + "BACKWATER": null + } + ], + "Transect": [ + { + "TRANSECT_ID": "{856bdfc5-debd-4419-82eb-9d060b9c3e41}", + "BWID": null, + "WWID": 4.01, + "STARTING_BANK": "", + "EVENT_ID": "{031dca27-b37e-4d2d-847f-5d5b9e0fe093}" + } + ], + "TransectMeasurements": [] +}