diff --git a/CHANGELOG.md b/CHANGELOG.md index 75f6b320..438af81c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ +## 1.27.0 + +Updated fixed site setup form. + ## 1.26.6 Fixed moth trap edit form. +Updated species dictionary. +Bug fixes. ## 1.26.5 diff --git a/package-lock.json b/package-lock.json index 1596ecb7..e04507d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "butterfly-count-app", - "version": "1.26.6", + "version": "1.27.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "butterfly-count-app", - "version": "1.26.6", + "version": "1.27.0", "dependencies": { "@capacitor-community/background-geolocation": "^1.2.19", "@capacitor/android": "^6.1.2", @@ -25,7 +25,7 @@ "@capacitor/status-bar": "6.0.1", "@flumens/bigu": "0.4.0", "@flumens/ionic": "2.0.0-alpha.64.1", - "@flumens/tailwind": "0.19.1", + "@flumens/tailwind": "0.21.0", "@heroicons/react": "^2.2.0", "@hookform/resolvers": "^3.9.1", "@ionic-native/core": "5.36.0", @@ -57,7 +57,6 @@ "mobx": "^6.13.5", "mobx-react": "9.1.1", "mobx-utils": "6.1.0", - "prop-types": "15.8.1", "react": "18.3.1", "react-aria-components": "^1.4.1", "react-autosuggest": "10.1.0", @@ -92,6 +91,7 @@ "@flumens/fetch-onedrive-excel": "0.3.3", "@flumens/prettier-config": "0.4.0", "@flumens/webpack-config": "5.5.0", + "@types/geojson": "^7946.0.14", "@types/jest": "29.5.14", "@types/lodash": "^4.17.13", "@types/react": "18.3.12", @@ -2831,9 +2831,9 @@ "dev": true }, "node_modules/@flumens/ionic": { - "version": "2.0.0-alpha.64", - "resolved": "https://registry.npmjs.org/@flumens/ionic/-/ionic-2.0.0-alpha.64.tgz", - "integrity": "sha512-QG4t6F2jjQsOs9DOlM7d6y/TdrnTZ4Ffk0dAshPJi/z4q4cj3NyDe6z/s7gu4fAPU+p4ZQ4FK9goeraAqePWQw==", + "version": "2.0.0-alpha.64.1", + "resolved": "https://registry.npmjs.org/@flumens/ionic/-/ionic-2.0.0-alpha.64.1.tgz", + "integrity": "sha512-9yR9RzjCvpCWBpECTD+LLoxrCU9qLUf2ZiLUDXEfDeZ0NIBJLXmXk300U5kd7H+o2Ooa4fvK10mNAomWHYWmwg==", "optionalDependencies": { "@capacitor/camera": "^5 || ^6", "@capacitor/core": "^5 || ^6", @@ -2931,9 +2931,9 @@ } }, "node_modules/@flumens/tailwind": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@flumens/tailwind/-/tailwind-0.19.1.tgz", - "integrity": "sha512-nNHw4B9tlaYoh3XkSYjo/K5MSZKNsmnQpCjUKXnNBa95FkueBXNNTZ/l1wx6iiQsmQHsfxfTOaOd3aY/07WOEA==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@flumens/tailwind/-/tailwind-0.21.0.tgz", + "integrity": "sha512-uuFCC2BbUaSSK/s4NdHmgp2InKh2vLkY/kFFgvDaBUg+IQgebaLNVaHoM/V7oo4IETX5iWadFPTTYaPu7q3dFg==", "dependencies": { "@heroicons/react": "^2.0.15", "react-aria-components": "^1.0.1", diff --git a/package.json b/package.json index 7bf216b5..b3531923 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "id": "uk.ac.ceh.ebms", "title": "ButterflyCount", "description": "ButterflyCount mobile application.", - "version": "1.26.6", + "version": "1.27.0", "homepage": "http://www.butterfly-monitoring.net", "scripts": { "prepare": "husky", @@ -40,7 +40,7 @@ "@capacitor/status-bar": "6.0.1", "@flumens/bigu": "0.4.0", "@flumens/ionic": "2.0.0-alpha.64.1", - "@flumens/tailwind": "0.19.1", + "@flumens/tailwind": "0.21.0", "@heroicons/react": "^2.2.0", "@hookform/resolvers": "^3.9.1", "@ionic-native/core": "5.36.0", @@ -72,7 +72,6 @@ "mobx": "^6.13.5", "mobx-react": "9.1.1", "mobx-utils": "6.1.0", - "prop-types": "15.8.1", "react": "18.3.1", "react-aria-components": "^1.4.1", "react-autosuggest": "10.1.0", @@ -107,6 +106,7 @@ "@flumens/fetch-onedrive-excel": "0.3.3", "@flumens/prettier-config": "0.4.0", "@flumens/webpack-config": "5.5.0", + "@types/geojson": "^7946.0.14", "@types/jest": "29.5.14", "@types/lodash": "^4.17.13", "@types/react": "18.3.12", diff --git a/src/App.tsx b/src/App.tsx index bee9e2d1..e2446c5e 100755 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,11 @@ import { observer } from 'mobx-react'; import { Route, Redirect } from 'react-router-dom'; -import { TailwindContext, TailwindContextValue } from '@flumens'; +import { + TailwindBlockContext, + TailwindContext, + TailwindContextValue, + defaultContext, +} from '@flumens'; import { IonApp as IonAppPlain, IonRouterOutlet, @@ -24,6 +29,11 @@ const IonApp = IonAppPlain as any; // IonApp has 'lang' prop missing. const platform = isPlatform('ios') ? 'ios' : 'android'; const tailwindContext: TailwindContextValue = { platform }; +const tailwindBlockContext = { + ...defaultContext, + ...tailwindContext, + basePath: '', +}; const HomeRedirect = () => ; @@ -34,20 +44,22 @@ const App = () => { - - - - - - - {Info} - {User} - {Survey} - {Location} - {Settings} - - - + + + + + + + + {Info} + {User} + {Survey} + {Location} + {Settings} + + + + diff --git a/src/Info/About/index.jsx b/src/Info/About/index.tsx similarity index 94% rename from src/Info/About/index.jsx rename to src/Info/About/index.tsx index 6c43cf3d..c2d22383 100644 --- a/src/Info/About/index.jsx +++ b/src/Info/About/index.tsx @@ -1,11 +1,11 @@ -import PropTypes from 'prop-types'; import { Trans as T } from 'react-i18next'; import { Page, Main, Header, Section } from '@flumens'; +import appModel from 'models/app'; import './styles.scss'; const { P } = Section; -const Component = ({ appModel }) => { +const Component = () => { const isEnglish = appModel.attrs.language === 'en'; return ( @@ -71,8 +71,4 @@ const Component = ({ appModel }) => { ); }; -Component.propTypes = { - appModel: PropTypes.object.isRequired, -}; - export default Component; diff --git a/src/Info/router.jsx b/src/Info/router.jsx index c29af215..e987d112 100644 --- a/src/Info/router.jsx +++ b/src/Info/router.jsx @@ -1,14 +1,12 @@ import { Route } from 'react-router-dom'; -import appModel from 'models/app'; import About from './About'; import Credits from './Credits'; import Guide from './Guide'; import Help from './Help'; -const AboutWrap = () => ; export default [ , , - , + , , ]; diff --git a/src/Location/MothTrap/Home/Main.tsx b/src/Location/MothTrap/Home/Main.tsx index 03a6a76f..e52bbc3a 100644 --- a/src/Location/MothTrap/Home/Main.tsx +++ b/src/Location/MothTrap/Home/Main.tsx @@ -23,8 +23,8 @@ type Props = { deleteLamp: (lamp: Lamp) => void; }; -const MothTrapSetupMain = ({ location, addNewLamp, deleteLamp }: Props) => { - const { type, lamps, location: loc, typeOther } = location.attrs; +const MothTrapHomeMain = ({ location, addNewLamp, deleteLamp }: Props) => { + const { type, lamps = [], location: loc = {}, typeOther } = location.attrs; const { t } = useTranslation(); @@ -150,4 +150,4 @@ const MothTrapSetupMain = ({ location, addNewLamp, deleteLamp }: Props) => { ); }; -export default observer(MothTrapSetupMain); +export default observer(MothTrapHomeMain); diff --git a/src/Location/MothTrap/Home/index.tsx b/src/Location/MothTrap/Home/index.tsx index 69982bc6..3690a935 100644 --- a/src/Location/MothTrap/Home/index.tsx +++ b/src/Location/MothTrap/Home/index.tsx @@ -87,6 +87,10 @@ const MothTrapSetup = ({ sample: location }: Props) => { const addNewLamp = () => { const cid = UUID(); + if (!location.attrs.lamps) { + location.attrs.lamps = []; + } + location.attrs.lamps.push({ cid, attrs: { type: '', quantity: 1, description: '' }, diff --git a/src/Location/MothTrap/Lamp/Main.tsx b/src/Location/MothTrap/Lamp/Main.tsx index 169e239a..8f16d4f9 100644 --- a/src/Location/MothTrap/Lamp/Main.tsx +++ b/src/Location/MothTrap/Lamp/Main.tsx @@ -12,7 +12,7 @@ type Props = { lamp: Lamp; }; -const MothTrapSetupMain = ({ location, lamp }: Props) => { +const MothTrapLampMain = ({ location, lamp }: Props) => { const { description, type } = lamp.attrs; const getCounterOnChange = (value: number) => { @@ -57,4 +57,4 @@ const MothTrapSetupMain = ({ location, lamp }: Props) => { ); }; -export default observer(MothTrapSetupMain); +export default observer(MothTrapLampMain); diff --git a/src/Survey/AreaCount/Area/Main/Favourites/index.tsx b/src/Survey/AreaCount/Area/Main/Favourites/index.tsx index 543fedc1..9d98a065 100644 --- a/src/Survey/AreaCount/Area/Main/Favourites/index.tsx +++ b/src/Survey/AreaCount/Area/Main/Favourites/index.tsx @@ -121,7 +121,7 @@ const Favourites = ({ {hasGroup && ( - + diff --git a/src/Survey/AreaCount/Area/NewLocationModal.tsx b/src/Survey/AreaCount/Area/NewLocationModal.tsx deleted file mode 100644 index c0481d91..00000000 --- a/src/Survey/AreaCount/Area/NewLocationModal.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import { forwardRef, useEffect, useState } from 'react'; -import clsx from 'clsx'; -import { resizeOutline } from 'ionicons/icons'; -import { Trans as T, useTranslation } from 'react-i18next'; -import { z, object } from 'zod'; -import { Main } from '@flumens'; -import TextInput from '@flumens/tailwind/dist/components/Input'; -import { isPlatform } from '@ionic/core'; -import { - IonButton, - IonButtons, - IonHeader, - IonIcon, - IonModal, - IonTitle, - IonToolbar, - useIonActionSheet, -} from '@ionic/react'; -import { getGeomString } from 'common/helpers/location'; -import { GROUP_SITE_TYPE } from 'common/models/location'; -import { AreaCountLocation, Group } from 'common/models/sample'; -import HeaderButton from 'Survey/common/HeaderButton'; - -const schema = object({ - id: z.string(), - locationTypeId: z.string(), - createdOn: z.string(), - updatedOn: z.string(), - name: z.string().min(1, 'Please fill in'), - lat: z.string(), - lon: z.string(), - centroidSref: z.string(), - centroidSrefSystem: z.string(), - boundaryGeom: z.string(), - comment: z.string().optional(), -}); - -type FixedLocation = z.infer; - -const useDismissHandler = (newLocation: any) => { - const { t } = useTranslation(); - const [present] = useIonActionSheet(); - - const canDismiss = (force?: boolean) => - new Promise(resolve => { - const isEmpty = !newLocation.name && !newLocation.comment; - if (isEmpty || force) { - resolve(true); - return; - } - - present({ - header: t('Are you sure?'), - subHeader: t('This will discard the form data.'), - buttons: [ - { text: t('Yes'), role: 'confirm' }, - { text: t('No'), role: 'cancel' }, - ], - onWillDismiss: ev => resolve(ev.detail.role === 'confirm'), - }); - }); - - return canDismiss; -}; - -const getNewLocationSeed = (location?: AreaCountLocation) => ({ - id: '', - locationTypeId: GROUP_SITE_TYPE, - createdOn: new Date().toISOString(), - updatedOn: new Date().toISOString(), - boundaryGeom: location?.shape ? getGeomString(location?.shape) : '', - lat: `${location?.latitude}`, - lon: `${location?.longitude}`, - centroidSref: `${location?.latitude} ${location?.longitude}`, - centroidSrefSystem: '4326', - name: '', - comment: '', -}); - -type Props = { - presentingElement: any; - isOpen: any; - onCancel: any; - onSave: (location: FixedLocation) => Promise; - group: Group; - location: AreaCountLocation; -}; - -const NewLocationModal = ( - { presentingElement, isOpen, onCancel, onSave, group, location }: Props, - ref: any -) => { - const [newLocation, setNewLocation] = useState( - getNewLocationSeed(location) - ); - - useEffect(() => { - // we don't care of overwriting the form values as the location shouldn't change while the GPS is off - setNewLocation(getNewLocationSeed(location)); - }, [location]); - - const { success: isValidLocation } = schema.safeParse(newLocation); - - const canDismiss = useDismissHandler(newLocation || {}); - - const cleanUp = () => setNewLocation(getNewLocationSeed(location)); - - const { area } = location; - - const dismiss = async (force?: boolean) => { - const closing = await ref.current?.dismiss(force); - if (closing) onCancel(); - }; - - const onSaveWrap = async () => { - if (!isValidLocation) return; - - const success = await onSave(newLocation); - if (!success) return; - - dismiss(true); - }; - - return ( - - - - - dismiss()}> - Cancel - - - - New location - - - - Save - - - - - -
- - Selected area: {area?.toLocaleString()} mĀ² -
-
-
-
- {group?.title} -
-
- -
-
- - setNewLocation({ ...newLocation, name: newVal }) - } - platform="ios" - /> - - setNewLocation({ ...newLocation, comment: newVal }) - } - platform="ios" - labelPlacement="floating" - isMultiline - /> -
-
-
- ); -}; - -export default forwardRef(NewLocationModal); diff --git a/src/Survey/AreaCount/Area/NewLocationModal/config.tsx b/src/Survey/AreaCount/Area/NewLocationModal/config.tsx new file mode 100644 index 00000000..844cdf95 --- /dev/null +++ b/src/Survey/AreaCount/Area/NewLocationModal/config.tsx @@ -0,0 +1,450 @@ +export const siteNameAttr = { + id: 'name', + type: 'text_input', + title: 'Site name', + container: 'inline', +} as const; + +export const siteAreaAttr = { + id: 'locAttr:376', + type: 'choice_input', + title: 'Site area', + appearance: 'button', + choices: [ + { title: '5 x 10 m', data_name: '23729' }, + { title: '20 x 25 m', data_name: '23730' }, + { title: '10 x 50 m', data_name: '23731' }, + { title: '5 x 100 m', data_name: '23732' }, + { title: 'other', data_name: '23733' }, + ], +} as const; + +export const habitatAttr = { + id: 'locAttr:340', + type: 'choice_input', + title: 'Dominant habitat', + appearance: 'button', + choices: [ + { title: 'Garden', data_name: '23571' }, + { title: 'Allotment gardens', data_name: '23573' }, + { title: 'Community garden', data_name: '23575' }, + { title: 'Balcony', data_name: '23577' }, + { title: 'Park (mixed vegetation)', data_name: '23579' }, + { title: 'Lawn', data_name: '23581' }, + { title: 'Flowering strip', data_name: '23583' }, + { title: 'Built-up area', data_name: '23585' }, + { title: 'Fallow land, abandonned area (urban)', data_name: '23587' }, + { title: 'Fallow land, abandonned fieldĀ  (rural)', data_name: '23589' }, + { title: 'Field edge', data_name: '23591' }, + { title: 'Arable field', data_name: '23593' }, + { title: 'Grassland / Meadow / Pasture', data_name: '23595' }, + { title: 'Orchard', data_name: '23597' }, + { title: 'Forest edge', data_name: '23599' }, + { title: 'Woodland/Forest', data_name: '23601' }, + { title: 'Coastal', data_name: '23603' }, + { title: 'Wetland', data_name: '23605' }, + { title: 'Scrubland/Heathland', data_name: '23607' }, + { title: 'Sparsely vegetated', data_name: '23609' }, + { title: 'Desert / barren', data_name: '23611' }, + { title: 'Other', data_name: '23613' }, + ], +} as const; + +export const grainsNumberAttr = { + id: 'locAttr:341', + type: 'number_input', + title: 'Arable field grains (wheat, barley, rye)', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const vegetablesNumberAttr = { + id: 'locAttr:342', + type: 'number_input', + title: 'Arable field fruits or vegetables', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const rapeseedNumberAttr = { + id: 'locAttr:343', + type: 'number_input', + title: 'Arable field rapeseed', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const cornNumberAttr = { + id: 'locAttr:344', + type: 'number_input', + title: 'Arable field corn', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const legumesNumberAttr = { + id: 'locAttr:345', + type: 'number_input', + title: 'Arable legumes', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const croppingNumberAttr = { + id: 'locAttr:346', + type: 'number_input', + title: 'Arable multi-cropping', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const fallowNumberAttr = { + id: 'locAttr:347', + type: 'number_input', + title: 'Arable fallow', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const managedGrasslandNumberAttr = { + id: 'locAttr:348', + type: 'number_input', + title: 'Grassland homogeneous/intensively managed', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const grasslandNumberAttr = { + id: 'locAttr:349', + type: 'number_input', + title: 'Grassland extensive or heterogeneous (pasture)', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const orchardNumberAttr = { + id: 'locAttr:350', + type: 'number_input', + title: 'Orchard, vineyard or grove (sparse, pasture among trees)', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const orchardManagedNumberAttr = { + id: 'locAttr:351', + type: 'number_input', + title: 'Orchard, vineyard or grove (intensely managed)', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const numberAttr = { + id: 'locAttr:352', + type: 'number_input', + title: 'Scrubland', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const wastelandNumberAttr = { + id: 'locAttr:353', + type: 'number_input', + title: 'Land laying fallow / wasteland', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const woodlandNumberAttr = { + id: 'locAttr:354', + type: 'number_input', + title: 'Sparse woodland', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const forestNumberAttr = { + id: 'locAttr:355', + type: 'number_input', + title: 'Dense woodland or forest', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const plantationNumberAttr = { + id: 'locAttr:356', + type: 'number_input', + title: 'Plantation', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const gardenNumberAttr = { + id: 'locAttr:357', + type: 'number_input', + title: 'Garden (single)', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const gardensNumberAttr = { + id: 'locAttr:358', + type: 'number_input', + title: 'Gardens (multiple, e.g. allotment gardens)', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const buildingsNumberAttr = { + id: 'locAttr:359', + type: 'number_input', + title: 'Building(s)', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const waterNumberAttr = { + id: 'locAttr:360', + type: 'number_input', + title: 'Pond, lake or sea', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const riverNumberAttr = { + id: 'locAttr:361', + type: 'number_input', + title: 'River or creek', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const wetlandNumberAttr = { + id: 'locAttr:362', + type: 'number_input', + title: 'Wetland', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const landNumberAttr = { + id: 'locAttr:363', + type: 'number_input', + title: 'Dunes or barren land', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const landscapeFeaturesAttr = { + id: 'locAttr:364', + type: 'choice_input', + title: + 'What landscape features are located in your observation location and/or within a radius of 50 meters?', + multiple: true, + appearance: 'button', + choices: [ + { title: 'Field edge(s)', data_name: '23615' }, + { title: 'Buffer-strip(s)', data_name: '23617' }, + { title: 'Flower-strip(s)', data_name: '23619' }, + { title: 'Hedge(s)', data_name: '23621' }, + { title: 'Scattered trees or trees in rows', data_name: '23623' }, + { title: 'Wooded area', data_name: '23625' }, + { title: 'Terraces, stone walls', data_name: '23627' }, + { title: 'Pond', data_name: '23629' }, + { title: 'River or creek', data_name: '23631' }, + { title: 'Path', data_name: '23633' }, + { + title: 'Street, road or railroad tracks (sealed, e.g. asphalt)', + data_name: '23635', + }, + { + title: 'Fences or other human-made linear structures', + data_name: '23637', + }, + { title: 'Dead tree, stumps or wood', data_name: '23639' }, + { title: 'Other', data_name: '23641' }, + ], +} as const; + +export const otherLandscapeFeaturesAttr = { + id: 'locAttr:375', + type: 'text_input', + title: 'Other landscape feature details', + appearance: 'multiline', + container: 'inline', +} as const; + +export const treeNumberAttr = { + id: 'locAttr:365', + type: 'number_input', + title: 'How many trees are there in the area of your observation site?', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const grassProportionAttr = { + id: 'locAttr:366', + type: 'number_input', + title: 'What proportion of the site is a lawn or a grassland?', + appearance: 'counter', + placeholder: '0', + validations: { min: 0, max: 100 }, +} as const; + +export const grassMownAttr = { + id: 'locAttr:367', + type: 'choice_input', + title: 'How often is this lawn mown?', + appearance: 'button', + choices: [ + { title: 'Not applicable (not a grass / lawn)', data_name: '23643' }, + { title: "I don't know", data_name: '23645' }, + { title: 'Frequent mowing, all (relevant) area', data_name: '23647' }, + { title: 'Rare mowing (1-2 times a year), all area', data_name: '23649' }, + { + title: 'Rare mowing (1-2 times a year), partial mowing (Staffelmahd)', + data_name: '23651', + }, + { title: 'Extensive grazing (few grazers, not mowed)', data_name: '23653' }, + { title: 'Intensive grazing (not mowed)', data_name: '23655' }, + ], +} as const; + +export const fertilizedAttr = { + id: 'locAttr:368', + type: 'choice_input', + title: 'Is the area fertilized?', + appearance: 'button', + choices: [ + { title: 'Not applicable', data_name: '23657' }, + { title: "I don't know", data_name: '23659' }, + { title: 'Frequent application', data_name: '23661' }, + { title: 'Rare application', data_name: '23663' }, + { title: 'No fertilizers used', data_name: '23665' }, + { title: 'Other', data_name: '23667' }, + ], +} as const; + +export const otherFertilizerAttr = { + id: 'locAttr:373', + type: 'text_input', + title: 'Other fertilizer details', + appearance: 'multiline', + container: 'inline', +} as const; + +export const pesticidesAttr = { + id: 'locAttr:369', + type: 'choice_input', + title: 'Are pesticides used?', + appearance: 'button', + choices: [ + { title: 'Not applicable', data_name: '23669' }, + { title: "I don't know", data_name: '23671' }, + { title: 'Frequent application', data_name: '23673' }, + { title: 'Rare application', data_name: '23675' }, + { title: 'No pesticides applied', data_name: '23677' }, + { title: 'Other', data_name: '23679' }, + ], +} as const; + +export const otherPesticideAttr = { + id: 'locAttr:374', + type: 'text_input', + title: 'Other pesticide details', + appearance: 'multiline', + container: 'inline', +} as const; + +export const speciesAttr = { + id: 'locAttr:370', + type: 'choice_input', + title: 'Are these plant species present in this location?', + multiple: true, + appearance: 'button', + choices: [ + { title: 'Fruit trees and shrubs', data_name: '23681' }, + { + title: 'Unmanaged corners (natural spaces, abandoned areas)', + data_name: '23683', + }, + { title: 'Vegetable patch', data_name: '23685' }, + { title: 'Lavender species', data_name: '23687' }, + { title: 'Geraniums & Pelargoniums', data_name: '23689' }, + { title: 'Valeriana', data_name: '23691' }, + { title: 'Legumes (Clover, Lupin, Lotus,...)', data_name: '23693' }, + { title: 'Marigold', data_name: '23695' }, + { title: 'Butterfly bush (or Summer Lilac)', data_name: '23697' }, + { + title: 'Aromatics like Thyme, Oregano, etc. ( Lamiaceae)', + data_name: '23699', + }, + { title: 'Nettle (Urtica dioica)', data_name: '23701' }, + { title: 'Thistle species', data_name: '23703' }, + { title: 'Brambles (Rubus fruticosa)', data_name: '23705' }, + { title: 'Ivy', data_name: '23707' }, + { title: 'Knappweed (Centaurea and Scabiosa spp.)', data_name: '23709' }, + { + title: 'Fennel, Carvi or others from the Carrot family (Apiaceae)', + data_name: '23713', + }, + { + title: 'Cabbage, Rucola or others Mustard-plant family (Brassicaceae)', + data_name: '23715', + }, + { title: 'Hemp-agrimony (Eupatorium spp.)', data_name: '23711' }, + ], +} as const; + +export const landOwnershipAttr = { + id: 'locAttr:371', + type: 'choice_input', + title: 'Do you know who owns the land?', + appearance: 'button', + choices: [ + { title: 'I own the site', data_name: '23717' }, + { title: 'Private space', data_name: '23719' }, + { title: 'Public space', data_name: '23721' }, + { title: 'Communal space', data_name: '23723' }, + { title: 'Prefer not to say', data_name: '23725' }, + { title: "I don't know", data_name: '23727' }, + ], +} as const; + +export const responsibleAttr = { + id: 'locAttr:372', + type: 'yes_no_input', + title: 'Are you responsible for gardening activities at the site?', + choices: [{ data_name: '0' }, { data_name: '1' }], +} as const; + +export const commentAttr = { + id: 'comment', + type: 'text_input', + title: 'Comments', + appearance: 'multiline', + container: 'inline', +} as const; diff --git a/src/Survey/AreaCount/Area/NewLocationModal/index.tsx b/src/Survey/AreaCount/Area/NewLocationModal/index.tsx new file mode 100644 index 00000000..ddf73370 --- /dev/null +++ b/src/Survey/AreaCount/Area/NewLocationModal/index.tsx @@ -0,0 +1,320 @@ +import { forwardRef, useEffect, useState } from 'react'; +import { observable, IObservableArray } from 'mobx'; +import clsx from 'clsx'; +import { Trans as T, useTranslation } from 'react-i18next'; +import { z } from 'zod'; +import { Capacitor } from '@capacitor/core'; +import { Block, Location, Main, PhotoPicker, captureImage } from '@flumens'; +import { isPlatform } from '@ionic/core'; +import { + IonButton, + IonButtons, + IonHeader, + IonModal, + IonTitle, + IonToolbar, + useIonActionSheet, +} from '@ionic/react'; +import config from 'common/config'; +import { getGeomCenter, getGeomString } from 'common/helpers/location'; +import LocationModel, { GROUP_SITE_TYPE } from 'common/models/location'; +import Media from 'common/models/media'; +import { Group } from 'common/models/sample'; +import HeaderButton from 'Survey/common/HeaderButton'; +import { + buildingsNumberAttr, + commentAttr, + cornNumberAttr, + croppingNumberAttr, + fallowNumberAttr, + fertilizedAttr, + forestNumberAttr, + gardenNumberAttr, + gardensNumberAttr, + grainsNumberAttr, + grassMownAttr, + grassProportionAttr, + grasslandNumberAttr, + habitatAttr, + landNumberAttr, + landOwnershipAttr, + landscapeFeaturesAttr, + legumesNumberAttr, + managedGrasslandNumberAttr, + numberAttr, + orchardManagedNumberAttr, + orchardNumberAttr, + otherFertilizerAttr, + otherLandscapeFeaturesAttr, + otherPesticideAttr, + pesticidesAttr, + plantationNumberAttr, + rapeseedNumberAttr, + responsibleAttr, + riverNumberAttr, + siteAreaAttr, + siteNameAttr, + speciesAttr, + treeNumberAttr, + vegetablesNumberAttr, + wastelandNumberAttr, + waterNumberAttr, + wetlandNumberAttr, + woodlandNumberAttr, +} from './config'; + +type Site = z.infer; + +const useDismissHandler = (newLocation: any) => { + const { t } = useTranslation(); + const [present] = useIonActionSheet(); + + const canDismiss = (force?: boolean) => + new Promise(resolve => { + const isEmpty = !newLocation.name && !newLocation.comment; + if (isEmpty || force) { + resolve(true); + return; + } + + present({ + header: t('Are you sure?'), + subHeader: t('This will discard the form data.'), + buttons: [ + { text: t('Yes'), role: 'confirm' }, + { text: t('No'), role: 'cancel' }, + ], + onWillDismiss: ev => resolve(ev.detail.role === 'confirm'), + }); + }); + + return canDismiss; +}; + +const getNewSiteSeed = (shape?: Location['shape']): Partial => ({ + locationTypeId: GROUP_SITE_TYPE, + boundaryGeom: shape ? getGeomString(shape) : undefined, + lat: shape ? `${getGeomCenter(shape)[1]}` : undefined, + lon: shape ? `${getGeomCenter(shape)[0]}` : undefined, + centroidSrefSystem: '4326', + centroidSref: shape + ? `${getGeomCenter(shape)[1]} ${getGeomCenter(shape)[0]}` + : undefined, + name: undefined, +}); + +type Props = { + presentingElement: any; + isOpen: any; + onCancel: any; + onSave: (location: Site, photos: Media[]) => Promise; + group: Group; + shape?: Location['shape']; +}; + +const NewLocationModal = ( + { presentingElement, isOpen, onCancel, onSave, group, shape }: Props, + ref: any +) => { + const [newLocation, setNewLocation] = useState>( + observable(getNewSiteSeed(shape)) + ); + const [photos, setPhotos] = useState>(observable([])); + + async function onAddPhoto() { + const images = await captureImage({ camera: true }); + if (!images.length) return; + + const getImageModel = async (image: any) => { + const imageModel: any = await Media.getImageModel( + isPlatform('hybrid') ? Capacitor.convertFileSrc(image) : image, + config.dataPath, + true + ); + + return imageModel; + }; + + const imageModels: Media[] = await Promise.all( + images.map(getImageModel) + ); + photos.push(imageModels[0]); + setPhotos(photos); + } + + const onRemovePhoto = (m: any) => { + photos.remove(m); + setPhotos(photos); + }; + + const resetState = () => { + setPhotos(observable([])); + setNewLocation(observable(getNewSiteSeed(shape))); + }; + useEffect(resetState, [shape]); + + const { success: isValidLocation } = + LocationModel.remoteSchema.safeParse(newLocation); + + const canDismiss = useDismissHandler(newLocation || {}); + + const cleanUp = () => resetState(); + + const dismiss = async (force?: boolean) => { + const closing = await ref.current?.dismiss(force); + if (closing) onCancel(); + }; + + const onSaveWrap = async () => { + if (!isValidLocation) return; + + const success = await onSave(newLocation as Site, photos); + if (!success) return; + + dismiss(true); + }; + + const isAgroecologyTRANSECT = group?.title.includes('Agroecology'); + const isCAP4GI = group?.title.includes('CAP4GI'); + const isVielFalterGarten = group?.title.includes('VielFalterGarten'); + const isUNPplus = group?.title.includes('UNPplus'); + + const getBlockAttrs = (attrConf: any) => ({ + record: newLocation, + block: attrConf, + onChange: (newVal: any) => { + setNewLocation({ ...newLocation, [attrConf.id]: newVal }); + return null; + }, + }); + + return ( + <> + + + + + dismiss()}> + Cancel + + + + New location + + + + Save + + + +
+ {group?.title} +
+
+ +
+
+ + + {(isAgroecologyTRANSECT || + isCAP4GI || + isVielFalterGarten || + isUNPplus) && } + + {(isVielFalterGarten || isUNPplus) && ( + + )} +
+ + {(isAgroecologyTRANSECT || isCAP4GI) && ( +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ )} + +
+ {(isAgroecologyTRANSECT || isCAP4GI) && ( + <> + + + + )} + + {(isVielFalterGarten || isUNPplus) && ( + <> + + + + )} + + {(isAgroecologyTRANSECT || + isCAP4GI || + isVielFalterGarten || + isUNPplus) && ( + <> + + + + + + + )} + + {(isVielFalterGarten || isUNPplus) && ( + <> + + + + + )} + + +
+ +
+ +
+
+
+ + ); +}; + +export default forwardRef(NewLocationModal); diff --git a/src/Survey/AreaCount/Area/index.tsx b/src/Survey/AreaCount/Area/index.tsx index 078488d1..7ca359c4 100644 --- a/src/Survey/AreaCount/Area/index.tsx +++ b/src/Survey/AreaCount/Area/index.tsx @@ -7,6 +7,7 @@ import { device, Location, useLoader, useToast } from '@flumens'; import { IonIcon, IonPage, isPlatform, NavContext } from '@ionic/react'; import groups from 'common/models/collections/groups'; import GroupModel from 'common/models/group'; +import Media from 'common/models/media'; import locations from 'models/collections/locations'; import LocationModel, { RemoteAttributes } from 'models/location'; import Sample, { AreaCountLocation } from 'models/sample'; @@ -117,7 +118,10 @@ const AreaController = ({ sample }: Props) => { const onCloseLocationModal = () => setNewLocationModalOpen(false); - const onSaveNewLocation = async (newSiteAttrs: RemoteAttributes) => { + const onSaveNewLocation = async ( + newSiteAttrs: RemoteAttributes, + media: Media[] + ) => { if ( !userModel.isLoggedIn() || !userModel.attrs.verified || @@ -125,8 +129,6 @@ const AreaController = ({ sample }: Props) => { ) return false; - console.log('Saving location', newSiteAttrs); - try { await loader.show('Please wait...'); @@ -137,9 +139,9 @@ const AreaController = ({ sample }: Props) => { const newSite = new LocationModel({ skipStore: true, attrs: newSiteAttrs as any, // any - to fix Moth trap attrs + media, }); await newSite.saveRemote(); - await group.addLocation(newSite); await refreshLocations(); @@ -180,7 +182,7 @@ const AreaController = ({ sample }: Props) => { onCancel={onCloseLocationModal} onSave={onSaveNewLocation} group={sample.attrs.group!} - location={location} + shape={location.shape} /> ); diff --git a/src/Survey/Moth/Location/Map/Traps.tsx b/src/Survey/Moth/Location/Map/Traps.tsx index 42d08e09..b32f9e51 100644 --- a/src/Survey/Moth/Location/Map/Traps.tsx +++ b/src/Survey/Moth/Location/Map/Traps.tsx @@ -22,8 +22,8 @@ const Traps = ({ sample, onSelect, mothTraps }: Props) => { geometry: { type: 'Point', coordinates: [ - trap.attrs.location.longitude, - trap.attrs.location.latitude, + trap.attrs.location?.longitude, + trap.attrs.location?.latitude, 0.0, ], }, diff --git a/src/Survey/Transect/Sections/List/Main/components/SVG.jsx b/src/Survey/Transect/Sections/List/Main/components/SVG.jsx index d2b3c6e6..701b0ae1 100644 --- a/src/Survey/Transect/Sections/List/Main/components/SVG.jsx +++ b/src/Survey/Transect/Sections/List/Main/components/SVG.jsx @@ -1,11 +1,10 @@ import { createRef, Component } from 'react'; import * as d3 from 'd3'; -import PropTypes from 'prop-types'; class SVG extends Component { - static propTypes = { - geom: PropTypes.any.isRequired, - }; + // static propTypes = { + // geom: PropTypes.any.isRequired, + // }; constructor(props) { super(props); diff --git a/src/Survey/common/TaxonSearch/components/Suggestions/components/Species.jsx b/src/Survey/common/TaxonSearch/components/Suggestions/components/Species.jsx index f3f57adb..2f0bb5e1 100644 --- a/src/Survey/common/TaxonSearch/components/Suggestions/components/Species.jsx +++ b/src/Survey/common/TaxonSearch/components/Suggestions/components/Species.jsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import { Trans as T } from 'react-i18next'; import { IonItem } from '@ionic/react'; import groups from 'common/data/groups'; @@ -37,6 +36,11 @@ function prettifyName(species, searchPhrase) { ); } +// Species.propTypes = { +// species: PropTypes.object.isRequired, +// searchPhrase: PropTypes.string.isRequired, +// onSelect: PropTypes.func.isRequired, +// }; const Species = ({ species, searchPhrase, onSelect }) => { const prettyName = prettifyName(species, searchPhrase); const { isRecorded } = species; @@ -56,10 +60,4 @@ const Species = ({ species, searchPhrase, onSelect }) => { ); }; -Species.propTypes = { - species: PropTypes.object.isRequired, - searchPhrase: PropTypes.string.isRequired, - onSelect: PropTypes.func.isRequired, -}; - export default Species; diff --git a/src/Survey/common/TaxonSearch/components/Suggestions/index.jsx b/src/Survey/common/TaxonSearch/components/Suggestions/index.jsx index 63d5a1e9..4909ca38 100644 --- a/src/Survey/common/TaxonSearch/components/Suggestions/index.jsx +++ b/src/Survey/common/TaxonSearch/components/Suggestions/index.jsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import { IonList } from '@ionic/react'; import InfoBackgroundMessage from 'Components/InfoBackgroundMessage'; import Species from './components/Species'; @@ -53,6 +52,12 @@ const getSearchInfo = () => ( ); +// Suggestions.propTypes = { +// searchResults: PropTypes.array, +// searchPhrase: PropTypes.string.isRequired, +// onSpeciesSelected: PropTypes.func.isRequired, +// }; + const Suggestions = ({ searchResults, searchPhrase, onSpeciesSelected }) => { if (!searchResults) { return ( @@ -93,10 +98,4 @@ const Suggestions = ({ searchResults, searchPhrase, onSpeciesSelected }) => { ); }; -Suggestions.propTypes = { - searchResults: PropTypes.array, - searchPhrase: PropTypes.string.isRequired, - onSpeciesSelected: PropTypes.func.isRequired, -}; - export default Suggestions; diff --git a/src/Survey/common/TaxonSearch/index.jsx b/src/Survey/common/TaxonSearch/index.jsx index 11a9cbfe..6834d73b 100644 --- a/src/Survey/common/TaxonSearch/index.jsx +++ b/src/Survey/common/TaxonSearch/index.jsx @@ -1,5 +1,4 @@ import { createRef, Component } from 'react'; -import PropTypes from 'prop-types'; import { IonSearchbar, withIonLifeCycle } from '@ionic/react'; import groups from 'common/data/groups'; import appModel from 'models/app'; @@ -17,12 +16,12 @@ function getDefaultState() { } class index extends Component { - static propTypes = { - onSpeciesSelected: PropTypes.func.isRequired, - recordedTaxa: PropTypes.array, - speciesGroups: PropTypes.array, - useDayFlyingMothsOnly: PropTypes.bool, - }; + // static propTypes = { + // onSpeciesSelected: PropTypes.func.isRequired, + // recordedTaxa: PropTypes.array, + // speciesGroups: PropTypes.array, + // useDayFlyingMothsOnly: PropTypes.bool, + // }; input = createRef(); diff --git a/src/common/Components/GridRefValue/index.jsx b/src/common/Components/GridRefValue/index.jsx index 3d1a0f07..17223f53 100644 --- a/src/common/Components/GridRefValue/index.jsx +++ b/src/common/Components/GridRefValue/index.jsx @@ -1,5 +1,4 @@ import { observer } from 'mobx-react'; -import PropTypes from 'prop-types'; import { prettyPrintLocation } from '@flumens'; import { IonSpinner } from '@ionic/react'; import './styles.scss'; @@ -12,14 +11,13 @@ function getValue(sample) { return prettyPrintLocation(sample.attrs.location); } +// GridRefValue.propTypes = { +// sample: PropTypes.object.isRequired, +// }; function GridRefValue({ sample }) { const value = getValue(sample); return
{value}
; } -GridRefValue.propTypes = { - sample: PropTypes.object.isRequired, -}; - export default observer(GridRefValue); diff --git a/src/common/flumens.ts b/src/common/flumens.ts index df95f741..d23b7688 100644 --- a/src/common/flumens.ts +++ b/src/common/flumens.ts @@ -1,3 +1,4 @@ +export { boolToWarehouseValue } from '@flumens/ionic/dist/models/Indicia/helpers'; export { default as Model, type Options as ModelOptions, @@ -86,7 +87,7 @@ export { prettyPrintLocation, updateModelLocation, normalizeCoords, - isValidLocation + isValidLocation, } from '@flumens/ionic/dist/utils/location'; export { useDisableBackButton, @@ -120,4 +121,13 @@ export { default as TailwindContext, type ContextValue as TailwindContextValue, } from '@flumens/tailwind/dist/components/Context'; +export { + type Block as BlockT, + type ChoiceValues, +} from '@flumens/tailwind/dist/Survey'; +export { default as Block } from '@flumens/tailwind/dist/components/Block'; +export { + default as TailwindBlockContext, + defaultContext, +} from '@flumens/tailwind/dist/components/Block/Context'; export { default as InfoBackgroundMessage } from '@flumens/tailwind/dist/components/InfoBackgroundMessage'; diff --git a/src/common/helpers/location.ts b/src/common/helpers/location.ts index ae51f5f1..5f9fcc86 100644 --- a/src/common/helpers/location.ts +++ b/src/common/helpers/location.ts @@ -1,5 +1,6 @@ import { toJS } from 'mobx'; import wkt from 'wellknown'; +import { Location } from '@flumens'; import { SphericalMercator } from '@mapbox/sphericalmercator'; type XYPoint = [number, number]; @@ -41,3 +42,11 @@ export function getGeomString(shape: any) { return wkt.stringify(geoJSON); } + +export function getGeomCenter(shape: Location['shape']): XYPoint { + const geoJSON: Location['shape'] = toJS(shape)!; + if (geoJSON.type === 'Polygon') { + return [geoJSON.coordinates[0][0][0], geoJSON.coordinates[0][0][1]]; + } + return [geoJSON.coordinates[0][0], geoJSON.coordinates[0][1]]; +} diff --git a/src/common/models/collections/locations/index.ts b/src/common/models/collections/locations/index.ts index 5c75da10..f96a992c 100644 --- a/src/common/models/collections/locations/index.ts +++ b/src/common/models/collections/locations/index.ts @@ -125,7 +125,7 @@ export class Locations extends Collection { const mothTraps = await fetch(MOTH_TRAP_TYPE); const transects = await fetchTransects(); - const locationList = transects.map(({ id }) => id); + const locationList = transects.map(({ id }) => id!); const transectSections = await fetchTransectSections(locationList); const groupLocations = await groups.fetchLocations(); diff --git a/src/common/models/location.tsx b/src/common/models/location.tsx index 64702b3d..2631caa4 100644 --- a/src/common/models/location.tsx +++ b/src/common/models/location.tsx @@ -1,5 +1,6 @@ -import { observable } from 'mobx'; +import { IObservableArray, observable } from 'mobx'; import axios, { AxiosError } from 'axios'; +import { snakeCase } from 'lodash'; import * as Yup from 'yup'; import { z, object } from 'zod'; import { Geolocation } from '@capacitor/geolocation'; @@ -14,9 +15,11 @@ import { updateModelLocation, ModelValidationMessage, UUID, + boolToWarehouseValue, } from '@flumens'; import CONFIG from 'common/config'; import userModel from 'models/user'; +import Media from './media'; import { locationsStore } from './store'; import { getLocalAttributes } from './utils'; @@ -83,7 +86,10 @@ export const verifyLocationSchema = Yup.mixed().test( validateLocation ); -type LocationOptions = ModelOptions & { skipStore?: boolean }; +type LocationOptions = ModelOptions & { + media?: any[]; + skipStore?: boolean; +}; export type Lamp = { cid: string; @@ -108,24 +114,24 @@ export type Attrs = RemoteAttributes & MothTrapAttrs & ModelAttrs; class LocationModel extends Model { static remoteSchema = object({ - /** - * Entity ID. - */ - id: z.string(), - createdOn: z.string(), - updatedOn: z.string(), - lat: z.string(), - lon: z.string(), + lat: z.string().min(1), + lon: z.string().min(1), /** * Location name. */ - name: z.string(), + name: z.string().min(1), /** * Location type e.g. transect = 777, transect section = 778 etc. */ - locationTypeId: z.string(), - centroidSref: z.string(), - centroidSrefSystem: z.string(), + locationTypeId: z.string().min(1), + centroidSref: z.string().min(1), + centroidSrefSystem: z.string().min(1), + /** + * Entity ID. + */ + id: z.string().optional(), + createdOn: z.string().optional(), + updatedOn: z.string().optional(), parentId: z.string().nullable().optional(), boundaryGeom: z.string().nullable().optional(), code: z.string().nullable().optional(), @@ -258,8 +264,8 @@ class LocationModel extends Model { metadata: { ...metadata, - createdOn: new Date(createdOn).getTime(), - updatedOn: new Date(updatedOn).getTime(), + createdOn: new Date(createdOn!).getTime(), + updatedOn: new Date(updatedOn!).getTime(), }, }; @@ -290,19 +296,17 @@ class LocationModel extends Model { // eslint-disable-next-line // @ts-ignore - attrs: Attrs = Model.extendAttrs(this.attrs, { - // eslint-disable-next-line - // @ts-ignore - location: this.attrs.location || {}, - typeOther: null, - lamps: [], - }); + attrs: Attrs = Model.extendAttrs(this.attrs, {}); - constructor({ skipStore, ...options }: LocationOptions) { + media: IObservableArray; + + constructor({ skipStore, media = [], ...options }: LocationOptions) { super({ store: skipStore ? undefined : locationsStore, ...options, }); + + this.media = observable(media); } isDraft = () => !this.id; @@ -316,15 +320,13 @@ class LocationModel extends Model { } async saveRemote() { - console.log('Location uploading'); - - const submission = this.toRemoteJSON(); - - const url = `${CONFIG.backend.indicia.url}/index.php/services/rest/locations`; - try { this.remote.synchronising = true; + const warehouseMediaNames = await this.uploadMedia(); + const submission = this.toRemoteJSON(warehouseMediaNames); + const url = `${CONFIG.backend.indicia.url}/index.php/services/rest/locations`; + const token = await userModel.getAccessToken(); const options: any = { @@ -399,7 +401,15 @@ class LocationModel extends Model { } private toRemoteMothTrapJSON() { - const { lamps, type, typeOther, location } = this.attrs; + const { + lamps, + type, + typeOther, + location, + comment, + boundaryGeom, + centroidSrefSystem, + } = this.attrs; const stringifyLamp = (lamp: any) => JSON.stringify(lamp); const getLampType = (lamp: any) => { @@ -426,9 +436,12 @@ class LocationModel extends Model { const trapType = LocationModel.schema.type.remote.values.find(byValue)?.id; return { - location_type_id: MOTH_TRAP_TYPE, // the model already has this, so probably not needed name: location.name, + location_type_id: MOTH_TRAP_TYPE, // the model already has this, so probably not needed + centroid_sref_system: centroidSrefSystem, centroid_sref: `${location.latitude} ${location.longitude}`, + boundary_geom: boundaryGeom, + comment, 'locAttr:306': stringifiedLamps, 'locAttr:330': trapType, 'locAttr:234': userModel.id, @@ -436,26 +449,44 @@ class LocationModel extends Model { }; } - toRemoteJSON() { - const { name, comment, boundaryGeom, centroidSref, centroidSrefSystem } = - this.attrs; + toRemoteJSON(warehouseMediaNames = {}) { + const toSnakeCase = (attrs: any) => { + return Object.entries(attrs).reduce((agg: any, [attr, value]): any => { + const attrModified = attr.includes('locAttr:') ? attr : snakeCase(attr); + agg[attrModified] = value; // eslint-disable-line no-param-reassign + return agg; + }, {}); + }; + + const transformBoolean = (attrs: any) => + Object.entries(attrs).reduce((agg: any, [attr, value]: any) => { + if (typeof value === 'boolean') { + agg[attr] = boolToWarehouseValue(value); // eslint-disable-line no-param-reassign + } + return agg; + }, attrs); - const customAttrs = this.attrs.type ? this.toRemoteMothTrapJSON() : {}; + const attrs = this.attrs.type + ? this.toRemoteMothTrapJSON() + : transformBoolean(toSnakeCase(this.attrs)); const submission: any = { values: { - name, external_key: this.cid, - location_type_id: this.attrs.locationTypeId, - centroid_sref: centroidSref, - centroid_sref_system: centroidSrefSystem, - boundary_geom: boundaryGeom, - comment, - - ...customAttrs, + ...attrs, }, + media: [], }; + this.media.forEach(model => { + const modelSubmission = model.getSubmission(warehouseMediaNames); + if (!modelSubmission) { + return; + } + + submission.media.push(modelSubmission); + }); + return submission; } @@ -548,6 +579,19 @@ class LocationModel extends Model { Geolocation.clearWatch({ id }); } + + private async uploadMedia() { + // return bulkUploadMedia(this.media); //TODO: take it from Indicia Sample + await Promise.all(this.media.map(m => m.uploadFile())); + + const warehouseMediaNames: any = {}; + + this.media.forEach(m => { + warehouseMediaNames[m.cid] = { name: m.attrs.queued }; + }); + + return warehouseMediaNames; + } } export const useValidateCheck = () => {