Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LF-4166 (4): Implement end to end animal creation flow #3397

Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
19eb066
LF-4166 Add missing properties to Animal/Batch types
SayakaOno Aug 27, 2024
c7e41f8
LF-4166 Correct organic status Option type
SayakaOno Aug 27, 2024
5f7e8f9
LF-4166 Correct addAnimalBatches mutation request body type
SayakaOno Aug 27, 2024
50b9556
LF-4166 Create format functions for animal and batch
SayakaOno Aug 27, 2024
6fcb104
LF-4166 Implement onSave function in AddAnimals
SayakaOno Aug 27, 2024
1c7b4dc
LF-4166 Fix format animal/batch functions
SayakaOno Aug 27, 2024
2582ce0
LF-4166 Fix dates' format in Animal model
SayakaOno Aug 28, 2024
b694cca
LF-4166 Finalize organic status type
SayakaOno Aug 28, 2024
6c9ff30
LF-4166 Rename animal util functions
SayakaOno Aug 28, 2024
5260f26
LF-4166 Create PostAnimalBatch type
SayakaOno Aug 28, 2024
b266c93
LF-4166 Correct types
SayakaOno Aug 28, 2024
8d85441
LF-4166 Move birth_date to origin section in format function
SayakaOno Aug 28, 2024
9f9560b
LF-4166 Add missing properties to AnimalBatch type
SayakaOno Aug 28, 2024
032ed32
LF-4166 Fix animal batch POST request body and type
SayakaOno Aug 29, 2024
2baba57
Merge branch 'integration' into LF-4166/Complete_Implement_End-to-End…
SayakaOno Aug 29, 2024
9fc8d20
Apply suggestions from code review
SayakaOno Aug 30, 2024
ad2b487
LF-4166 Create OrganicStatus type
SayakaOno Aug 30, 2024
e7e4078
LF-4166 /animal_type_use_relationship
SayakaOno Aug 30, 2024
1211b98
Filter uses in animal details
SayakaOno Aug 30, 2024
f73d909
Adjust add animal summary image width
SayakaOno Aug 30, 2024
79566c6
Merge pull request #3402 from LiteFarmOrg/Add_animal_fix
Duncan-Brain Sep 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/api/src/models/animalModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,14 @@ class Animal extends baseModel {
custom_breed_id: { type: ['integer', 'null'] },
sex_id: { type: ['integer', 'null'] },
name: { type: ['string', 'null'] },
birth_date: { type: ['string', 'null'], format: 'date' },
birth_date: { type: ['string', 'null'], format: 'date-time' },
identifier: { type: ['string', 'null'] },
identifier_color_id: { type: ['integer', 'null'] },
origin_id: { type: ['integer', 'null'] },
dam: { type: ['string', 'null'] },
sire: { type: ['string', 'null'] },
brought_in_date: { type: ['string', 'null'], format: 'date' },
weaning_date: { type: ['string', 'null'], format: 'date' },
brought_in_date: { type: ['string', 'null'], format: 'date-time' },
weaning_date: { type: ['string', 'null'], format: 'date-time' },
notes: { type: ['string', 'null'] },
photo_url: { type: ['string', 'null'] },
animal_removal_reason_id: { type: ['integer', 'null'] },
Expand Down
40 changes: 30 additions & 10 deletions packages/webapp/src/containers/Animals/AddAnimals/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,16 @@ import { MultiStepForm, VARIANT } from '../../../components/Form/MultiStepForm';
import AddAnimalBasics, { animalBasicsDefaultValues } from './AddAnimalBasics';
import AddAnimalDetails from './AddAnimalDetails';
import AddAnimalSummary from './AddAnimalSummary';
import { useAddAnimalBatchesMutation, useAddAnimalsMutation } from '../../../store/api/apiSlice';
import {
useAddAnimalBatchesMutation,
useAddAnimalsMutation,
useGetAnimalOriginsQuery,
} from '../../../store/api/apiSlice';
import { Animal, AnimalBatch } from '../../../store/api/types';
import { enqueueErrorSnackbar } from '../../Snackbar/snackbarSlice';
import { formatAnimalDetailsToDBStructure, formatBatchDetailsToDBStructure } from './utils';
import { AnimalDetailsFormFields } from './types';
import { AnimalOrBatchKeys } from '../types';

export const STEPS = {
BASICS: 'basics',
Expand All @@ -45,24 +51,42 @@ function AddAnimals({ isCompactSideMenu, history }: AddAnimalsProps) {
const [addAnimals] = useAddAnimalsMutation();
const [addAnimalBatches] = useAddAnimalBatchesMutation();

const { data: orgins = [] } = useGetAnimalOriginsQuery();

const onSave = async (
data: any,
onGoForward: () => void,
setFormResultData: (data: any) => void,
) => {
const formattedAnimals = formatAnimalDetailsToDBStructure([]);
const formattedBatches = formatBatchDetailsToDBStructure([]);
const details = data[STEPS.DETAILS] as AnimalDetailsFormFields[];
const broughtInId = orgins.find((origin) => origin.key === 'BROUGHT_IN')?.id;

const formattedAnimals: Partial<Animal>[] = [];
const formattedBatches: Partial<AnimalBatch>[] = [];

details.forEach((animalOrBatch) => {
if (animalOrBatch.animal_or_batch === AnimalOrBatchKeys.ANIMAL) {
formattedAnimals.push(formatAnimalDetailsToDBStructure(animalOrBatch, broughtInId));
} else {
formattedBatches.push(formatBatchDetailsToDBStructure(animalOrBatch, broughtInId));
}
});

let animalsResult: Animal[] = [];
let batchesResult: AnimalBatch[] = [];

try {
animalsResult = await addAnimals(formattedAnimals).unwrap();
if (formattedAnimals.length) {
animalsResult = await addAnimals(formattedAnimals).unwrap();
}
} catch (e) {
console.error(e);
dispatch(enqueueErrorSnackbar(t('message:ANIMALS.FAILED_CREATE_ANIMALS')));
}
try {
batchesResult = await addAnimalBatches(formattedBatches).unwrap();
if (formattedBatches.length) {
batchesResult = await addAnimalBatches(formattedBatches).unwrap();
}
} catch (e) {
console.error(e);
dispatch(enqueueErrorSnackbar(t('message:ANIMALS.FAILED_CREATE_BATCHES')));
Expand All @@ -72,11 +96,7 @@ function AddAnimals({ isCompactSideMenu, history }: AddAnimalsProps) {
return;
}

const resultData: { animals: Animal[]; batches: AnimalBatch[] } = {
animals: animalsResult,
batches: batchesResult,
};
setFormResultData(resultData);
setFormResultData({ animals: animalsResult, batches: batchesResult });
onGoForward();
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { TFunction } from 'react-i18next';
import { STEPS } from '../AddAnimals';
import type { Option as AnimalSelectOption } from '../../../components/Animals/AddAnimalsFormCard/AnimalSelect';
import type { Details as SexDetailsType } from '../../../components/Form/SexDetails/SexDetailsPopover';
import { OrganicStatus } from '../../../types';

export const BasicsFields = {
TYPE: 'type',
Expand Down Expand Up @@ -89,7 +90,7 @@ export type Option = {
[DetailsFields.USE]: ReactSelectOption<number>;
[DetailsFields.TAG_COLOR]: ReactSelectOption<number>;
[DetailsFields.TAG_TYPE]: ReactSelectOption<number>;
[DetailsFields.ORGANIC_STATUS]: ReactSelectOption<'Non-Organic' | 'Transitional' | 'Organic'>;
[DetailsFields.ORGANIC_STATUS]: ReactSelectOption<OrganicStatus>;
[DetailsFields.SEX]: ReactSelectOption<number>;
[DetailsFields.ORIGIN]: ReactSelectOption<number>;
};
Expand Down
146 changes: 136 additions & 10 deletions packages/webapp/src/containers/Animals/AddAnimals/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@
*/

import i18n from '../../../locales/i18n';

import {
AnimalSummary,
BatchSummary,
} from '../../../components/Animals/AddAnimalsSummaryCard/types';
import {
Animal,
AnimalBatch,
Expand All @@ -28,15 +23,146 @@ import {
DefaultAnimalBreed,
DefaultAnimalType,
} from '../../../store/api/types';
import { toLocalISOString } from '../../../util/moment';
import { DetailsFields, type AnimalDetailsFormFields } from './types';
import {
AnimalSummary,
BatchSummary,
} from '../../../components/Animals/AddAnimalsSummaryCard/types';
import { chooseAnimalBreedLabel, chooseAnimalTypeLabel } from '../Inventory/useAnimalInventory';

// TODO
export const formatAnimalDetailsToDBStructure = (data: any) => {
return data;
const formatFormTypeOrBreed = (
typeOrBreed: 'type' | 'breed',
data?: { label: string; value: string; __isNew__?: boolean },
kathyavini marked this conversation as resolved.
Show resolved Hide resolved
) => {
if (!data?.value) {
return {};
}
if (data.__isNew__) {
return { [`${typeOrBreed}_name`]: data.label };
}
const [defaultOrCustom, id] = data.value.split('_');

return { [`${defaultOrCustom}_${typeOrBreed}_id`]: +id };
};

const formatFormSexDetailsAndCount = (data: AnimalDetailsFormFields): Partial<AnimalBatch> => {
if (!data[DetailsFields.SEX_DETAILS] || !data[DetailsFields.SEX_DETAILS].length) {
return { count: data[DetailsFields.COUNT]! };
}

return {
count: data[DetailsFields.COUNT]!,
sex_detail: data[DetailsFields.SEX_DETAILS].map(({ id, count }) => {
return { sex_id: id, count };
}),
};
};

const formatFormUse = (
isAnimal: boolean,
use: AnimalDetailsFormFields[DetailsFields.USE],
otherUse: AnimalDetailsFormFields[DetailsFields.OTHER_USE],
) => {
if (!use || !use.length) {
return {};
}

const key = `animal${isAnimal ? '' : '_batch'}_use_relationships`;

const useRelations: { use_id: number; other_use?: string }[] = [];

use.forEach(({ value: useId, key }, index: number) => {
useRelations.push({ use_id: useId });
if (key === 'OTHER' && otherUse) {
useRelations[index].other_use = otherUse;
}
});

return { [key]: useRelations };
};

const convertFormDate = (date?: string): string | undefined => {
if (!date) {
return undefined;
}
return toLocalISOString(date);
};

const formatOrigin = (
data: AnimalDetailsFormFields,
broughtInId?: number,
): Partial<Animal | AnimalBatch> => {
if (!broughtInId && !data[DetailsFields.ORIGIN]) {
return { birth_date: convertFormDate(data[DetailsFields.DATE_OF_BIRTH]) };
}

const isBroughtIn = broughtInId === data[DetailsFields.ORIGIN];

return {
birth_date: convertFormDate(data[DetailsFields.DATE_OF_BIRTH]),
origin_id: data[DetailsFields.ORIGIN],
...(isBroughtIn
? {
brought_in_date: convertFormDate(data[DetailsFields.BROUGHT_IN_DATE]),
supplier: data[DetailsFields.SUPPLIER],
price: data[DetailsFields.PRICE] ? +data[DetailsFields.PRICE] : undefined,
}
: {
dam: data[DetailsFields.DAM],
sire: data[DetailsFields.SIRE],
}),
};
};

const formatCommonDetails = (
isAnimal: boolean,
data: AnimalDetailsFormFields,
broughtInId?: number,
): Partial<Animal | AnimalBatch> => {
return {
// General
...formatFormTypeOrBreed('type', data[DetailsFields.TYPE]),
...formatFormTypeOrBreed('breed', data[DetailsFields.BREED]),
...(isAnimal ? { sex_id: data[DetailsFields.SEX] } : formatFormSexDetailsAndCount(data)),
...formatFormUse(isAnimal, data[DetailsFields.USE], data[DetailsFields.OTHER_USE]),

// Other
organic_status: data[DetailsFields.ORGANIC_STATUS]?.value,
notes: data[DetailsFields.OTHER_DETAILS],
photo_url: data[DetailsFields.ANIMAL_IMAGE],

// Origin
...formatOrigin(data, broughtInId),

// Unique (animal) | General (batch)
name: data[DetailsFields.NAME],
};
};

export const formatAnimalDetailsToDBStructure = (
data: AnimalDetailsFormFields,
broughtInId?: number,
): Partial<Animal> => {
return {
...formatCommonDetails(true, data, broughtInId),

// Other
weaning_date: convertFormDate(data[DetailsFields.WEANING_DATE]),

// Unique
identifier: data[DetailsFields.TAG_NUMBER],
identifier_type_id: data[DetailsFields.TAG_TYPE]?.value,
identifier_color_id: data[DetailsFields.TAG_COLOR]?.value,
identifier_type_other: data[DetailsFields.TAG_TYPE_INFO],
};
};

export const formatBatchDetailsToDBStructure = (data: any) => {
return data;
export const formatBatchDetailsToDBStructure = (
data: AnimalDetailsFormFields,
broughtInId?: number,
): Partial<AnimalBatch> => {
return formatCommonDetails(false, data, broughtInId);
};

export const getSexMap = (
Expand Down
14 changes: 14 additions & 0 deletions packages/webapp/src/store/api/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/

import { TASK_TYPES } from '../../../containers/Task/constants';
import { OrganicStatus } from '../../../types';

// If we don't necessarily want to type an endpoint
export type Result = Array<{ [key: string]: any }>;
Expand All @@ -30,6 +31,8 @@ export interface Animal {
group_ids: number[];
id: number;
identifier: string | null;
identifier_type_id: number | null;
identifier_type_other: string | null;
identifier_color_id: number | null;
internal_identifier: number;
name: string | null;
Expand All @@ -39,15 +42,21 @@ export interface Animal {
sex_id: number;
sire: string | null;
weaning_date: string | null;
organic_status: OrganicStatus;
supplier: string | null;
price: number | null;
animal_removal_reason_id: number | null;
removal_explanation: string | null;
removal_date: string | null;
}

export interface AnimalBatch {
birth_date: string | null;
brought_in_date: string | null;
count: number;
custom_breed_id: number | null;
custom_type_id: number | null;
dam: string | null;
default_breed_id: number | null;
default_type_id: number | null;
farm_id: string;
Expand All @@ -56,8 +65,13 @@ export interface AnimalBatch {
internal_identifier: number;
name: string | null;
notes: string | null;
origin_id: number;
photo_url: string | null;
sex_detail: { sex_id: number; count: number }[];
sire: string | null;
organic_status: OrganicStatus;
supplier: string | null;
price: number | null;
animal_removal_reason_id: number | null;
removal_explanation: string | null;
removal_date: string | null;
Expand Down
12 changes: 6 additions & 6 deletions packages/webapp/src/stories/Animals/Details/mockData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from '../../../containers/Animals/AddAnimals/types';
import { FileEvent } from '../../../components/ImagePicker';
import { GetOnFileUpload } from '../../../components/ImagePicker/useImagePickerUpload';
import { OrganicStatus } from '../../../types';

export const sexOptions = [
{ value: 0, label: `I don't know` },
Expand Down Expand Up @@ -54,12 +55,11 @@ export const tagColorOptions = [
{ value: 6, label: 'Red' },
];

export const organicStatusOptions: ReactSelectOption<'Non-Organic' | 'Organic' | 'Transitional'>[] =
[
{ value: 'Non-Organic', label: 'Non-Organic' },
{ value: 'Organic', label: 'Organic' },
{ value: 'Transitional', label: 'Transitioning' },
];
export const organicStatusOptions: ReactSelectOption<OrganicStatus>[] = [
{ value: OrganicStatus.NON_ORGANIC, label: 'Non-Organic' },
{ value: OrganicStatus.ORGANIC, label: 'Organic' },
{ value: OrganicStatus.TRANSITIONAL, label: 'Transitioning' },
];

export const originOptions = [
{ value: 1, label: 'Brought in', key: 'BROUGHT_IN' },
Expand Down
20 changes: 20 additions & 0 deletions packages/webapp/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2024 LiteFarm.org
* This file is part of LiteFarm.
*
* LiteFarm is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LiteFarm is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
*/

export enum OrganicStatus {
'NON_ORGANIC' = 'Non-Organic',
'TRANSITIONAL' = 'Transitional',
'ORGANIC' = 'Organic',
}
Loading