From 233b3fd38c63b758c6801d37bf5aa514edabef9e Mon Sep 17 00:00:00 2001 From: Alessandro Macanha Date: Fri, 5 Jun 2020 09:02:53 -0300 Subject: [PATCH 01/10] update routes from benefit --- backend/database/seeders/benefitsProducts.ts | 40 ++++++++++ backend/database/seeders/index.ts | 2 + backend/src/models/benefitProducts.ts | 16 ++++ backend/src/models/benefits.ts | 77 ++++++++++++++++++++ backend/src/routes/benefits.ts | 10 ++- backend/src/schemas/benefitProducts.ts | 6 +- backend/src/schemas/benefits.ts | 6 ++ backend/src/schemas/products.ts | 8 ++ 8 files changed, 159 insertions(+), 6 deletions(-) create mode 100644 backend/database/seeders/benefitsProducts.ts diff --git a/backend/database/seeders/benefitsProducts.ts b/backend/database/seeders/benefitsProducts.ts new file mode 100644 index 00000000..7dbbcef7 --- /dev/null +++ b/backend/database/seeders/benefitsProducts.ts @@ -0,0 +1,40 @@ +import db from '../../src/schemas'; +import { BenefitProduct } from '../../src/schemas/benefitProducts'; + +const list = [ + { + productsId: 5, + benefitsId: 1, + amount: 2 + }, + { + productsId: 6, + benefitsId: 2, + amount: 2 + }, + { + productsId: 7, + benefitsId: 2, + amount: 2 + }, + { + productsId: 8, + benefitsId: 2, + amount: 2 + } +] as BenefitProduct[]; + +/** + * Seed the benefits table + */ +const seed = async () => { + const alreadyCreated = await db.benefitProducts.findAll(); + if (alreadyCreated.length < list.length) { + await db.benefitProducts.bulkCreate(list); + console.log(`[seed] Benefits: Seeded successfully - ${list.length} new created`); + } else { + console.log(`[seed] Benefits: Nothing to seed`); + } +}; + +export default { seed, groupList: list }; diff --git a/backend/database/seeders/index.ts b/backend/database/seeders/index.ts index ee9c3ea2..7997e335 100644 --- a/backend/database/seeders/index.ts +++ b/backend/database/seeders/index.ts @@ -9,6 +9,7 @@ import families from './families'; import consumptions from './consumptions'; import dependents from './dependents'; import products from './products'; +import benefitsProducts from './benefitsProducts'; /** * Seed all tables @@ -26,6 +27,7 @@ const seedAll = async () => { await dependents.seed(); await consumptions.seed(); await products.seed(); + await benefitsProducts.seed(); } else { // Production seed - one city and admin user await cities.seed(); diff --git a/backend/src/models/benefitProducts.ts b/backend/src/models/benefitProducts.ts index d1e55db5..8ceb8f51 100644 --- a/backend/src/models/benefitProducts.ts +++ b/backend/src/models/benefitProducts.ts @@ -14,6 +14,22 @@ export const getAll = (): Promise => { }); }; +/** + * Get all products associated to benefit + * @param benefitsId unique ID of the desired benefit + * @returns Promise + */ +export const getAllProductsByBenefitId = ( + benefitsId: NonNullable +): Promise => { + return db.benefitProducts.findAll({ + where: { + benefitsId + }, + include: [{ model: db.products, as: 'products' }] + }); +}; + /** * Get a single item using the unique ID * @param id unique ID of the desired item diff --git a/backend/src/models/benefits.ts b/backend/src/models/benefits.ts index f45a2e42..e17ed369 100644 --- a/backend/src/models/benefits.ts +++ b/backend/src/models/benefits.ts @@ -11,6 +11,20 @@ export const getAll = (cityId: NonNullable): Promise + */ +export const getAllWithProduct = (cityId: NonNullable): Promise => { + return db.benefits.findAll({ + include: [ + { model: db.institutions, as: 'institution', where: { cityId } }, + { model: db.benefitProducts, as: 'benefitProduct', include: [{ model: db.products, as: 'products' }] } + ] + }); +}; + /** * Get a single item using the unique ID * @param id unique ID of the desired item @@ -30,6 +44,25 @@ export const create = (values: Benefit | SequelizeBenefit): Promise + */ +export const createWithProduct = async (values: Benefit | SequelizeBenefit): Promise => { + const created = await db.benefits.create(values); + + if (values.products && created) { + const productList = values.products.map((i) => { + i.benefitsId = created.id as number; + return i; + }); + db.benefitProducts.bulkCreate(productList); + } + + return created; +}; + /** * Function to update a row on the table by the unique ID * @param id unique ID of the desired item @@ -52,6 +85,50 @@ export const updateById = async ( return null; }; +/** + * Function to update a row on the table by the unique ID + * @param id unique ID of the desired item + * @param values object with the new data + * @param cityId logged user city ID + * @returns Promise + */ +export const updateWithProduct = async ( + id: NonNullable, + values: Benefit | SequelizeBenefit, + cityId: NonNullable +): Promise => { + // Trying to get item on the city + const cityItem = await getById(id, cityId); + if (cityItem) { + // The update return an array [count, item[]], so I'm destructuring to get the updated benefit + const [, [updated]] = await db.benefits.update(values, { where: { id }, returning: true }); + const updatedProducts = await db.benefitProducts.findAll({ where: { benefitsId: updated.id as number } }); + + if (values.products) { + const list = values.products.map((i) => { + i.benefitsId = updated.id as number; + return i; + }); + const productToUpdate = list.filter((f) => f.id); + const productToAdd = list.filter((f) => !f.id); + const productToRemove = updatedProducts.filter((a) => { + const index = productToUpdate.find((f) => f.id === a.id); + if (!index) return a; + }); + + await db.benefitProducts.bulkCreate(productToAdd); + + productToRemove.map(async (dt) => { + await db.benefitProducts.destroy({ where: { id: dt.id } }); + }); + productToUpdate.map(async (up) => { + await db.benefitProducts.update({ amount: up.amount }, { where: { id: up.id } }); + }); + } + } + return null; +}; + /** * Function to delete a row on the table by the unique ID * @param id unique ID of the desired item diff --git a/backend/src/routes/benefits.ts b/backend/src/routes/benefits.ts index 0adac76c..8a0febed 100644 --- a/backend/src/routes/benefits.ts +++ b/backend/src/routes/benefits.ts @@ -10,7 +10,7 @@ const router = express.Router({ mergeParams: true }); router.get('/', async (req, res) => { try { if (!req.user?.cityId) throw Error('User without selected city'); - const items = await benefitModel.getAll(req.user.cityId); + const items = await benefitModel.getAllWithProduct(req.user.cityId); res.send(items); } catch (error) { logging.error(error); @@ -41,7 +41,9 @@ router.get('/:id', async (req, res) => { router.post('/', async (req, res) => { try { if (!req.user?.cityId) throw Error('User without selected city'); - const item = await benefitModel.create(req.body); + let item; + if (req.body.value) item = await benefitModel.create(req.body); + else item = await benefitModel.createWithProduct(req.body); res.send(item); } catch (error) { logging.error(error); @@ -55,7 +57,9 @@ router.post('/', async (req, res) => { router.put('/:id', async (req, res) => { try { if (!req.user?.cityId) throw Error('User without selected city'); - const item = await benefitModel.updateById(req.params.id, req.body, req.user.cityId); + let item; + if (req.body.value) item = await benefitModel.updateById(req.params.id, req.body, req.user.cityId); + else item = await benefitModel.updateWithProduct(req.params.id, req.body, req.user.cityId); res.send(item); } catch (error) { logging.error(error); diff --git a/backend/src/schemas/benefitProducts.ts b/backend/src/schemas/benefitProducts.ts index 6ad965b1..60b66e40 100644 --- a/backend/src/schemas/benefitProducts.ts +++ b/backend/src/schemas/benefitProducts.ts @@ -3,8 +3,8 @@ import { Sequelize, Model, DataTypes, BuildOptions, ModelCtor } from 'sequelize' // Simple item type export interface BenefitProduct { readonly id?: number | string; - productId: number | string; - benefitId: number | string; + productsId: number | string; + benefitsId: number | string; amount: number; createdAt?: number | Date | null; updatedAt?: number | Date | null; @@ -49,7 +49,7 @@ export const attributes = { } }; -const tableName = 'Benefits'; +const tableName = 'BenefitProducts'; /** * Sequelize model initializer function diff --git a/backend/src/schemas/benefits.ts b/backend/src/schemas/benefits.ts index b0493adf..2f48223f 100644 --- a/backend/src/schemas/benefits.ts +++ b/backend/src/schemas/benefits.ts @@ -1,4 +1,5 @@ import { Sequelize, Model, DataTypes, BuildOptions, ModelCtor } from 'sequelize'; +import { BenefitProduct } from './benefitProducts'; // Simple item type export interface Benefit { @@ -8,6 +9,7 @@ export interface Benefit { title: string; month: number; year: number; + products?: BenefitProduct[]; value?: number; createdAt?: number | Date | null; updatedAt?: number | Date | null; @@ -76,6 +78,10 @@ export const initBenefitSchema = (sequelize: Sequelize): SequelizeBenefitModel = foreignKey: 'institutionId', as: 'institution' }); + Schema.hasMany(models.benefitProducts, { + foreignKey: 'benefitsId', + as: 'benefitProduct' + }); }; return Schema; diff --git a/backend/src/schemas/products.ts b/backend/src/schemas/products.ts index 63364cba..b2ab6502 100644 --- a/backend/src/schemas/products.ts +++ b/backend/src/schemas/products.ts @@ -62,5 +62,13 @@ export const initProductSchema = (sequelize: Sequelize): SequelizeProductModel = // Sequelize relations // Schema.associate = (models): void => {}; + Schema.associate = (models): void => { + // Sequelize relations + Schema.hasMany(models.benefitProducts, { + foreignKey: 'productsId', + as: 'benefitProduct' + }); + }; + return Schema; }; From edd8a043873b9e31849fd7e6b7bc5e12bb51b682 Mon Sep 17 00:00:00 2001 From: Alessandro Macanha Date: Fri, 5 Jun 2020 09:58:45 -0300 Subject: [PATCH 02/10] change year/month to date --- .../20200605124402-alter-table-beneficio.js | 24 +++++++++++++++++++ backend/src/models/consumptions.ts | 9 ++++--- backend/src/schemas/benefits.ts | 11 +++------ backend/tests/balance-refactor.test.ts | 3 +-- backend/tests/consumption.test.ts | 3 +-- 5 files changed, 35 insertions(+), 15 deletions(-) create mode 100644 backend/database/migrations/20200605124402-alter-table-beneficio.js diff --git a/backend/database/migrations/20200605124402-alter-table-beneficio.js b/backend/database/migrations/20200605124402-alter-table-beneficio.js new file mode 100644 index 00000000..d7454d64 --- /dev/null +++ b/backend/database/migrations/20200605124402-alter-table-beneficio.js @@ -0,0 +1,24 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + queryInterface.removeColumn('Benefits', 'year'); + queryInterface.removeColumn('Benefits', 'month'); + queryInterface.addColumn('Benefits', 'date', { + type: Sequelize.DATE, + allowNull: false + }); + }, + + down: async (queryInterface, Sequelize) => { + queryInterface.removeColumn('Benefits', 'date'); + queryInterface.addColumn('Benefits', 'year', { + type: Sequelize.INTEGER, + allowNull: false + }); + queryInterface.addColumn('Benefits', 'month', { + type: Sequelize.INTEGER, + allowNull: false + }); + } +}; diff --git a/backend/src/models/consumptions.ts b/backend/src/models/consumptions.ts index 357aaab2..083b0477 100644 --- a/backend/src/models/consumptions.ts +++ b/backend/src/models/consumptions.ts @@ -50,13 +50,16 @@ export const getFamilyDependentBalance = async (family: Family, availableBenefit const endYear = moment(dependent.deactivatedAt as Date).year(); for (const benefit of availableBenefits) { + const benefitDate = moment(benefit.date); if (benefit.groupName !== family.groupName) continue; // Don't check if it's from another group // Check all the dates - const notInFuture = benefit.year < todayYear || (benefit.year === todayYear && benefit.month <= todayMonth); - const afterCreation = benefit.year > startYear || (benefit.year === startYear && benefit.month >= startMonth); + const notInFuture = + benefitDate.year() < todayYear || (benefitDate.year() === todayYear && benefitDate.month() + 1 <= todayMonth); + const afterCreation = + benefitDate.year() > startYear || (benefitDate.year() === startYear && benefitDate.month() + 1 >= startMonth); const beforeDeactivation = dependent.deactivatedAt - ? benefit.year < endYear || (benefit.year === endYear && benefit.month < endMonth) + ? benefitDate.year() < endYear || (benefitDate.year() === endYear && benefitDate.month() + 1 < endMonth) : true; if (notInFuture && afterCreation && beforeDeactivation) { diff --git a/backend/src/schemas/benefits.ts b/backend/src/schemas/benefits.ts index b0493adf..76200cd7 100644 --- a/backend/src/schemas/benefits.ts +++ b/backend/src/schemas/benefits.ts @@ -6,8 +6,7 @@ export interface Benefit { institutionId: number | string; groupName: string; title: string; - month: number; - year: number; + date: Date; value?: number; createdAt?: number | Date | null; updatedAt?: number | Date | null; @@ -46,12 +45,8 @@ export const attributes = { type: DataTypes.STRING, allowNull: false }, - month: { - type: DataTypes.INTEGER, - allowNull: false - }, - year: { - type: DataTypes.INTEGER, + date: { + type: DataTypes.DATE, allowNull: false }, value: { diff --git a/backend/tests/balance-refactor.test.ts b/backend/tests/balance-refactor.test.ts index 6d649103..1e6f897c 100644 --- a/backend/tests/balance-refactor.test.ts +++ b/backend/tests/balance-refactor.test.ts @@ -38,8 +38,7 @@ let createdFamily: Family; const benefit = { title: '[CAD25123] Auxilio merenda', groupName: getFamilyGroupByCode(0)?.key, - month: moment().month() + 1, - year: moment().year(), + date: moment().toDate(), value: 500, institutionId: 0 } as Benefit; diff --git a/backend/tests/consumption.test.ts b/backend/tests/consumption.test.ts index d1a65fc3..5d74b7a6 100644 --- a/backend/tests/consumption.test.ts +++ b/backend/tests/consumption.test.ts @@ -27,8 +27,7 @@ let createdFamily: Family; const benefit = { title: '[CAD25123] Auxilio municipal de alimentação', groupName: getFamilyGroupByCode(1)?.key, - month: 5, - year: 2020, + date: moment().toDate(), value: 500, institutionId: 0 } as Benefit; From a31b3070f325bd6508ed19ca579f047e2f51f17f Mon Sep 17 00:00:00 2001 From: Alessandro Macanha Date: Fri, 5 Jun 2020 10:18:11 -0300 Subject: [PATCH 03/10] merge to 281 --- backend/src/models/consumptions.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/backend/src/models/consumptions.ts b/backend/src/models/consumptions.ts index 357aaab2..49f07af8 100644 --- a/backend/src/models/consumptions.ts +++ b/backend/src/models/consumptions.ts @@ -13,9 +13,24 @@ import { scrapeNFCeData } from '../utils/nfceScraper'; import { SequelizeProduct } from '../schemas/products'; /** - * a - * @param family a - * @param availableBenefits a + * Get balance report by dependent when product + * @param family the family + */ +export const getFamilyDependentBalanceProduct = async (family: Family) => { + //Family groupName + const familyBenefits = await db.benefits.findAll({ where: { groupName: family.groupName } }); + //Filter benefit by family date + const familyBenefitsFilterDate = familyBenefits.filter((benefit) => { + benefit.year >= moment(family.createdAt).year(); + }); + + return null; +}; + +/** + * Get balance report by dependent + * @param family the family + * @param availableBenefits has benefis */ export const getFamilyDependentBalance = async (family: Family, availableBenefits?: Benefit[]) => { if (!family.dependents || !family.consumptions) { From 7c3685f89940f57b88d1b99381503109195ba59b Mon Sep 17 00:00:00 2001 From: Alessandro Macanha Date: Fri, 5 Jun 2020 11:17:14 -0300 Subject: [PATCH 04/10] creating product validation --- backend/database/seeders/benefits.ts | 13 ++++++----- backend/src/models/consumptions.ts | 32 ++++++++++++++++++++++++-- backend/src/routes/index.ts | 2 +- backend/src/routes/public.ts | 15 ++++++++++++ backend/src/schemas/benefitProducts.ts | 2 +- 5 files changed, 54 insertions(+), 10 deletions(-) diff --git a/backend/database/seeders/benefits.ts b/backend/database/seeders/benefits.ts index 12475b7a..34acd079 100644 --- a/backend/database/seeders/benefits.ts +++ b/backend/database/seeders/benefits.ts @@ -1,26 +1,27 @@ import db from '../../src/schemas'; import { Benefit } from '../../src/schemas/benefits'; +import moment from 'moment'; const list = [ { title: '[CAD25123] Auxilio municipal de alimentação', groupName: 'extreme-poverty', - month: 5, - year: 2020, + institutionId: 1, + date: moment().toDate(), value: 600 }, { title: '[CAD25123] Auxilio municipal de alimentação', groupName: 'poverty-line', - month: 5, - year: 2020, + institutionId: 1, + date: moment().toDate(), value: 400 }, { title: '[CAD25123] Auxilio municipal de alimentação', groupName: 'cad', - month: 5, - year: 2020, + institutionId: 1, + date: moment().toDate(), value: 300 } ] as Benefit[]; diff --git a/backend/src/models/consumptions.ts b/backend/src/models/consumptions.ts index 1039d8c3..43ab17bd 100644 --- a/backend/src/models/consumptions.ts +++ b/backend/src/models/consumptions.ts @@ -20,9 +20,37 @@ export const getFamilyDependentBalanceProduct = async (family: Family) => { //Family groupName const familyBenefits = await db.benefits.findAll({ where: { groupName: family.groupName } }); //Filter benefit by family date - const familyBenefitsFilterDate = familyBenefits.filter((benefit) => { - benefit.year >= moment(family.createdAt).year(); + const familyBenefitsFilterDate = familyBenefits + .filter((benefit) => { + const isAfter = moment(benefit.date).isSameOrAfter(moment(family.createdAt || moment())); + let isBefore = true; + if (family.deactivatedAt) isBefore = moment(benefit.date).isBefore(moment(family.deactivatedAt)); + return isAfter && isBefore ? benefit : null; + }) + .filter((f) => f); + //Get all products by benefit + const benefitsIds = familyBenefitsFilterDate.map((item) => { + return item.id; }); + const listOfProductsAvailable = await db.benefitProducts.findAll({ + where: { + benefitsId: benefitsIds + } + }); + //Get all family Consumptions + const familyConsumption = await db.consumptions.findAll({ + where: { familyId: family.id } + }); + //Get all Product used by family consumption + const consumptionIds = familyConsumption.map((item) => { + return item.id; + }); + const productsFamilyConsumption = await db.consumptionProducts.findAll({ + where: { + consumptionsId: consumptionIds + } + }); + //Get difference between available products and consumed products return null; }; diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index 758a49f4..bec0f947 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -27,7 +27,7 @@ router.get('/', (req, res) => res.send({ version: process.env.npm_package_versio // Sub-routers router.use('/auth', authRoutes); router.use('/health', healthRoutes); -router.use('/public', requirePublicAuth, publicRoutes); +router.use('/public', publicRoutes); router.use('/cities', jwtMiddleware, cityRoutes); router.use('/places', jwtMiddleware, placeRoutes); router.use('/place-stores', jwtMiddleware, placeStoreRoutes); diff --git a/backend/src/routes/public.ts b/backend/src/routes/public.ts index 9427996e..f17cdeeb 100644 --- a/backend/src/routes/public.ts +++ b/backend/src/routes/public.ts @@ -22,6 +22,21 @@ router.get('/families', async (req, res) => { } }); +/** + * Search of family by NIS number + */ +router.get('/familiesTest', async (req, res) => { + try { + const item = await familyModel.findByNis(req.query.nis as string, req.query.cityId as string, undefined, true); + if (!item) return res.status(404).send('Not found'); + const balance = await consumptionModel.getFamilyDependentBalanceProduct(item); + return res.send({ ...item.toJSON(), balance }); + } catch (error) { + logging.error(error); + return res.status(500).send(error.message); + } +}); + /** * Get list of place stores */ diff --git a/backend/src/schemas/benefitProducts.ts b/backend/src/schemas/benefitProducts.ts index 6ad965b1..c61f97c7 100644 --- a/backend/src/schemas/benefitProducts.ts +++ b/backend/src/schemas/benefitProducts.ts @@ -49,7 +49,7 @@ export const attributes = { } }; -const tableName = 'Benefits'; +const tableName = 'BenefitProducts'; /** * Sequelize model initializer function From 5d59deb92f5718cf0eb3998982821535eccc034b Mon Sep 17 00:00:00 2001 From: Allan Amaral Date: Fri, 5 Jun 2020 14:35:47 -0300 Subject: [PATCH 05/10] #259 - Changed benefit CRUD to accept product list --- admin/src/components/numberPicker/index.tsx | 36 +++ admin/src/components/numberPicker/styles.tsx | 20 ++ .../src/components/resourceSelector/index.tsx | 84 +++++++ admin/src/interfaces/benefit.ts | 4 + admin/src/pages/benefits/form.tsx | 223 ++++++++++-------- admin/src/pages/benefits/list.tsx | 37 ++- admin/src/pages/benefits/productSelector.tsx | 59 +++++ admin/src/pages/benefits/styles.tsx | 9 + 8 files changed, 372 insertions(+), 100 deletions(-) create mode 100644 admin/src/components/numberPicker/index.tsx create mode 100644 admin/src/components/numberPicker/styles.tsx create mode 100644 admin/src/components/resourceSelector/index.tsx create mode 100644 admin/src/pages/benefits/productSelector.tsx diff --git a/admin/src/components/numberPicker/index.tsx b/admin/src/components/numberPicker/index.tsx new file mode 100644 index 00000000..09054c4d --- /dev/null +++ b/admin/src/components/numberPicker/index.tsx @@ -0,0 +1,36 @@ +import { PlusOutlined, MinusOutlined } from '@ant-design/icons'; +import React, { useCallback } from 'react'; +import { ActionWrapper, ActionButton } from './styles'; + +export type NumberPickerProps = { + value: number; + onChange?: (value: number) => void; +}; + +/** + * Resource selector component + * @param props component props + */ +export const NumberPicker: React.FC = ({ value, onChange }) => { + // Whenever a + or - button is clicked, send the updated value to the onChange event + const handleChangeAmount = useCallback( + (value: number) => { + if (onChange) { + onChange(value); + } + }, + [onChange] + ); + + return ( + + handleChangeAmount(value - 1)}> + + + {value} + handleChangeAmount(value + 1)}> + + + + ); +}; diff --git a/admin/src/components/numberPicker/styles.tsx b/admin/src/components/numberPicker/styles.tsx new file mode 100644 index 00000000..bc2c603d --- /dev/null +++ b/admin/src/components/numberPicker/styles.tsx @@ -0,0 +1,20 @@ +import styled from 'styled-components'; +import { Button } from 'antd'; + +export const ActionWrapper = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +`; + +export const ActionButton = styled(Button)` + display: inline-block; + border-radius: 50%; + && { + width: ${(props) => props.theme.spacing.md}; + min-width: ${(props) => props.theme.spacing.md}; + height: ${(props) => props.theme.spacing.md}; + } + padding: 4px; + font-size: 12px; +`; diff --git a/admin/src/components/resourceSelector/index.tsx b/admin/src/components/resourceSelector/index.tsx new file mode 100644 index 00000000..80ee02b1 --- /dev/null +++ b/admin/src/components/resourceSelector/index.tsx @@ -0,0 +1,84 @@ +import { Table } from 'antd'; +import React, { useCallback, useMemo } from 'react'; +import { NumberPicker } from '../numberPicker'; + +type IdType = { + [I in IdName]: string | number; +}; + +export type ResourceSelectorDataSourceItem = { + name: string; + amount: number; +} & IdType; + +export type ResourceSelectorValueItem = { + amount: number; +} & IdType; + +export type ResourceSelectorProps = { + idName: IdName; + title?: string; + datasource: ResourceSelectorDataSourceItem[]; + value: ResourceSelectorValueItem[]; + onChange?: (value: ResourceSelectorValueItem[]) => void; + pagination?: boolean; +}; + +/** + * Resource selector component + * @param props component props + */ +export function ResourceSelector({ + idName, + title, + datasource, + value, + onChange, + pagination +}: ResourceSelectorProps) { + // Reset the state every time the datasource changes + const resourceList = useMemo( + () => + datasource.map((item) => ({ + [idName]: item[idName], + name: item.name, + amount: value.find((resource) => resource[idName] === item[idName])?.amount || 0 + })), + [datasource, value, idName] + ); + + // Whenever a + or - button is clicked, send the updated list of resources to the onChange event + const handleChangeAmount = useCallback( + (id: string | number, amount: number) => { + if (onChange) { + onChange( + resourceList + .map((resource) => + resource[idName] === id + ? { [idName]: id, amount } + : { [idName]: resource[idName], amount: resource.amount } + ) + .filter((resource) => resource.amount > 0) as ResourceSelectorValueItem[] + ); + } + }, + [resourceList, onChange, idName] + ); + + return ( + + + ) => { + return handleChangeAmount(item[idName], value)} />; + }} + /> +
+ ); +} diff --git a/admin/src/interfaces/benefit.ts b/admin/src/interfaces/benefit.ts index edf65fec..834b486d 100644 --- a/admin/src/interfaces/benefit.ts +++ b/admin/src/interfaces/benefit.ts @@ -11,4 +11,8 @@ export interface Benefit { createdAt?: number | Date | null; updatedAt?: number | Date | null; deletedAt?: number | Date | null; + products?: { + productId: number | string; + amount: number; + }[]; } diff --git a/admin/src/pages/benefits/form.tsx b/admin/src/pages/benefits/form.tsx index 50f1c549..bc16e4d1 100644 --- a/admin/src/pages/benefits/form.tsx +++ b/admin/src/pages/benefits/form.tsx @@ -12,6 +12,11 @@ import { requestGetInstitution } from '../../redux/institution/actions'; import { AppState } from '../../redux/rootReducer'; import { familyGroupList } from '../../utils/constraints'; import yup from '../../utils/yup'; +import { ProductSelector } from './productSelector'; +import { env } from '../../env'; + +const TYPE = env.REACT_APP_CONSUMPTION_TYPE as 'ticket' | 'product'; +const showProductList = TYPE === 'product'; const { Option } = Select; @@ -21,7 +26,17 @@ const schema = yup.object().shape({ title: yup.string().label('Nome').required(), month: yup.number().label('Mês').required(), year: yup.number().label('Ano').required(), - value: yup.string().label('Valor').required() + value: !showProductList ? yup.string().label('Valor').required() : yup.string().label('Valor').nullable(), + products: showProductList + ? yup + .array() + .test( + 'atLeastOne', + 'Pelo menos um produto deve ser selecionado', + (value?: { productId: number | string; amount: number }[]) => + !!value && value.length > 0 && value.reduce((total, item) => total + (item ? item.amount : 0), 0) > 0 + ) + : yup.array().nullable() }); /** @@ -64,7 +79,8 @@ export const BenefitForm: React.FC> = (props title: '', month: undefined, year: undefined, - value: undefined + value: undefined, + products: undefined }, validationSchema: schema, onSubmit: (values, { setStatus }) => { @@ -85,6 +101,7 @@ export const BenefitForm: React.FC> = (props const yearMeta = getFieldMeta('year'); const valueMeta = getFieldMeta('value'); const institutionIdMeta = getFieldMeta('institutionId'); + const productsMeta = getFieldMeta('products'); return ( > = (props onCancel={() => history.push('/beneficios')} onOk={submitForm} confirmLoading={loading} + width={showProductList ? 840 : undefined} okType={errors && Object.keys(errors).length > 0 && touched ? 'danger' : 'primary'} > {status && }
- - - - - - - - - - - + + + + + - - - setFieldValue('month', Number(date?.format('MM')))} - /> + - - + - setFieldValue('year', Number(date?.format('YYYY')))} - /> + + + + + + setFieldValue('month', Number(date?.format('MM')))} + /> + + + + + setFieldValue('year', Number(date?.format('YYYY')))} + /> + + + + + {/* Value per dependent should only be shown when the TYPE is `ticket` */} + {!showProductList && ( + + + + )} + {showProductList && ( + { + setFieldValue('products', value); + }} + /> + )} - - - -
diff --git a/admin/src/pages/benefits/list.tsx b/admin/src/pages/benefits/list.tsx index b9cb761a..c9c2dd59 100644 --- a/admin/src/pages/benefits/list.tsx +++ b/admin/src/pages/benefits/list.tsx @@ -5,10 +5,16 @@ import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Link } from 'react-router-dom'; import { Benefit } from '../../interfaces/benefit'; +import { Product } from '../../interfaces/product'; import { requestDeleteBenefit, requestGetBenefit } from '../../redux/benefit/actions'; import { AppState } from '../../redux/rootReducer'; import { familyGroupList } from '../../utils/constraints'; import { ActionWrapper, PageContainer } from './styles'; +import { env } from '../../env'; +import { requestGetProduct } from '../../redux/product/actions'; + +const TYPE = env.REACT_APP_CONSUMPTION_TYPE as 'ticket' | 'product'; +const showProductList = TYPE === 'product'; /** * List component @@ -17,11 +23,15 @@ import { ActionWrapper, PageContainer } from './styles'; export const BenefitList: React.FC<{}> = () => { // Redux state const list = useSelector((state) => state.benefitReducer.list as Benefit[]); + const productList = useSelector((state) => state.productReducer.list as Product[]); + // Redux actions const dispatch = useDispatch(); useEffect(() => { dispatch(requestGetBenefit()); + dispatch(requestGetProduct()); }, [dispatch]); + return ( = () => { } > - +
= () => { /> - `R$ ${data}`} - /> + {/* Show the product list column depending on the type of benefit */} + {showProductList ? ( + + data?.map((product) => ( +
{`${product.amount}x ${ + productList.find((p) => p.id === product.productId)?.name + }`}
+ )) + } + /> + ) : ( + `R$ ${data}`} + /> + )} []) => void; + value?: ResourceSelectorValueItem<'productId'>[]; + validateStatus?: FormItemProps['validateStatus']; + help?: FormItemProps['help']; +} + +/** + * List component + * @param props component props + */ +export const ProductSelector: React.FC = (props) => { + // Redux state + const list = useSelector((state) => state.productReducer.list as Product[]); + // Redux actions + const dispatch = useDispatch(); + useEffect(() => { + dispatch(requestGetProduct()); + }, [dispatch]); + + // For each item in the list, + const datasource = useMemo( + () => + list.map((product) => ({ + productId: product.id || 0, + name: product.name, + amount: 0 + })), + [list] + ); + + return ( + <> + + + +
+ + + + + + ); +}; diff --git a/admin/src/pages/benefits/styles.tsx b/admin/src/pages/benefits/styles.tsx index 2614a21d..7a8defe0 100644 --- a/admin/src/pages/benefits/styles.tsx +++ b/admin/src/pages/benefits/styles.tsx @@ -1,4 +1,5 @@ import styled from 'styled-components'; +import { Col, Divider as AntdDivider } from 'antd'; export const PageContainer = styled.div` padding: ${(props) => props.theme.spacing.md}; @@ -24,3 +25,11 @@ export const ActionWrapper = styled.div` margin-left: ${(props) => props.theme.spacing.sm}; } `; + +export const DividerColumn = styled(Col).attrs({ span: 1, style: { display: 'flex' } })` + justify-content: center; +`; + +export const Divider = styled(AntdDivider).attrs({ type: 'vertical' })` + height: 100%; +`; From 8af1820752e5700e712d099d868158ec8dd2ecf6 Mon Sep 17 00:00:00 2001 From: Alessandro Macanha Date: Fri, 5 Jun 2020 15:14:16 -0300 Subject: [PATCH 06/10] finalized crud and added test --- backend/src/models/consumptions.ts | 49 ++++++++++++++++++---- backend/src/routes/index.ts | 2 +- backend/src/routes/public.ts | 15 ------- backend/src/schemas/benefitProducts.ts | 6 ++- backend/src/schemas/index.ts | 2 +- backend/tests/balance-refactor.test.ts | 2 + backend/tests/balance.test.ts | 3 +- backend/tests/benefits.test.ts | 57 ++++++++++++++++++++++++++ backend/tests/consumption.test.ts | 2 + 9 files changed, 108 insertions(+), 30 deletions(-) create mode 100644 backend/tests/benefits.test.ts diff --git a/backend/src/models/consumptions.ts b/backend/src/models/consumptions.ts index 43ab17bd..edee025d 100644 --- a/backend/src/models/consumptions.ts +++ b/backend/src/models/consumptions.ts @@ -28,18 +28,22 @@ export const getFamilyDependentBalanceProduct = async (family: Family) => { return isAfter && isBefore ? benefit : null; }) .filter((f) => f); + if (familyBenefitsFilterDate.length === 0) { + throw { status: 422, message: 'Nenhum benefício disponível' }; + } //Get all products by benefit const benefitsIds = familyBenefitsFilterDate.map((item) => { return item.id; }); const listOfProductsAvailable = await db.benefitProducts.findAll({ where: { - benefitsId: benefitsIds - } + benefitsId: benefitsIds as number[] + }, + include: [{ model: db.products, as: 'products' }] }); //Get all family Consumptions const familyConsumption = await db.consumptions.findAll({ - where: { familyId: family.id } + where: { familyId: family.id as number } }); //Get all Product used by family consumption const consumptionIds = familyConsumption.map((item) => { @@ -47,20 +51,36 @@ export const getFamilyDependentBalanceProduct = async (family: Family) => { }); const productsFamilyConsumption = await db.consumptionProducts.findAll({ where: { - consumptionsId: consumptionIds + consumptionsId: consumptionIds as number[] } }); //Get difference between available products and consumed products + const differenceProducts = listOfProductsAvailable.map((product) => { + const item = productsFamilyConsumption.find((f) => f.productsId === product.productsId); + let amountDifference = 0; + if (item) { + amountDifference = product.amount - item.amount; + if (amountDifference < 0) { + logging.critical('Family with negative amount', { family }); + } + } + return { + product: { id: product.id, name: product.products?.name }, + amountAvailable: amountDifference, + amountGranted: product.amount, + amountConsumed: item ? item.amount : 0 + }; + }); - return null; + return differenceProducts; }; /** - * Get balance report by dependent + * Get balance report by dependent when ticket * @param family the family * @param availableBenefits has benefis */ -export const getFamilyDependentBalance = async (family: Family, availableBenefits?: Benefit[]) => { +export const getFamilyDependentBalanceTicket = async (family: Family, availableBenefits?: Benefit[]) => { if (!family.dependents || !family.consumptions) { // Be sure that everything necessesary is populated const populatedFamily = await db.families.findByPk(family.id, { @@ -105,7 +125,7 @@ export const getFamilyDependentBalance = async (family: Family, availableBenefit ? benefitDate.year() < endYear || (benefitDate.year() === endYear && benefitDate.month() + 1 < endMonth) : true; - if (notInFuture && afterCreation && beforeDeactivation) { + if (benefit.value && notInFuture && afterCreation && beforeDeactivation) { // Valid benefit balance += benefit.value; } @@ -122,6 +142,17 @@ export const getFamilyDependentBalance = async (family: Family, availableBenefit return balance - consumption; }; +/** + * Get balance report by dependent when product + * @param family the family + * @param availableBenefits benefit + */ +export const getFamilyDependentBalance = async (family: Family, availableBenefits?: Benefit[]) => { + const isTicket = process.env.CONSUMPTION_TYPE === 'ticket'; + if (isTicket) return getFamilyDependentBalanceTicket(family, availableBenefits); + else return getFamilyDependentBalanceProduct(family); +}; + /** * Get balance report for all families * @param cityId logged user city unique ID @@ -142,7 +173,7 @@ export const getBalanceReport = async (cityId: NonNullable) => { const balanceList: (Family & { balance: number })[] = []; for (const family of families) { const balance = await getFamilyDependentBalance(family, benefits); - balanceList.push({ ...(family.toJSON() as Family), balance }); + balanceList.push({ ...(family.toJSON() as Family), balance: balance as number }); } return balanceList; diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index bec0f947..758a49f4 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -27,7 +27,7 @@ router.get('/', (req, res) => res.send({ version: process.env.npm_package_versio // Sub-routers router.use('/auth', authRoutes); router.use('/health', healthRoutes); -router.use('/public', publicRoutes); +router.use('/public', requirePublicAuth, publicRoutes); router.use('/cities', jwtMiddleware, cityRoutes); router.use('/places', jwtMiddleware, placeRoutes); router.use('/place-stores', jwtMiddleware, placeStoreRoutes); diff --git a/backend/src/routes/public.ts b/backend/src/routes/public.ts index f17cdeeb..9427996e 100644 --- a/backend/src/routes/public.ts +++ b/backend/src/routes/public.ts @@ -22,21 +22,6 @@ router.get('/families', async (req, res) => { } }); -/** - * Search of family by NIS number - */ -router.get('/familiesTest', async (req, res) => { - try { - const item = await familyModel.findByNis(req.query.nis as string, req.query.cityId as string, undefined, true); - if (!item) return res.status(404).send('Not found'); - const balance = await consumptionModel.getFamilyDependentBalanceProduct(item); - return res.send({ ...item.toJSON(), balance }); - } catch (error) { - logging.error(error); - return res.status(500).send(error.message); - } -}); - /** * Get list of place stores */ diff --git a/backend/src/schemas/benefitProducts.ts b/backend/src/schemas/benefitProducts.ts index c61f97c7..1e8fd11e 100644 --- a/backend/src/schemas/benefitProducts.ts +++ b/backend/src/schemas/benefitProducts.ts @@ -1,11 +1,13 @@ import { Sequelize, Model, DataTypes, BuildOptions, ModelCtor } from 'sequelize'; +import { Product } from './products'; // Simple item type export interface BenefitProduct { readonly id?: number | string; - productId: number | string; - benefitId: number | string; + productsId: number | string; + benefitsId: number | string; amount: number; + products: Product | null; createdAt?: number | Date | null; updatedAt?: number | Date | null; deletedAt?: number | Date | null; diff --git a/backend/src/schemas/index.ts b/backend/src/schemas/index.ts index a3470487..143f482b 100644 --- a/backend/src/schemas/index.ts +++ b/backend/src/schemas/index.ts @@ -10,9 +10,9 @@ import { initConsumptionSchema } from './consumptions'; import { initDependentSchema } from './depedents'; import { initProductSchema } from './products'; import { initBenefitProductSchema } from './benefitProducts'; +import { initConsumptionProductsSchema } from './consumptionProducts'; import * as config from '../../database/config'; -import { initConsumptionProductsSchema } from './ConsumptionProducts'; const sequelize = new Sequelize(config as Options); diff --git a/backend/tests/balance-refactor.test.ts b/backend/tests/balance-refactor.test.ts index 1e6f897c..fff0a10b 100644 --- a/backend/tests/balance-refactor.test.ts +++ b/backend/tests/balance-refactor.test.ts @@ -76,6 +76,7 @@ test(`[${testName}] Consume all the balance`, async () => { const balance = await consumptionModel.getFamilyDependentBalance(createdFamily); const consumption: Consumption = { value: balance, + invalidValue: 0, familyId: createdFamily.id as number, nfce: new Date().getTime().toString(), placeStoreId: placeStore.id as number @@ -91,6 +92,7 @@ test(`[${testName}] Consume all the balance`, async () => { test(`[${testName}] Consume more than the balance`, async () => { const consumption: Consumption = { value: 100, + invalidValue: 0, familyId: createdFamily.id as number, nfce: new Date().getTime().toString(), placeStoreId: placeStore.id as number diff --git a/backend/tests/balance.test.ts b/backend/tests/balance.test.ts index 60626bab..afdf00c8 100644 --- a/backend/tests/balance.test.ts +++ b/backend/tests/balance.test.ts @@ -27,8 +27,7 @@ let createdFamily: Family; const benefit = { title: '[CAD25123] Auxilio municipal de alimentação', groupName: getFamilyGroupByCode(1)?.key, - month: moment().month() + 1, - year: moment().year(), + date: moment().toDate(), value: 500, institutionId: 0 } as Benefit; diff --git a/backend/tests/benefits.test.ts b/backend/tests/benefits.test.ts new file mode 100644 index 00000000..964b92c8 --- /dev/null +++ b/backend/tests/benefits.test.ts @@ -0,0 +1,57 @@ +import moment from 'moment'; +import db, { sequelize } from '../src/schemas'; +import * as consumptionModel from '../src/models/consumptions'; +import { Family } from '../src/schemas/families'; +import { getFamilyGroupByCode } from '../src/utils/constraints'; +import { Benefit } from '../src/schemas/benefits'; + +afterAll(() => { + sequelize.close(); +}); + +const testName = 'benefits'; + +const benefit = { + institutionId: 1, + groupName: 'BenefitTest', + title: 'BenefitTest 1', + date: moment('05/05/2020', 'DD/MM/YYYY').toDate() +} as Benefit; +let createdBenefit: Benefit | null; + +const family = { + code: Math.floor(Math.random() * 10000000).toString(), + groupName: getFamilyGroupByCode(1)?.key, + responsibleName: 'Family After', + responsibleBirthday: moment('01/01/1980', 'DD/MM/YYYY').toDate(), + responsibleNis: Math.floor(Math.random() * 10000000000).toString(), + responsibleMotherName: '', + createdAt: moment('05/06/2020', 'DD/MM/YYYY').toDate(), + cityId: 0 +} as Family; + +test(`[${testName}] Create mock data`, async () => { + createdBenefit = await db.benefits.findOne({ where: { title: benefit.title } }); + if (!createdBenefit) createdBenefit = await db.benefits.create({ ...benefit, institutionId: 1 }); + expect(createdBenefit).toBeDefined(); + expect(createdBenefit.id).toBeDefined(); +}); + +test(`[${testName}] Get family after benefit`, async () => { + try { + family.createdAt = moment('06/06/2020', 'DD/MM/YYYY').toDate(); + await consumptionModel.getFamilyDependentBalance(family); + } catch (e) { + expect(e.message).toBe('Nenhum benefício disponível'); + } +}); + +test(`[${testName}] Get family before benefit`, async () => { + const [, [family]] = await db.families.update( + { createdAt: moment('03/03/2020', 'DD/MM/YYYY').toDate() }, + { where: { responsibleNis: '1234' }, returning: true } + ); + let balance; + if (family) balance = await consumptionModel.getFamilyDependentBalance(family); + expect(balance).toStrictEqual([]); +}); diff --git a/backend/tests/consumption.test.ts b/backend/tests/consumption.test.ts index 5d74b7a6..c3e18f84 100644 --- a/backend/tests/consumption.test.ts +++ b/backend/tests/consumption.test.ts @@ -54,6 +54,7 @@ test(`[${testName}] Consume all the balance`, async () => { let balance = await consumptionModel.getFamilyBalance(createdFamily); const consumption: Consumption = { value: balance, + invalidValue: 0, familyId: createdFamily.id as number, nfce: new Date().getTime().toString(), placeStoreId: placeStore.id as number @@ -69,6 +70,7 @@ test(`[${testName}] Consume all the balance`, async () => { test(`[${testName}] Consume more than the balance`, async () => { const consumption: Consumption = { value: 100, + invalidValue: 0, familyId: createdFamily.id as number, nfce: new Date().getTime().toString(), placeStoreId: placeStore.id as number From b656d9d7f1976291702d4263ecefb10dad58cdd7 Mon Sep 17 00:00:00 2001 From: Allan Amaral Date: Fri, 5 Jun 2020 18:42:08 -0300 Subject: [PATCH 07/10] #259 - Fix to CRUD and route --- admin/src/interfaces/benefit.ts | 5 ++- admin/src/pages/benefits/form.tsx | 10 ++--- admin/src/pages/benefits/list.tsx | 8 ++-- admin/src/pages/benefits/productSelector.tsx | 8 ++-- ...-consumption-add-cascade-to-consumption.js | 39 +++++++++++++++++++ backend/src/models/benefits.ts | 26 +++++++++---- backend/src/schemas/benefits.ts | 6 ++- 7 files changed, 78 insertions(+), 24 deletions(-) create mode 100644 backend/database/migrations/20200605210814-alter-table-product-consumption-add-cascade-to-consumption.js diff --git a/admin/src/interfaces/benefit.ts b/admin/src/interfaces/benefit.ts index 834b486d..ea7cfbcb 100644 --- a/admin/src/interfaces/benefit.ts +++ b/admin/src/interfaces/benefit.ts @@ -11,8 +11,9 @@ export interface Benefit { createdAt?: number | Date | null; updatedAt?: number | Date | null; deletedAt?: number | Date | null; - products?: { - productId: number | string; + benefitProduct?: { + id: number | string; + productsId: number | string; amount: number; }[]; } diff --git a/admin/src/pages/benefits/form.tsx b/admin/src/pages/benefits/form.tsx index bc16e4d1..a93286f9 100644 --- a/admin/src/pages/benefits/form.tsx +++ b/admin/src/pages/benefits/form.tsx @@ -27,7 +27,7 @@ const schema = yup.object().shape({ month: yup.number().label('Mês').required(), year: yup.number().label('Ano').required(), value: !showProductList ? yup.string().label('Valor').required() : yup.string().label('Valor').nullable(), - products: showProductList + benefitProduct: showProductList ? yup .array() .test( @@ -80,7 +80,7 @@ export const BenefitForm: React.FC> = (props month: undefined, year: undefined, value: undefined, - products: undefined + benefitProduct: undefined }, validationSchema: schema, onSubmit: (values, { setStatus }) => { @@ -101,7 +101,7 @@ export const BenefitForm: React.FC> = (props const yearMeta = getFieldMeta('year'); const valueMeta = getFieldMeta('value'); const institutionIdMeta = getFieldMeta('institutionId'); - const productsMeta = getFieldMeta('products'); + const productsMeta = getFieldMeta('benefitProduct'); return ( > = (props { - setFieldValue('products', value); + setFieldValue('benefitProduct', value); }} /> )} diff --git a/admin/src/pages/benefits/list.tsx b/admin/src/pages/benefits/list.tsx index c9c2dd59..d9d434c9 100644 --- a/admin/src/pages/benefits/list.tsx +++ b/admin/src/pages/benefits/list.tsx @@ -55,11 +55,11 @@ export const BenefitList: React.FC<{}> = () => { {showProductList ? ( + dataIndex="benefitProduct" + render={(data: Benefit['benefitProduct']) => data?.map((product) => ( -
{`${product.amount}x ${ - productList.find((p) => p.id === product.productId)?.name +
{`${product.amount}x ${ + productList.find((p) => p.id === product.productsId)?.name }`}
)) } diff --git a/admin/src/pages/benefits/productSelector.tsx b/admin/src/pages/benefits/productSelector.tsx index adbef1bd..3492f722 100644 --- a/admin/src/pages/benefits/productSelector.tsx +++ b/admin/src/pages/benefits/productSelector.tsx @@ -9,8 +9,8 @@ import { ResourceSelector, ResourceSelectorValueItem } from '../../components/re import { FormItemProps } from 'antd/lib/form'; export interface ProductSelectorProps { - onChange?: (value: ResourceSelectorValueItem<'productId'>[]) => void; - value?: ResourceSelectorValueItem<'productId'>[]; + onChange?: (value: ResourceSelectorValueItem<'productsId'>[]) => void; + value?: ResourceSelectorValueItem<'productsId'>[]; validateStatus?: FormItemProps['validateStatus']; help?: FormItemProps['help']; } @@ -32,7 +32,7 @@ export const ProductSelector: React.FC = (props) => { const datasource = useMemo( () => list.map((product) => ({ - productId: product.id || 0, + productsId: product.id || 0, name: product.name, amount: 0 })), @@ -47,7 +47,7 @@ export const ProductSelector: React.FC = (props) => {
{ + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.sequelize.query( + 'ALTER TABLE "BenefitProducts" DROP CONSTRAINT "BenefitProducts_benefitsId_fkey"', + { transaction } + ); + await queryInterface.sequelize.query( + 'ALTER TABLE "BenefitProducts" ADD CONSTRAINT "BenefitProducts_benefitsId_fkey" FOREIGN KEY ("benefitsId") REFERENCES "Benefits" ("id") ON DELETE CASCADE;', + { transaction } + ); + return transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + }, + + down: async (queryInterface) => { + const transaction = await queryInterface.sequelize.transaction(); + try { + await queryInterface.sequelize.query( + 'ALTER TABLE "BenefitProducts" DROP CONSTRAINT "BenefitProducts_benefitsId_fkey"', + { transaction } + ); + await queryInterface.sequelize.query( + 'ALTER TABLE "BenefitProducts" ADD CONSTRAINT "BenefitProducts_benefitsId_fkey" FOREIGN KEY ("benefitsId") REFERENCES "Benefits" ("id");', + { transaction } + ); + return transaction.commit(); + } catch (error) { + await transaction.rollback(); + throw error; + } + } +}; diff --git a/backend/src/models/benefits.ts b/backend/src/models/benefits.ts index e17ed369..ed465809 100644 --- a/backend/src/models/benefits.ts +++ b/backend/src/models/benefits.ts @@ -52,14 +52,18 @@ export const create = (values: Benefit | SequelizeBenefit): Promise => { const created = await db.benefits.create(values); - if (values.products && created) { - const productList = values.products.map((i) => { + if (values.benefitProduct && created) { + const productList = values.benefitProduct.map((i) => { i.benefitsId = created.id as number; return i; }); db.benefitProducts.bulkCreate(productList); } + await created.reload({ + include: [{ model: db.benefitProducts, as: 'benefitProduct', include: [{ model: db.products, as: 'products' }] }] + }); + return created; }; @@ -104,8 +108,8 @@ export const updateWithProduct = async ( const [, [updated]] = await db.benefits.update(values, { where: { id }, returning: true }); const updatedProducts = await db.benefitProducts.findAll({ where: { benefitsId: updated.id as number } }); - if (values.products) { - const list = values.products.map((i) => { + if (values.benefitProduct) { + const list = values.benefitProduct.map((i) => { i.benefitsId = updated.id as number; return i; }); @@ -114,19 +118,27 @@ export const updateWithProduct = async ( const productToRemove = updatedProducts.filter((a) => { const index = productToUpdate.find((f) => f.id === a.id); if (!index) return a; + return null; }); await db.benefitProducts.bulkCreate(productToAdd); productToRemove.map(async (dt) => { - await db.benefitProducts.destroy({ where: { id: dt.id } }); + if (dt.id) await db.benefitProducts.destroy({ where: { id: dt.id } }); }); productToUpdate.map(async (up) => { - await db.benefitProducts.update({ amount: up.amount }, { where: { id: up.id } }); + if (up.id) await db.benefitProducts.update({ amount: up.amount }, { where: { id: up.id } }); }); } } - return null; + + return await db.benefits.findOne({ + where: { id }, + include: [ + { model: db.institutions, as: 'institution', where: { cityId } }, + { model: db.benefitProducts, as: 'benefitProduct', include: [{ model: db.products, as: 'products' }] } + ] + }); }; /** diff --git a/backend/src/schemas/benefits.ts b/backend/src/schemas/benefits.ts index 2f48223f..bdea999e 100644 --- a/backend/src/schemas/benefits.ts +++ b/backend/src/schemas/benefits.ts @@ -9,7 +9,7 @@ export interface Benefit { title: string; month: number; year: number; - products?: BenefitProduct[]; + benefitProduct?: BenefitProduct[]; value?: number; createdAt?: number | Date | null; updatedAt?: number | Date | null; @@ -80,7 +80,9 @@ export const initBenefitSchema = (sequelize: Sequelize): SequelizeBenefitModel = }); Schema.hasMany(models.benefitProducts, { foreignKey: 'benefitsId', - as: 'benefitProduct' + as: 'benefitProduct', + onDelete: 'CASCADE', + hooks: true }); }; From d7e07292714adaaf7d8cb2e64cbaf4e902e74c21 Mon Sep 17 00:00:00 2001 From: Allan Amaral Date: Mon, 8 Jun 2020 09:50:12 -0300 Subject: [PATCH 08/10] #293 - Error feedback in consumption route --- .../consumptionFamilySearch/index.tsx | 19 +++++++++++++++++- .../consumptionFamilySearch/styles.ts | 6 ++++++ admin/src/redux/families/actions.ts | 20 ++++++++++++++++++- backend/src/routes/families.ts | 10 +++++++--- 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/admin/src/components/consumptionFamilySearch/index.tsx b/admin/src/components/consumptionFamilySearch/index.tsx index a739498d..aea30d6b 100644 --- a/admin/src/components/consumptionFamilySearch/index.tsx +++ b/admin/src/components/consumptionFamilySearch/index.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { Alert, Button, Descriptions, Form, Input, Typography } from 'antd'; import { useSelector, useDispatch } from 'react-redux'; import { Flex } from '../flex'; -import { FamilyActions, FamilyWrapper } from './styles'; +import { FamilyActions, FamilyWrapper, InfoContainer } from './styles'; import { AppState } from '../../redux/rootReducer'; import { requestGetFamily } from '../../redux/families/actions'; import { Family } from '../../interfaces/family'; @@ -27,6 +27,9 @@ export const ConsumptionFamilySearch: React.FC = (props) => { const [birthday, setBirthday] = useState(''); // Redux state const familyLoading = useSelector((state) => state.familiesReducer.familyLoading); + const familyError = useSelector( + (state) => state.familiesReducer.familyError + ); const family = useSelector((state) => state.familiesReducer.familyItem); // .env @@ -67,6 +70,20 @@ export const ConsumptionFamilySearch: React.FC = (props) => { }} /> + + {familyError && !familyLoading && ( + + + {familyError.message} + + } + /> + + )} + {family && props.askForBirthday && ( props.theme.spacing.sm} 0; + justify-content: center; +`; diff --git a/admin/src/redux/families/actions.ts b/admin/src/redux/families/actions.ts index 179fb8d9..36d6a55b 100644 --- a/admin/src/redux/families/actions.ts +++ b/admin/src/redux/families/actions.ts @@ -284,7 +284,25 @@ export const requestGetFamily = (nis: string, cityId: string): ThunkResult } } catch (error) { // Request failed: dispatch error - logging.error(error); + if (error.response) { + error.status = error.response.status; + switch (Number(error.response.status)) { + case 404: + error.message = 'Não encontramos nenhuma família utilizando esse NIS.'; + break; + default: + logging.error(error); + error.message = 'Ocorreu uma falha inesperada e os programadores foram avisados.'; + break; + } + } else if (error.message === 'Network Error' && !window.navigator.onLine) { + error.message = + 'Ocorreu um erro ao conectar ao servidor. ' + + 'Verifique se a conexão com a internet está funcionando corretamente.'; + } else { + logging.error(error); + error.message = 'Ocorreu uma falha inesperada e os programadores foram avisados.'; + } dispatch(doGetFamilyFailed(error)); } }; diff --git a/backend/src/routes/families.ts b/backend/src/routes/families.ts index a8d69092..7f33d9f7 100644 --- a/backend/src/routes/families.ts +++ b/backend/src/routes/families.ts @@ -12,9 +12,13 @@ router.get('/', async (req, res) => { try { if (!req.user?.cityId) throw Error('User without selected city'); const item = await familyModel.findByNis(req.query.nis as string, req.user.cityId, true); - // const balance = await consumptionModel.getFamilyBalance(item); - const balance = await consumptionModel.getFamilyDependentBalance(item); - res.send({ ...item.toJSON(), balance }); + if (item) { + // const balance = await consumptionModel.getFamilyBalance(item); + const balance = await consumptionModel.getFamilyDependentBalance(item); + res.send({ ...item.toJSON(), balance }); + } else { + res.status(404).send('Not found'); + } } catch (error) { logging.error(error); res.status(500).send(error.message); From 1130f39c4e7f2da7688f043d51c1daf7b0dbaaa3 Mon Sep 17 00:00:00 2001 From: Allan Amaral Date: Mon, 8 Jun 2020 09:51:00 -0300 Subject: [PATCH 09/10] #293 - Extended feedback to family search --- admin/src/components/familySearch/index.tsx | 19 ++++++++++++------- admin/src/components/familySearch/styles.ts | 2 +- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/admin/src/components/familySearch/index.tsx b/admin/src/components/familySearch/index.tsx index c0b84dad..f9c38592 100644 --- a/admin/src/components/familySearch/index.tsx +++ b/admin/src/components/familySearch/index.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Form, Input, Typography, Card, Descriptions, Row, Col } from 'antd'; +import { Form, Input, Typography, Descriptions, Row, Col, Alert } from 'antd'; import { useSelector, useDispatch } from 'react-redux'; import { FamilyWrapper, InfoContainer } from './styles'; import { AppState } from '../../redux/rootReducer'; @@ -25,7 +25,9 @@ export const FamilySearch: React.FC = () => { const [nis, setNis] = useState(''); // Redux state const familyLoading = useSelector((state) => state.familiesReducer.familyLoading); - const familyError = useSelector((state) => state.familiesReducer.familyError); + const familyError = useSelector( + (state) => state.familiesReducer.familyError + ); const family = useSelector((state) => state.familiesReducer.familyItem); // .env @@ -54,11 +56,14 @@ export const FamilySearch: React.FC = () => { {familyError && !familyLoading && ( - - - Não encontramos nenhuma família utilizando esse NIS. - - + + {familyError.message} + + } + /> )} diff --git a/admin/src/components/familySearch/styles.ts b/admin/src/components/familySearch/styles.ts index 524021bb..592c0718 100644 --- a/admin/src/components/familySearch/styles.ts +++ b/admin/src/components/familySearch/styles.ts @@ -13,7 +13,7 @@ export const PriceStyle = { export const InfoContainer = styled.div` display: flex; - margin-top: ${(props) => props.theme.spacing.sm}; + margin: ${(props) => props.theme.spacing.sm} 0; justify-content: center; `; From 28c2b8cc435bba76c44cb14ae14165bb25b0558d Mon Sep 17 00:00:00 2001 From: Allan Amaral Date: Mon, 8 Jun 2020 10:53:36 -0300 Subject: [PATCH 10/10] #292 - Adjustments in the Admin --- admin/src/components/sidebar/index.tsx | 32 +++++++++--------- admin/src/pages/consumption/index.tsx | 2 +- admin/src/pages/families/list.tsx | 7 +--- admin/src/pages/user/form.tsx | 47 +++++++++++++++----------- backend/src/models/users.ts | 2 +- 5 files changed, 47 insertions(+), 43 deletions(-) diff --git a/admin/src/components/sidebar/index.tsx b/admin/src/components/sidebar/index.tsx index c85b5e8c..601d508b 100644 --- a/admin/src/components/sidebar/index.tsx +++ b/admin/src/components/sidebar/index.tsx @@ -41,31 +41,36 @@ const routes: RouteItem[] = [ icon: () => , name: 'Validar Produtos' }, - // { - // path: '/relatorios', - // icon: () => , - // name: 'Relatórios' - // }, { - path: '/beneficios', + path: '/consumo', icon: () => , - name: 'Beneficios' + name: 'Informar consumo' }, { path: '/familias', icon: () => , name: 'Famílias' }, + { + path: '/beneficios', + icon: () => , + name: 'Beneficios' + }, { path: '/usuarios', icon: () => , name: 'Usuários' }, { - path: '/consumo', - icon: () => , - name: 'Informar consumo' - }, + path: '/instituicoes', + icon: () => , + name: 'Instituições' + } + // { + // path: '/relatorios', + // icon: () => , + // name: 'Relatórios' + // }, // { // path: '/lojas', // icon: () => , @@ -76,11 +81,6 @@ const routes: RouteItem[] = [ // icon: () => , // name: 'Estabelecimentos' // }, - { - path: '/instituicoes', - icon: () => , - name: 'Instituições' - } ]; const privateRoutes: RouteItem[] = [ diff --git a/admin/src/pages/consumption/index.tsx b/admin/src/pages/consumption/index.tsx index b69997de..c9bd2731 100644 --- a/admin/src/pages/consumption/index.tsx +++ b/admin/src/pages/consumption/index.tsx @@ -124,7 +124,7 @@ export const ConsumptionForm: React.FC> = () return ( - + Informar consumo}>
setFieldValue('familyId', id)} /> diff --git a/admin/src/pages/families/list.tsx b/admin/src/pages/families/list.tsx index 7f1c935f..966800f3 100644 --- a/admin/src/pages/families/list.tsx +++ b/admin/src/pages/families/list.tsx @@ -111,12 +111,7 @@ export const FamiliesList: React.FC<{}> = () => {
- {`Famílias`} - - - - - + {`Famílias`}} loading={dashboardLoading}> + isCreating ? schema.required() : schema + ), cpf: yup.string().label('CPF').required(), email: yup.string().label('Email').required(), - role: yup.string().label('Cargo').required() + role: yup.string().label('Cargo').required(), + isCreating: yup.boolean().label('CriandoUsuario').nullable() }); /** @@ -57,10 +63,15 @@ export const UserForm: React.FC> = (props) = cpf: '', email: '', role: 'admin', - active: false + active: false, + isCreating }, validationSchema: schema, - onSubmit: (values, { setStatus }) => { + onSubmit: (formikValues, { setStatus }) => { + const values = { ...formikValues }; + // Prevent sending an empty password when editing an already existing user + if (!isCreating && user && !values.password) values.password = user.password; + setStatus(); dispatch( requestSaveUser( @@ -108,21 +119,19 @@ export const UserForm: React.FC> = (props) = - {isCreating && ( - - - - )} + + +