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 13 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
42 changes: 31 additions & 11 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 { Animal, AnimalBatch } from '../../../store/api/types';
import {
useAddAnimalBatchesMutation,
useAddAnimalsMutation,
useGetAnimalOriginsQuery,
} from '../../../store/api/apiSlice';
import { Animal, AnimalBatch, PostAnimalBatch } 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: PostAnimalBatch[] = [];

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 @@ -89,7 +89,7 @@ export type Option = {
[DetailsFields.USE]: ReactSelectOption<number>;
[DetailsFields.TAG_COLOR]: ReactSelectOption<number>;
[DetailsFields.TAG_TYPE]: ReactSelectOption<number>;
[DetailsFields.ORGANIC_STATUS]: ReactSelectOption<number>;
[DetailsFields.ORGANIC_STATUS]: ReactSelectOption<'Non-Organic' | 'Transitional' | 'Organic'>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could consider making 'Non-Organic' | 'Transitional' | 'Organic' a reusable type -- its used twice here and would be useful for any locations that are going to be added like pasture.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely. I got a bit lazy about finding the right place for it, sorry. I'll make sure to add it!

[DetailsFields.SEX]: ReactSelectOption<number>;
[DetailsFields.ORIGIN]: ReactSelectOption<number>;
};
Expand Down
142 changes: 137 additions & 5 deletions packages/webapp/src/containers/Animals/AddAnimals/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,143 @@
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
*/

// TODO
export const formatAnimalDetailsToDBStructure = (data: any) => {
return data;
import i18n from '../../../locales/i18n';
import { Animal, AnimalBatch, PostAnimalBatch, PostBatchSexDetail } from '../../../store/api/types';
import { toLocalISOString } from '../../../util/moment';
import { DetailsFields, type AnimalDetailsFormFields } from './types';

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,
): Pick<PostAnimalBatch, 'count' | 'animal_batch_sex_detail'> => {
if (!data[DetailsFields.SEX_DETAILS] || !data[DetailsFields.SEX_DETAILS].length) {
return { count: data[DetailsFields.COUNT]! };
}

return {
count: data[DetailsFields.COUNT]!,
animal_batch_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, label }, index: number) => {
useRelations.push({ use_id: useId });
if (label === i18n.t('animal:USE.OTHER') && otherUse) {
SayakaOno marked this conversation as resolved.
Show resolved Hide resolved
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 | PostAnimalBatch> => {
if (!broughtInId && !data[DetailsFields.ORIGIN]) {
return { birth_date: convertFormDate(data[DetailsFields.DATE_OF_BIRTH]) };
}

const isBroughtIn = broughtInId && data[DetailsFields.ORIGIN];
SayakaOno marked this conversation as resolved.
Show resolved Hide resolved

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 | PostAnimalBatch> => {
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,
): PostAnimalBatch => {
return formatCommonDetails(false, data, broughtInId);
};
3 changes: 2 additions & 1 deletion packages/webapp/src/store/api/apiSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import type {
AnimalIdentifierColor,
AnimalOrigin,
AnimalUse,
PostAnimalBatch,
} from './types';

export const api = createApi({
Expand Down Expand Up @@ -184,7 +185,7 @@ export const api = createApi({
}),
invalidatesTags: ['Animals'],
}),
addAnimalBatches: build.mutation<AnimalBatch[], Partial<AnimalBatch>[]>({
addAnimalBatches: build.mutation<AnimalBatch[], PostAnimalBatch[]>({
query: (body) => ({
url: `${animalBatchesUrl}`,
method: 'POST',
Expand Down
19 changes: 19 additions & 0 deletions packages/webapp/src/store/api/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,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 +41,21 @@ export interface Animal {
sex_id: number;
sire: string | null;
weaning_date: string | null;
organic_status: 'Non-Organic' | 'Transitional' | 'Organic';
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,13 +64,24 @@ 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: 'Non-Organic' | 'Transitional' | 'Organic';
supplier: string | null;
price: number | null;
animal_removal_reason_id: number | null;
removal_explanation: string | null;
removal_date: string | null;
}

export interface PostBatchSexDetail {
animal_batch_sex_detail?: { sex_id: number; count: number }[];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my only question is if this is to match up a name difference from frontend and backend -- is it possible to fix it to be one name?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's actually a bug...!!! Sex detail will not be added unless the key is sex_detail.
Thank you so much for pointing that out!

}

export type PostAnimalBatch = Partial<AnimalBatch> & PostBatchSexDetail;

export interface AnimalGroup {
farm_id: string;
id: number;
Expand Down
12 changes: 7 additions & 5 deletions packages/webapp/src/stories/Animals/Details/mockData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ChangeEvent, DragEvent } from 'react';
import {
DetailsFields,
AnimalDetailsFormFields,
ReactSelectOption,
} from '../../../containers/Animals/AddAnimals/types';
import { FileEvent } from '../../../components/ImagePicker';
import { GetOnFileUpload } from '../../../components/ImagePicker/useImagePickerUpload';
Expand Down Expand Up @@ -53,11 +54,12 @@ export const tagColorOptions = [
{ value: 6, label: 'Red' },
];

export const organicStatusOptions = [
{ value: 1, label: 'Non-Organic' },
{ value: 2, label: 'Organic' },
{ value: 3, label: 'Transitioning' },
];
export const organicStatusOptions: ReactSelectOption<'Non-Organic' | 'Organic' | 'Transitional'>[] =
[
{ value: 'Non-Organic', label: 'Non-Organic' },
{ value: 'Organic', label: 'Organic' },
{ value: 'Transitional', label: 'Transitioning' },
];

export const originOptions = [
{ value: 1, label: 'Brought in', key: 'BROUGHT_IN' },
Expand Down
Loading