diff --git a/package.json b/package.json index 323fb76a..5a700cf2 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "react-i18next": "^11.7.3", "react-redux": "^7.2.3", "react-router-dom": "^5.2.0", + "react-select": "^4.3.1", "react-table": "^7.6.3", "react-to-typescript-definitions": "^3.0.1", "reactstrap": "^8.7.1", @@ -70,6 +71,7 @@ "@pagopa/openapi-codegen-ts": "9.0.0", "@pagopa/ts-commons": "9.1.0", "@svgr/parcel-plugin-svgr": "^5.5.0", + "@types/react-select": "^4.0.15", "@types/react-google-recaptcha": "^2.1.0", "@types/classnames": "^2.2.11", "@types/file-saver": "^2.0.2", diff --git a/src/api/api.yaml b/src/api/api.yaml index ab0e45c9..33932e0e 100644 --- a/src/api/api.yaml +++ b/src/api/api.yaml @@ -25,8 +25,24 @@ tags: description: API Token - name: help description: Help + - name: geolocation-token + description: API Geolocation Token paths: + /geolocation-token: + get: + tags: + - geolocation-token + summary: Get token for geolocation + operationId: getGeolocationToken + responses: + 200: + description: Token for geolocation + schema: + $ref: '#/definitions/GeolocationToken' + 403: + $ref: '#/responses/Forbidden' + /agreements: post: tags: @@ -906,6 +922,9 @@ definitions: creationDate: type: string format: date + suspendedReasonMessage: + type: string + maxLength: 250 DiscountState: type: string enum: @@ -927,23 +946,25 @@ definitions: Address: type: object required: - - street - - zipCode - - city - - district + - fullAddress + - coordinates properties: - street: - type: string - zipCode: + fullAddress: type: string - minLength: 5 - maxLength: 5 - city: - type: string - district: - type: string - minLength: 2 - maxLength: 2 + minLength: 10 + coordinates: + $ref: '#/definitions/Coordinates' + + Coordinates: + type: object + required: + - latitude + - longitude + properties: + latitude: + type: number + longitude: + type: number Documents: type: object @@ -1003,6 +1024,14 @@ definitions: message: type: string maxLength: 200 + GeolocationToken: + type: object + required: + - token + properties: + token: + type: string + minLength: 1 responses: InvalidRequest: diff --git a/src/api/api_backoffice.yaml b/src/api/api_backoffice.yaml index e81c0e64..9016db34 100644 --- a/src/api/api_backoffice.yaml +++ b/src/api/api_backoffice.yaml @@ -58,6 +58,8 @@ paths: format: date - $ref: '#/parameters/PageSize' - $ref: '#/parameters/PageNumber' + - $ref: '#/parameters/RequestColumnSort' + - $ref: '#/parameters/SortDirection' responses: 200: description: List of agreements @@ -320,7 +322,27 @@ parameters: enum: - Agreement - ManifestationOfInterest - + RequestColumnSort: + name: sortColumn + in: query + description: Sort by column + required: false + type: string + enum: + - Operator + - RequestDate + - State + - Assignee + SortDirection: + name: sortDirection + in: query + description: Sort Direction + required: false + type: string + default: ASC + enum: + - ASC + - DESC definitions: Agreements: type: object @@ -669,7 +691,8 @@ definitions: type: array minItems: 1 items: - $ref: '#/definitions/Address' + type: string + minLength: 10 BothChannels: allOf: @@ -685,7 +708,8 @@ definitions: type: array minItems: 1 items: - $ref: '#/definitions/Address' + type: string + minLength: 10 SalesChannelType: type: string @@ -693,26 +717,6 @@ definitions: - OnlineChannel - OfflineChannel - BothChannels - Address: - type: object - required: - - street - - zipCode - - city - - district - properties: - street: - type: string - zipCode: - type: string - minLength: 5 - maxLength: 5 - city: - type: string - district: - type: string - minLength: 2 - maxLength: 2 Documents: type: array diff --git a/src/api/api_public.yaml b/src/api/api_public.yaml index b27794cd..afe736cc 100644 --- a/src/api/api_public.yaml +++ b/src/api/api_public.yaml @@ -45,6 +45,7 @@ definitions: - emailAddress - category - message + - recaptchaToken properties: legalName: type: string @@ -71,6 +72,8 @@ definitions: message: type: string maxLength: 200 + recaptchaToken: + type: string responses: diff --git a/src/api/index.ts b/src/api/index.ts index 49d69c61..df3ce1c8 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,6 +1,6 @@ import axios, { AxiosError } from 'axios'; import { getCookie, logout } from '../utils/cookie'; -import {AgreementApi, ProfileApi, DiscountApi, DocumentApi, DocumentTemplateApi, ApiTokenApi, HelpApi } from './generated'; +import {AgreementApi, ProfileApi, DiscountApi, DocumentApi, DocumentTemplateApi, ApiTokenApi, HelpApi, GeolocationTokenApi } from './generated'; const token = getCookie(); @@ -30,4 +30,5 @@ export default { DocumentTemplate: new DocumentTemplateApi(undefined, process.env.BASE_API_PATH, axiosInstance), ApiToken: new ApiTokenApi(undefined, process.env.BASE_API_PATH, axiosInstance), Help: new HelpApi(undefined, process.env.BASE_API_PATH, axiosInstance), + GeolocationToken: new GeolocationTokenApi(undefined, process.env.BASE_API_PATH, axiosInstance), }; diff --git a/src/components/Form/CreateProfileForm/ProfileData/ProfileData.tsx b/src/components/Form/CreateProfileForm/ProfileData/ProfileData.tsx index 5bef76c7..5009967e 100644 --- a/src/components/Form/CreateProfileForm/ProfileData/ProfileData.tsx +++ b/src/components/Form/CreateProfileForm/ProfileData/ProfileData.tsx @@ -21,7 +21,7 @@ const defaultSalesChannel = { channelType: "", websiteUrl: "", discountCodeType: "", - addresses: [{ street: "", zipCode: "", city: "", district: "" }] + addresses: [{ fullAddress: "", coordinates: { latitude: "", longitude: "" } }] }; const defaultInitialValues = { @@ -63,6 +63,7 @@ const ProfileData = ({ const [initialValues, setInitialValues] = useState(defaultInitialValues); const { triggerTooltip } = useTooltip(); const [loading, setLoading] = useState(true); + const [geolocationToken, setGeolocationToken] = useState(); useEffect(() => { window.scrollTo(0, 0); @@ -105,9 +106,22 @@ const ProfileData = ({ .map(response => response.data) .fold( () => setLoading(false), - profile => { + (profile: any) => { setInitialValues({ ...profile, + salesChannel: + profile.salesChannel.channelType === "OfflineChannel" + ? { + ...profile.salesChannel, + addresses: profile.salesChannel.addresses.map( + (address: any) => ({ + ...address, + value: address.fullAddress, + label: address.fullAddress + }) + ) + } + : profile.salesChannel, hasDifferentFullName: !!profile.name }); setLoading(false); @@ -115,6 +129,15 @@ const ProfileData = ({ ) .run(); + const getGeolocationToken = async () => + await tryCatch(() => Api.GeolocationToken.getGeolocationToken(), toError) + .map(response => response.data) + .fold( + () => void 0, + token => setGeolocationToken(token.token) + ) + .run(); + useEffect(() => { if (isCompleted) { setLoading(true); @@ -122,6 +145,7 @@ const ProfileData = ({ } else { setLoading(false); } + void getGeolocationToken(); }, []); const getSalesChannel = (salesChannel: any) => { @@ -166,7 +190,7 @@ const ProfileData = ({ }); }} > - {({ values }) => ( + {({ values, setFieldValue }) => (
@@ -174,9 +198,11 @@ const ProfileData = ({
diff --git a/src/components/Form/CreateProfileForm/ProfileData/SalesChannels.tsx b/src/components/Form/CreateProfileForm/ProfileData/SalesChannels.tsx index 5f3c5a16..3231746a 100644 --- a/src/components/Form/CreateProfileForm/ProfileData/SalesChannels.tsx +++ b/src/components/Form/CreateProfileForm/ProfileData/SalesChannels.tsx @@ -1,10 +1,14 @@ import React from "react"; import { Field, FieldArray } from "formik"; import { Button, Icon } from "design-react-kit"; +import AsyncSelect from "react-select/async"; +import Axios from "axios"; +import { tryCatch } from "fp-ts/lib/TaskEither"; +import { toError } from "fp-ts/lib/Either"; import FormSection from "../../FormSection"; -import InputFieldMultiple from "../../InputFieldMultiple"; import PlusCircleIcon from "../../../../assets/icons/plus-circle.svg"; import CustomErrorMessage from "../../CustomErrorMessage"; +import chainAxios from "../../../../utils/chainAxios"; import SalesChannelDiscountCodeType from "./SalesChannelDiscountCodeType"; const hasOfflineOrBothChannels = (channelType: string) => @@ -19,237 +23,231 @@ type Props = { handleBack: any; formValues: any; isValid: boolean; + setFieldValue: any; + geolocationToken: string; }; -const SalesChannels = ({ handleBack, formValues, isValid }: Props) => ( - <> - -
-
- - -
-
- - -
-
- - -
-
-
- {hasOnlineOrBothChannels(formValues.salesChannel?.channelType) && ( - - )} - {hasOfflineOrBothChannels(formValues.salesChannel?.channelType) && ( - ( - <> - {formValues.salesChannel?.addresses?.map( - (address: any, index: number) => ( - = 2 ? `Indirizzo ${index + 1}` : `Indirizzo` - } - description="Inserisci l'indirizzo del punto vendita, se si hanno più punti vendita inserisci gli indirizzi aggiuntivi" - required={index + 1 === 1} - isVisible - > -
- {!!index && ( - arrayHelpers.remove(index)} - /> - )} -
-
- - - - -
-
- - - - -
-
-
-
- - - - -
-
- - - - -
-
+const SalesChannels = ({ + handleBack, + formValues, + isValid, + setFieldValue, + geolocationToken +}: Props) => { + const autocomplete = async (q: any) => + await tryCatch( + () => + Axios.get("https://geocode.search.hereapi.com/v1/geocode", { + params: { + apiKey: geolocationToken, + q, + lang: "it" + } + }), + toError + ) + .chain(chainAxios) + .map((response: any) => response.data) + .fold( + () => [{ value: "", label: "" }], + profile => + profile.items.map((item: any) => ({ + value: item.title, + label: item.title, + fullAddress: item.title, + coordinates: { + latitude: item.position.lat, + longitude: item.position.lng + } + })) + ) + .run(); - {formValues.salesChannel?.addresses?.length === - index + 1 && ( - <> -
- arrayHelpers.push({ - street: "", - zipCode: "", - city: "", - district: "" - }) - } - > - - - Aggiungi un indirizzo - -
- {!hasBothChannels( - formValues.salesChannel?.channelType - ) && ( -
- - -
- )} - - )} -
-
- ) - )} - - )} - >
- )} - {hasOnlineOrBothChannels(formValues.salesChannel?.channelType) && ( + return ( + <> - - -
- - +
+
+ + +
+
+ + +
+
+ + +
- )} - -); + {hasOnlineOrBothChannels(formValues.salesChannel?.channelType) && ( + + )} + {hasOfflineOrBothChannels(formValues.salesChannel?.channelType) && ( + ( + <> + {formValues.salesChannel?.addresses?.map( + (address: any, index: number) => ( + = 2 ? `Indirizzo ${index + 1}` : `Indirizzo` + } + description="Inserisci l'indirizzo del punto vendita, se si hanno più punti vendita inserisci gli indirizzi aggiuntivi" + required={index + 1 === 1} + isVisible + > +
+ {!!index && ( + arrayHelpers.remove(index)} + /> + )} +
+
+ "Nessun risultato"} + value={formValues.salesChannel.addresses[index]} + onChange={(e: any) => + setFieldValue( + `salesChannel.addresses[${index}]`, + e + ) + } + /> +
+
+ {formValues.salesChannel?.addresses?.length === + index + 1 && ( + <> +
+ arrayHelpers.push({ + fullAddress: "", + coordinates: { latitude: "", longitude: "" } + }) + } + > + + + Aggiungi un indirizzo + +
+ {!hasBothChannels( + formValues.salesChannel?.channelType + ) && ( +
+ + +
+ )} + + )} +
+
+ ) + )} + + )} + >
+ )} + {hasOnlineOrBothChannels(formValues.salesChannel?.channelType) && ( + + + +
+ + +
+
+ )} + + ); +}; export default SalesChannels; diff --git a/src/components/Form/EditOperatorDataForm/EditOperatorDataForm.tsx b/src/components/Form/EditOperatorDataForm/EditOperatorDataForm.tsx index 597321b1..7abfafb9 100644 --- a/src/components/Form/EditOperatorDataForm/EditOperatorDataForm.tsx +++ b/src/components/Form/EditOperatorDataForm/EditOperatorDataForm.tsx @@ -19,7 +19,7 @@ const defaultSalesChannel = { channelType: "", websiteUrl: "", discountCodeType: "", - addresses: [{ street: "", zipCode: "", city: "", district: "" }] + addresses: [{ fullAddress: "", coordinates: { latitude: "", longitude: "" } }] }; const defaultInitialValues = { @@ -49,6 +49,7 @@ const EditOperatorDataForm = () => { const user = useSelector((state: RootState) => state.user.data); const [initialValues, setInitialValues] = useState(defaultInitialValues); const [loading, setLoading] = useState(true); + const [geolocationToken, setGeolocationToken] = useState(); const updateProfile = async (discount: any) => { if (agreement) { @@ -69,9 +70,22 @@ const EditOperatorDataForm = () => { .map(response => response.data) .fold( () => setLoading(false), - profile => { + (profile: any) => { setInitialValues({ ...profile, + salesChannel: + profile.salesChannel.channelType === "OfflineChannel" + ? { + ...profile.salesChannel, + addresses: profile.salesChannel.addresses.map( + (address: any) => ({ + ...address, + value: address.fullAddress, + label: address.fullAddress + }) + ) + } + : profile.salesChannel, hasDifferentFullName: !!profile.name }); setLoading(false); @@ -79,9 +93,19 @@ const EditOperatorDataForm = () => { ) .run(); + const getGeolocationToken = async (agreementId: string) => + await tryCatch(() => Api.GeolocationToken.getGeolocationToken(), toError) + .map(response => response.data) + .fold( + () => void 0, + token => setGeolocationToken(token.token) + ) + .run(); + useEffect(() => { setLoading(true); void getProfile(agreement.id); + void getGeolocationToken(agreement.id); }, []); const getSalesChannel = (salesChannel: any) => { @@ -126,13 +150,15 @@ const EditOperatorDataForm = () => { }); }} > - {({ values }) => ( + {({ values, setFieldValue }) => (
history.push(DASHBOARD)} formValues={values} isValid diff --git a/src/components/Form/ValidationSchemas.ts b/src/components/Form/ValidationSchemas.ts index a51b4bfd..872bd5c0 100644 --- a/src/components/Form/ValidationSchemas.ts +++ b/src/components/Form/ValidationSchemas.ts @@ -1,4 +1,5 @@ import * as Yup from 'yup'; +import { string } from 'yup/lib/locale'; import { HelpRequestCategoryEnum } from '../../api/generated'; import Help from '../../pages/Help'; @@ -41,7 +42,7 @@ export const ProfileDataValidationSchema = Yup.object().shape({ description: Yup.string().required(REQUIRED_FIELD), salesChannel: Yup.object().shape({ channelType: Yup.mixed().oneOf([ 'OnlineChannel', 'OfflineChannel', 'BothChannels' ]), - websiteUrl: Yup.string().when('channelType', { + websiteUrl: Yup.string().nullable().when('channelType', { is: (val: string) => val === 'OnlineChannel' || val === 'BothChannels', then: Yup.string() .matches( @@ -58,14 +59,13 @@ export const ProfileDataValidationSchema = Yup.object().shape({ is: (val: string) => val === 'OfflineChannel' || val === 'BothChannels', then: Yup.array().of( Yup.object().shape({ - street: Yup.string().required(REQUIRED_FIELD), - zipCode: Yup.string() - .matches(/^[0-9]*$/, ONLY_NUMBER) - .min(5, 'Deve essere di 5 caratteri') - .max(5, 'Deve essere di 5 caratteri') - .required(REQUIRED_FIELD), - city: Yup.string().required(REQUIRED_FIELD), - district: Yup.string().required(REQUIRED_FIELD) + fullAddress: Yup.string().min(10).required(REQUIRED_FIELD), + coordinates: Yup.object().shape({ + latitude: Yup.number().required(REQUIRED_FIELD), + longitude: Yup.number().required(REQUIRED_FIELD) + }), + label: Yup.string(), + value: Yup.string() }) ) }) diff --git a/src/components/OperatorConvention/OperatorData.tsx b/src/components/OperatorConvention/OperatorData.tsx index 1e5a4a15..06933148 100644 --- a/src/components/OperatorConvention/OperatorData.tsx +++ b/src/components/OperatorConvention/OperatorData.tsx @@ -15,7 +15,7 @@ const OperatorData = ({ profile }: { profile: ApprovedAgreementProfile }) => { {salesChannel.addresses?.map((address, i: number) => { - const textAddress = `${address.city}, ${address.street} ${address.district}, ${address.zipCode}`; + const textAddress = `${address}`; return (