diff --git a/src/App.tsx b/src/App.tsx index daa740e..2b012b6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,8 +17,10 @@ import Login from './pages/Login'; import Home from './pages/Home'; import Perfil from './pages/Perfil'; import PerfilEditar from './pages/PerfilEditar'; -import CadastroCompletar from './pages/CadastroCompletar'; import CadastroVan from './pages/CadastroVan'; +import CadastroCompletar from './pages/CadastroCompletar/CadastroCompletar'; +import CompletarDocumento from './pages/CadastroCompletar/CompletarDocumento'; +import CompletarTelefone from './pages/CadastroCompletar/CompletarTelefone'; /* Core CSS required for Ionic components to work properly */ import '@ionic/react/css/core.css'; @@ -41,6 +43,7 @@ import './theme/variables.css'; import { search, home, person } from 'ionicons/icons'; import { useState, useContext } from 'react'; import React from 'react'; +import MinhasVans from './pages/MinhasVans'; setupIonicReact(); @@ -48,12 +51,19 @@ const routes = ( <> + + + + + + + diff --git a/src/constants/routes/usersRoutes.ts b/src/constants/routes/usersRoutes.ts index 4ebe2d0..18216e9 100644 --- a/src/constants/routes/usersRoutes.ts +++ b/src/constants/routes/usersRoutes.ts @@ -8,6 +8,9 @@ const usersRoutes = { }, update: { url: `${usersRoutesDefault}/edit` + }, + getSocialInfo: { + url: `${usersRoutesDefault}/social` } } diff --git a/src/constants/routes/vansLocatorsRoutes.ts b/src/constants/routes/vansLocatorsRoutes.ts new file mode 100644 index 0000000..1f57945 --- /dev/null +++ b/src/constants/routes/vansLocatorsRoutes.ts @@ -0,0 +1,17 @@ +const vansRoutesDefault = '/vans/locator'; +const vansRoutes = { + list: { + url: `${vansRoutesDefault}/list` + }, + getById: { + url: `${vansRoutesDefault}/` + }, + create: { + url: `${vansRoutesDefault}/` + }, + update: { + url: `${vansRoutesDefault}/edit` + } +} + +export default vansRoutes; \ No newline at end of file diff --git a/src/constants/routes/vansRoutes.ts b/src/constants/routes/vansRoutes.ts new file mode 100644 index 0000000..7bfcd2d --- /dev/null +++ b/src/constants/routes/vansRoutes.ts @@ -0,0 +1,20 @@ +const vansRoutesDefault = '/vans'; +const vansRoutes = { + list: { + url: `${vansRoutesDefault}/list` + }, + getByPlate: { + url: `${vansRoutesDefault}/plate` + }, + getByUserId: { + url: `${vansRoutesDefault}/user` + }, + create: { + url: `${vansRoutesDefault}/` + }, + update: { + url: `${vansRoutesDefault}/` + } +} + +export default vansRoutes; \ No newline at end of file diff --git a/src/pages/Cadastro/Cadastro.tsx b/src/pages/Cadastro/Cadastro.tsx index 184691a..82c6b9a 100644 --- a/src/pages/Cadastro/Cadastro.tsx +++ b/src/pages/Cadastro/Cadastro.tsx @@ -1,17 +1,23 @@ import { IonToast, IonProgressBar, IonItem, IonLabel, IonInput, IonBackButton, IonButton, IonButtons, IonCardTitle, IonCol, IonContent, IonGrid, IonHeader, IonPage, IonRow, IonToolbar } from '@ionic/react'; import { arrowBack, logoFacebook, mail } from 'ionicons/icons'; import { Action } from '../../components/Action'; -import { useEffect, useState } from 'react'; +import { useContext, useEffect, useState } from 'react'; import { useHistory, useParams } from 'react-router'; import './Cadastro.css'; import ModalExample from '../../components/Email'; import * as UsersService from '../../services/api/users' +import LocalStorage from '../../LocalStorage'; +import { UserContext } from '../../App'; +import { Color } from '@ionic/react/node_modules/@ionic/core'; const Cadastro: React.FC = () => { const history = useHistory(); + + const user = useContext(UserContext); const [showToast, setShowToast] = useState(false); - const [messageToast, setMessageToast ] = useState(''); + const [messageToast, setMessageToast] = useState(''); + const [toastColor, setToastColor] = useState("primary"); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); @@ -53,7 +59,7 @@ const Cadastro: React.FC = () => { clearResult(); if(!emailValidate()) { - lResult.error = 'O EMAIL é inválido!'; + lResult.error = 'E-mail inválido!'; lResult.success = false; return lResult; } else if(password.length < 7 || password.length > 12) { //TODO: validar de acordo com a documentação @@ -66,45 +72,56 @@ const Cadastro: React.FC = () => { }; const handleSubmit = async () => { - - if(name != '' && email != '' && birthDate != '' && password != '' && confirmPassword != '') { - if(password === confirmPassword){ - const signUpForm = { - name: firstName, - lastname: lastName, - email: email, - birth_date: birthDate, - password: password - } - - let result = fieldValidate(); - if((await result).success) { - - let retorno = await UsersService.create(signUpForm); - console.log(retorno); - if(retorno.token) { - // let signIn = await Api.signIn(email, passwordField); - // if(signIn.token) { - // await AsyncStorage.setItem('token', signIn.token); - // await AsyncStorage.setItem('cpf', retorno.cpf); - history.push('home'); - - } else { - setMessageToast(retorno.message); - setShowToast(true); - } - } else{ - setMessageToast(lResult.error); - setShowToast(true); - } - } else { - setMessageToast('As senhas devem ser iguais!'); - setShowToast(true); - } - } else { - setMessageToast('Nenhum campo pode ser nulo!'); + if(name === '' || email === '' || birthDate === '' || password === '' || confirmPassword === '') { + setToastColor("warning") + setMessageToast('Nenhum campo pode estar vazio!'); setShowToast(true); + return } + + if(password !== confirmPassword) { + setToastColor("warning") + setMessageToast('As senhas devem ser iguais!'); + setShowToast(true); + return + } + + const signUpForm = { + name: firstName, + lastname: lastName, + email: email, + birth_date: birthDate, + password: password + } + + let result = fieldValidate(); + if(!(await result).success) { + setToastColor("warning") + setMessageToast(lResult.error); + setShowToast(true); + return + } + + let retorno = await UsersService.create(signUpForm); + + if(!retorno.token) { + setToastColor('danger') + setMessageToast(retorno.message); + setShowToast(true); + return + } + + LocalStorage.setToken(retorno.token.token); + + user.setIsLoggedIn(true); + + history.push({ pathname: '/home', state: { + redirectData: { + showToastMessage: true, + toastColor: "success", + toastMessage: "Usuário cadastrado com sucesso!", + } + }}) }; const { name } = useParams<{ name: string; }>(); @@ -121,19 +138,11 @@ const Cadastro: React.FC = () => { - - {/* Como você deseja se cadastrar? */} - Cadastro - + + {/* Como você deseja se cadastrar? */} + Cadastro + - {/* - - Continuar com Facebook - - - - Continuar com e-mail - */}
@@ -142,7 +151,7 @@ const Cadastro: React.FC = () => { setFirstName(e.target.value)} + onIonChange={(e: any) => setFirstName(e.target.value)} > @@ -150,7 +159,7 @@ const Cadastro: React.FC = () => { Sobrenome setLastName(e.target.value)} + onIonChange={(e: any) => setLastName(e.target.value)} > @@ -161,7 +170,7 @@ const Cadastro: React.FC = () => { setEmail(e.target.value)} + onIonChange={(e: any) => setEmail(e.target.value)} > @@ -170,7 +179,7 @@ const Cadastro: React.FC = () => { Data de nascimento setBirthDate(e.target.value)} + onIonChange={(e: any) => setBirthDate(e.target.value)} > @@ -180,7 +189,7 @@ const Cadastro: React.FC = () => { setPassword(e.target.value)} + onIonChange={(e: any) => setPassword(e.target.value)} > @@ -188,7 +197,7 @@ const Cadastro: React.FC = () => { setConfirmPassword(e.target.value)} + onIonChange={(e: any) => setConfirmPassword(e.target.value)} > @@ -201,9 +210,9 @@ const Cadastro: React.FC = () => { {/*
*/} + setShowToast(false)} message={messageToast} diff --git a/src/pages/CadastroCompletar.tsx b/src/pages/CadastroCompletar.tsx deleted file mode 100644 index f1b6654..0000000 --- a/src/pages/CadastroCompletar.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { -IonBackButton, -IonButton, -IonButtons, -IonCard, -IonCardContent, -IonContent, -IonHeader, -IonIcon, -IonItem, -IonLabel, -IonPage, -IonTitle, -IonToolbar -} from "@ionic/react"; -import React, { useEffect, useReducer, useState } from "react"; - -import './Perfil.css' -import { useHistory, useLocation } from "react-router"; -import { bookOutline, callOutline, documentTextOutline, homeOutline, logoWhatsapp } from "ionicons/icons"; - -import isEqual from 'lodash.isequal'; - -import * as usersRoutes from '../services/api/users'; - -import './Cadastro/Cadastro.css' - -interface userData { - name: string; - lastname: string; - email: string; - birth_date: string; - bio: string; -} - -interface LocationState { - userData: userData -} - -const items = [ - // TODO, CPF e CNH - { - icon: documentTextOutline, - label: 'Documentos', - description: 'Cadastre seus documentos para que seu perfil possa ser verificado.' - }, - // TODO, telefone e WhatsApp - { - icon: callOutline, - label: 'Informações de contato', - description: 'Cadastre seu número de telefone celular que para possam contatar você.' - }, - { - icon: homeOutline, - label: 'Endereço de residência', - description: 'Diga-nos seu endereço para que possa começar a solicitar vagas.' - }, - { - icon: bookOutline, - label: 'Instituição de ensino', - description: 'Diga-nos sua IES para que possa começar a solicitar vagas.' - }, -] - -const CadastroCompletar: React.FC = () => { - const history = useHistory(); - const location = useLocation(); - - const [showToast, setShowToast] = useState(false); - const [messageToast, setMessageToast] = useState(''); - - const [userData, setUserData] = useState({ - name: '', - lastname: '', - email: '', - birth_date: '', - bio: '', - }); - - const [inputValues, setInputValues] = useReducer( - (state: any, newState: any) => ({ ...state, ...newState }), - { - name: '', - lastname: '', - email: '', - birth_date: '', - bio: '', - } - ); - - useEffect(() => { - let userData = location.state.userData - - setUserData(location.state.userData) - setInputValues({ - 'name': userData.name, - 'lastname': userData.lastname, - 'email': userData.email, - 'birth_date': userData.birth_date, - 'bio': userData.bio - }); - }, [userData]); - - const handleUpdateUserData = () => { - usersRoutes.update(inputValues).then(response => { - if (response.status === 'error') { - setMessageToast(response.message); - setShowToast(true); - - return - } - - console.log(response) - }).catch((err) => { - setMessageToast(err); - setShowToast(true); - }) - } - - return ( - - - - Completar cadastro - - - - - - - - { items.map((item, index) => { - return ( - - - - {item.label} - - - {item.description} - - ) - })} - - - ); -}; - -export default CadastroCompletar; diff --git a/src/pages/CadastroCompletar/CadastroCompletar.tsx b/src/pages/CadastroCompletar/CadastroCompletar.tsx new file mode 100644 index 0000000..de2c677 --- /dev/null +++ b/src/pages/CadastroCompletar/CadastroCompletar.tsx @@ -0,0 +1,142 @@ +import { +IonBackButton, +IonButtons, +IonCard, +IonCardContent, +IonContent, +IonHeader, +IonIcon, +IonItem, +IonLabel, +IonPage, +IonTitle, +IonToast, +IonToolbar +} from "@ionic/react"; +import React, { useEffect, useReducer, useState } from "react"; + +import '../Perfil.css' +import { useHistory, useLocation } from "react-router"; +import { callOutline, documentTextOutline } from "ionicons/icons"; + +import '../Cadastro/Cadastro.css' +import { Color } from "@ionic/react/node_modules/@ionic/core"; + +interface cardItem { + icon: string; + label: string; + description: string; + url: string; + required: boolean; +} + +interface userData { + name: string; + lastname: string; + email: string; + phone_number: string; + birth_date: string; + bio: string; + document_type: string; + document: string; +} + +interface LocationState { + userData: userData; + + redirectData?: { + showToastMessage: boolean; + toastColor: Color; + toastMessage: string; + } +} + +let items: cardItem[] = [ + { + icon: documentTextOutline, + label: 'Documento', + description: 'Cadastre seu documento para que seu perfil possa ser verificado', + url: '/perfil/completar/documento', + required: false + }, + { + icon: callOutline, + label: 'Informações de contato', + description: 'Cadastre seu número de telefone celular que para possam contatar você', + url: '/perfil/completar/telefone', + required: false + } +] + +const CadastroCompletar: React.FC = () => { + const history = useHistory(); + const location = useLocation(); + + const [showToast, setShowToast] = useState(false); + const [toastMessage, setToastMessage] = useState(''); + const [toastColor, setToastColor] = useState("primary"); + + const handleCardClick = (item: cardItem) => { + if (!item.required) return + + history.push({ pathname: item.url, state: { userData: location.state.userData } }); + } + + useEffect(() => { + if (!location.state || !location.state.userData) { + history.push({ pathname: '/perfil' }) + } + + if (location.state && location.state.redirectData) { + const redirectData = location.state.redirectData + + if (redirectData.showToastMessage) { + setToastColor(redirectData.toastColor) + setToastMessage(redirectData.toastMessage) + setShowToast(true) + } + } + + if (!location.state.userData.document) items[0].required = true + if (!location.state.userData.phone_number) items[1].required = true + }, []); + + return ( + + + + Completar cadastro + + + + + + + + { items.map((item, index) => { + return ( + { handleCardClick(item) }}> + + + {item.label} + + + {item.description} + + ) + })} + + setShowToast(false)} + message={toastMessage} + duration={2500} + /> + + + ); +}; + +export default CadastroCompletar; diff --git a/src/pages/CadastroCompletar/CompletarDocumento.tsx b/src/pages/CadastroCompletar/CompletarDocumento.tsx new file mode 100644 index 0000000..5fc9a8c --- /dev/null +++ b/src/pages/CadastroCompletar/CompletarDocumento.tsx @@ -0,0 +1,219 @@ +import { + IonBackButton, + IonButtons, + IonContent, + IonFab, + IonFabButton, + IonHeader, + IonIcon, + IonList, + IonPage, + IonSelect, + IonSelectOption, + IonTitle, + IonToast, + IonToolbar +} from "@ionic/react"; +import React, { useEffect, useState } from "react"; +import { IonGrid, IonRow, IonCol } from "@ionic/react"; +import { useHistory, useLocation } from "react-router-dom"; +import { + IonItem, + IonLabel, + IonInput, +} from "@ionic/react"; + +import { saveOutline } from "ionicons/icons"; + +import * as usersRoutes from '../../services/api/users'; + +import validateCpf from '../../services/validateCpf' + +interface documentTypesInterface { + label: string; + name: string; +} + +interface userData { + name: string; + lastname: string; + email: string; + phone_number: string; + birth_date: string; + bio: string; + document_type: string; + document: string; +} + +interface LocationState { + userData: userData; +} + +const CompletarDocumento: React.FC = () => { + const location = useLocation(); + + const [hasChangedSinceInitialState, setHasChangedSinceInitialState] = useState(false); + + const [documentTypes, setDocumentTypes] = useState([]); + + const [document, setDocument] = useState(''); + const [documentType, setDocumentType] = useState(''); + + const [documentMaxLength, setDocumentMaxLength] = useState(0); + + const [showToast, setShowToast] = useState(false); + const [messageToast, setMessageToast] = useState(''); + + const history = useHistory(); + + const validateform = () => { + if (isNaN((Number)(document))) { + setMessageToast('Documento pode conter apenas números!') + setShowToast(true) + return false + } + + if (documentType === 'cpf' && !validateCpf(document)) { + setMessageToast('CPF inválido!') + setShowToast(true) + return false + } + + return true + } + + const handleUpdateUserDocuments = async () => { + if (!validateform()) { + return + } + + usersRoutes.update({ document_type: documentType, document: document }).then(response => { + if (response.status === 'error') { + setMessageToast(response.message); + setShowToast(true); + + return + } + + history.push({ pathname: '/perfil', state: { + redirectData: { + showToastMessage: true, + toastColor: "success", + toastMessage: response.message, + } + }}) + }).catch((err) => { + setMessageToast(err); + setShowToast(true); + }) + }; + + const handleChangeDocumentType = (document_type: string) => { + switch(document_type) { + case 'cpf': + setDocumentType('cpf') // workaround para o problema de setState para valores vindos de um evento sendo triggerado por um ion-select + setDocumentMaxLength(11) + break; + case 'cnh': + setDocumentType('cnh') // workaround para o problema de setState para valores vindos de um evento sendo triggerado por um ion-select + setDocumentMaxLength(11) + break; + case 'rg': + setDocumentType('rg') // workaround para o problema de setState para valores vindos de um evento sendo triggerado por um ion-select + setDocumentMaxLength(9) + break; + } + } + + useEffect(() => { + if (!location.state.userData) { + history.push({ pathname: '/perfil', state: { + redirectData: { + showToastMessage: true, + toastColor: "warning", + toastMessage: "Houve um erro. Por favor, tente novamente.", + }, userData: location.state.userData + }}) + } + + setDocumentTypes([ + { + name: 'cpf', + label: 'CPF', + }, + { + name: 'rg', + label: 'RG', + }, + { + name: 'cnh', + label: 'CNH', + }, + ]) + }, []) + + return ( + + + + Completar cadastro + + + + + + + + + + Completar cadastro + + + + + + + + + Tipo de documento + { handleChangeDocumentType(e.detail.value) } }> + { documentTypes ? documentTypes.map((document, index) => { + return ({document.label}) + }) : <> } + + + + Documento + { setDocument(e.target.value); setHasChangedSinceInitialState(true) }} + > + + + + + + + + + + + + + setShowToast(false)} + message={messageToast} + duration={2500} + /> + + + ); +}; + +export default CompletarDocumento; + \ No newline at end of file diff --git a/src/pages/CadastroCompletar/CompletarTelefone.tsx b/src/pages/CadastroCompletar/CompletarTelefone.tsx new file mode 100644 index 0000000..f739146 --- /dev/null +++ b/src/pages/CadastroCompletar/CompletarTelefone.tsx @@ -0,0 +1,169 @@ +import { + IonBackButton, + IonButtons, + IonContent, + IonFab, + IonFabButton, + IonHeader, + IonIcon, + IonList, + IonPage, + IonTitle, + IonToast, + IonToolbar +} from "@ionic/react"; +import React, { useState, useEffect } from "react"; +import { IonGrid, IonRow, IonCol } from "@ionic/react"; +import { useHistory, useLocation } from "react-router-dom"; +import { + IonItem, + IonLabel, + IonInput, +} from "@ionic/react"; + +import { saveOutline } from "ionicons/icons"; + +import * as usersRoutes from '../../services/api/users'; +import { Color } from "@ionic/react/node_modules/@ionic/core"; + +interface documentTypesInterface { + label: string; + name: string; +} + +interface userData { + name: string; + lastname: string; + email: string; + phone_number: string; + birth_date: string; + bio: string; + document_type: string; + document: string; +} + +interface LocationState { + userData: userData; +} + +const CompletarTelefone: React.FC = () => { + const location = useLocation(); + + const [hasChangedSinceInitialState, setHasChangedSinceInitialState] = useState(false); + + const [phone, setPhone] = useState(''); + + const [showToast, setShowToast] = useState(false); + const [messageToast, setMessageToast] = useState(''); + const [toastColor, setToastColor] = useState("primary"); + + const history = useHistory(); + + const validateform = () => { + if (isNaN((Number)(phone))) { + setMessageToast('O telefone pode conter apenas números!') + setShowToast(true) + return false + } + + return true + } + + const handleUpdateUserDocuments = async () => { + if (!validateform()) { + return + } + + usersRoutes.update({ phone_number: phone }).then(response => { + if (response.status === 'error') { + setToastColor("warning") + setMessageToast(response.message); + setShowToast(true); + + return + } + + const userDataObj = location.state.userData + + history.push({ pathname: '/perfil', state: { + redirectData: { + showToastMessage: true, + toastColor: "success", + toastMessage: response.message, + }, + }}) + }).catch((err) => { + setMessageToast(err); + setShowToast(true); + }) + }; + + useEffect(() => { + if (!location.state.userData) { + history.push({ pathname: '/perfil', state: { + redirectData: { + showToastMessage: true, + toastColor: "warning", + toastMessage: "Houve um erro. Por favor, tente novamente.", + } + }}) + } + }, []) + + return ( + + + + Completar cadastro + + + + + + + + + + Completar cadastro + + + + + + + + + Telefone + { setPhone(e.target.value); setHasChangedSinceInitialState(true) }} + > + + + + + + + + + + + + + setShowToast(false)} + message={messageToast} + duration={2500} + /> + + + ); +}; + +export default CompletarTelefone; + \ No newline at end of file diff --git a/src/pages/CadastroVan.tsx b/src/pages/CadastroVan.tsx index 7862f9d..7091ce7 100644 --- a/src/pages/CadastroVan.tsx +++ b/src/pages/CadastroVan.tsx @@ -19,18 +19,23 @@ import { } from "@ionic/react"; import React, { useEffect, useReducer, useState } from "react"; +import { useHistory } from "react-router-dom"; -import * as yup from 'yup'; - -import { ApiClient } from "../services/api-client.service"; +// import * as yup from 'yup'; import carsService from '../services/functions/carsService' +import * as vansRoutes from '../services/api/vans'; + import "./CadastroVan.css"; +import { Color } from "@ionic/react/node_modules/@ionic/core"; const CadastroVan: React.FC = () => { + const history = useHistory(); + const [showToast, setShowToast] = useState(false); const [toastMessage, setToastMessage] = useState(""); + const [toastColor, setToastColor] = useState("primary"); const [carModels, setCarModels] = useState([{ id_model: '', @@ -43,77 +48,34 @@ const CadastroVan: React.FC = () => { carPlate: '', carBrand: '', carModel: '', - maxPassengers: 1, - isRent: false, - carRentalName: '', - postalCode: '', - street: '', - number: '', - complement: '', - city: '', - state: '', + seats_number: 1, + isRented: false, + locator_name: '', + locator_address: '', + locator_complement: '', + locator_city: '', + locator_state: '', } ); - // TODO, yup - let schema = yup.object().shape({ - carPlate: yup.string().required(), - carBrand: yup.string().required(), - carModel: yup.string().required(), - maxPassengers: yup.number().integer().min(1).max(100).required(), - isRented: yup.boolean().required(), - carRentalName: yup.string(), // .required(), - postalCode: yup.string(), // .required(), - street: yup.string(), // .required(), - number: yup.number().integer(), // .required(), - complement: yup.string(), // .required(), - city: yup.string(), // .required(), - state: yup.string(), // .required(), - - // name: yup.string().required(), - // age: yup.number().required().positive().integer(), - // email: yup.string().email(), - // website: yup.string().url(), - // createdOn: yup.date().default(function () { - // return new Date(); - // }), - }); - - const vanForm = { - carPlate: inputValues.carPlate, - carBrand: inputValues.carBrand, - carModel: inputValues.carModel, - maxPassengers: inputValues.maxPassengers, - isRented: inputValues.isRented, - carRentalName: inputValues.carRentalName, - carRentalAddress: { - postalCode: inputValues.postalCode, - street: inputValues.street, - number: inputValues.number, - complement: inputValues.complement, - city: inputValues.city, - state: inputValues.state, - } - }; - const clearRentalData = () => { setInputValues({ - carPlate: '', - carBrand: '', - carModel: '', - maxPassengers: 1, - isRent: false, - carRentalName: '', - postalCode: '', - street: '', - number: '', - complement: '', - city: '', - state: '', + carRentalName: '', + complement: '', + city: '', + state: '', }) }; const validateForm = (): boolean => { + const vanForm = { + carPlate: inputValues.carPlate, + carBrand: inputValues.carBrand, + carModel: inputValues.carModel, + seats_number: inputValues.seats_number, + isRented: inputValues.isRented, + }; + if ( !vanForm.carPlate || vanForm.carPlate.length !== 7 || @@ -136,12 +98,18 @@ const CadastroVan: React.FC = () => { return false; } - if (!vanForm.maxPassengers || !parseInt(`${vanForm.maxPassengers}`)) { + if (!vanForm.seats_number || !parseInt(`${vanForm.seats_number}`)) { setToastMessage("Número de passageiros inválido"); setShowToast(true); return false; } + if ((Number)(vanForm.seats_number) < 1) { + setToastMessage("Número de passageiros deve ser positivo!"); + setShowToast(true); + return false; + } + if (vanForm.isRented) { return validateRentalForm(); } else { @@ -152,43 +120,32 @@ const CadastroVan: React.FC = () => { }; const validateRentalForm = (): boolean => { - if (!vanForm.carRentalName) { + const locatorForm = { + locator_name: inputValues.locator_name, + locator_address: inputValues.locator_address, + locator_complement: inputValues.locator_complement, + locator_city: inputValues.locator_city, + locator_state: inputValues.locator_state, + } + + if (!locatorForm.locator_name) { setToastMessage("Nome do Locador é obrigatório"); setShowToast(true); return false; } if ( - !vanForm.carRentalAddress.postalCode || - vanForm.carRentalAddress.postalCode.length !== 8 || - !vanForm.carRentalAddress.postalCode.match(/([0-9]){8}/g) + !locatorForm.locator_city || + !locatorForm.locator_city.match(/([A-zà-úÀ-Ú])/g) ) { - setToastMessage("Cep inválido"); + setToastMessage("Cidade inválida"); setShowToast(true); return false; } if ( - !vanForm.carRentalAddress.number || - !parseInt(`${vanForm.carRentalAddress.number}`) - ) { - setToastMessage("Número inválido"); - setShowToast(true); - return false; - } - - if ( - !vanForm.carRentalAddress.city || - !vanForm.carRentalAddress.city.match(/([A-zà-úÀ-Ú])/g) - ) { - setToastMessage("Cidade inválido"); - setShowToast(true); - return false; - } - - if ( - !vanForm.carRentalAddress.state || - !vanForm.carRentalAddress.state.match(/([A-zà-úÀ-Ú])/g) + !locatorForm.locator_state || + !locatorForm.locator_state.match(/([A-zà-úÀ-Ú])/g) ) { setToastMessage("Estado inválido"); setShowToast(true); @@ -199,9 +156,41 @@ const CadastroVan: React.FC = () => { }; const handleSubmit = async () => { - if (validateForm()) { - await ApiClient.doPost("/cadastro-van", vanForm); + if (!validateForm()) { + return } + + // cria registro da van + await vansRoutes.create({ + plate: inputValues.carPlate, + brand: inputValues.carBrand, + model: inputValues.carModel, + seats_number: inputValues.seats_number, + locator_name: inputValues.locator_name, + locator_address: inputValues.locator_address, + locator_complement: inputValues.locator_complement, + locator_city: inputValues.locator_city, + locator_state: inputValues.locator_state + }).then(response => { + if (response.status === 'error') { + setToastMessage(response.message); + setShowToast(true); + + return + } + + history.push({ pathname: '/perfil', state: { + redirectData: { + showToastMessage: true, + toastColor: "success", + toastMessage: response.message, + }, + }}) + }).catch((err) => { + setToastColor("danger") + setToastMessage(err); + setShowToast(true); + }) }; useEffect(() => { @@ -211,7 +200,9 @@ const CadastroVan: React.FC = () => { const carModelsRes = await carsService.getAllCarModels() if (carModelsRes.error) { - console.log('Houve um erro') + setToastColor("danger") + setToastMessage(carModelsRes.error.errorMessage); + setShowToast(true); return } @@ -248,24 +239,16 @@ const CadastroVan: React.FC = () => { setInputValues({ carPlate: e.target.value })} + onIonChange={(e: any) => setInputValues({ carPlate: e.target.value })} /> - {/* - Marca - setInputValues({ carBrand: e.target.value })} - /> - */} - + {/* TODO, problema de setState para valores vindos de um evento sendo triggerado por um ion-select */} Marca - + { setInputValues({ carBrand: e.detail.value }) }}> { carModels ? carModels.map((carModel, index) => { return ({carModel.name}) }) : <> } @@ -278,19 +261,20 @@ const CadastroVan: React.FC = () => { type='text' clearInput placeholder='Digite o Modelo do Veículo' - onIonInput={(e: any) => setInputValues({ carModel: e.target.value })} + onIonChange={(e: any) => setInputValues({ carModel: e.target.value })} /> - Número Máximo de Passageiros + Número de assentos setInputValues({ maxPassengers: e.target.value })} + placeholder='podem ser ocupados por passageiros' + onIonChange={(e: any) => setInputValues({ seats_number: e.target.value })} /> @@ -313,38 +297,32 @@ const CadastroVan: React.FC = () => { type='text' clearInput placeholder='Nome completo do Locador' - onIonInput={(e: any) => setInputValues({ carRentalName: e.target.value })} + onIonChange={(e: any) => setInputValues({ locator_name: e.target.value })} /> setInputValues({ postalCode: e.target.value })} - /> - setInputValues({ number: e.target.value })} + onIonChange={(e: any) => setInputValues({ locator_address: e.target.value })} /> setInputValues({ complement: e.target.value })} + onIonChange={(e: any) => setInputValues({ locator_complement: e.target.value })} /> setInputValues({ city: e.target.value })} + onIonChange={(e: any) => setInputValues({ locator_city: e.target.value })} /> setInputValues({ state: e.target.value })} + onIonChange={(e: any) => setInputValues({ locator_state: e.target.value })} />
@@ -362,12 +340,13 @@ const CadastroVan: React.FC = () => { setShowToast(false)} message={toastMessage} duration={2500} - /> + />
); diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 1134d4d..1306fd4 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,14 +1,74 @@ -import { IonItem, IonLabel, IonInput, IonButton, IonCardTitle, IonCol, IonContent, IonGrid, IonPage, IonRow } from '@ionic/react'; -import { useHistory } from 'react-router'; -import { Action } from '../components/Action'; +import { IonContent, IonPage, IonToast } from '@ionic/react'; +import { Color } from '@ionic/react/node_modules/@ionic/core'; +import { useContext, useEffect, useState } from 'react'; +import { useLocation } from 'react-router'; + +import { UserContext } from '../App'; + +import * as sessionRoutes from '../services/api/session'; + +interface LocationState { + redirectData?: { + showToastMessage: boolean; + toastColor: Color; + toastMessage: string; + } +} const Home: React.FC = () => { - const history = useHistory() + const location = useLocation(); + + const user = useContext(UserContext); + + const [showToast, setShowToast] = useState(false); + const [toastMessage, setToastMessage] = useState(''); + const [toastColor, setToastColor] = useState("primary"); + + useEffect(() => { + if (location.state && location.state.redirectData) { + const redirectData = location.state.redirectData + + if (redirectData.showToastMessage) { + setToastColor(redirectData.toastColor) + setToastMessage(redirectData.toastMessage) + setShowToast(true) + } + } + + const refreshUserToken = async () => { + await sessionRoutes.refresh().then(response => { + if (response.status === 'error') { + // setMessageToast(response.message); + // setShowToast(true); + + return + } + + user.setIsLoggedIn(true); + }).catch(error => { + // if (!error.response) return + + // se o backend retornou uma mensagem de erro customizada + // if (error.response.data.message) { + console.dir('Houve um erro: ', { error }) + alert('Houve um erro') + }) + } + + refreshUserToken() + }, []) return ( - { history.push({ pathname: '/usuario/56520ae7-faf8-4444-a82b-7f3990ab02d8' }); }}>Ir para o perfil de outra pessoa + setShowToast(false)} + message={toastMessage} + duration={2500} + /> ); diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index afa8665..5e02cec 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -89,7 +89,11 @@ const Page: React.FC = () => { user.setIsLoggedIn(true); - history.push({ pathname: '/home' }); + history.push({ pathname: '/home', state: { redirectData: { + showToastMessage: true, + toastColor: "success", + toastMessage: "Usuário autenticado com sucesso!", + }}}) }).catch(error => { // if (!error.response) return @@ -155,7 +159,7 @@ const Page: React.FC = () => { setShowToast(false)} diff --git a/src/pages/MinhasVans.tsx b/src/pages/MinhasVans.tsx new file mode 100644 index 0000000..904478c --- /dev/null +++ b/src/pages/MinhasVans.tsx @@ -0,0 +1,118 @@ +import { IonBackButton, IonButtons, IonCard, IonCardContent, IonCardHeader, IonCardSubtitle, IonCardTitle, IonContent, IonHeader, IonIcon, IonItem, IonLabel, IonPage, IonTitle, IonToast, IonToolbar } from '@ionic/react'; +import { Color } from '@ionic/react/node_modules/@ionic/core'; +import { carOutline } from 'ionicons/icons'; +import { useContext, useEffect, useState } from 'react'; +import { useHistory, useLocation } from 'react-router'; + +import { UserContext } from '../App'; + +import * as vansRoutes from '../services/api/vans'; + +import sessionsService from '../services/functions/sessionsService' + +interface VanInfo { + plate: string; + brand: string; + model: string; + seats_number: string; + document_status: boolean, + locator_name: string; + locator_address: string; + locator_complement: string; + locator_city: string; + locator_state: string; +} + +const MinhasVans: React.FC = () => { + const history = useHistory(); + + const [showToast, setShowToast] = useState(false); + const [toastMessage, setToastMessage] = useState(''); + const [toastColor, setToastColor] = useState("primary"); + + const [userVans, setUserVans] = useState(); + + const redirectUserToLogin = () => { + history.push({ pathname: '/login' }); + } + + useEffect(() => { + const getUserVans = async () => { + let userId = '' + + const refreshSessionRes = await sessionsService.refreshSession() + + if (refreshSessionRes.error) { + redirectUserToLogin() + return + } + + if (refreshSessionRes.userId) { + userId = refreshSessionRes.userId + } + + vansRoutes.getByUserId(userId).then(response => { + if (response.status === 'error') { + setToastColor("danger") + setToastMessage(response.message); + setShowToast(true); + + return + } + + setUserVans(response.data) + }).catch((err) => { + setToastColor("danger") + setToastMessage(err); + setShowToast(true); + }) + } + + getUserVans() + }, []) + + return ( + + + + Minhas vans + + + + + + + + { userVans ? userVans.map((van, index) => { + return ( + + + {van.plate} + {van.brand} - {van.model} + + { van.locator_name ? + <> + {van.seats_number} assentos - Locador: {van.locator_name} + : + <> + {van.seats_number} assentos - Não é alugado + + } + + ) + }) : <>} + + setShowToast(false)} + message={toastMessage} + duration={2500} + /> + + + ); +}; + +export default MinhasVans; diff --git a/src/pages/Perfil.tsx b/src/pages/Perfil.tsx index f01d39e..1a4cb0c 100644 --- a/src/pages/Perfil.tsx +++ b/src/pages/Perfil.tsx @@ -1,4 +1,7 @@ import { + IonBackButton, + IonBadge, + IonButtons, IonCard, IonCardContent, IonCardHeader, @@ -16,9 +19,9 @@ import { IonToast, IonToolbar, } from "@ionic/react"; -import { useHistory } from "react-router-dom"; +import { useHistory, useLocation } from "react-router-dom"; import React, { useState, useEffect, useReducer, useContext } from "react"; -import { cardOutline, carOutline, createOutline, exitOutline, shieldCheckmarkOutline, starOutline } from "ionicons/icons"; +import { callOutline, cardOutline, carOutline, createOutline, exitOutline, logoFacebook, logoWhatsapp, shieldCheckmarkOutline, starOutline } from "ionicons/icons"; import './Perfil.css' import LocalStorage from "../LocalStorage"; @@ -26,6 +29,7 @@ import LocalStorage from "../LocalStorage"; import sessionsService from '../services/functions/sessionsService' import usersService from '../services/functions/usersService' import { UserContext } from "../App"; +import { Color } from "@ionic/react/node_modules/@ionic/core"; interface ScanNewProps { match: { @@ -35,31 +39,47 @@ interface ScanNewProps { } } +interface LocationState { + redirectData?: { + showToastMessage: boolean; + toastColor: Color; + toastMessage: string; + } +} + const Perfil: React.FC = (props) => { const user = useContext(UserContext); + const history = useHistory(); + const location = useLocation(); + const [isVisitor, setIsVisitor] = useState(true) + const [incompleteProfile, setIncompleteProfile] = useState(false) + const [incompleteProfileCounter, setIncompleteProfileCounter] = useState(0) + const [showToast, setShowToast] = useState(false); - const [messageToast, setMessageToast] = useState(''); + const [toastMessage, setToastMessage] = useState(''); + const [toastColor, setToastColor] = useState("primary"); const [inputValues, setInputValues] = useReducer( (state: any, newState: any) => ({ ...state, ...newState }), { + id: '', name: '', lastname: '', email: '', + phone_number: '', birth_date: '', bio: '', + document_type: '', + document: '', } ); - const history = useHistory(); - const redirectUserToLogin = () => { - // TODO, não impede o usuário de retornar a página de login history.push({ pathname: '/login' }); - setMessageToast("Por favor, autentique-se!"); + setToastMessage("Por favor, autentique-se!"); setShowToast(true); } @@ -70,6 +90,16 @@ const Perfil: React.FC = (props) => { } useEffect(() => { + if (location.state && location.state.redirectData) { + const redirectData = location.state.redirectData + + if (redirectData.showToastMessage) { + setToastColor(redirectData.toastColor) + setToastMessage(redirectData.toastMessage) + setShowToast(true) + } + } + const loadUserData = async () => { let userId = '' @@ -94,11 +124,11 @@ const Perfil: React.FC = (props) => { if (getByIdRes.error) { if (isVisitor && props.match.params.id) { - setMessageToast('Usuário não existe!') + setToastMessage('Usuário não existe!') setShowToast(true) history.push({ pathname: '/home' }) } else { - setMessageToast(getByIdRes.error.errorMessage) + setToastMessage(getByIdRes.error.errorMessage) setShowToast(true) } @@ -110,16 +140,31 @@ const Perfil: React.FC = (props) => { if (isMounted) { setInputValues({ + 'id': userId, 'name': userData.name, 'lastname': userData.lastname, 'email': userData.email, + 'phone_number': userData.phone_number, 'birth_date': userData.birth_date, - 'bio': userData.bio + 'bio': userData.bio, + 'document_type': userData.document_type, + 'document': userData.document }); if (!props.match.params.id) { setIsVisitor(false) } + + if (!userData.document || !userData.phone_number) { + setIncompleteProfile(true) + + let counter = 0 + + if (!userData.document) counter++ + if (!userData.phone_number) counter++ + + setIncompleteProfileCounter(counter) + } } } } @@ -142,6 +187,9 @@ const Perfil: React.FC = (props) => { Seu perfil + + + @@ -154,8 +202,8 @@ const Perfil: React.FC = (props) => { - {/* avatar */} - avatar + avatar + {/* avatar */} {inputValues.name} {inputValues.lastname} @@ -174,11 +222,31 @@ const Perfil: React.FC = (props) => { Biografia - {inputValues.bio ? inputValues.bio : 'Sem biografia.' } + {inputValues.bio ? inputValues.bio : 'Sem biografia.' } - {/* // TODO, card de informações de contato */} + + + Informações de contato + + + { !inputValues.phone_number ? + <>Sem informações de contato. + : <> + { + inputValues.phone_number ? + <> + + + {inputValues.phone_number} + + : <> + } + + } + + { !isVisitor ? @@ -187,14 +255,25 @@ const Perfil: React.FC = (props) => { Editar perfil - history.push({ pathname: '/perfil/completar', state: { userData: inputValues } })}> - - Completar cadastro - + + { incompleteProfile ? + <> + history.push({ pathname: '/perfil/completar', state: { userData: inputValues } })}> + + Completar cadastro + {incompleteProfileCounter} + + + : <> } + history.push({ pathname: '/cadastro-van'})}> Cadastrar Van + history.push({ pathname: '/minhas-vans'})}> + + Minhas Vans + Pagamentos @@ -211,10 +290,11 @@ const Perfil: React.FC = (props) => { } setShowToast(false)} - message={messageToast} + message={toastMessage} duration={2500} /> diff --git a/src/pages/PerfilEditar.tsx b/src/pages/PerfilEditar.tsx index e7c8d1f..d3fa677 100644 --- a/src/pages/PerfilEditar.tsx +++ b/src/pages/PerfilEditar.tsx @@ -28,6 +28,7 @@ import isEqual from 'lodash.isequal'; import * as usersRoutes from '../services/api/users'; import './Cadastro/Cadastro.css' +import { Color } from "@ionic/react/node_modules/@ionic/core"; interface userData { name: string; @@ -47,6 +48,7 @@ const PerfilEditar: React.FC = () => { const [showToast, setShowToast] = useState(false); const [messageToast, setMessageToast] = useState(''); + const [toastColor, setToastColor] = useState("primary"); const [userData, setUserData] = useState({ name: '', @@ -68,6 +70,10 @@ const PerfilEditar: React.FC = () => { ); useEffect(() => { + if (!location.state) { + history.push({ pathname: '/perfil' }) + } + let userData = location.state.userData setUserData(location.state.userData) @@ -83,14 +89,22 @@ const PerfilEditar: React.FC = () => { const handleUpdateUserData = () => { usersRoutes.update(inputValues).then(response => { if (response.status === 'error') { + setToastColor("danger") setMessageToast(response.message); setShowToast(true); return } - console.log(response) + history.push({ pathname: '/perfil', state: { + redirectData: { + showToastMessage: true, + toastColor: "success", + toastMessage: response.message, + }, + }}) }).catch((err) => { + setToastColor("danger") setMessageToast(err); setShowToast(true); }) @@ -172,7 +186,7 @@ const PerfilEditar: React.FC = () => { setShowToast(false)} message={messageToast} diff --git a/src/services/api/users.ts b/src/services/api/users.ts index 424190a..d2d16e4 100644 --- a/src/services/api/users.ts +++ b/src/services/api/users.ts @@ -37,9 +37,12 @@ export interface CadastroRequest { } export interface UpdateUserRequest { - name: string; - email: string; - bio: string; + name?: string; + email?: string; + bio?: string; + document_type?: string; + document?: string; + phone_number?: string; } // export async function get(cpf) { @@ -68,4 +71,12 @@ export async function update(userData: UpdateUserRequest) { const response = await instance.patch(userRoutes.update.url, userData, { headers: header }); return response.data; +} + +// TODO, continuar +export async function getSocialInfo(userId: string) { + updateHeader(); + + const response = await instance.get(userRoutes.getSocialInfo.url + `/${userId}`, { headers: header }); + return response.data; } \ No newline at end of file diff --git a/src/services/api/vans.ts b/src/services/api/vans.ts new file mode 100644 index 0000000..f4b5fd3 --- /dev/null +++ b/src/services/api/vans.ts @@ -0,0 +1,73 @@ +import instance from "./api"; + +import vansRoutes from "../../constants/routes/vansRoutes"; +import { AxiosRequestHeaders } from "axios"; +import LocalStorage from "../../LocalStorage"; + +let token: string; +let header: AxiosRequestHeaders; + +function updateHeader() { + token = LocalStorage.getToken(); + + header = { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }; +} + +export async function getByPlate(vanId: string) { + updateHeader(); + + const response = await instance.get(vansRoutes.getByPlate.url + `/${vanId}`, { + headers: header, + }); + + return response.data; +} + +export async function getByUserId(userId: string) { + updateHeader(); + + const response = await instance.get(vansRoutes.getByUserId.url + `/${userId}`, { + headers: header, + }); + + return response.data; +} + +interface CreateVanBody { + plate: string; + brand: string; + model: string; + seats_number: string; + locator_name: string; + locator_address: string; + locator_complement: string; + locator_city: string; + locator_state: string; +} + +export async function create(CreateVanBody: CreateVanBody) { + updateHeader(); + + const response = await instance.post(vansRoutes.create.url, CreateVanBody, { headers: header }); + return response.data; +} + +interface UpdateVanBody { + brand?: string; + model?: string; + seats_number?: string; +} + +export async function update(vanData: UpdateVanBody) { + updateHeader(); + + const response = await instance.patch(vansRoutes.update.url, vanData, { + headers: header, + }); + + return response.data; +} diff --git a/src/services/functions/usersService.ts b/src/services/functions/usersService.ts index 4bb9a56..34b25be 100644 --- a/src/services/functions/usersService.ts +++ b/src/services/functions/usersService.ts @@ -5,8 +5,11 @@ interface getByIdReturn { name: string; lastname: string; email: string; + phone_number: string; birth_date: string; bio: string; + document_type: string; + document: string; }, error?: { errorMessage: string; @@ -21,8 +24,11 @@ interface getByIdRes { name: string; lastname: string; email: string; + phone_number: string; birth_date: string; bio: string; + document_type: string; + document: string; }, } @@ -50,4 +56,40 @@ const getById = async (userId: string): Promise => { } }; -export default { getById } \ No newline at end of file +interface getByIdReturn { + data?: { + phone: '', + whatsapp: '', + facebook: '', + telegram: '', + }, + error?: { + errorMessage: string; + } +} + +const getUserSocialInfo = async (userId: string): Promise => { + try { + let res: getByIdRes = await usersRoutes.getSocialInfo(userId) + + if (res.status === "error") { + return { + error: { + errorMessage: res.message, + } + }; + } + + return { + userData: res.data, + }; + } catch(err) { + return { + error: { + errorMessage: "Por favor, autentique-se.", + } + }; + } +}; + +export default { getById, getUserSocialInfo } \ No newline at end of file diff --git a/src/services/validateCpf.ts b/src/services/validateCpf.ts new file mode 100644 index 0000000..d713b68 --- /dev/null +++ b/src/services/validateCpf.ts @@ -0,0 +1,25 @@ +export default function validateCpf(cpf: string): Boolean { + let soma = 0, resto; + + if (cpf === "00000000000") return false; + + for (let i = 1; i <= 9; i++) + soma = soma + parseInt(cpf.substring(i - 1, i)) * (11 - i); + + resto = (soma * 10) % 11; + + if (resto === 10 || resto === 11) resto = 0; + if (resto !== parseInt(cpf.substring(9, 10))) return false; + + soma = 0; + for (let i = 1; i <= 10; i++) + soma = soma + parseInt(cpf.substring(i - 1, i)) * (12 - i); + + resto = (soma * 10) % 11; + + if (resto === 10 || resto === 11) resto = 0; + + if (resto !== parseInt(cpf.substring(10, 11))) return false; + + return true; +}; \ No newline at end of file