Skip to content

Commit

Permalink
production modification save, backend errors displayed in frontend
Browse files Browse the repository at this point in the history
  • Loading branch information
benjaminpochat committed Mar 5, 2024
1 parent 4f50cb7 commit 6df479b
Show file tree
Hide file tree
Showing 16 changed files with 149 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import java.util.List;

public interface ProductionRepository extends CrudRepository<Production, Integer> {
@Query("SELECT p FROM Production p WHERE p.producer = :producer")
@Query("select p from Production p where p.producer = :producer")
List<Production> findByProducer(@Param("producer") Producer producer);

@Query("select p from Sale s inner join s.productions p where s.id = :saleId")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package eu.viandeendirect.repository;

import eu.viandeendirect.model.Producer;
import eu.viandeendirect.model.Production;
import eu.viandeendirect.model.Sale;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
Expand All @@ -11,4 +12,12 @@
public interface SaleRepository extends CrudRepository<Sale, Integer> {
@Query("select s from Sale s where s.seller = :seller")
List<Sale> findBySeller(@Param("seller") Producer seller);

@Query("""
select s
from Sale s
inner join s.productions p
where p = :production
""")
List<Sale> findByProduction(@Param("production") Production production);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@
import eu.viandeendirect.model.BeefProduction;
import eu.viandeendirect.model.PackageLot;
import eu.viandeendirect.model.Production;
import eu.viandeendirect.model.Sale;
import eu.viandeendirect.repository.PackageLotRepository;
import eu.viandeendirect.repository.ProductionRepository;
import eu.viandeendirect.repository.SaleRepository;
import eu.viandeendirect.service.specs.AuthenticationServiceSpecs;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Service
public class BeefProductionService implements BeefProductionsApiDelegate {
Expand All @@ -26,6 +30,8 @@ public class BeefProductionService implements BeefProductionsApiDelegate {

@Autowired
AuthenticationServiceSpecs producerService;
@Autowired
private SaleRepository saleRepository;

@Override
public ResponseEntity<BeefProduction> getBeefProduction(Integer beefProductionId) {
Expand All @@ -36,6 +42,7 @@ public ResponseEntity<BeefProduction> getBeefProduction(Integer beefProductionId

@Override
public ResponseEntity<BeefProduction> createBeefProduction(BeefProduction beefProduction) {
checkBeefProduction(beefProduction);
beefProduction.setProducer(producerService.getAuthenticatedProducer());
BeefProduction productionCreated = productionRepository.save(beefProduction);
beefProduction.getLots().forEach(lot -> lot.setProduction(productionCreated));
Expand All @@ -45,4 +52,11 @@ public ResponseEntity<BeefProduction> createBeefProduction(BeefProduction beefPr
productionCreated.setLots(lotsCreatedAsList);
return new ResponseEntity<>(productionCreated, HttpStatus.CREATED);
}

void checkBeefProduction(BeefProduction beefProduction) {
List<Sale> sales = saleRepository.findByProduction(beefProduction);
if(!sales.isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Il n'est pas possible de modifier une production déjà mise en vente.");
}
}
}
1 change: 1 addition & 0 deletions backend/app/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
server.port=8080
server.error.include-message=always
spring.jackson.date-format=eu.viandeendirect.RFC3339DateFormat
spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=false
logging.level.org.springframework.security=TRACE
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package eu.viandeendirect.service;

import eu.viandeendirect.model.BeefProduction;
import eu.viandeendirect.model.Sale;
import eu.viandeendirect.repository.ProductionRepository;
import eu.viandeendirect.repository.SaleRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.web.server.ResponseStatusException;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@ActiveProfiles(value = "test")
@ExtendWith({SpringExtension.class})
class TestBeefProductionService_checkBeefProduction {
@Autowired
BeefProductionService beefProductionService;

@Autowired
ProductionRepository productionRepository;

@Autowired
SaleRepository saleRepository;

@Test
void should_not_raise_an_error_if_production_not_related_to_a_sale() {
// given
BeefProduction production = productionRepository.save(new BeefProduction());

// when / then
Assertions.assertThatNoException().isThrownBy(() -> beefProductionService.checkBeefProduction(production));
}

@Test
void should_raise_an_error_if_production_related_to_a_sale() {
// given
BeefProduction production = productionRepository.save(new BeefProduction());
Sale sale = new Sale();
sale.setProductions(List.of(production));
saleRepository.save(sale);

// when / then
Assertions.assertThatThrownBy(() -> beefProductionService.checkBeefProduction(production))
.isInstanceOf(ResponseStatusException.class)
.satisfies(throwable -> assertThat(((ResponseStatusException) throwable).getBody().getDetail()).isEqualTo("Il n'est pas possible de modifier une production déjà mise en vente."));
}

}
20 changes: 10 additions & 10 deletions frontend/app/src/api/ApiInvoker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,49 @@ export class ApiInvoker {

apiBuilder = new ApiBuilder()

callApiAnonymously(getApiFunction, arg, callBackFunction) {
callApiAnonymously(getApiFunction, arg, successCallbackFunction, errorCallbackFunction) {
this.apiBuilder.getAnonymousApi().then(api => {
this.apiBuilder.invokeAnonymousApi(() => {
const apiFunction = getApiFunction(api)
if (arg) {
apiFunction.call(api, arg, (error, data) => {
if (error) {
console.error(error)
errorCallbackFunction(error)
} else {
callBackFunction(data)
successCallbackFunction(data)
}
})
} else {
apiFunction.call(api, (error, data) => {
if (error) {
console.error(error)
errorCallbackFunction(error)
} else {
callBackFunction(data)
successCallbackFunction(data)
}
})
}
})
})
}

callApiAuthenticatedly(getApiFunction, arg, callBackFunction, keycloak) {
callApiAuthenticatedly(keycloak, getApiFunction, arg, successCallbackFunction, errorCallbackFunction) {
this.apiBuilder.getAuthenticatedApi(keycloak).then(api => {
this.apiBuilder.invokeAuthenticatedApi(() => {
const apiFunction = getApiFunction(api)
if (arg) {
apiFunction.call(api, arg, (error, data) => {
if (error) {
console.error(error)
errorCallbackFunction(error)
} else {
callBackFunction(data)
successCallbackFunction(data)
}
})
} else {
apiFunction.call(api, (error, data) => {
if (error) {
console.error(error)
errorCallbackFunction(error)
} else {
callBackFunction(data)
successCallbackFunction(data)
}
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default function CustomerCreationForm({returnCallback: returnCallback, cu
customer.user.firstName = userFormData.firstName
customer.user.email = userFormData.email
customer.user.phone = userFormData.phone
apiInvoker.callApiAuthenticatedly(api => api.createCustomer, customer, returnCallback, keycloak)
apiInvoker.callApiAuthenticatedly(keycloak, api => api.createCustomer, customer, returnCallback)
}

return <Box component="main" sx={{ flexGrow: 1, p: 3 }}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import dayjs from 'dayjs'
export default function BeefProductionCard({
production: production,
showActions: showActions,
viewBeefProductionCallback: viewBeefProductionCallback}) {
clickCallback: clickCallback}) {

const [beefProduction, setBeefProduction] = useState(production)
const { keycloak, initialized } = useKeycloak()
Expand All @@ -31,7 +31,7 @@ export default function BeefProductionCard({

return (
<Card>
<CardActionArea onClick={() => viewBeefProductionCallback(beefProduction)}>
<CardActionArea onClick={() => clickCallback(beefProduction)}>
<CardContent>
<Typography color="text.secondary" gutterBottom>
Abattage bovin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import BeefProductionCard from "./BeefProductionCard.tsx"
export default function ProductionCard({
production: production,
showActions: showActions,
viewBeefProductionCallback: viewBeefProductionCallback
clickCallback: clickCallback
}) {

switch (production.productionType) {
Expand All @@ -13,7 +13,7 @@ export default function ProductionCard({
key={production.id}
production={production}
showActions={showActions}
viewBeefProductionCallback={viewBeefProductionCallback}>
clickCallback={clickCallback}>
</BeefProductionCard>
default :
return <></>
Expand Down
12 changes: 7 additions & 5 deletions frontend/app/src/domains/production/views/ProductionsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,12 @@ export default function ProductionsList({


function getProductionCards() {
return productions.map(production => <ProductionCard
production={production}
showActions={true}
viewBeefProductionCallback={viewBeefProductionCallback}>
</ProductionCard>)
return productions.map(production => <div className='card-clickable'>
<ProductionCard
production={production}
showActions={true}
clickCallback={viewBeefProductionCallback}>
</ProductionCard>
</div>)
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import React from 'react'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { Typography, ButtonGroup, Button, Tab, Tabs } from '@mui/material'
import { Typography, ButtonGroup, Button, Tab, Tabs, Alert } from '@mui/material'
import dayjs from 'dayjs'
import BreedingPropertiesForm, { mapBreedingFormDataToBeefProduction } from './forms/BreedingPropertiesForm.tsx'
import SlaughterPropertiesForm, { mapSlaughterFormDataToBeefProduction } from './forms/SlaughterPropertiesForm.tsx'
import CuttingPropertiesForm, { mapCuttingFormDataToBeefProduction } from './forms/CuttingPropertiesForm.tsx'
import BeefProduction from "viandeendirect_eu/dist/model/BeefProduction.js"
import PackageLotsCreator from '../PackageLotsCreator.tsx'
import { ApiInvoker } from '../../../../api/ApiInvoker.ts'
import { useKeycloak } from '@react-keycloak/web'

export default function BeefProductionView({ beefProduction: beefProduction, backCallback: backCallback }) {

Expand All @@ -16,13 +18,17 @@ export default function BeefProductionView({ beefProduction: beefProduction, bac
const CUTTING_PROPERTIES_TAB = 2
const PRODUCTS_TAB = 3

const apiInvoker = new ApiInvoker()
const { keycloak } = useKeycloak()

const [currentTab, setCurrentTab] = useState<number>(BREEDING_PROPERTIES_TAB)
const [readOnly, setReadOnly] = useState<boolean>(true)
const [production, setProduction] = useState<BeefProduction>(
{...beefProduction,
lots: beefProduction.lots ? beefProduction.lots.map((lot) => {return {...lot}}) : undefined
})
const [saveEnabled, setSaveEnabled] = useState<boolean>(true)
const [alerts, setAlerts] = useState<string>(undefined)

const changeTab = (event: React.SyntheticEvent, newValue: number) => {
setCurrentTab(newValue);
Expand Down Expand Up @@ -51,6 +57,7 @@ export default function BeefProductionView({ beefProduction: beefProduction, bac
<Tab label="Découpe" disabled={!readOnly}/>
<Tab label="Colis" disabled={!readOnly}/>
</Tabs>
{displayAlerts()}
<div hidden={currentTab !== 0}>
<BreedingPropertiesForm
form={breedingPropertiesForm}
Expand Down Expand Up @@ -90,36 +97,49 @@ export default function BeefProductionView({ beefProduction: beefProduction, bac
</ButtonGroup>
}
return <ButtonGroup>
<Button variant="contained" size="small" onClick={saveUpdate()} disabled={!saveEnabled}>Sauvegarder</Button>
<Button variant="contained" size="small" onClick={handleSave()} disabled={!saveEnabled}>Sauvegarder</Button>
<Button variant="outlined" size="small" onClick={cancelUpdate} >Abandonner</Button>
</ButtonGroup>
}

function saveUpdate() {
function handleSave() {
switch (currentTab) {
case BREEDING_PROPERTIES_TAB:
return breedingPropertiesForm.handleSubmit((breedingFormData) => {
setProduction(mapBreedingFormDataToBeefProduction(breedingFormData, production))
setReadOnly(true)
const updatedProduction = mapBreedingFormDataToBeefProduction(breedingFormData, production)
setProduction(updatedProduction)
saveProduction(updatedProduction)
})
case SLAUGHTER_PROPERTIES_TAB:
return slaughterPropertiesForm.handleSubmit((slaughterFormData) => {
setProduction(mapSlaughterFormDataToBeefProduction(slaughterFormData, production))
setReadOnly(true)
const updatedProduction = mapSlaughterFormDataToBeefProduction(slaughterFormData, production)
setProduction(updatedProduction)
saveProduction(updatedProduction)
})
case CUTTING_PROPERTIES_TAB:
return cuttingPropertiesForm.handleSubmit((cuttingFormData) => {
setProduction(mapCuttingFormDataToBeefProduction(cuttingFormData, production))
setReadOnly(true)
const updatedProduction = mapCuttingFormDataToBeefProduction(cuttingFormData, production)
setProduction(updatedProduction)
saveProduction(updatedProduction)
})
case PRODUCTS_TAB:
return () => {
setReadOnly(true)
}
return () => saveProduction(production)
}
}

function saveProduction(updatedProduction) {
setAlerts(undefined)
apiInvoker.callApiAuthenticatedly(
keycloak,
api => api.createBeefProduction,
updatedProduction,
() => setReadOnly(true),
error => setAlerts(error.response.body.message)
)
}

function cancelUpdate() {
setAlerts(undefined)
switch (currentTab) {
case BREEDING_PROPERTIES_TAB:
return breedingPropertiesForm.reset(() => {
Expand Down Expand Up @@ -148,4 +168,10 @@ export default function BeefProductionView({ beefProduction: beefProduction, bac
setReadOnly(true)
}
}

function displayAlerts() {
if (alerts) {
return <Alert severity="error">{alerts}</Alert>
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,12 @@ export default function SaleProductionSelector({selectProduction: selectProducti
}

return productionsForSale.map(production => {
return <div className='card-clickable' onClick={() => handleProductSelection(production)}>
return <div className='card-clickable'>
<ProductionCard
key={'production-card-' + production.id}
production={production}
showActions={false}>
showActions={false}
clickCallback={() => handleProductSelection(production)}>
</ProductionCard>
</div>
})
Expand Down
Loading

0 comments on commit 6df479b

Please sign in to comment.