Skip to content

Commit

Permalink
Ajouter une fonction 'vente privée' #25
Browse files Browse the repository at this point in the history
  • Loading branch information
benjaminpochat committed Sep 15, 2024
1 parent 75ca796 commit 1517499
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.lang.NonNull;

import java.util.List;

Expand All @@ -20,4 +21,6 @@ public interface SaleRepository extends CrudRepository<Sale, Integer> {
where p = :production
""")
List<Sale> findByProduction(@Param("production") Production production);

List<Sale> findByPrivateAccessKeyIgnoreCase(@NonNull String privateAccessKey);
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,13 @@ public ResponseEntity<Sale> getSale(Integer saleId) {
}

@Override
public ResponseEntity<List<Sale>> getSales() {
public ResponseEntity<List<Sale>> getSales(String privateAccessKey) {
List<Sale> sales = new ArrayList<>();
saleRepository.findAll().forEach(sales::add);
if (privateAccessKey == null || privateAccessKey.isBlank()) {
saleRepository.findAll().forEach(sales::add);
} else {
saleRepository.findByPrivateAccessKeyIgnoreCase(privateAccessKey).forEach(sales::add);
}
return new ResponseEntity<>(sales, OK);
}

Expand Down
36 changes: 30 additions & 6 deletions backend/model/src/main/java/eu/viandeendirect/model/Sale.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ public class Sale {
@JsonProperty("deliveryZipCode")
private String deliveryZipCode;

@JsonProperty("privateAccessKey")
private String privateAccessKey;

public Sale id(Integer id) {
this.id = id;
return this;
Expand Down Expand Up @@ -169,11 +172,11 @@ public Sale deliveryStart(OffsetDateTime deliveryStart) {
}

/**
*
* Get deliveryStart
* @return deliveryStart
*/
@Valid
@Schema(name = "deliveryStart", description = "", required = false)
@Schema(name = "deliveryStart", required = false)
public OffsetDateTime getDeliveryStart() {
return deliveryStart;
}
Expand All @@ -188,11 +191,11 @@ public Sale deliveryStop(OffsetDateTime deliveryStop) {
}

/**
*
* Get deliveryStop
* @return deliveryStop
*/
@Valid
@Schema(name = "deliveryStop", description = "", required = false)
@Schema(name = "deliveryStop", required = false)
public OffsetDateTime getDeliveryStop() {
return deliveryStop;
}
Expand Down Expand Up @@ -296,6 +299,25 @@ public void setDeliveryZipCode(String deliveryZipCode) {
this.deliveryZipCode = deliveryZipCode;
}

public Sale privateAccessKey(String privateAccessKey) {
this.privateAccessKey = privateAccessKey;
return this;
}

/**
* if this key is not null, the sale is private, and the user must enter this key to access the sale
* @return privateAccessKey
*/

@Schema(name = "privateAccessKey", description = "if this key is not null, the sale is private, and the user must enter this key to access the sale", required = false)
public String getPrivateAccessKey() {
return privateAccessKey;
}

public void setPrivateAccessKey(String privateAccessKey) {
this.privateAccessKey = privateAccessKey;
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand All @@ -315,12 +337,13 @@ public boolean equals(Object o) {
Objects.equals(this.deliveryAddressLine1, sale.deliveryAddressLine1) &&
Objects.equals(this.deliveryAddressLine2, sale.deliveryAddressLine2) &&
Objects.equals(this.deliveryCity, sale.deliveryCity) &&
Objects.equals(this.deliveryZipCode, sale.deliveryZipCode);
Objects.equals(this.deliveryZipCode, sale.deliveryZipCode) &&
Objects.equals(this.privateAccessKey, sale.privateAccessKey);
}

@Override
public int hashCode() {
return Objects.hash(id, seller, productions, orders, deliveryStart, deliveryStop, deliveryAddressName, deliveryAddressLine1, deliveryAddressLine2, deliveryCity, deliveryZipCode);
return Objects.hash(id, seller, productions, orders, deliveryStart, deliveryStop, deliveryAddressName, deliveryAddressLine1, deliveryAddressLine2, deliveryCity, deliveryZipCode, privateAccessKey);
}

@Override
Expand All @@ -338,6 +361,7 @@ public String toString() {
sb.append(" deliveryAddressLine2: ").append(toIndentedString(deliveryAddressLine2)).append("\n");
sb.append(" deliveryCity: ").append(toIndentedString(deliveryCity)).append("\n");
sb.append(" deliveryZipCode: ").append(toIndentedString(deliveryZipCode)).append("\n");
sb.append(" privateAccessKey: ").append(toIndentedString(privateAccessKey)).append("\n");
sb.append("}");
return sb.toString();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ export default function EditableTextField({
InputProps={{ readOnly: !writable }}
disabled={!writable && !(value?.length > 0)}
onChange={(event) => setValue(event.target.value)}

fullWidth
/>
{getActions()}
Expand Down
41 changes: 40 additions & 1 deletion frontend/app/src/domains/sale/views/SaleForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react'
import { useState } from 'react'
import { useLoaderData, useNavigate } from 'react-router-dom'
import { useKeycloak } from '@react-keycloak/web'
import { Button, ButtonGroup, Stepper, Step, StepLabel, StepContent, Typography, Autocomplete } from "@mui/material"
import { Button, ButtonGroup, Stepper, Step, StepLabel, StepContent, Typography, Autocomplete, Switch } from "@mui/material"
import { ApiBuilder } from '../../../api/ApiBuilder.ts'
import { DatePickerElement, TextFieldElement, FormContainer, TimePickerElement } from 'react-hook-form-mui'
import 'dayjs/locale/fr';
Expand All @@ -25,6 +25,7 @@ export default function SaleForm() {
const SET_DELIVERY_DATE_STEP = 'SET_DELIVERY_DATE_STEP'
const SET_DELIVERY_ADDRESS_STEP = 'SET_DELIVERY_ADDRESS_STEP'
const CONFIRMATION_STEP = 'CONFIRMATION_STEP'
const ACCESS_TYPE_STEP = 'ACCESS_TYPE_STEP'

const { keycloak } = useKeycloak()
const navigate = useNavigate()
Expand All @@ -37,6 +38,7 @@ export default function SaleForm() {
const [activeStep, setActiveStep] = useState(SELECT_PRODUCTION_STEP)
const [sale, setSale] = useState<Sale>({})
const [selectedAddress, setSelectedAddress] = useState(undefined)
const [privateSale, setPrivateSale] = useState<boolean>(false)

return <>
<Typography variant='h6'>Nouvelle vente</Typography>
Expand Down Expand Up @@ -123,6 +125,27 @@ export default function SaleForm() {
</div>
</StepContent>
</Step>
<Step active={activeStep === ACCESS_TYPE_STEP}>
<StepLabel>Type d'accès</StepLabel>
<StepContent>
<div>
<FormContainer onSuccess={validateAccessType}>
<div>
Vente publique
<Switch onChange={(event) => setPrivateSale(event.target.checked)}></Switch>
Vente privée
</div>
{displayPrivateAccessField()}
<div>
<ButtonGroup>
<Button type='submit' variant="contained" size="small">Valider</Button>
<Button variant="outlined" size="small" onClick={() => cancel()}>Abandonner</Button>
</ButtonGroup>
</div>
</FormContainer>
</div>
</StepContent>
</Step>
<Step active={activeStep === CONFIRMATION_STEP}>
<StepLabel>Récapitulatif</StepLabel>
<StepContent>
Expand Down Expand Up @@ -176,6 +199,22 @@ export default function SaleForm() {
sale.deliveryStop.setMinutes(deliveryDateFormData.stopTime.toDate().getMinutes())

setSale(sale)
setActiveStep(ACCESS_TYPE_STEP)
}

function displayPrivateAccessField(): React.ReactNode {
if (privateSale) {
return <div>
<TextFieldElement required={privateSale} name="privateAccessCode" label="Code accès vente privée" variant="standard" />
</div>
}
return <></>
}

function validateAccessType(accessTypeFormData) {
sale.privateAccessKey = privateSale ? accessTypeFormData.privateAccessCode : undefined

setSale(sale)
setActiveStep(CONFIRMATION_STEP)
}

Expand Down
39 changes: 31 additions & 8 deletions frontend/app/src/domains/welcome/Welcome.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react'
import React, { useState } from 'react'

import { AppBar, Box, CssBaseline, Toolbar, Typography } from '@mui/material'
import { Box, Button, Stack, TextField, Toolbar, Typography } from '@mui/material'
import ForwardIcon from '@mui/icons-material/Forward';

import { useLoaderData } from 'react-router-dom';

Expand All @@ -9,27 +10,43 @@ import { Sale } from '@viandeendirect/api/dist/models/Sale.js'
import SaleCustomerCard from '../../domains/sale/components/SaleCustomerCard.tsx'
import { ApiBuilder } from '../../api/ApiBuilder.ts'
import { Producer } from '@viandeendirect/api/dist/models/Producer';
import { FormContainer } from 'react-hook-form-mui';


export default function Welcome() {

const loadedData = useLoaderData();
const sales: Array<Sale> = loadedData.sales
const randomProducer: Producer = loadedData.randomProducer
const [privateAccessKey, setPrivateAccessKey] = useState<string | undefined>(undefined)
const [displayedSales, setDisplayedSales] = useState<Array<Sale>>(sales)

return <Box
component="main"
sx={{ flexGrow: 1, p: 3 }}>
<Toolbar />
{getWelcomeMessage()}
<br/>
{sales.map(getSaleCard)}
<Box sx={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
flexWrap: 'wrap'
}}>
{getWelcomeMessage()}

<TextField size='small'
label="Clé d'accès vente privée"
onChange={(event) => setPrivateAccessKey(event.target.value)}
onKeyDown={(event) => {if (event.key === 'Enter') {reloadDisplayedSales(privateAccessKey)}}}
InputProps={{endAdornment: (<ForwardIcon sx={{':hover': {cursor: "pointer"}}} onClick={() => reloadDisplayedSales(privateAccessKey)}/>)}}/>

</Box>
{displayedSales.map(getSaleCard)}
{getRandomProducerSlideshow()}
</Box>

function getWelcomeMessage() {
if (sales.length > 0) {
return <Typography variant='h5'>Nos prochaines ventes :</Typography>
if (displayedSales.length > 0) {
return <Typography sx={{marginBottom: '1rem'}} variant='h5'>Nos prochaines ventes :</Typography>
}
return <>
<Box sx={{
Expand All @@ -44,7 +61,7 @@ export default function Welcome() {
}

function getRandomProducerSlideshow() {
if (sales.length === 0) {
if (displayedSales.length === 0) {
return <div style={{width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center'}}>
<div style={{width: '-webkit-fill-available', maxWidth: '50rem', aspectRatio: '165/100'}}>
<iframe title="randomProducerSlideshow"
Expand All @@ -64,6 +81,12 @@ export default function Welcome() {
return <SaleCustomerCard key={`sale-card-${sale.id}`} sale={sale}></SaleCustomerCard>
}

async function reloadDisplayedSales(privateAccessKey: string | undefined) {
const apiBuilder = new ApiBuilder()
const api = await apiBuilder.getAnonymousApi()
const reloadedSales = await api.getSales({privateAccessKey: privateAccessKey})
setDisplayedSales(reloadedSales)
}
}

export async function loadWelcomeData(): Promise<Array<Sale>> {
Expand Down
12 changes: 10 additions & 2 deletions openapi/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,13 @@ paths:

/sales:
summary: Path used to manage the list of all public sales.
parameters:
- name: privateAccessKey
description: the key to access private sales
schema:
type: string
in: query
required: false
get:
security:
- oAuth2ForViandeEnDirect: [read]
Expand Down Expand Up @@ -886,11 +893,9 @@ components:
$ref: '#/components/schemas/Order'
x-field-extra-annotation: '@jakarta.persistence.OneToMany(mappedBy = "sale")'
deliveryStart:
description: ""
type: string
format: date-time
deliveryStop:
description: ""
type: string
format: date-time
deliveryAddressName:
Expand All @@ -903,6 +908,9 @@ components:
type: string
deliveryZipCode:
type: string
privateAccessKey:
type: string
description: "if this key is not null, the sale is private, and the user must enter this key to access the sale"
x-class-extra-annotation: '@jakarta.persistence.Entity @jakarta.persistence.Table(name = "sales")'
StripeAccount:
description: an acount for Stripe payment service
Expand Down

0 comments on commit 1517499

Please sign in to comment.