From a4912d27292aec9e0d4c759774b1840a50f93ec7 Mon Sep 17 00:00:00 2001 From: Uinicivs Date: Sun, 22 Jun 2025 18:14:26 -0300 Subject: [PATCH 1/4] feat(mobile): added address to sign in --- src/strada/mobile/.env | 10 +- .../src/interfaces/credentials.interface.ts | 26 +- .../mobile/src/screens/login/LoginView.tsx | 291 ++++++++++++++---- 3 files changed, 250 insertions(+), 77 deletions(-) diff --git a/src/strada/mobile/.env b/src/strada/mobile/.env index 2943acf..ec747a8 100644 --- a/src/strada/mobile/.env +++ b/src/strada/mobile/.env @@ -1,15 +1,11 @@ EXPO_PUBLIC_BASE_DOMAIN=strada.appbr.store EXPO_PUBLIC_USE_HTTPS=true -EXPO_PUBLIC_GOOGLE_CLIENT_ID_ANDROID= -EXPO_PUBLIC_GOOGLE_CLIENT_ID_ANDROID= -EXPO_PUBLIC_GOOGLE_CLIENT_ID_ANDROID= +EXPO_PUBLIC_GOOGLE_CLIENT_ID_ANDROID=asd EXPO_PUBLIC_GOOGLE_CALLBACK_URL=http://localhost:3000/auth/oauth2/callback/google -EXPO_PUBLIC_GITHUB_OAUTH_URL=https://github.com/login/oauth/authorize?client_id=Ov23liUYlBuWhFrqDKRP&redirect_uri=http://localhost:3000/auth/oauth2/callback/github&scope=user -fb6979 -00284c +EXPO_PUBLIC_GITHUB_OAUTH_URL=https://github.com/login/oauth/authorize?client_id=Ov23liUYlBuWhFrqDKRP&redirect_uri=http://localhost:3000/auth/oauth2/callback/github&scope=userfb697900284c # Para produção -# EXPO_PUBLIC_USE_HTTPS=true +EXPO_PUBLIC_USE_HTTPS=true # Para desenvolvimento # EXPO_PUBLIC_BASE_DOMAIN=localhost:3000 diff --git a/src/strada/mobile/src/interfaces/credentials.interface.ts b/src/strada/mobile/src/interfaces/credentials.interface.ts index 76a46a2..6ac24d3 100644 --- a/src/strada/mobile/src/interfaces/credentials.interface.ts +++ b/src/strada/mobile/src/interfaces/credentials.interface.ts @@ -1,4 +1,24 @@ +// Define uma nova interface para a estrutura do endereço. +// É uma boa prática exportá-la caso precise ser usada em outro lugar. +export interface IAddress { + cep: string; + street: string; + number: string; + neighborhood: string; + city: string; + state: string; +} + +// Modifica a interface principal de credenciais. export interface ICreadentials { - username: string; - password: string; -} \ No newline at end of file + username: string; // Usado para o email + password?: string; // Senha pode ser opcional durante as etapas iniciais + + // Adiciona o objeto de endereço. + // É opcional (?) porque não existe nas primeiras etapas do cadastro. + address?: IAddress; + + // Você também pode adicionar outros campos do passo 1 aqui, se necessário: + // name?: string; + // loginUsername?: string; +} diff --git a/src/strada/mobile/src/screens/login/LoginView.tsx b/src/strada/mobile/src/screens/login/LoginView.tsx index b4dfd98..703ced6 100644 --- a/src/strada/mobile/src/screens/login/LoginView.tsx +++ b/src/strada/mobile/src/screens/login/LoginView.tsx @@ -16,26 +16,32 @@ import { fonts } from "@/src/constants/fonts"; import { AppImages } from "@/src/assets"; import Icon from "react-native-vector-icons/MaterialIcons"; import { useRouter } from "expo-router"; -import { ICreadentials } from "@/src/interfaces/credentials.interface"; +// --- MODIFICAÇÃO: A interface de credenciais agora inclui um objeto de endereço +import { ICreadentials, IAddress } from "@/src/interfaces/credentials.interface"; import { getAccessToken } from "@/src/services/auth.service"; interface Props { animationController: React.RefObject; } +// --- MODIFICAÇÃO: Adicionado o novo passo 'register-step-3' type ActionType = | "login" | "register-step-1" | "register-step-2" + | "register-step-3" // Novo passo | "password-recovery"; +// --- MODIFICAÇÃO: O InputField agora aceita 'value' e 'editable' para campos controlados const InputField: React.FC<{ iconName: string; iconLibrary: "Ionicons" | "SimpleLineIcons"; placeholder: string; secureTextEntry?: boolean; - keyboardType?: "default" | "email-address"; + keyboardType?: "default" | "email-address" | "numeric"; theme: Theme; + value?: string; // Adicionado + editable?: boolean; // Adicionado onChangeText?: (text: string) => void; styles: ReturnType; toggleSecure?: () => void; @@ -47,36 +53,49 @@ const InputField: React.FC<{ keyboardType, theme, styles, + value, + editable, toggleSecure, onChangeText, }) => ( - - {iconLibrary === "Ionicons" ? ( - - ) : ( - - )} - - {toggleSecure && ( - - - - )} - - ); + + {iconLibrary === "Ionicons" ? ( + + ) : ( + + )} + + {toggleSecure && ( + + + + )} + +); const LoginScreen: React.FC = ({ animationController }) => { const [actionType, setActionType] = useState("login"); const [credentials, setCredentials] = useState({ username: "", password: "", + // --- MODIFICAÇÃO: Inicializa o estado do endereço + address: { + cep: "", + street: "", + number: "", + neighborhood: "", + city: "", + state: "", + }, }); const [secureEntry, setSecureEntry] = useState(true); const [slideAnim] = useState(new Animated.Value(0)); @@ -85,14 +104,45 @@ const LoginScreen: React.FC = ({ animationController }) => { const window = useWindowDimensions(); const router = useRouter(); - const slideContainerAnim = animationController?.current?.interpolate({ - inputRange: [0, 0.8, 1], - outputRange: [window.width, window.width, 0], - }) ?? new Animated.Value(0); - const titleTextAnim = animationController?.current?.interpolate({ - inputRange: [0, 0.6, 0.8, 1], - outputRange: [26 * 10, 26 * 10, 26 * 10, 0], - }); + const slideContainerAnim = + animationController?.current?.interpolate({ + inputRange: [0, 0.8, 1], + outputRange: [window.width, window.width, 0], + }) ?? new Animated.Value(0); + const titleTextAnim = + animationController?.current?.interpolate({ + inputRange: [0, 0.6, 0.8, 1], + outputRange: [26 * 10, 26 * 10, 26 * 10, 0], + }); + + // --- NOVO: Hook para buscar o CEP quando ele for alterado e tiver 8 dígitos + useEffect(() => { + const fetchAddress = async () => { + if (credentials.address?.cep?.replace(/\D/g, "").length === 8) { + try { + const response = await fetch( + `https://viacep.com.br/ws/${credentials.address.cep}/json/` + ); + const data = await response.json(); + if (!data.erro) { + setCredentials((prev) => ({ + ...prev, + address: { + ...prev.address!, + street: data.logradouro, + neighborhood: data.bairro, + city: data.localidade, + state: data.uf, + }, + })); + } + } catch (error) { + console.error("Erro ao buscar CEP:", error); + } + } + }; + fetchAddress(); + }, [credentials.address?.cep]); const animateSlideTransition = useCallback( (newActionType: ActionType, direction: "left" | "right") => { @@ -120,18 +170,24 @@ const LoginScreen: React.FC = ({ animationController }) => { ); const handleTextChange = useCallback( - (key: keyof ICreadentials, value: string) => { - console.log(key, value); + (key: keyof Omit, value: string) => { setCredentials((prev) => ({ ...prev, [key]: value })); - console.log(credentials); }, [setCredentials] ); - useEffect(() => { - console.log("Updated credentials:", credentials); - }, [credentials]); + // --- NOVO: Handler para atualizar os campos de endereço + const handleAddressChange = useCallback( + (key: keyof IAddress, value: string) => { + setCredentials((prev) => ({ + ...prev, + address: { ...prev.address!, [key]: value }, + })); + }, + [setCredentials] + ); + // --- MODIFICAÇÃO: Atualizada a lógica de navegação para incluir o step 3 const handleSignup = useCallback(() => { if (actionType === "login") { return animateSlideTransition("register-step-1", "left"); @@ -140,9 +196,14 @@ const LoginScreen: React.FC = ({ animationController }) => { return animateSlideTransition("register-step-2", "left"); } if (actionType === "register-step-2") { + return animateSlideTransition("register-step-3", "left"); // Vai para o passo 3 + } + if (actionType === "register-step-3") { + // Finaliza o cadastro + console.log("Dados finais do cadastro:", credentials); return router.push("/home"); } - }, [actionType, animateSlideTransition]); + }, [actionType, animateSlideTransition, credentials, router]); const handleLogin = useCallback( (type: "local" | "github" | "google") => { @@ -163,6 +224,7 @@ const LoginScreen: React.FC = ({ animationController }) => { [credentials, animateSlideTransition] ); + // --- MODIFICAÇÃO: Atualizada a lógica de "voltar" const handleGoBack = useCallback(() => { if ( actionType === "register-step-1" || @@ -171,10 +233,13 @@ const LoginScreen: React.FC = ({ animationController }) => { animateSlideTransition("login", "right"); } else if (actionType === "register-step-2") { animateSlideTransition("register-step-1", "right"); + } else if (actionType === "register-step-3") { + animateSlideTransition("register-step-2", "right"); // Volta do passo 3 para o 2 } else { router.back(); } }, [actionType, router, animateSlideTransition]); + const renderForm = () => ( = ({ animationController }) => { )} - {(actionType === "register-step-2" || actionType === "login") && ( + {actionType === "register-step-2" && ( <> = ({ animationController }) => { onChangeText={(text) => handleTextChange("password", text)} toggleSecure={() => setSecureEntry((prev) => !prev)} /> - {actionType === "login" && ( - <> - - Forgot Password? - - handleLogin("local")} - > - Login - - - )} - {actionType === "register-step-2" && ( - <> - - Voltar - - - Sign Up - - - )} + + Voltar + + + Avançar + + + )} + {/* --- NOVO: Formulário para a etapa 3 (Endereço) --- */} + {actionType === "register-step-3" && ( + <> + handleAddressChange("cep", text)} + /> + handleAddressChange("street", text)} + editable={false} // Desabilita edição + /> + handleAddressChange("neighborhood", text)} + editable={false} // Desabilita edição + /> + handleAddressChange("city", text)} + editable={false} // Desabilita edição + /> + handleAddressChange("state", text)} + editable={false} // Desabilita edição + /> + handleAddressChange("number", text)} + /> + + Voltar + + + Sign Up + + + )} + + {actionType === "login" && ( + <> + handleTextChange("username", text)} + styles={styles} + /> + handleTextChange("password", text)} + toggleSecure={() => setSecureEntry((prev) => !prev)} + /> + + Forgot Password? + + handleLogin("local")} + > + Login + )} @@ -315,7 +470,7 @@ const LoginScreen: React.FC = ({ animationController }) => { Não tem uma conta? Sign up ) : ( - handleLogin("local")}> + animateSlideTransition("login", "right")}> Ja tem uma conta? Login )} @@ -327,6 +482,7 @@ const LoginScreen: React.FC = ({ animationController }) => { export default LoginScreen; +// Os estilos permanecem os mesmos const createStyles = (theme: Theme) => StyleSheet.create({ container: { @@ -371,6 +527,7 @@ const createStyles = (theme: Theme) => flex: 1, paddingHorizontal: 10, fontFamily: fonts.Light, + color: theme.blue, // Adicionado para melhor visualização do texto }, forgotPasswordText: { textAlign: "right", @@ -431,4 +588,4 @@ const createStyles = (theme: Theme) => color: theme.blue, fontFamily: fonts.Regular, }, - }); + }); \ No newline at end of file From 4e1d2338fb17f1e99ccb3bb7816afe5a232c6086 Mon Sep 17 00:00:00 2001 From: Uinicivs Date: Wed, 25 Jun 2025 23:27:04 -0300 Subject: [PATCH 2/4] fix(mobile): create user fixed --- src/strada/mobile/.env | 6 +- .../mobile/src/constants/coordinates.ts | 4 + .../src/interfaces/credentials.interface.ts | 32 +-- .../src/interfaces/user-request.interface.ts | 16 +- .../mobile/src/screens/login/LoginView.tsx | 190 ++++++++++-------- .../screens/offer-ride/OfferRideScreen.tsx | 62 ++---- .../src/services/helpers/interceptors.ts | 3 +- .../mobile/src/services/user.service.ts | 12 +- 8 files changed, 160 insertions(+), 165 deletions(-) create mode 100644 src/strada/mobile/src/constants/coordinates.ts diff --git a/src/strada/mobile/.env b/src/strada/mobile/.env index ec747a8..4bf23ec 100644 --- a/src/strada/mobile/.env +++ b/src/strada/mobile/.env @@ -5,10 +5,10 @@ EXPO_PUBLIC_GOOGLE_CALLBACK_URL=http://localhost:3000/auth/oauth2/callback/googl EXPO_PUBLIC_GITHUB_OAUTH_URL=https://github.com/login/oauth/authorize?client_id=Ov23liUYlBuWhFrqDKRP&redirect_uri=http://localhost:3000/auth/oauth2/callback/github&scope=userfb697900284c # Para produção -EXPO_PUBLIC_USE_HTTPS=true +# EXPO_PUBLIC_USE_HTTPS=true # Para desenvolvimento -# EXPO_PUBLIC_BASE_DOMAIN=localhost:3000 -# EXPO_PUBLIC_USE_HTTPS=false +EXPO_PUBLIC_BASE_DOMAIN=10.0.2.2:3000 +EXPO_PUBLIC_USE_HTTPS=false EXPO_PUBLIC_GOOGLE_MAPS_API_KEY=AIzaSyCYOruhSUWQavp9_pnB42oGOyIlJW0VHm4 \ No newline at end of file diff --git a/src/strada/mobile/src/constants/coordinates.ts b/src/strada/mobile/src/constants/coordinates.ts new file mode 100644 index 0000000..df8fb3f --- /dev/null +++ b/src/strada/mobile/src/constants/coordinates.ts @@ -0,0 +1,4 @@ +export const educareCoordinates = { + latitude: -19.96778, + longitude: -44.19833, +}; diff --git a/src/strada/mobile/src/interfaces/credentials.interface.ts b/src/strada/mobile/src/interfaces/credentials.interface.ts index 6ac24d3..54e4d0e 100644 --- a/src/strada/mobile/src/interfaces/credentials.interface.ts +++ b/src/strada/mobile/src/interfaces/credentials.interface.ts @@ -1,24 +1,12 @@ -// Define uma nova interface para a estrutura do endereço. -// É uma boa prática exportá-la caso precise ser usada em outro lugar. -export interface IAddress { - cep: string; - street: string; - number: string; - neighborhood: string; - city: string; - state: string; -} - -// Modifica a interface principal de credenciais. export interface ICreadentials { - username: string; // Usado para o email - password?: string; // Senha pode ser opcional durante as etapas iniciais - - // Adiciona o objeto de endereço. - // É opcional (?) porque não existe nas primeiras etapas do cadastro. - address?: IAddress; - - // Você também pode adicionar outros campos do passo 1 aqui, se necessário: - // name?: string; - // loginUsername?: string; + name?: string; // Campo do Passo 1 + username?: string; // Campo do Passo 1 (nome de usuário) + email?: string; // Campo do Passo 2 + password?: string; // Campo do Passo 2 + cep?: string; + street?: string; // Corresponderá à rua + number?: string; // O número da residência + neighborhood?: string; // O bairro + city?: string; + state?: string; } diff --git a/src/strada/mobile/src/interfaces/user-request.interface.ts b/src/strada/mobile/src/interfaces/user-request.interface.ts index 6416551..70f8199 100644 --- a/src/strada/mobile/src/interfaces/user-request.interface.ts +++ b/src/strada/mobile/src/interfaces/user-request.interface.ts @@ -1,7 +1,11 @@ export interface IUserRequest { - name: string; - email: string; - imgUrl: string; - username: string; - password: string; -} \ No newline at end of file + name?: string; + email?: string; + username?: string; + password?: string; + imgUrl?: string; + address?: string; + cep?: string; + city?: string; + state?: string; +} diff --git a/src/strada/mobile/src/screens/login/LoginView.tsx b/src/strada/mobile/src/screens/login/LoginView.tsx index 703ced6..6b36499 100644 --- a/src/strada/mobile/src/screens/login/LoginView.tsx +++ b/src/strada/mobile/src/screens/login/LoginView.tsx @@ -7,6 +7,8 @@ import { TouchableOpacity, useWindowDimensions, View, + Alert, + ActivityIndicator, } from "react-native"; import React, { useState, useCallback, useEffect } from "react"; import Ionicons from "react-native-vector-icons/Ionicons"; @@ -16,23 +18,22 @@ import { fonts } from "@/src/constants/fonts"; import { AppImages } from "@/src/assets"; import Icon from "react-native-vector-icons/MaterialIcons"; import { useRouter } from "expo-router"; -// --- MODIFICAÇÃO: A interface de credenciais agora inclui um objeto de endereço -import { ICreadentials, IAddress } from "@/src/interfaces/credentials.interface"; +import { ICreadentials } from "@/src/interfaces/credentials.interface"; import { getAccessToken } from "@/src/services/auth.service"; +import { IUserRequest } from "@/src/interfaces/user-request.interface"; +import { createUser } from "@/src/services/user.service"; interface Props { animationController: React.RefObject; } -// --- MODIFICAÇÃO: Adicionado o novo passo 'register-step-3' type ActionType = | "login" | "register-step-1" | "register-step-2" - | "register-step-3" // Novo passo + | "register-step-3" | "password-recovery"; -// --- MODIFICAÇÃO: O InputField agora aceita 'value' e 'editable' para campos controlados const InputField: React.FC<{ iconName: string; iconLibrary: "Ionicons" | "SimpleLineIcons"; @@ -40,8 +41,8 @@ const InputField: React.FC<{ secureTextEntry?: boolean; keyboardType?: "default" | "email-address" | "numeric"; theme: Theme; - value?: string; // Adicionado - editable?: boolean; // Adicionado + value?: string; + editable?: boolean; onChangeText?: (text: string) => void; styles: ReturnType; toggleSecure?: () => void; @@ -70,8 +71,8 @@ const InputField: React.FC<{ placeholderTextColor={theme.blue} secureTextEntry={secureTextEntry} keyboardType={keyboardType ?? "default"} - value={value} // Adicionado - editable={editable} // Adicionado + value={value} + editable={editable} onChangeText={onChangeText} /> {toggleSecure && ( @@ -85,18 +86,19 @@ const InputField: React.FC<{ const LoginScreen: React.FC = ({ animationController }) => { const [actionType, setActionType] = useState("login"); const [credentials, setCredentials] = useState({ + name: "", username: "", + email: "", password: "", - // --- MODIFICAÇÃO: Inicializa o estado do endereço - address: { - cep: "", - street: "", - number: "", - neighborhood: "", - city: "", - state: "", - }, + cep: "", + street: "", + number: "", + neighborhood: "", + city: "", + state: "", }); + + const [isCreatingUser, setIsCreatingUser] = useState(false); const [secureEntry, setSecureEntry] = useState(true); const [slideAnim] = useState(new Animated.Value(0)); const theme = lightTheme; @@ -109,31 +111,31 @@ const LoginScreen: React.FC = ({ animationController }) => { inputRange: [0, 0.8, 1], outputRange: [window.width, window.width, 0], }) ?? new Animated.Value(0); + const titleTextAnim = animationController?.current?.interpolate({ inputRange: [0, 0.6, 0.8, 1], outputRange: [26 * 10, 26 * 10, 26 * 10, 0], - }); + }) ?? new Animated.Value(0); - // --- NOVO: Hook para buscar o CEP quando ele for alterado e tiver 8 dígitos useEffect(() => { const fetchAddress = async () => { - if (credentials.address?.cep?.replace(/\D/g, "").length === 8) { + const cep = credentials.cep?.replace(/\D/g, ""); + if (cep?.length === 8) { try { - const response = await fetch( - `https://viacep.com.br/ws/${credentials.address.cep}/json/` - ); + const response = await fetch(`https://viacep.com.br/ws/${cep}/json/`); + if (!response.ok) { + console.error(`Erro na API do ViaCEP. Status: ${response.status}.`); + return; + } const data = await response.json(); if (!data.erro) { setCredentials((prev) => ({ ...prev, - address: { - ...prev.address!, - street: data.logradouro, - neighborhood: data.bairro, - city: data.localidade, - state: data.uf, - }, + street: data.logradouro, + neighborhood: data.bairro, + city: data.localidade, + state: data.uf, })); } } catch (error) { @@ -142,7 +144,7 @@ const LoginScreen: React.FC = ({ animationController }) => { } }; fetchAddress(); - }, [credentials.address?.cep]); + }, [credentials.cep]); const animateSlideTransition = useCallback( (newActionType: ActionType, direction: "left" | "right") => { @@ -170,25 +172,13 @@ const LoginScreen: React.FC = ({ animationController }) => { ); const handleTextChange = useCallback( - (key: keyof Omit, value: string) => { + (key: keyof ICreadentials, value: string) => { setCredentials((prev) => ({ ...prev, [key]: value })); }, - [setCredentials] + [] ); - // --- NOVO: Handler para atualizar os campos de endereço - const handleAddressChange = useCallback( - (key: keyof IAddress, value: string) => { - setCredentials((prev) => ({ - ...prev, - address: { ...prev.address!, [key]: value }, - })); - }, - [setCredentials] - ); - - // --- MODIFICAÇÃO: Atualizada a lógica de navegação para incluir o step 3 - const handleSignup = useCallback(() => { + const handleSignup = useCallback(async () => { if (actionType === "login") { return animateSlideTransition("register-step-1", "left"); } @@ -196,35 +186,57 @@ const LoginScreen: React.FC = ({ animationController }) => { return animateSlideTransition("register-step-2", "left"); } if (actionType === "register-step-2") { - return animateSlideTransition("register-step-3", "left"); // Vai para o passo 3 + return animateSlideTransition("register-step-3", "left"); } if (actionType === "register-step-3") { - // Finaliza o cadastro - console.log("Dados finais do cadastro:", credentials); - return router.push("/home"); + setIsCreatingUser(true); + + const userData: IUserRequest = { + name: credentials.name, + email: credentials.email, + username: credentials.username, + password: credentials.password, + imgUrl: "", + address: `${credentials.street}, ${credentials.number}`, + cep: credentials.cep, + city: credentials.city, + state: credentials.state, + }; + + try { + await createUser(userData); + Alert.alert("Sucesso!", "Seu cadastro foi realizado com sucesso.", [ + { text: "OK", onPress: () => animateSlideTransition("login", "right") } + ]); + } catch (error) { + Alert.alert("Erro no Cadastro", "Não foi possível criar seu usuário. Por favor, tente novamente."); + } finally { + setIsCreatingUser(false); + } } - }, [actionType, animateSlideTransition, credentials, router]); + }, [actionType, animateSlideTransition, credentials]); const handleLogin = useCallback( (type: "local" | "github" | "google") => { if (type === "local") { - getAccessToken(credentials) + const loginCredentials = { username: credentials.email!, password: credentials.password! }; + getAccessToken(loginCredentials) .then((tokens) => { console.log("Login com sucesso", tokens); router.replace("/home"); }) .catch((error) => { console.error("Erro no login:", error); + Alert.alert("Erro de Login", "Email ou senha inválidos."); }); return; } animateSlideTransition("login", "left"); router.push(`/social-auth/${type}`); }, - [credentials, animateSlideTransition] + [credentials.email, credentials.password, animateSlideTransition, router] ); - // --- MODIFICAÇÃO: Atualizada a lógica de "voltar" const handleGoBack = useCallback(() => { if ( actionType === "register-step-1" || @@ -234,7 +246,7 @@ const LoginScreen: React.FC = ({ animationController }) => { } else if (actionType === "register-step-2") { animateSlideTransition("register-step-1", "right"); } else if (actionType === "register-step-3") { - animateSlideTransition("register-step-2", "right"); // Volta do passo 3 para o 2 + animateSlideTransition("register-step-2", "right"); } else { router.back(); } @@ -252,16 +264,20 @@ const LoginScreen: React.FC = ({ animationController }) => { handleTextChange("name", text)} /> handleTextChange("username", text)} /> Voltar @@ -279,6 +295,7 @@ const LoginScreen: React.FC = ({ animationController }) => { )} + {actionType === "register-step-2" && ( <> = ({ animationController }) => { placeholder="Seu email" keyboardType="email-address" theme={theme} - onChangeText={(text) => handleTextChange("username", text)} styles={styles} + value={credentials.email} + onChangeText={(text) => handleTextChange("email", text)} /> = ({ animationController }) => { secureTextEntry={secureEntry} theme={theme} styles={styles} + value={credentials.password} onChangeText={(text) => handleTextChange("password", text)} toggleSecure={() => setSecureEntry((prev) => !prev)} /> @@ -311,7 +330,7 @@ const LoginScreen: React.FC = ({ animationController }) => { )} - {/* --- NOVO: Formulário para a etapa 3 (Endereço) --- */} + {actionType === "register-step-3" && ( <> = ({ animationController }) => { keyboardType="numeric" theme={theme} styles={styles} - value={credentials.address?.cep} - onChangeText={(text) => handleAddressChange("cep", text)} + value={credentials.cep} + onChangeText={(text) => handleTextChange("cep", text)} /> = ({ animationController }) => { placeholder="Rua" theme={theme} styles={styles} - value={credentials.address?.street} - onChangeText={(text) => handleAddressChange("street", text)} - editable={false} // Desabilita edição + value={credentials.street} + editable={false} /> - handleAddressChange("neighborhood", text)} - editable={false} // Desabilita edição + value={credentials.neighborhood} + editable={false} /> = ({ animationController }) => { placeholder="Cidade" theme={theme} styles={styles} - value={credentials.address?.city} - onChangeText={(text) => handleAddressChange("city", text)} - editable={false} // Desabilita edição + value={credentials.city} + editable={false} /> = ({ animationController }) => { placeholder="Estado" theme={theme} styles={styles} - value={credentials.address?.state} - onChangeText={(text) => handleAddressChange("state", text)} - editable={false} // Desabilita edição + value={credentials.state} + editable={false} /> = ({ animationController }) => { keyboardType="numeric" theme={theme} styles={styles} - value={credentials.address?.number} - onChangeText={(text) => handleAddressChange("number", text)} + value={credentials.number} + onChangeText={(text) => handleTextChange("number", text)} /> Voltar - Sign Up + {isCreatingUser ? ( + + ) : ( + Finalizar Cadastro + )} )} @@ -394,8 +414,9 @@ const LoginScreen: React.FC = ({ animationController }) => { placeholder="Seu email" keyboardType="email-address" theme={theme} - onChangeText={(text) => handleTextChange("username", text)} styles={styles} + value={credentials.email} + onChangeText={(text) => handleTextChange("email", text)} /> = ({ animationController }) => { secureTextEntry={secureEntry} theme={theme} styles={styles} + value={credentials.password} onChangeText={(text) => handleTextChange("password", text)} toggleSecure={() => setSecureEntry((prev) => !prev)} /> @@ -471,7 +493,7 @@ const LoginScreen: React.FC = ({ animationController }) => { ) : ( animateSlideTransition("login", "right")}> - Ja tem uma conta? Login + Já tem uma conta? Login )} @@ -482,7 +504,6 @@ const LoginScreen: React.FC = ({ animationController }) => { export default LoginScreen; -// Os estilos permanecem os mesmos const createStyles = (theme: Theme) => StyleSheet.create({ container: { @@ -527,7 +548,7 @@ const createStyles = (theme: Theme) => flex: 1, paddingHorizontal: 10, fontFamily: fonts.Light, - color: theme.blue, // Adicionado para melhor visualização do texto + color: theme.blue, }, forgotPasswordText: { textAlign: "right", @@ -543,6 +564,9 @@ const createStyles = (theme: Theme) => borderRadius: 100, marginTop: 20, }, + disabledButton: { + backgroundColor: "#a9a9a9", // Um cinza para o botão desabilitado + }, loginText: { color: theme.background, fontSize: 20, diff --git a/src/strada/mobile/src/screens/offer-ride/OfferRideScreen.tsx b/src/strada/mobile/src/screens/offer-ride/OfferRideScreen.tsx index 6653325..11ef4f8 100644 --- a/src/strada/mobile/src/screens/offer-ride/OfferRideScreen.tsx +++ b/src/strada/mobile/src/screens/offer-ride/OfferRideScreen.tsx @@ -26,6 +26,9 @@ import DateTimePicker from "@react-native-community/datetimepicker"; import polyline from "@mapbox/polyline"; import { getStoredUserID } from "@/src/services/user.service"; import { createRide } from "@/src/services/ride.service"; +// --- MODIFICAÇÃO 1: Importar as coordenadas --- +import { educareCoordinates } from "@/src/constants/coordinates"; + // Tipos para os dados interface LocationDto { @@ -64,7 +67,16 @@ interface CreateRideData { routePath: RoutePoint[]; } -const GOOGLE_MAPS_API_KEY = process.env.EXPO_PUBLIC_GOOGLE_MAPS_API_KEY || ""; +const Maps_API_KEY = process.env.EXPO_PUBLIC_Maps_API_KEY || ""; + +// --- MODIFICAÇÃO 2: Criar um objeto de destino fixo --- +const fixedDestination: LocationDto = { + latitude: educareCoordinates.latitude, + longitude: educareCoordinates.longitude, + name: "Educare", + address: "Educare - Centro, Betim" // Você pode ajustar o endereço conforme necessário +}; + const OfferRideScreen = () => { const insets = useSafeAreaInsets(); @@ -74,8 +86,9 @@ const OfferRideScreen = () => { const [originLocation, setOriginLocation] = useState( null ); + // --- MODIFICAÇÃO 3: Usar o objeto fixo como estado inicial --- const [destinationLocation, setDestinationLocation] = - useState(null); + useState(fixedDestination); // const [startDate, setstartDate] = useState(new Date()); const [endDate, setEndDate] = useState(new Date()); const [time, setTime] = useState(new Date()); @@ -155,7 +168,7 @@ const OfferRideScreen = () => { method: "POST", // ← faltava isso headers: { "Content-Type": "application/json", - "X-Goog-Api-Key": GOOGLE_MAPS_API_KEY, + "X-Goog-Api-Key": Maps_API_KEY, "X-Goog-FieldMask": "routes.duration,routes.distanceMeters,routes.polyline.encodedPolyline", }, @@ -222,45 +235,6 @@ const OfferRideScreen = () => { order: index, })); - // const points: RoutePoint[] = []; - // let index = 0; - // let lat = 0; - // let lng = 0; - - // while (index < encoded.length) { - // let b; - // let shift = 0; - // let result = 0; - - // do { - // b = encoded.charCodeAt(index++) - 63; - // result |= (b & 0x1f) << shift; - // shift += 5; - // } while (b >= 0x20); - - // const dlat = (result & 1) !== 0 ? ~(result >> 1) : result >> 1; - // lat += dlat; - - // shift = 0; - // result = 0; - - // do { - // b = encoded.charCodeAt(index++) - 63; - // result |= (b & 0x1f) << shift; - // shift += 5; - // } while (b >= 0x20); - - // const dlng = (result & 1) !== 0 ? ~(result >> 1) : result >> 1; - // lng += dlng; - - // points.push({ - // latitude: lat / 1e5, - // longitude: lng / 1e5, - // order: points.length, - // }); - // } - - // return points; }; // Função para criar a corrida @@ -763,6 +737,7 @@ const OfferRideScreen = () => { ); }; +// ... (seus estilos permanecem os mesmos) const styles = StyleSheet.create({ container: { flex: 1, @@ -1112,5 +1087,4 @@ const styles = StyleSheet.create({ color: colors.white, }, }); - -export default OfferRideScreen; +export default OfferRideScreen; \ No newline at end of file diff --git a/src/strada/mobile/src/services/helpers/interceptors.ts b/src/strada/mobile/src/services/helpers/interceptors.ts index d1c1ad3..4cb32b2 100644 --- a/src/strada/mobile/src/services/helpers/interceptors.ts +++ b/src/strada/mobile/src/services/helpers/interceptors.ts @@ -6,7 +6,7 @@ const BASE_DOMAIN = process.env.EXPO_PUBLIC_BASE_DOMAIN ?? "localhost:3000"; const USE_HTTPS = process.env.EXPO_PUBLIC_USE_HTTPS === "true"; const createApiUrl = (subdomain = "") => { - if (BASE_DOMAIN.includes("localhost")) { + if (BASE_DOMAIN.includes("3000")) { return `http://${BASE_DOMAIN}`; } const protocol = USE_HTTPS ? "https" : "http"; @@ -108,7 +108,6 @@ export const createAxiosInstance = (subdomain = "") => { } } - return Promise.reject(new Error(error.message)); } ); diff --git a/src/strada/mobile/src/services/user.service.ts b/src/strada/mobile/src/services/user.service.ts index f3c2a44..23543fd 100644 --- a/src/strada/mobile/src/services/user.service.ts +++ b/src/strada/mobile/src/services/user.service.ts @@ -4,6 +4,8 @@ import { IUserRequest } from "../interfaces/user-request.interface"; export const createUser = async (user: IUserRequest) => { try { + console.log(user); + console.log(authAxios.defaults); const response = await authAxios.post(`/users`, user); return response.data; } catch (error) { @@ -25,7 +27,7 @@ export const getUser = async (id: string) => { export const updateUser = async (id: string, user: IUserRequest) => { try { const response = await authAxios.put(`/users/${id}`, user); - storeUser(response.data) + storeUser(response.data); return response.data; } catch (error) { console.error(error); @@ -34,16 +36,16 @@ export const updateUser = async (id: string, user: IUserRequest) => { }; export const storeUserID = async (id: string) => { - await SecureStore.setItemAsync("userID", id); + await SecureStore.setItemAsync("userID", id); }; export const getStoredUser = async () => { return await SecureStore.getItemAsync("user"); -} +}; export const storeUser = async (user: Partial) => { await SecureStore.setItemAsync("user", JSON.stringify(user)); -} +}; export const getStoredUserID = async () => { return await SecureStore.getItemAsync("userID"); @@ -51,4 +53,4 @@ export const getStoredUserID = async () => { export const removeStoredUserID = async () => { await SecureStore.deleteItemAsync("userID"); -}; \ No newline at end of file +}; From c8dc7f5b0b2183ec3fb3235d87c00e871051b724 Mon Sep 17 00:00:00 2001 From: Uinicivs Date: Sat, 28 Jun 2025 21:00:02 -0300 Subject: [PATCH 3/4] fix(offer-ride): new fields, cnh validation, to/from fixed to educare --- src/strada/mobile/.env | 8 +- .../src/components/shared/SearchBar.tsx | 404 +++----- .../src/interfaces/user-request.interface.ts | 4 + src/strada/mobile/src/screens/home/Home.tsx | 3 +- .../screens/offer-ride/OfferRideScreen.tsx | 961 ++++-------------- .../src/screens/offer-ride/VehicleModal.tsx | 639 ++++-------- .../mobile/src/services/user.service.ts | 9 +- 7 files changed, 561 insertions(+), 1467 deletions(-) diff --git a/src/strada/mobile/.env b/src/strada/mobile/.env index 4bf23ec..da1ece5 100644 --- a/src/strada/mobile/.env +++ b/src/strada/mobile/.env @@ -5,10 +5,10 @@ EXPO_PUBLIC_GOOGLE_CALLBACK_URL=http://localhost:3000/auth/oauth2/callback/googl EXPO_PUBLIC_GITHUB_OAUTH_URL=https://github.com/login/oauth/authorize?client_id=Ov23liUYlBuWhFrqDKRP&redirect_uri=http://localhost:3000/auth/oauth2/callback/github&scope=userfb697900284c # Para produção -# EXPO_PUBLIC_USE_HTTPS=true +EXPO_PUBLIC_USE_HTTPS=true # Para desenvolvimento -EXPO_PUBLIC_BASE_DOMAIN=10.0.2.2:3000 -EXPO_PUBLIC_USE_HTTPS=false +# EXPO_PUBLIC_BASE_DOMAIN=10.0.2.2:3000 +# EXPO_PUBLIC_USE_HTTPS=false -EXPO_PUBLIC_GOOGLE_MAPS_API_KEY=AIzaSyCYOruhSUWQavp9_pnB42oGOyIlJW0VHm4 \ No newline at end of file +EXPO_PUBLIC_GOOGLE_MAPS_API_KEY=AIzaSyCZDYi8cAv8vBuk2Z1EKIoh_Lj6FsuBhkU \ No newline at end of file diff --git a/src/strada/mobile/src/components/shared/SearchBar.tsx b/src/strada/mobile/src/components/shared/SearchBar.tsx index 8c1994d..b082071 100644 --- a/src/strada/mobile/src/components/shared/SearchBar.tsx +++ b/src/strada/mobile/src/components/shared/SearchBar.tsx @@ -13,6 +13,7 @@ import { } from "react-native"; import * as AsyncStorage from "expo-secure-store"; import Icon from "react-native-vector-icons/MaterialIcons"; +import { getStoredUser } from "@/src/services/user.service"; // Cores já definidas no seu app const colors = { @@ -34,7 +35,6 @@ const colors = { // Configuração da API do Google Places const GOOGLE_PLACES_API_KEY = process.env.EXPO_PUBLIC_GOOGLE_MAPS_API_KEY || ""; - const STORAGE_KEY = "recent_searches"; const AutocompleteSearch = ({ @@ -47,36 +47,57 @@ const AutocompleteSearch = ({ const [recentSearches, setRecentSearches] = useState([]); const [isFocused, setIsFocused] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [homeLocation, setHomeLocation] = useState<{ address: string; name: string } | null>(null); + const inputRef = useRef(null); const suggestionHeight = useRef(new Animated.Value(0)).current; const { height: screenHeight } = Dimensions.get("window"); const searchTimeoutRef = useRef(null); - // Carregar buscas recentes do AsyncStorage - const loadRecentSearches = async () => { - try { - const storedSearches = await AsyncStorage.getItemAsync(STORAGE_KEY); - if (storedSearches) { - const parsedSearches = JSON.parse(storedSearches); - setRecentSearches(parsedSearches.slice(0, 5)); // Manter apenas as últimas 5 + // Carregar buscas recentes e endereço de casa + useEffect(() => { + const loadInitialData = async () => { + // Carregar buscas recentes + try { + const storedSearches = await AsyncStorage.getItemAsync(STORAGE_KEY); + if (storedSearches) { + const parsedSearches = JSON.parse(storedSearches); + setRecentSearches(parsedSearches.slice(0, 5)); + } + } catch (error) { + console.error("Erro ao carregar buscas recentes:", error); } - } catch (error) { - console.error("Erro ao carregar buscas recentes:", error); - } - }; - // Salvar busca recente no AsyncStorage + // Carregar endereço de casa + try { + const userString = await getStoredUser(); + if (userString) { + const userData = JSON.parse(userString); + if (userData && userData.address) { + setHomeLocation({ address: userData.address, name: "Casa" }); + } + } + } catch (error) { + console.error("Erro ao carregar dados do usuário para endereço de casa:", error); + } + }; + loadInitialData(); + }, []); + + // Foco automático no input + useEffect(() => { + setTimeout(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, 100); + }, []); + const saveRecentSearch = async (searchData) => { try { const storedSearches = await AsyncStorage.getItemAsync(STORAGE_KEY); let searches = storedSearches ? JSON.parse(storedSearches) : []; - - // Remover busca duplicada se existir - searches = searches.filter( - (search) => search.place_id !== searchData.place_id - ); - - // Adicionar nova busca no início + searches = searches.filter((search) => search.place_id !== searchData.place_id); searches.unshift({ id: searchData.place_id, main: searchData.main, @@ -84,21 +105,17 @@ const AutocompleteSearch = ({ description: searchData.description, place_id: searchData.place_id, types: searchData.types, - icon: searchData.icon, + icon: searchData.icon || 'home', timestamp: new Date().toISOString(), }); - - // Manter apenas as últimas 20 buscas no storage (para não sobrecarregar) searches = searches.slice(0, 20); - await AsyncStorage.setItemAsync(STORAGE_KEY, JSON.stringify(searches)); - setRecentSearches(searches.slice(0, 10)); // Mostrar apenas as últimas 5 + setRecentSearches(searches.slice(0, 5)); } catch (error) { console.error("Erro ao salvar busca recente:", error); } }; - // Limpar todas as buscas recentes const clearRecentSearches = async () => { try { await AsyncStorage.deleteItemAsync(STORAGE_KEY); @@ -107,30 +124,29 @@ const AutocompleteSearch = ({ console.error("Erro ao limpar buscas recentes:", error); } }; + + const getIconForPlace = (types) => { + if (types.includes("airport")) return "flight"; + if (types.includes("bus_station") || types.includes("transit_station")) return "directions-bus"; + if (types.includes("subway_station")) return "directions-subway"; + if (types.includes("hospital")) return "local-hospital"; + if (types.includes("school") || types.includes("university")) return "school"; + if (types.includes("shopping_mall")) return "shopping-cart"; + if (types.includes("restaurant") || types.includes("meal_takeaway")) return "restaurant"; + if (types.includes("gas_station")) return "local-gas-station"; + if (types.includes("bank")) return "account-balance"; + return "location-on"; + }; - // Carregar buscas recentes quando o componente é montado - useEffect(() => { - loadRecentSearches(); - }, []); - - // Função para buscar sugestões da API do Google Places const searchGooglePlaces = async (query) => { if (!query || query.length < 2) { setSuggestions([]); return; } - setIsLoading(true); - try { - const response = await fetch( - `https://maps.googleapis.com/maps/api/place/autocomplete/json?input=${encodeURIComponent( - query - )}&key=${GOOGLE_PLACES_API_KEY}&language=pt-BR&components=country:br` - ); - + const response = await fetch(`https://maps.googleapis.com/maps/api/place/autocomplete/json?input=${encodeURIComponent(query)}&key=${GOOGLE_PLACES_API_KEY}&language=pt-BR&components=country:br`); const data = await response.json(); - if (data.status === "OK") { const formattedSuggestions = data.predictions.map((prediction) => ({ id: prediction.place_id, @@ -141,255 +157,163 @@ const AutocompleteSearch = ({ types: prediction.types, icon: getIconForPlace(prediction.types), })); - setSuggestions(formattedSuggestions); - - // Animar a abertura do painel de sugestões Animated.timing(suggestionHeight, { - toValue: Math.min( - formattedSuggestions.length * 62, - screenHeight * 0.4 - ), + toValue: Math.min(formattedSuggestions.length * 62, screenHeight * 0.4), duration: 200, useNativeDriver: false, }).start(); } else { - console.error("Erro na API do Google Places:", data.status); setSuggestions([]); } } catch (error) { - console.error("Erro ao buscar lugares:", error); - Alert.alert( - "Erro", - "Não foi possível buscar os endereços. Tente novamente." - ); + Alert.alert("Erro", "Não foi possível buscar os endereços. Tente novamente."); setSuggestions([]); } finally { setIsLoading(false); } }; - // Função para obter detalhes do lugar (incluindo lat/lng) + // Debounce para pesquisa + useEffect(() => { + if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current); + if (searchText.length > 0) { + searchTimeoutRef.current = setTimeout(() => searchGooglePlaces(searchText), 300); + } else { + setSuggestions([]); + Animated.timing(suggestionHeight, { toValue: 0, duration: 150, useNativeDriver: false }).start(); + } + return () => { + if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current); + }; + }, [searchText]); + const getPlaceDetails = async (placeId) => { try { - const response = await fetch( - `https://maps.googleapis.com/maps/api/place/details/json?place_id=${placeId}&fields=geometry,formatted_address,name&key=${GOOGLE_PLACES_API_KEY}` - ); - + const response = await fetch(`https://maps.googleapis.com/maps/api/place/details/json?place_id=${placeId}&fields=geometry,formatted_address,name&key=${GOOGLE_PLACES_API_KEY}`); const data = await response.json(); - if (data.status === "OK") { const place = data.result; - const location = { + return { latitude: place.geometry.location.lat, longitude: place.geometry.location.lng, address: place.formatted_address, name: place.name, }; - console.log("Localização do lugar:", location); - return location; - } else { - throw new Error("Erro ao obter detalhes do lugar"); } + throw new Error("Erro ao obter detalhes do lugar"); } catch (error) { - console.error("Erro ao obter detalhes:", error); Alert.alert("Erro", "Não foi possível obter os detalhes do endereço."); return null; } }; - // Função para determinar o ícone baseado no tipo do lugar - const getIconForPlace = (types) => { - if (types.includes("airport")) return "flight"; - if (types.includes("bus_station") || types.includes("transit_station")) - return "directions-bus"; - if (types.includes("subway_station")) return "directions-subway"; - if (types.includes("hospital")) return "local-hospital"; - if (types.includes("school") || types.includes("university")) - return "school"; - if (types.includes("shopping_mall")) return "shopping-cart"; - if (types.includes("restaurant") || types.includes("meal_takeaway")) - return "restaurant"; - if (types.includes("gas_station")) return "local-gas-station"; - if (types.includes("bank")) return "account-balance"; - return "location-on"; - }; - - // Debounce para pesquisa - useEffect(() => { - if (searchTimeoutRef.current) { - clearTimeout(searchTimeoutRef.current); - } - - if (searchText.length > 0) { - searchTimeoutRef.current = setTimeout(() => { - searchGooglePlaces(searchText); - }, 300); // Aguarda 300ms após o usuário parar de digitar - } else { - setSuggestions([]); - // Animar o fechamento do painel de sugestões - Animated.timing(suggestionHeight, { - toValue: 0, - duration: 150, - useNativeDriver: false, - }).start(); - } - - return () => { - if (searchTimeoutRef.current) { - clearTimeout(searchTimeoutRef.current); - } - }; - }, [searchText]); - - // Foco automático no input quando o componente é montado - useEffect(() => { - setTimeout(() => { - if (inputRef.current) { - inputRef.current.focus(); - } - }, 100); - }, []); - const handleSelectSuggestion = async (suggestion) => { setIsLoading(true); - - // Obter detalhes do lugar incluindo coordenadas const placeDetails = await getPlaceDetails(suggestion.place_id); - if (placeDetails) { - const selectedPlace = { - ...suggestion, - ...placeDetails, - searchType, // 'start' ou 'destination' - }; - - // Salvar a busca recente + const selectedPlace = { ...suggestion, ...placeDetails, searchType }; await saveRecentSearch(suggestion); - - await onSelectPlace(selectedPlace); + onSelectPlace(selectedPlace); setSearchText(""); Keyboard.dismiss(); } + setIsLoading(false); + }; + const geocodeAddress = async (address) => { + try { + const response = await fetch(`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${GOOGLE_PLACES_API_KEY}&language=pt-BR`); + const data = await response.json(); + if (data.status === 'OK' && data.results.length > 0) { + const result = data.results[0]; + return { + latitude: result.geometry.location.lat, + longitude: result.geometry.location.lng, + address: result.formatted_address, + name: "Casa", + }; + } + throw new Error(data.error_message || "Endereço não pôde ser encontrado no mapa."); + } catch (error) { + Alert.alert("Erro de Localização", "Não foi possível encontrar as coordenadas para o seu endereço de casa."); + return null; + } + }; + + const handleSelectHome = async () => { + if (!homeLocation || !homeLocation.address) { + Alert.alert("Endereço de Casa", "Seu endereço de casa não foi encontrado. Por favor, verifique seu cadastro."); + return; + } + setIsLoading(true); + Keyboard.dismiss(); + const geocodedHome = await geocodeAddress(homeLocation.address); + if (geocodedHome) { + const placeData = { + ...geocodedHome, + id: `home-${geocodedHome.latitude}-${geocodedHome.longitude}`, + place_id: `home-${geocodedHome.latitude}-${geocodedHome.longitude}`, + main: "Casa", + secondary: geocodedHome.address, + icon: "home", + }; + await saveRecentSearch(placeData); + onSelectPlace(placeData); + } setIsLoading(false); }; const handleClearText = () => { setSearchText(""); - if (inputRef.current) { - inputRef.current.focus(); - } + if (inputRef.current) inputRef.current.focus(); }; - // Renderiza cada item de sugestão const renderSuggestionItem = ({ item }) => ( - handleSelectSuggestion(item)} - activeOpacity={0.7} - > - - - + handleSelectSuggestion(item)} activeOpacity={0.7}> + - - {item.main} - - - {item.secondary} - + {item.main} + {item.secondary} ); - // Renderiza cada item das buscas recentes const renderRecentItem = ({ item }) => ( - handleSelectSuggestion(item)} - activeOpacity={0.7} - > - - - + handleSelectSuggestion(item)} activeOpacity={0.7}> + - - {item.main} - - {item.secondary && ( - - {item.secondary} - - )} + {item.main} + {item.secondary && {item.secondary}} ); - // Renderiza itens recentes quando não há texto na busca const renderRecentSearches = () => { if (recentSearches.length === 0) return null; - return ( Buscas recentes - - Limpar - + Limpar - - item.id} - showsVerticalScrollIndicator={false} - scrollEnabled={false} - /> + item.id} showsVerticalScrollIndicator={false} scrollEnabled={false} /> ); }; return ( - {/* Cabeçalho com barra de pesquisa */} - {searchText.length > 0 && ( - + )} - {isLoading && ( - - ... - - )} + {isLoading && ...} - {/* Lista de sugestões */} - 0 ? 1 : 0, - }, - ]} - > - {suggestions.length > 0 ? ( - item.id} - keyboardShouldPersistTaps="handled" - showsVerticalScrollIndicator={false} - contentContainerStyle={styles.suggestionsList} - /> - ) : null} + 0 ? 1 : 0 }]}> + {suggestions.length > 0 && item.id} keyboardShouldPersistTaps="handled" showsVerticalScrollIndicator={false} contentContainerStyle={styles.suggestionsList} />} - {/* Mostrar buscas recentes quando não há texto */} {searchText.length === 0 && renderRecentSearches()} - {/* Sugestões para origem/destino quando relevante */} {searchText.length === 0 && ( Locais comuns - - - + + Casa - - Definir meu endereço de casa + + {homeLocation ? homeLocation.address : "Endereço não definido no perfil"} - - - - - - - Trabalho - - Definir meu endereço de trabalho - - - - - Selecionar no mapa @@ -661,4 +539,4 @@ const styles = StyleSheet.create({ }, }); -export default AutocompleteSearch; +export default AutocompleteSearch; \ No newline at end of file diff --git a/src/strada/mobile/src/interfaces/user-request.interface.ts b/src/strada/mobile/src/interfaces/user-request.interface.ts index 70f8199..92e8819 100644 --- a/src/strada/mobile/src/interfaces/user-request.interface.ts +++ b/src/strada/mobile/src/interfaces/user-request.interface.ts @@ -8,4 +8,8 @@ export interface IUserRequest { cep?: string; city?: string; state?: string; + cnh?: string; + vehicle_model?: string; + vehicle_color?: string; + license_plate?: string; } diff --git a/src/strada/mobile/src/screens/home/Home.tsx b/src/strada/mobile/src/screens/home/Home.tsx index 7d5a849..10e2343 100644 --- a/src/strada/mobile/src/screens/home/Home.tsx +++ b/src/strada/mobile/src/screens/home/Home.tsx @@ -21,7 +21,7 @@ import { AppImages } from "@/src/assets"; import { colors } from "@/src/constants/colors"; import AutocompleteSearch from "@/src/components/shared/SearchBar"; import { getPopularRoutes, searchRides } from "@/src/services/ride.service"; -import { getStoredUserID, getUser } from "@/src/services/user.service"; +import { getStoredUserID, getUser, storeUser } from "@/src/services/user.service"; interface PopularRoute { id: string; @@ -186,6 +186,7 @@ const HomeScreen = () => { const storedUserId = await getStoredUserID(); if (storedUserId) { const user = await getUser(storedUserId); + storeUser(user); setUser(user); } } catch (error) { diff --git a/src/strada/mobile/src/screens/offer-ride/OfferRideScreen.tsx b/src/strada/mobile/src/screens/offer-ride/OfferRideScreen.tsx index 11ef4f8..3d27dd8 100644 --- a/src/strada/mobile/src/screens/offer-ride/OfferRideScreen.tsx +++ b/src/strada/mobile/src/screens/offer-ride/OfferRideScreen.tsx @@ -21,16 +21,13 @@ import CustomCalendar from "@/src/components/shared/CustomCalendar"; import RecurringInDays from "@/src/components/offer-ride/ReccuringInDays"; import AutocompleteSearch from "@/src/components/shared/SearchBar"; import VehicleSelector from "./VehicleModal"; -import RouteSelector from "./RouteSelector"; import DateTimePicker from "@react-native-community/datetimepicker"; import polyline from "@mapbox/polyline"; -import { getStoredUserID } from "@/src/services/user.service"; +import { getStoredUserID, getStoredUser } from "@/src/services/user.service"; import { createRide } from "@/src/services/ride.service"; -// --- MODIFICAÇÃO 1: Importar as coordenadas --- import { educareCoordinates } from "@/src/constants/coordinates"; - -// Tipos para os dados +// --- INTERFACES E TIPOS --- interface LocationDto { latitude: number; longitude: number; @@ -69,27 +66,21 @@ interface CreateRideData { const Maps_API_KEY = process.env.EXPO_PUBLIC_Maps_API_KEY || ""; -// --- MODIFICAÇÃO 2: Criar um objeto de destino fixo --- -const fixedDestination: LocationDto = { +const educareLocation: LocationDto = { latitude: educareCoordinates.latitude, longitude: educareCoordinates.longitude, name: "Educare", - address: "Educare - Centro, Betim" // Você pode ajustar o endereço conforme necessário + address: "Educare - Centro, Betim", }; - const OfferRideScreen = () => { const insets = useSafeAreaInsets(); const router = useRouter(); - // Estados para os campos do formulário - const [originLocation, setOriginLocation] = useState( - null - ); - // --- MODIFICAÇÃO 3: Usar o objeto fixo como estado inicial --- - const [destinationLocation, setDestinationLocation] = - useState(fixedDestination); // - const [startDate, setstartDate] = useState(new Date()); + // --- ESTADOS DO COMPONENTE --- + const [originLocation, setOriginLocation] = useState(null); + const [destinationLocation, setDestinationLocation] = useState(educareLocation); + const [startDate, setStartDate] = useState(new Date()); const [endDate, setEndDate] = useState(new Date()); const [time, setTime] = useState(new Date()); const [price, setPrice] = useState(""); @@ -106,53 +97,77 @@ const OfferRideScreen = () => { const [vehicle, setVehicle] = useState<{ model: string; color: string; - plate: string; + licensePlate: string; } | null>(null); const [searchModalVisible, setSearchModalVisible] = useState(false); - const [searchType, setSearchType] = useState<"origin" | "destination">( - "origin" - ); + const [searchType, setSearchType] = useState<"origin" | "destination">("origin"); const [driverId, setDriverId] = useState(null); const [isLoading, setIsLoading] = useState(false); - // ID do motorista - deve vir do contexto de autenticação + // --- LÓGICA DE CARREGAMENTO INICIAL --- useEffect(() => { - const fetchDriverId = async () => { + const loadInitialData = async () => { const userId = await getStoredUserID(); setDriverId(userId); + + const userString = await getStoredUser(); + if (userString) { + const userData = JSON.parse(userString); + if (userData.vehicle_model && userData.vehicle_color && userData.license_plate) { + setVehicle({ + model: userData.vehicle_model, + color: userData.vehicle_color, + licensePlate: userData.license_plate, + }); + } + } }; - fetchDriverId(); + loadInitialData(); }, []); + // --- FUNÇÕES E MANIPULADORES DE EVENTOS --- + + const handleGoBack = useCallback(() => { + router.back(); + }, [router]); + + const handleSwapLocations = useCallback(() => { + setOriginLocation(destinationLocation); + setDestinationLocation(originLocation); + }, [originLocation, destinationLocation]); + const openSearchModal = (type: "origin" | "destination") => { + const isOriginFixed = originLocation?.name === educareLocation.name; + const isDestinationFixed = destinationLocation?.name === educareLocation.name; + + if (type === 'origin' && isOriginFixed) { + Alert.alert("Ação bloqueada", "A origem é a Educare. Use o botão de troca para oferecer uma carona PARA a escola."); + return; + } + + if (type === 'destination' && isDestinationFixed) { + Alert.alert("Ação bloqueada", "O destino é a Educare. Use o botão de troca para oferecer uma carona SAINDO da escola."); + return; + } + setSearchType(type); setSearchModalVisible(true); }; - const closeSearchModal = () => { - setSearchModalVisible(false); - }; + const closeSearchModal = () => setSearchModalVisible(false); const handlePlaceSelected = (place: any) => { - console.log("Lugar selecionado:", place); - setSearchModalVisible(false); const location: LocationDto = { latitude: place.latitude, longitude: place.longitude, address: place.address, name: place.name, }; - - if (searchType === "origin") { - setOriginLocation(location); - } else { - setDestinationLocation(location); - } - + if (searchType === "origin") setOriginLocation(location); + else setDestinationLocation(location); closeSearchModal(); }; - // Função para buscar a rota no Google Maps const getRouteFromGoogleMaps = async ( origin: LocationDto, destination: LocationDto @@ -162,63 +177,28 @@ const OfferRideScreen = () => { estimatedDistance: number; }> => { try { - const response = await fetch( - `https://routes.googleapis.com/directions/v2:computeRoutes`, - { - method: "POST", // ← faltava isso - headers: { - "Content-Type": "application/json", - "X-Goog-Api-Key": Maps_API_KEY, - "X-Goog-FieldMask": - "routes.duration,routes.distanceMeters,routes.polyline.encodedPolyline", - }, - body: JSON.stringify({ - // ← aqui precisa ser string - origin: { - location: { - latLng: { - latitude: origin.latitude, - longitude: origin.longitude, - }, - }, - }, - destination: { - location: { - latLng: { - latitude: destination.latitude, - longitude: destination.longitude, - }, - }, - }, - travelMode: "DRIVE", - routingPreference: "TRAFFIC_AWARE", - computeAlternativeRoutes: false, - routeModifiers: { - avoidTolls: false, - avoidHighways: false, - avoidFerries: false, - }, - languageCode: "en-US", - units: "METRIC", - }), - } - ); - + const response = await fetch(`https://routes.googleapis.com/directions/v2:computeRoutes`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Goog-Api-Key": Maps_API_KEY, + "X-Goog-FieldMask": "routes.duration,routes.distanceMeters,routes.polyline.encodedPolyline", + }, + body: JSON.stringify({ + origin: { location: { latLng: { latitude: origin.latitude, longitude: origin.longitude } } }, + destination: { location: { latLng: { latitude: destination.latitude, longitude: destination.longitude } } }, + travelMode: "DRIVE", + routingPreference: "TRAFFIC_AWARE", + }), + }); const data = await response.json(); - - console.log("Dados da rota:", data); - + if (!data.routes || data.routes.length === 0) throw new Error("Rota não encontrada"); const route = data.routes[0]; - - // Decodificar polyline para obter pontos da rota - const routePath: RoutePoint[] = decodePolyline( - route.polyline.encodedPolyline - ); - + const routePath: RoutePoint[] = polyline.decode(route.polyline.encodedPolyline).map((p, i) => ({ latitude: p[0], longitude: p[1], order: i })); return { routePath, - estimatedDuration: +route.duration.split("s")[0], - estimatedDistance: route.distanceMeters, // Você pode calcular a distância se necessário + estimatedDuration: +route.duration.slice(0, -1), + estimatedDistance: route.distanceMeters, }; } catch (error) { console.error("Erro ao buscar rota:", error); @@ -226,57 +206,12 @@ const OfferRideScreen = () => { } }; - // Função para decodificar polyline do Google Maps - const decodePolyline = (encoded: string): RoutePoint[] => { - const decodedPolyline = polyline.decode(encoded); - return decodedPolyline.map((point, index) => ({ - latitude: point[0], - longitude: point[1], - order: index, - })); - - }; - - // Função para criar a corrida - const saveRide = async (rideData: CreateRideData) => { - console.log("Dados da corrida:", rideData); - try { - return await createRide(rideData); - } catch (error) { - console.error("Erro ao criar corrida:", error); - throw error; - } - }; - - // Função para voltar à tela anterior - const handleGoBack = useCallback(() => { - router.back(); - }, [router]); - - // Gerenciamento dos pickers de data e hora - const handleStartEndDateChange = ( - startDateValue: Date | null, // Renamed to avoid conflict with state variable - endDateValue: Date | null // Renamed to avoid conflict with state variable - ) => { - setstartDate(startDateValue); + const handleStartEndDateChange = (startDateValue: Date | null, endDateValue: Date | null) => { + setStartDate(startDateValue); setEndDate(endDateValue); - - // Now, handle the UI logic based on the *newly updated* state - if (!startDateValue && !endDateValue) { - // Both are null, meaning selection was cleared or never made + if (startDateValue && endDateValue) { + setIsRecurringRide(startDateValue.getTime() !== endDateValue.getTime()); setShowDatePicker(false); - setIsRecurringRide(false); // No selection, so not recurring - } else if (startDateValue && !endDateValue) { - // Only start date is selected - setIsRecurringRide(false); // Not yet a recurring ride (needs an end date) - } else if (startDateValue && endDateValue) { - // Both start and end dates are selected - if (startDateValue.getTime() === endDateValue.getTime()) { - setIsRecurringRide(false); - } else { - setIsRecurringRide(true); // Different dates, not recurring - } - setShowDatePicker(false); // Close date picker once a range is selected } }; @@ -287,62 +222,30 @@ const OfferRideScreen = () => { const formatDate = (date: Date) => { if (!date) return ""; - - const day = date.getDate().toString().padStart(2, "0"); - const month = date - .toLocaleString("pt-BR", { month: "short" }) - .replace(".", ""); - const year = date.getFullYear(); - - return `${day} ${month} ${year}`; + return `${date.getDate().toString().padStart(2, "0")} ${date.toLocaleString("pt-BR", { month: "short" }).replace(".", "")} ${date.getFullYear()}`; }; - const formatTime = (time: Date) => { - return time?.toLocaleTimeString("pt-BR", { - hour: "2-digit", - minute: "2-digit", - }); - }; - - const handleVehicleChange = (vehicle: any) => { - setVehicle(vehicle); - }; + const formatTime = (time: Date) => time?.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" }); - // Ajustar número de assentos disponíveis - const decreaseSeats = () => { - if (availableSeats > 1) setAvailableSeats(availableSeats - 1); + const handleVehicleChange = (newVehicleData: { model: string; color: string; licensePlate: string; } | null) => { + setVehicle(newVehicleData); }; - const increaseSeats = () => { - if (availableSeats <= 42) setAvailableSeats(availableSeats + 1); - }; + const decreaseSeats = () => setAvailableSeats((s) => Math.max(1, s - 1)); + const increaseSeats = () => setAvailableSeats((s) => Math.min(42, s + 1)); - // Publicar a carona const handlePublishRide = useCallback(async () => { - // Validação básica dos campos - if (!originLocation || !destinationLocation || !price) { - Alert.alert( - "Dados incompletos", - "Por favor, preencha todos os campos obrigatórios." - ); + if (!originLocation || !destinationLocation || !price || !driverId || !vehicle) { + Alert.alert("Dados incompletos", "Por favor, preencha todos os campos obrigatórios, incluindo o veículo e a CNH."); return; } - setIsLoading(true); - try { - // Buscar rota no Google Maps - const routeData = await getRouteFromGoogleMaps( - originLocation, - destinationLocation - ); - - // Criar data de partida combinando data e hora + const routeData = await getRouteFromGoogleMaps(originLocation, destinationLocation); const departureDateTime = new Date(startDate); departureDateTime.setHours(time.getHours()); departureDateTime.setMinutes(time.getMinutes()); - // Preparar dados para o endpoint const rideData: CreateRideData = { driverId, startLocation: originLocation, @@ -350,29 +253,17 @@ const OfferRideScreen = () => { departureTime: departureDateTime.toISOString(), availableSeats, pricePerSeat: parseFloat(price.replace(",", ".")), - vehicle: { - model: vehicle?.model, - color: vehicle?.color, - licensePlate: vehicle?.licensePlate, - }, - preferences: { - allowSmoking, - allowPets, - allowLuggage, - }, + vehicle: { model: vehicle.model, color: vehicle.color, licensePlate: vehicle.licensePlate }, + preferences: { allowSmoking, allowPets, allowLuggage }, estimatedDuration: routeData.estimatedDuration, estimatedDistance: routeData.estimatedDistance, routePath: routeData.routePath, + seats: availableSeats, }; - - // Criar a corrida - await saveRide(rideData); + await createRide(rideData); // Usando a função createRide importada router.push("/ride-history/ride-history"); } catch (error) { - Alert.alert( - "Erro", - "Ocorreu um erro ao publicar sua carona. Tente novamente." - ); + Alert.alert("Erro", "Ocorreu um erro ao publicar sua carona. Tente novamente."); } finally { setIsLoading(false); } @@ -387,14 +278,13 @@ const OfferRideScreen = () => { allowPets, allowLuggage, driverId, + vehicle, router, ]); return ( - - {/* Header */} @@ -408,41 +298,48 @@ const OfferRideScreen = () => { showsVerticalScrollIndicator={false} contentContainerStyle={styles.contentContainer} > - {/* Seção: Veículo */} - {/* Seção: Rota */} - openSearchModal(value)} - > + + + + openSearchModal('origin')}> + + {originLocation?.address || 'Escolher ponto de partida'} + + + openSearchModal('destination')}> + + {destinationLocation?.address || 'Escolher destino'} + + + + + + + + handlePlaceSelected(value)} + onSelectPlace={handlePlaceSelected} onBack={closeSearchModal} /> - {/* Seção: Data e Hora */} Data e Hora - - setShowDatePicker(true)} - > + setShowDatePicker(true)}> {formatDate(startDate)} @@ -452,639 +349,147 @@ const OfferRideScreen = () => { - - setShowTimePicker(true)} - > + setShowTimePicker(true)}> {formatTime(time)} - {showTimePicker && ( - + )} - - {/* Pickers para data e hora (visíveis quando clicados) */} {showDatePicker && ( - + )} - Carona recorrente - + {isRecurringRide && } - - - Selecione o range de datas e horário para a carona. Se a carona - ocorrer em um dia específico, selecione a mesma data para início e - fim. - + Selecione o range de datas e horário para a carona. Se a carona ocorrer em um dia específico, selecione a mesma data para início e fim. - {/* Seção: Preço e Assentos */} Preço e Assentos - R$ - + por pessoa - Assentos disponíveis - - + + {availableSeats} - = 42} - > - = 42 ? colors.grey : colors.darkGrey} - /> + = 42}> + = 42 ? colors.grey : colors.darkGrey} /> - {/* Seção: Preferências */} Preferências - - - - - Permitir fumar - - - - - - - - Permitir animais - - - - - - - - Permitir bagagem - - - - - - - - Pode fazer paradas - - - + Permitir fumar + Permitir animais + Permitir bagagem + Pode fazer paradas - Tamanho máximo de bagagem: - setLuggageSize("small")} - > - - - Pequena - - - - setLuggageSize("medium")} - > - - - Média - - - - setLuggageSize("large")} - > - - - Grande - - + setLuggageSize("small")}>Pequena + setLuggageSize("medium")}>Média + setLuggageSize("large")}>Grande - {/* Seção: Observações */} Observações - - + - {/* Footer com botão de publicar */} - - {isLoading ? ( - - ) : ( - Publicar Carona - )} + + {isLoading ? : Publicar Carona} ); }; -// ... (seus estilos permanecem os mesmos) const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.neutralLight, - }, - header: { - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", - paddingHorizontal: 16, - paddingVertical: 12, - backgroundColor: colors.white, - borderBottomWidth: 1, - borderBottomColor: colors.lightGrey, - }, - backButton: { - padding: 4, - }, - headerTitle: { - fontSize: 18, - fontWeight: "600", - color: colors.black, - }, - headerRight: { - width: 32, - }, - scrollView: { - flex: 1, - }, - contentContainer: { - paddingVertical: 16, - paddingBottom: 100, - }, - section: { - backgroundColor: colors.white, - borderRadius: 12, - padding: 16, - marginBottom: 16, - marginHorizontal: 16, - shadowColor: "#000", - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.05, - shadowRadius: 8, - elevation: 2, - }, - sectionHeader: { - flexDirection: "row", - alignItems: "center", - marginBottom: 16, - }, - sectionTitle: { - fontSize: 16, - fontWeight: "600", - color: colors.black, - marginLeft: 8, - }, - vehicleCard: { - flexDirection: "row", - alignItems: "center", - backgroundColor: colors.neutralLight, - borderRadius: 8, - padding: 12, - }, - vehicleImage: { - width: 80, - height: 60, - marginRight: 12, - }, - routeInfoText: { - fontSize: 13, - color: colors.primaryBlue, - marginLeft: 8, - flex: 1, - lineHeight: 18, - }, - routeInfoContainer: { - flexDirection: "row", - alignItems: "center", - marginTop: 16, - paddingTop: 16, - borderTopWidth: 1, - borderTopColor: colors.lightGrey, - }, - vehicleInfo: { - flex: 1, - }, - vehicleModel: { - fontSize: 16, - fontWeight: "500", - color: colors.black, - marginBottom: 2, - }, - vehicleDetails: { - fontSize: 14, - color: colors.darkGrey, - marginBottom: 6, - }, - changeVehicleButton: { - flexDirection: "row", - alignItems: "center", - }, - changeVehicleText: { - fontSize: 14, - color: colors.primaryBlue, - marginRight: 4, - }, - locationInput: { - flexDirection: "row", - alignItems: "center", - backgroundColor: colors.neutralLight, - borderRadius: 8, - paddingHorizontal: 12, - paddingVertical: 14, - marginBottom: 10, - }, - locationDot: { - width: 12, - height: 12, - borderRadius: 6, - marginRight: 12, - }, - originDot: { - backgroundColor: colors.primaryPink, - }, - destinationDot: { - backgroundColor: colors.primaryBlue, - }, - locationTextContainer: { - flex: 1, - }, - locationLabel: { - fontSize: 12, - color: colors.darkGrey, - marginBottom: 2, - }, - locationText: { - fontSize: 15, - color: colors.black, - }, - placeholderText: { - color: colors.darkGrey, - fontStyle: "italic", - }, - addStopButton: { - flexDirection: "row", - alignItems: "center", - justifyContent: "center", - paddingVertical: 12, - borderWidth: 1, - borderColor: colors.primaryBlue, - borderRadius: 8, - backgroundColor: colors.white, - borderStyle: "dashed", - }, - addStopText: { - fontSize: 14, - color: colors.primaryBlue, - marginLeft: 6, - }, - dateTimeContainer: { - flexDirection: "row", - gap: 12, - }, - dateTimeInput: { - flex: 1, - flexDirection: "row", - alignItems: "center", - backgroundColor: colors.neutralLight, - borderRadius: 8, - paddingHorizontal: 12, - paddingVertical: 14, - }, - dateTimeText: { - fontSize: 15, - color: colors.black, - marginLeft: 8, - marginRight: 8, - width: "100%", - }, - switchContainer: { - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", - paddingVertical: 12, - borderTopWidth: 1, - borderTopColor: colors.lightGrey, - marginTop: 16, - }, - switchLabel: { - fontSize: 15, - color: colors.black, - }, - priceInputContainer: { - flexDirection: "row", - alignItems: "center", - backgroundColor: colors.neutralLight, - borderRadius: 8, - paddingHorizontal: 16, - paddingVertical: 14, - marginBottom: 16, - }, - currencySymbol: { - fontSize: 18, - fontWeight: "600", - color: colors.black, - marginRight: 8, - }, - priceInput: { - flex: 1, - fontSize: 18, - fontWeight: "600", - color: colors.black, - padding: 0, - }, - perPersonText: { - fontSize: 14, - color: colors.darkGrey, - }, - seatsContainer: { - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", - }, - seatsLabel: { - fontSize: 15, - color: colors.black, - }, - seatsSelector: { - flexDirection: "row", - alignItems: "center", - backgroundColor: colors.neutralLight, - borderRadius: 8, - paddingHorizontal: 4, - paddingVertical: 4, - }, - seatButton: { - width: 36, - height: 36, - borderRadius: 18, - alignItems: "center", - justifyContent: "center", - backgroundColor: colors.white, - marginHorizontal: 2, - }, - seatsCountContainer: { - paddingHorizontal: 16, - }, - seatsCount: { - fontSize: 18, - fontWeight: "600", - color: colors.black, - }, - preferencesContainer: { - gap: 16, - }, - preferenceRow: { - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", - }, - preferenceInfo: { - flexDirection: "row", - alignItems: "center", - flex: 1, - }, - preferenceLabel: { - fontSize: 15, - color: colors.black, - marginLeft: 12, - }, - luggageSizeContainer: { - marginTop: 20, - paddingTop: 16, - borderTopWidth: 1, - borderTopColor: colors.lightGrey, - }, - luggageLabel: { - fontSize: 15, - color: colors.black, - marginBottom: 12, - }, - luggageOptions: { - flexDirection: "row", - gap: 8, - }, - luggageOption: { - flex: 1, - flexDirection: "row", - alignItems: "center", - justifyContent: "center", - paddingVertical: 12, - paddingHorizontal: 8, - borderRadius: 8, - backgroundColor: colors.neutralLight, - borderWidth: 1, - borderColor: colors.lightGrey, - }, - luggageSelected: { - backgroundColor: colors.primaryPink, - borderColor: colors.primaryPink, - }, - luggageText: { - fontSize: 13, - color: colors.darkGrey, - marginLeft: 6, - }, - luggageTextSelected: { - color: colors.white, - }, - notesInput: { - backgroundColor: colors.neutralLight, - borderRadius: 8, - paddingHorizontal: 12, - paddingVertical: 12, - fontSize: 15, - color: colors.black, - minHeight: 100, - textAlignVertical: "top", - }, - footer: { - backgroundColor: colors.white, - paddingHorizontal: 16, - paddingTop: 16, - borderTopWidth: 1, - borderTopColor: colors.lightGrey, - }, - publishButton: { - backgroundColor: colors.primaryPink, - borderRadius: 12, - paddingVertical: 16, - alignItems: "center", - justifyContent: "center", - shadowColor: colors.primaryPink, - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.3, - shadowRadius: 8, - elevation: 4, - }, - publishButtonDisabled: { - backgroundColor: colors.grey, - shadowOpacity: 0, - elevation: 0, - }, - publishButtonText: { - fontSize: 16, - fontWeight: "600", - color: colors.white, - }, + container: { flex: 1, backgroundColor: colors.neutralLight }, + header: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 16, paddingVertical: 12, backgroundColor: colors.white, borderBottomWidth: 1, borderBottomColor: colors.lightGrey }, + backButton: { padding: 4 }, + headerTitle: { fontSize: 18, fontWeight: "600", color: colors.black }, + headerRight: { width: 32 }, + scrollView: { flex: 1 }, + contentContainer: { paddingVertical: 16, paddingBottom: 100 }, + section: { backgroundColor: colors.white, borderRadius: 12, padding: 16, marginBottom: 16, marginHorizontal: 16, shadowColor: "#000", shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.05, shadowRadius: 8, elevation: 2 }, + sectionHeader: { flexDirection: "row", alignItems: "center", marginBottom: 16 }, + sectionTitle: { fontSize: 16, fontWeight: "600", color: colors.black, marginLeft: 8 }, + routeInfoText: { fontSize: 13, color: colors.primaryBlue, marginLeft: 8, flex: 1, lineHeight: 18 }, + routeInfoContainer: { flexDirection: "row", alignItems: "center", marginTop: 16, paddingTop: 16, borderTopWidth: 1, borderTopColor: colors.lightGrey }, + dateTimeContainer: { flexDirection: "row", gap: 12 }, + dateTimeInput: { flex: 1, flexDirection: "row", alignItems: "center", backgroundColor: colors.neutralLight, borderRadius: 8, paddingHorizontal: 12, paddingVertical: 14 }, + dateTimeText: { fontSize: 15, color: colors.black, marginLeft: 8, marginRight: 8 }, + switchContainer: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingVertical: 12, borderTopWidth: 1, borderTopColor: colors.lightGrey, marginTop: 16 }, + switchLabel: { fontSize: 15, color: colors.black }, + priceInputContainer: { flexDirection: "row", alignItems: "center", backgroundColor: colors.neutralLight, borderRadius: 8, paddingHorizontal: 16, paddingVertical: 14, marginBottom: 16 }, + currencySymbol: { fontSize: 18, fontWeight: "600", color: colors.black, marginRight: 8 }, + priceInput: { flex: 1, fontSize: 18, fontWeight: "600", color: colors.black, padding: 0 }, + perPersonText: { fontSize: 14, color: colors.darkGrey }, + seatsContainer: { flexDirection: "row", alignItems: "center", justifyContent: "space-between" }, + seatsLabel: { fontSize: 15, color: colors.black }, + seatsSelector: { flexDirection: "row", alignItems: "center", backgroundColor: colors.neutralLight, borderRadius: 8, paddingHorizontal: 4, paddingVertical: 4 }, + seatButton: { width: 36, height: 36, borderRadius: 18, alignItems: "center", justifyContent: "center", backgroundColor: colors.white, marginHorizontal: 2 }, + seatsCountContainer: { paddingHorizontal: 16 }, + seatsCount: { fontSize: 18, fontWeight: "600", color: colors.black }, + preferencesContainer: { gap: 16 }, + preferenceRow: { flexDirection: "row", alignItems: "center", justifyContent: "space-between" }, + preferenceInfo: { flexDirection: "row", alignItems: "center", flex: 1 }, + preferenceLabel: { fontSize: 15, color: colors.black, marginLeft: 12 }, + luggageSizeContainer: { marginTop: 20, paddingTop: 16, borderTopWidth: 1, borderTopColor: colors.lightGrey }, + luggageLabel: { fontSize: 15, color: colors.black, marginBottom: 12 }, + luggageOptions: { flexDirection: "row", gap: 8 }, + luggageOption: { flex: 1, flexDirection: "row", alignItems: "center", justifyContent: "center", paddingVertical: 12, paddingHorizontal: 8, borderRadius: 8, backgroundColor: colors.neutralLight, borderWidth: 1, borderColor: colors.lightGrey }, + luggageSelected: { backgroundColor: colors.primaryPink, borderColor: colors.primaryPink }, + luggageText: { fontSize: 13, color: colors.darkGrey, marginLeft: 6 }, + luggageTextSelected: { color: colors.white }, + notesInput: { backgroundColor: colors.neutralLight, borderRadius: 8, paddingHorizontal: 12, paddingVertical: 12, fontSize: 15, color: colors.black, minHeight: 100, textAlignVertical: "top" }, + footer: { backgroundColor: colors.white, paddingHorizontal: 16, paddingTop: 16, borderTopWidth: 1, borderTopColor: colors.lightGrey }, + publishButton: { backgroundColor: colors.primaryPink, borderRadius: 12, paddingVertical: 16, alignItems: "center", justifyContent: "center", shadowColor: colors.primaryPink, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8, elevation: 4 }, + publishButtonDisabled: { backgroundColor: colors.grey, shadowOpacity: 0, elevation: 0 }, + publishButtonText: { fontSize: 16, fontWeight: "600", color: colors.white }, + routeContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }, + routeFields: { flex: 1 }, + swapButton: { padding: 8, marginLeft: 8 }, + separatorLine: { height: 1, backgroundColor: colors.lightGrey, marginVertical: 4, marginLeft: 28 }, + routeLocationInput: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10, flex: 1 }, + routeLocationText: { fontSize: 16, color: colors.black, marginLeft: 12, flexShrink: 1 }, + locationDot: { width: 12, height: 12, borderRadius: 6 }, + originDot: { backgroundColor: colors.primaryPink }, + destinationDot: { backgroundColor: colors.primaryBlue }, }); + export default OfferRideScreen; \ No newline at end of file diff --git a/src/strada/mobile/src/screens/offer-ride/VehicleModal.tsx b/src/strada/mobile/src/screens/offer-ride/VehicleModal.tsx index 5f8790b..11187ef 100644 --- a/src/strada/mobile/src/screens/offer-ride/VehicleModal.tsx +++ b/src/strada/mobile/src/screens/offer-ride/VehicleModal.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { StyleSheet, View, @@ -6,14 +6,18 @@ import { TextInput, TouchableOpacity, Modal, + Keyboard, KeyboardAvoidingView, Platform, ScrollView, Alert, + ActivityIndicator, } from "react-native"; import Icon from "react-native-vector-icons/MaterialIcons"; +import { getStoredUser, getStoredUserID, updateUser } from "@/src/services/user.service"; +import { IUserRequest } from "@/src/interfaces/user-request.interface"; -// Cores já definidas no seu app +// Cores const colors = { white: "#FFFFFF", black: "#1A1A1A", @@ -29,490 +33,285 @@ const colors = { lightGrey: "#F0F0F0", grey: "#BDBDBD", darkGrey: "#8C8C8C", + success: '#28a745', + danger: '#dc3545', }; -/** - * VehicleModal Component: Allows users to add or edit vehicle information. - * @param {object} props - Component props. - * @param {boolean} props.visible - Controls modal visibility. - * @param {function} props.onClose - Function to call when the modal is closed. - * @param {function} props.onSaveVehicle - Function to call with vehicle data when saved. - * @param {object | null} props.initialVehicle - Initial vehicle data for editing. - */ const VehicleModal = ({ visible, onClose, - onSaveVehicle, - initialVehicle = null, + onSave, + initialVehicle, + initialCnh, + isSaving, }) => { - const [model, setModel] = useState(initialVehicle?.model || ""); - const [color, setColor] = useState(initialVehicle?.color || ""); - const [licensePlate, setLicensePlate] = useState( - initialVehicle?.licensePlate || "" - ); + const [model, setModel] = useState(""); + const [color, setColor] = useState(""); + const [licensePlate, setLicensePlate] = useState(""); + const [cnh, setCnh] = useState(""); + const [cnhStatus, setCnhStatus] = useState<"idle" | "validating" | "valid" | "invalid">("idle"); - const handleSave = () => { - // Validações - if (!model.trim()) { - Alert.alert("Erro", "Por favor, informe o modelo do veículo"); - return; + // Efeito para sincronizar o estado interno quando o modal abre + useEffect(() => { + if (visible) { + setModel(initialVehicle?.model || ""); + setColor(initialVehicle?.color || ""); + setLicensePlate(initialVehicle?.licensePlate || ""); + setCnh(initialCnh || ""); + setCnhStatus(initialCnh ? 'valid' : 'idle'); } + }, [visible, initialVehicle, initialCnh]); + + // Efeito para lidar com a simulação de validação da CNH + useEffect(() => { + if (cnhStatus !== 'validating') return; + const timer = setTimeout(() => { + if (cnh.endsWith("0")) { + setCnhStatus('invalid'); + Alert.alert("Validação Falhou", "Este número de CNH não pôde ser validado."); + } else { + setCnhStatus('valid'); + } + }, 2000); + return () => clearTimeout(timer); + }, [cnhStatus]); - if (!color.trim()) { - Alert.alert("Erro", "Por favor, informe a cor do veículo"); + const handleValidateCnh = () => { + Keyboard.dismiss(); + if (cnh.length !== 11) { + Alert.alert("CNH Inválida", "A CNH deve conter exatamente 11 números."); return; } - - if (!licensePlate.trim()) { - Alert.alert("Erro", "Por favor, informe a placa do veículo"); + setCnhStatus('validating'); + }; + + const handleSave = () => { + if (!model.trim() || !color.trim() || !licensePlate.trim()) { + Alert.alert("Erro", "Por favor, preencha todos os dados do veículo."); + return; + } + if (cnhStatus !== 'valid') { + Alert.alert("Validação Pendente", "Por favor, valide sua CNH para continuar."); return; } - - // Validação simples da placa (formato brasileiro) - // Supports old (AAA-1234) and Mercosul (AAA1B23) plates const plateRegex = /^[A-Z]{3}[0-9][A-Z0-9][0-9]{2}$/; - const cleanPlate = licensePlate.replace(/[^A-Z0-9]/g, "").toUpperCase(); - - if (!plateRegex.test(cleanPlate)) { - Alert.alert( - "Erro", - "Formato de placa inválido. Use o formato ABC1234 ou ABC1D23" - ); + if (!plateRegex.test(licensePlate)) { + Alert.alert("Erro", "Formato de placa inválido."); return; } - - const vehicleData = { + + onSave({ model: model.trim(), color: color.trim(), - licensePlate: cleanPlate, - }; - - onSaveVehicle(vehicleData); - handleClose(); // Close the modal after saving + licensePlate, + cnh, + }); }; - const handleClose = () => { - // Clear fields only if there's no initial vehicle (new creation) - if (!initialVehicle) { - setModel(""); - setColor(""); - setLicensePlate(""); - } - onClose(); + const handlePlateChange = (text) => { + setLicensePlate(text.replace(/[^A-Z0-9]/g, "").toUpperCase().substring(0, 7)); }; - const formatLicensePlate = (text) => { - // Remove non-alphanumeric characters and convert to uppercase - const cleaned = text.replace(/[^A-Z0-9]/g, "").toUpperCase(); - - // Limit to 7 characters and format with hyphen for old standard - let formatted = cleaned.substring(0, 7); - if ( - formatted.length > 3 && - /^[A-Z]{3}[0-9]{4}$/.test(cleaned.substring(0, 7)) - ) { - // Apply hyphen only for old format - formatted = formatted.substring(0, 3) + "-" + formatted.substring(3); - } else if ( - formatted.length > 3 && - /^[A-Z]{3}[0-9][A-Z0-9][0-9]{2}$/.test(cleaned.substring(0, 7)) - ) { - // Mercosul format (AAA1B23) doesn't use hyphen in the middle - formatted = cleaned; // Just ensure it's cleaned and uppercase + const renderValidationButtonContent = () => { + switch (cnhStatus) { + case 'validating': return ; + case 'valid': return <> Válida; + case 'invalid': return <> Inválida; + default: return Validar; } - - return formatted; - }; - - const handlePlateChange = (text) => { - const formattedPlate = formatLicensePlate(text.toUpperCase()); - setLicensePlate(formattedPlate); }; return ( - - + + - - - - - {initialVehicle ? "Editar Veículo" : "Adicionar Veículo"} - - - Salvar + + {initialVehicle ? "Editar Dados" : "Adicionar Dados"} + + {isSaving ? : Salvar} - Informações do Veículo - - Informe os dados do seu veículo para compartilhar a carona - - - - - Modelo do Veículo * - - - - - Cor * - + Informações para Carona + Informe seus dados de motorista e do veículo para compartilhar a carona. - + - Placa * - - - Formato: ABC-1234 (padrão antigo) ou ABC1D23 (Mercosul) - - - - - - - - - - Por que precisamos dessas informações? - - - Os dados do veículo ajudam os passageiros a identificar você no - ponto de encontro e garantem mais segurança para todos. - + Número da CNH * + + { + const cleanedText = text.replace(/[^0-9]/g, ''); + setCnh(cleanedText); + if (cnhStatus !== 'idle') { + setCnhStatus('idle'); + } + }} + keyboardType="numeric" + maxLength={11} + editable={cnhStatus !== 'valid'} + /> + + {renderValidationButtonContent()} + + Digite os 11 números da sua CNH. + + Modelo do Veículo * + Cor * + Placa *Formato: ABC1D23 (Mercosul) ou ABC1234 + Por que precisamos dessas informações?Os dados do veículo e da CNH garantem mais segurança para todos os usuários da plataforma. + ); }; -/** - * VehicleSelector Component: Displays current vehicle info and allows opening the VehicleModal. - * @param {object} props - Component props. - * @param {object | null} props.vehicle - Current vehicle data to display. - * @param {function} props.onVehicleChange - Function to call when vehicle data is updated. - */ + const VehicleSelector = ({ vehicle, onVehicleChange }) => { const [modalVisible, setModalVisible] = useState(false); + const [currentUserCnh, setCurrentUserCnh] = useState(null); + const [isSaving, setIsSaving] = useState(false); + + // Carrega os dados do usuário sempre que o modal for ser aberto + const handleOpenModal = async () => { + try { + const userString = await getStoredUser(); + if (userString) { + const userData = JSON.parse(userString); + setCurrentUserCnh(userData.cnh || null); + } + } catch (e) { + console.error("Falha ao carregar dados do usuário", e); + setCurrentUserCnh(null); + } finally { + setModalVisible(true); + } + }; + + const handleSaveData = async (data: { model: string; color: string; licensePlate: string; cnh: string }) => { + setIsSaving(true); + try { + const userId = await getStoredUserID(); + if (!userId) throw new Error("ID do usuário não encontrado."); - const handleSaveVehicle = (vehicleData) => { - onVehicleChange(vehicleData); + const userString = await getStoredUser(); + const existingUserData = userString ? JSON.parse(userString) : {}; + + const payload: IUserRequest = { + ...existingUserData, + vehicle_model: data.model, + vehicle_color: data.color, + license_plate: data.licensePlate, + cnh: data.cnh, + }; + + await updateUser(userId, payload); + + onVehicleChange({ model: data.model, color: data.color, licensePlate: data.licensePlate }); + setCurrentUserCnh(data.cnh); + + Alert.alert("Sucesso!", "Seus dados foram salvos."); + setModalVisible(false); + } catch (error) { + console.error("Erro ao salvar dados:", error); + Alert.alert("Erro", "Não foi possível salvar seus dados. Tente novamente."); + } finally { + setIsSaving(false); + } }; return ( <> - setModalVisible(true)} - activeOpacity={0.7} - > + - - - - - Seu Veículo - - {vehicle ? "Dados do veículo cadastrado" : "Toque para adicionar"} - - - + + Seu Veículo{vehicle ? "Clique para editar seus dados" : "Adicione seus dados para continuar"} + - {vehicle ? ( - - - Modelo - {vehicle.model} - - - - - Cor - {vehicle.color} - - - Placa - {vehicle.licensePlate} - - + Modelo{vehicle.model} + Cor{vehicle.color}Placa{vehicle.licensePlate} ) : ( - - - - - Adicione as informações do seu veículo - - - Modelo, cor e placa para facilitar a identificação - - - + Adicionar dados do Veículo e CNHEssas informações são necessárias para oferecer caronas )} - setModalVisible(false)} - onSaveVehicle={handleSaveVehicle} + onSave={handleSaveData} initialVehicle={vehicle} + initialCnh={currentUserCnh} + isSaving={isSaving} /> ); }; const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.white, - }, - header: { - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", - paddingHorizontal: 16, - paddingVertical: 12, - borderBottomWidth: 1, - borderBottomColor: colors.lightGrey, - paddingTop: Platform.OS === "ios" ? 50 : 12, - }, - closeButton: { - padding: 8, - }, - section: { - backgroundColor: colors.white, - borderRadius: 12, - padding: 16, - marginBottom: 16, - marginHorizontal: 16, - shadowColor: "#000", - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.05, - shadowRadius: 8, - elevation: 2, - }, - headerTitle: { - fontSize: 18, - fontWeight: "600", - color: colors.black, - }, - saveButton: { - padding: 8, - }, - saveButtonText: { - fontSize: 16, - fontWeight: "600", - color: colors.primaryBlue, - }, - content: { - flex: 1, - paddingHorizontal: 16, - }, - section: { - paddingVertical: 20, - }, - sectionTitle: { - fontSize: 18, - fontWeight: "700", - color: colors.black, - marginLeft: 8, - }, - sectionDescription: { - fontSize: 15, - color: colors.darkGrey, - lineHeight: 22, - }, - inputContainer: { - marginBottom: 20, - }, - inputLabel: { - fontSize: 16, - fontWeight: "600", - color: colors.black, - marginBottom: 8, - }, - input: { - backgroundColor: colors.neutralLight, - borderRadius: 12, - paddingHorizontal: 16, - paddingVertical: 14, - fontSize: 16, - color: colors.black, - borderWidth: 1, - borderColor: colors.lightGrey, - }, - inputHelper: { - fontSize: 13, - color: colors.darkGrey, - marginTop: 6, - }, - infoCard: { - flexDirection: "row", - backgroundColor: colors.softBlue, - borderRadius: 12, - padding: 16, - marginTop: 20, - marginBottom: 30, - }, - infoIconContainer: { - marginRight: 12, - marginTop: 2, - }, - infoTextContainer: { - flex: 1, - }, - infoTitle: { - fontSize: 14, - fontWeight: "600", - color: colors.primaryBlue, - marginBottom: 6, - }, - infoText: { - fontSize: 13, - color: colors.primaryBlue, - lineHeight: 18, - }, - // --- Estilos melhorados para o VehicleSelector Card --- - vehicleSelectCard: { - backgroundColor: colors.white, - borderRadius: 16, - padding: 20, - marginHorizontal: 16, - marginVertical: 12, - borderWidth: 1, - borderColor: colors.lightGrey, - shadowColor: "#000", - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.08, - shadowRadius: 8, - elevation: 4, - }, - cardHeader: { - flexDirection: "row", - alignItems: "center", - marginBottom: 16, - }, - iconContainer: { - width: 48, - height: 48, - borderRadius: 24, - backgroundColor: colors.lightPink, - justifyContent: "center", - alignItems: "center", - marginRight: 16, - }, - headerTextContainer: { - flex: 1, - }, - cardTitle: { - fontSize: 18, - fontWeight: "700", - color: colors.black, - marginBottom: 2, - }, - cardSubtitle: { - fontSize: 14, - color: colors.darkGrey, - }, - chevronIcon: { - marginLeft: 8, - }, - vehicleInfoDisplay: { - paddingTop: 8, - }, - vehicleInfoRow: { - flexDirection: "row", - justifyContent: "space-between", - marginBottom: 12, - }, - infoItem: { - flex: 1, - marginRight: 16, - }, - infoLabel: { - fontSize: 12, - fontWeight: "600", - color: colors.darkGrey, - textTransform: "uppercase", - letterSpacing: 0.5, - marginBottom: 4, - }, - infoValue: { - fontSize: 16, - fontWeight: "600", - color: colors.black, - }, - addVehicleContainer: { - paddingTop: 8, - }, - addVehicleContent: { - alignItems: "center", - paddingVertical: 20, - }, - addIcon: { - marginBottom: 12, - }, - addVehicleText: { - fontSize: 16, - fontWeight: "600", - color: colors.primaryPink, - textAlign: "center", - marginBottom: 4, - }, - addVehicleSubtext: { - fontSize: 14, - color: colors.darkGrey, - textAlign: "center", - lineHeight: 20, - }, + container: { flex: 1, backgroundColor: colors.white }, + header: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 16, paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: colors.lightGrey, paddingTop: Platform.OS === "ios" ? 50 : 12 }, + closeButton: { padding: 8 }, + headerTitle: { fontSize: 18, fontWeight: "600", color: colors.black }, + saveButton: { padding: 8 }, + saveButtonText: { fontSize: 16, fontWeight: "600", color: colors.primaryBlue }, + content: { flex: 1, paddingHorizontal: 16 }, + section: { paddingVertical: 20 }, + sectionTitle: { fontSize: 18, fontWeight: "700", color: colors.black }, + sectionDescription: { fontSize: 15, color: colors.darkGrey, lineHeight: 22, marginTop: 4 }, + inputContainer: { marginBottom: 24 }, + inputLabel: { fontSize: 16, fontWeight: "600", color: colors.black, marginBottom: 8 }, + input: { backgroundColor: colors.neutralLight, borderRadius: 12, paddingHorizontal: 16, paddingVertical: 14, fontSize: 16, color: colors.black, borderWidth: 1, borderColor: colors.lightGrey }, + inputHelper: { fontSize: 13, color: colors.darkGrey, marginTop: 6 }, + infoCard: { flexDirection: "row", backgroundColor: colors.softBlue, borderRadius: 12, padding: 16, marginTop: 20, marginBottom: 30 }, + infoIconContainer: { marginRight: 12, marginTop: 2 }, + infoTextContainer: { flex: 1 }, + infoTitle: { fontSize: 14, fontWeight: "600", color: colors.primaryBlue, marginBottom: 6 }, + infoText: { fontSize: 13, color: colors.primaryBlue, lineHeight: 18 }, + vehicleSelectCard: { backgroundColor: colors.white, borderRadius: 16, padding: 20, marginHorizontal: 16, marginVertical: 12, borderWidth: 1, borderColor: colors.lightGrey, shadowColor: "#000", shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.08, shadowRadius: 8, elevation: 4 }, + cardHeader: { flexDirection: "row", alignItems: "center", marginBottom: 16 }, + iconContainer: { width: 48, height: 48, borderRadius: 24, backgroundColor: colors.lightPink, justifyContent: "center", alignItems: "center", marginRight: 16 }, + headerTextContainer: { flex: 1 }, + cardTitle: { fontSize: 18, fontWeight: "700", color: colors.black, marginBottom: 2 }, + cardSubtitle: { fontSize: 14, color: colors.darkGrey }, + chevronIcon: { marginLeft: 8 }, + vehicleInfoDisplay: { paddingTop: 8 }, + vehicleInfoRow: { flexDirection: "row", justifyContent: "space-between", marginBottom: 12 }, + infoItem: { flex: 1, marginRight: 16 }, + infoLabel: { fontSize: 12, fontWeight: "600", color: colors.darkGrey, textTransform: "uppercase", letterSpacing: 0.5, marginBottom: 4 }, + infoValue: { fontSize: 16, fontWeight: "600", color: colors.black }, + addVehicleContainer: { paddingTop: 8 }, + addVehicleContent: { alignItems: "center", paddingVertical: 20 }, + addIcon: { marginBottom: 12 }, + addVehicleText: { fontSize: 16, fontWeight: "600", color: colors.primaryPink, textAlign: "center", marginBottom: 4 }, + addVehicleSubtext: { fontSize: 14, color: colors.darkGrey, textAlign: "center", lineHeight: 20 }, + cnhInputWrapper: { flexDirection: 'row', alignItems: 'center' }, + cnhInput: { flex: 1, backgroundColor: colors.neutralLight, borderRadius: 12, paddingHorizontal: 16, paddingVertical: 14, fontSize: 16, color: colors.black, borderWidth: 1, borderColor: colors.lightGrey }, + validationButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginLeft: 10, paddingHorizontal: 16, height: 50, borderRadius: 12, backgroundColor: colors.softBlue }, + validationButtonText: { fontSize: 15, fontWeight: '600', color: colors.primaryBlue }, + idleButton: {}, + validatingButton: { backgroundColor: colors.lightGrey }, + validButton: { backgroundColor: '#e9f7ef', borderColor: colors.success, borderWidth: 1 }, + invalidButton: { backgroundColor: '#fdecea', borderColor: colors.danger, borderWidth: 1 }, + disabledButton: { backgroundColor: colors.lightGrey, opacity: 0.7, }, }); -export default VehicleSelector; +export default VehicleSelector; \ No newline at end of file diff --git a/src/strada/mobile/src/services/user.service.ts b/src/strada/mobile/src/services/user.service.ts index 23543fd..1442093 100644 --- a/src/strada/mobile/src/services/user.service.ts +++ b/src/strada/mobile/src/services/user.service.ts @@ -17,6 +17,8 @@ export const createUser = async (user: IUserRequest) => { export const getUser = async (id: string) => { try { const response = await authAxios.get(`/users/${id}`); + console.log("getting user\n\n"); + console.log(response.data); return response.data; } catch (error) { console.error(error); @@ -26,7 +28,11 @@ export const getUser = async (id: string) => { export const updateUser = async (id: string, user: IUserRequest) => { try { + console.log("updating user"); + console.log(user); const response = await authAxios.put(`/users/${id}`, user); + console.log("sent"); + console.log(response.data); storeUser(response.data); return response.data; } catch (error) { @@ -40,7 +46,8 @@ export const storeUserID = async (id: string) => { }; export const getStoredUser = async () => { - return await SecureStore.getItemAsync("user"); + const user = await SecureStore.getItemAsync("user"); + return user; }; export const storeUser = async (user: Partial) => { From 7d4209ca412c53acc2af065fc9c8e5f4ce1aef69 Mon Sep 17 00:00:00 2001 From: Uinicivs Date: Sat, 28 Jun 2025 23:54:16 -0300 Subject: [PATCH 4/4] fix(offer-ride): single from-to, cnh validation, car info persistence --- .../20250628004113_add_cnh_to_user/migration.sql | 2 ++ .../20250628221349_add_car_info/migration.sql | 4 ++++ src/strada/auth-service/prisma/schema.prisma | 10 ++++++++-- .../src/user/core/entities/user.entity.ts | 6 ++++++ .../core/interfaces/user/user-request.interface.ts | 7 ++++++- .../src/user/core/mappers/user.mapper.ts | 7 +++++++ .../src/user/core/use-cases/update-user.use-case.ts | 6 ++++++ .../infrastructure/controllers/user.controller.ts | 9 +-------- .../repositories/user.repository.impl.ts | 12 ++++++++++++ src/strada/mobile/.env | 2 +- 10 files changed, 53 insertions(+), 12 deletions(-) create mode 100644 src/strada/auth-service/prisma/migrations/20250628004113_add_cnh_to_user/migration.sql create mode 100644 src/strada/auth-service/prisma/migrations/20250628221349_add_car_info/migration.sql diff --git a/src/strada/auth-service/prisma/migrations/20250628004113_add_cnh_to_user/migration.sql b/src/strada/auth-service/prisma/migrations/20250628004113_add_cnh_to_user/migration.sql new file mode 100644 index 0000000..ebee3a6 --- /dev/null +++ b/src/strada/auth-service/prisma/migrations/20250628004113_add_cnh_to_user/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "cnh" TEXT; diff --git a/src/strada/auth-service/prisma/migrations/20250628221349_add_car_info/migration.sql b/src/strada/auth-service/prisma/migrations/20250628221349_add_car_info/migration.sql new file mode 100644 index 0000000..8be68b3 --- /dev/null +++ b/src/strada/auth-service/prisma/migrations/20250628221349_add_car_info/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "license_plate" TEXT, +ADD COLUMN "vehicle_color" TEXT, +ADD COLUMN "vehicle_model" TEXT; diff --git a/src/strada/auth-service/prisma/schema.prisma b/src/strada/auth-service/prisma/schema.prisma index fed322d..9b9ef33 100644 --- a/src/strada/auth-service/prisma/schema.prisma +++ b/src/strada/auth-service/prisma/schema.prisma @@ -24,8 +24,14 @@ model User { authProvider String? @default("local") // Informações pessoais e documentos (opcionais) - cpf String? @unique - rg String? + cpf String? @unique + rg String? + cnh String? + + vehicle_model String? + vehicle_color String? + license_plate String? + birthDate DateTime? phone String? address String? diff --git a/src/strada/auth-service/src/user/core/entities/user.entity.ts b/src/strada/auth-service/src/user/core/entities/user.entity.ts index c5602f6..e74943a 100644 --- a/src/strada/auth-service/src/user/core/entities/user.entity.ts +++ b/src/strada/auth-service/src/user/core/entities/user.entity.ts @@ -15,6 +15,12 @@ export class User { // Novos campos de documentos e informações pessoais (opcionais) cpf?: string; rg?: string; + cnh?: string; + + vehicle_model?: string; + vehicle_color?: string; + license_plate?: string; + birthDate?: Date; phone?: string; address?: string; diff --git a/src/strada/auth-service/src/user/core/interfaces/user/user-request.interface.ts b/src/strada/auth-service/src/user/core/interfaces/user/user-request.interface.ts index 8f15b97..2fcb2cf 100644 --- a/src/strada/auth-service/src/user/core/interfaces/user/user-request.interface.ts +++ b/src/strada/auth-service/src/user/core/interfaces/user/user-request.interface.ts @@ -1,4 +1,4 @@ -import { UserType } from "@prisma/client"; +import { UserType } from '@prisma/client'; export interface IUserRequest { name?: string; @@ -9,6 +9,7 @@ export interface IUserRequest { authProvider?: string; cpf?: string; rg?: string; + cnh?: string; birthDate?: Date; phone?: string; address?: string; @@ -17,4 +18,8 @@ export interface IUserRequest { state?: string; userType?: UserType; isActive?: boolean; + + vehicle_model?: string; + vehicle_color?: string; + license_plate?: string; } diff --git a/src/strada/auth-service/src/user/core/mappers/user.mapper.ts b/src/strada/auth-service/src/user/core/mappers/user.mapper.ts index ecb334f..955bf3d 100644 --- a/src/strada/auth-service/src/user/core/mappers/user.mapper.ts +++ b/src/strada/auth-service/src/user/core/mappers/user.mapper.ts @@ -23,6 +23,13 @@ export class UserMapper { if (data.state !== undefined) partialUser.state = data.state; if (data.userType !== undefined) partialUser.userType = data.userType; if (data.isActive !== undefined) partialUser.isActive = data.isActive; + if (data.cnh !== undefined) partialUser.cnh = data.cnh; + if (data.vehicle_model !== undefined) + partialUser.vehicle_model = data.vehicle_model; + if (data.vehicle_color !== undefined) + partialUser.vehicle_color = data.vehicle_color; + if (data.license_plate !== undefined) + partialUser.license_plate = data.license_plate; return partialUser; } diff --git a/src/strada/auth-service/src/user/core/use-cases/update-user.use-case.ts b/src/strada/auth-service/src/user/core/use-cases/update-user.use-case.ts index e710993..6840e7c 100644 --- a/src/strada/auth-service/src/user/core/use-cases/update-user.use-case.ts +++ b/src/strada/auth-service/src/user/core/use-cases/update-user.use-case.ts @@ -14,6 +14,9 @@ export class UpdateUserUseCase { async execute(id: string, dto: IUserRequest): Promise { const existingUser = await this.findUserByIdUseCase.execute(id); + console.log('updating...'); + console.log(dto); + try { const userToUpdate = UserMapper.toPartialEntity({ ...existingUser, @@ -28,6 +31,9 @@ export class UpdateUserUseCase { delete userToUpdate.password; } + console.log('use to update'); + console.log(userToUpdate); + return await this.userRepository.update(id, userToUpdate); } catch (error) { console.log(error); diff --git a/src/strada/auth-service/src/user/infrastructure/controllers/user.controller.ts b/src/strada/auth-service/src/user/infrastructure/controllers/user.controller.ts index 3c16988..113be1f 100644 --- a/src/strada/auth-service/src/user/infrastructure/controllers/user.controller.ts +++ b/src/strada/auth-service/src/user/infrastructure/controllers/user.controller.ts @@ -1,10 +1,4 @@ -import { - Body, - Controller, Get, Param, - Post, - Put, - Query -} from '@nestjs/common'; +import { Body, Controller, Get, Param, Post, Put, Query } from '@nestjs/common'; import { CreateUserUseCase } from '@/src/user/core/use-cases/create-user.use-case'; import { UserRequestDto } from '../dto/user-request.dto'; import { UserResponseDto } from '../dto/user-response.dto'; @@ -49,5 +43,4 @@ export class UserController { ) { return await this.findAllUseCase.execute({ name, page, limit }); } - } diff --git a/src/strada/auth-service/src/user/infrastructure/repositories/user.repository.impl.ts b/src/strada/auth-service/src/user/infrastructure/repositories/user.repository.impl.ts index 6388be6..af9bc4a 100644 --- a/src/strada/auth-service/src/user/infrastructure/repositories/user.repository.impl.ts +++ b/src/strada/auth-service/src/user/infrastructure/repositories/user.repository.impl.ts @@ -26,6 +26,7 @@ export class UserRepository implements IUserRepository { authProvider: user.authProvider, cpf: user.cpf, rg: user.rg, + cnh: user.cnh, birthDate: user.birthDate, phone: user.phone, address: user.address, @@ -33,6 +34,9 @@ export class UserRepository implements IUserRepository { city: user.city, state: user.state, userType: user.userType, + vehicle_model: user.vehicle_model, + vehicle_color: user.vehicle_color, + license_plate: user.license_plate, }, select: { id: true, @@ -42,6 +46,7 @@ export class UserRepository implements IUserRepository { imgUrl: true, cpf: true, rg: true, + cnh: true, birthDate: true, phone: true, address: true, @@ -53,6 +58,9 @@ export class UserRepository implements IUserRepository { isActive: true, authProvider: false, password: false, + vehicle_model: true, + vehicle_color: true, + license_plate: true, }, }); @@ -70,6 +78,7 @@ export class UserRepository implements IUserRepository { authProvider: user.authProvider, cpf: user.cpf, rg: user.rg, + cnh: user.cnh, birthDate: user.birthDate, phone: user.phone, address: user.address, @@ -77,6 +86,9 @@ export class UserRepository implements IUserRepository { city: user.city, state: user.state, userType: user.userType, + vehicle_model: user.vehicle_model, + vehicle_color: user.vehicle_color, + license_plate: user.license_plate, }; if (user.password) { diff --git a/src/strada/mobile/.env b/src/strada/mobile/.env index da1ece5..d9060a9 100644 --- a/src/strada/mobile/.env +++ b/src/strada/mobile/.env @@ -11,4 +11,4 @@ EXPO_PUBLIC_USE_HTTPS=true # EXPO_PUBLIC_BASE_DOMAIN=10.0.2.2:3000 # EXPO_PUBLIC_USE_HTTPS=false -EXPO_PUBLIC_GOOGLE_MAPS_API_KEY=AIzaSyCZDYi8cAv8vBuk2Z1EKIoh_Lj6FsuBhkU \ No newline at end of file +EXPO_PUBLIC_GOOGLE_MAPS_API_KEY=a \ No newline at end of file