Primeiro deploy
This commit is contained in:
parent
4492a12ca2
commit
358c0e5aec
13 changed files with 1462 additions and 1257 deletions
3
toptran-app/.vscode/settings.json
vendored
3
toptran-app/.vscode/settings.json
vendored
|
|
@ -3,5 +3,6 @@
|
||||||
"source.fixAll": "explicit",
|
"source.fixAll": "explicit",
|
||||||
"source.organizeImports": "explicit",
|
"source.organizeImports": "explicit",
|
||||||
"source.sortMembers": "explicit"
|
"source.sortMembers": "explicit"
|
||||||
}
|
},
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,16 @@
|
||||||
"icon": "./assets/toptranico.png",
|
"icon": "./assets/toptranico.png",
|
||||||
"scheme": "toptranapp",
|
"scheme": "toptranapp",
|
||||||
"userInterfaceStyle": "automatic",
|
"userInterfaceStyle": "automatic",
|
||||||
"newArchEnabled": true,
|
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true
|
"supportsTablet": true,
|
||||||
|
"bundleIdentifier": "com.rayankonecny.toptranapp"
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"edgeToEdgeEnabled": true,
|
"predictiveBackGestureEnabled": false,
|
||||||
"predictiveBackGestureEnabled": false
|
"package": "com.rayankonecny.toptranapp",
|
||||||
|
"permissions": [
|
||||||
|
"android.permission.RECORD_AUDIO"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"bundler": "metro",
|
"bundler": "metro",
|
||||||
|
|
@ -23,10 +26,25 @@
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"expo-router",
|
"expo-router",
|
||||||
"expo-secure-store",
|
"expo-secure-store",
|
||||||
"expo-asset"
|
"expo-asset",
|
||||||
|
[
|
||||||
|
"expo-image-picker",
|
||||||
|
{
|
||||||
|
"photosPermission": "O app precisa de acesso à galeria para trocar sua foto de perfil."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"expo-font",
|
||||||
|
"expo-sharing",
|
||||||
|
"react-native-edge-to-edge"
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
"typedRoutes": true
|
"typedRoutes": true
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"router": {},
|
||||||
|
"eas": {
|
||||||
|
"projectId": "46790e12-238d-4e86-b778-603ee512de3b"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 189 KiB |
6
toptran-app/babel.config.js
Normal file
6
toptran-app/babel.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = function (api) {
|
||||||
|
api.cache(true);
|
||||||
|
return {
|
||||||
|
presets: ['babel-preset-expo'],
|
||||||
|
};
|
||||||
|
};
|
||||||
24
toptran-app/eas.json
Normal file
24
toptran-app/eas.json
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"cli": {
|
||||||
|
"version": ">= 18.8.1",
|
||||||
|
"appVersionSource": "remote"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"development": {
|
||||||
|
"developmentClient": true,
|
||||||
|
"distribution": "internal"
|
||||||
|
},
|
||||||
|
"preview": {
|
||||||
|
"distribution": "internal",
|
||||||
|
"android": {
|
||||||
|
"buildType": "apk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"autoIncrement": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"submit": {
|
||||||
|
"production": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
toptran-app/metro.config.js
Normal file
5
toptran-app/metro.config.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
const { getDefaultConfig } = require('expo/metro-config');
|
||||||
|
|
||||||
|
const config = getDefaultConfig(__dirname);
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
2187
toptran-app/package-lock.json
generated
2187
toptran-app/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -4,36 +4,40 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "expo start",
|
"start": "expo start",
|
||||||
"android": "expo start --android",
|
"android": "expo run:android",
|
||||||
"ios": "expo start --ios",
|
"ios": "expo run:ios",
|
||||||
"web": "expo start --web"
|
"web": "expo start --web"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
"@react-native-picker/picker": "^2.7.5",
|
"@react-native-picker/picker": "^2.7.5",
|
||||||
"axios": "^1.15.2",
|
"axios": "^1.15.2",
|
||||||
"expo": "~54.0.33",
|
"expo": "^55.0.19",
|
||||||
"expo-asset": "~12.0.13",
|
"expo-asset": "~55.0.16",
|
||||||
"expo-constants": "~18.0.13",
|
"expo-constants": "~55.0.15",
|
||||||
"expo-file-system": "~19.0.22",
|
"expo-dev-client": "~55.0.30",
|
||||||
"expo-font": "~14.0.11",
|
"expo-file-system": "~55.0.17",
|
||||||
"expo-linking": "~8.0.11",
|
"expo-font": "~55.0.6",
|
||||||
"expo-print": "~15.0.8",
|
"expo-image-picker": "~55.0.19",
|
||||||
"expo-router": "~6.0.23",
|
"expo-linking": "~55.0.14",
|
||||||
"expo-secure-store": "~15.0.8",
|
"expo-print": "~55.0.13",
|
||||||
"expo-sharing": "~14.0.8",
|
"expo-router": "~55.0.13",
|
||||||
"expo-splash-screen": "~31.0.13",
|
"expo-secure-store": "~55.0.13",
|
||||||
"expo-sqlite": "^55.0.15",
|
"expo-sharing": "~55.0.18",
|
||||||
"expo-status-bar": "~3.0.9",
|
"expo-splash-screen": "~55.0.19",
|
||||||
"react": "19.1.0",
|
"expo-sqlite": "~55.0.15",
|
||||||
"react-dom": "19.1.0",
|
"expo-status-bar": "~55.0.5",
|
||||||
"react-native": "0.81.5",
|
"expo-system-ui": "~55.0.16",
|
||||||
|
"react": "19.2.0",
|
||||||
|
"react-dom": "19.2.0",
|
||||||
|
"react-native": "0.83.6",
|
||||||
|
"react-native-edge-to-edge": "^1.8.1",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.23.0",
|
||||||
"react-native-web": "~0.21.0"
|
"react-native-web": "~0.21.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "~19.1.0",
|
"@types/react": "~19.2.10",
|
||||||
"react-test-renderer": "19.1.0",
|
"react-test-renderer": "19.1.0",
|
||||||
"typescript": "~5.9.2"
|
"typescript": "~5.9.2"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { BORDER_RADIUS, COLORS, SPACING } from "@/constants/theme";
|
import { BORDER_RADIUS, COLORS, SPACING } from "@/constants/theme";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { obterCorridas } from "@/services/db";
|
import { getSetting, obterCorridas } from "@/services/db";
|
||||||
import { router, useFocusEffect } from "expo-router";
|
import { router, useFocusEffect } from "expo-router";
|
||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
Image,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
|
|
@ -44,28 +45,33 @@ export default function Home() {
|
||||||
totalKm: 0,
|
totalKm: 0,
|
||||||
totalGanhos: 0,
|
totalGanhos: 0,
|
||||||
});
|
});
|
||||||
|
const [photoUri, setPhotoUri] = useState<string | null>(null);
|
||||||
|
|
||||||
const loadStats = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
if (!user?.id) return;
|
if (!user?.id) return;
|
||||||
try {
|
try {
|
||||||
const corridas = await obterCorridas(user.id);
|
const [corridas, photo] = await Promise.all([
|
||||||
|
obterCorridas(user.id),
|
||||||
|
getSetting("profilePhoto"),
|
||||||
|
]);
|
||||||
setStats({
|
setStats({
|
||||||
totalCorridas: corridas.length,
|
totalCorridas: corridas.length,
|
||||||
totalKm: corridas.reduce((acc, c) => acc + c.km, 0),
|
totalKm: corridas.reduce((acc, c) => acc + c.km, 0),
|
||||||
totalGanhos: corridas.reduce((acc, c) => acc + c.total, 0),
|
totalGanhos: corridas.reduce((acc, c) => acc + c.total, 0),
|
||||||
});
|
});
|
||||||
|
if (photo) setPhotoUri(photo);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao carregar estatísticas:", error);
|
console.error("Erro ao carregar dados:", error);
|
||||||
}
|
}
|
||||||
}, [user?.id]);
|
}, [user?.id]);
|
||||||
|
|
||||||
useFocusEffect(useCallback(() => { loadStats(); }, [loadStats]));
|
useFocusEffect(useCallback(() => { loadData(); }, [loadData]));
|
||||||
|
|
||||||
const handleProfilePress = () => {
|
const handleProfilePress = () => {
|
||||||
Alert.alert(userName, user?.email ?? "", [
|
Alert.alert(userName, user?.email ?? "", [
|
||||||
{
|
{
|
||||||
text: "Editar Perfil",
|
text: "Editar Perfil",
|
||||||
onPress: () => Alert.alert("Em breve", "Funcionalidade em desenvolvimento."),
|
onPress: () => router.push("/perfil" as any),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Sair",
|
text: "Sair",
|
||||||
|
|
@ -146,9 +152,13 @@ export default function Home() {
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity style={styles.avatarButton} onPress={handleProfilePress} activeOpacity={0.8}>
|
<TouchableOpacity style={styles.avatarButton} onPress={handleProfilePress} activeOpacity={0.8}>
|
||||||
<View style={styles.avatar}>
|
<View style={styles.avatar}>
|
||||||
<Text style={styles.avatarText}>
|
{photoUri ? (
|
||||||
{firstName.charAt(0).toUpperCase()}
|
<Image source={{ uri: photoUri }} style={styles.avatarImage} />
|
||||||
</Text>
|
) : (
|
||||||
|
<Text style={styles.avatarText}>
|
||||||
|
{firstName.charAt(0).toUpperCase()}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.avatarOnline} />
|
<View style={styles.avatarOnline} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
@ -262,6 +272,11 @@ const styles = StyleSheet.create({
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
|
avatarImage: {
|
||||||
|
width: 42,
|
||||||
|
height: 42,
|
||||||
|
borderRadius: BORDER_RADIUS.round,
|
||||||
|
},
|
||||||
avatarText: {
|
avatarText: {
|
||||||
color: COLORS.text,
|
color: COLORS.text,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
|
|
|
||||||
340
toptran-app/src/app/perfil.tsx
Normal file
340
toptran-app/src/app/perfil.tsx
Normal file
|
|
@ -0,0 +1,340 @@
|
||||||
|
import { Input } from "@/components/Input";
|
||||||
|
import { BORDER_RADIUS, COLORS, SPACING } from "@/constants/theme";
|
||||||
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
|
import { getSetting, setSetting } from "@/services/db";
|
||||||
|
import { api } from "@/server/api";
|
||||||
|
import { File, Paths } from "expo-file-system";
|
||||||
|
import * as ImagePicker from "expo-image-picker";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
Alert,
|
||||||
|
Image,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
Platform,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
|
|
||||||
|
export default function PerfilPage() {
|
||||||
|
const { user, updateUser } = useAuth();
|
||||||
|
|
||||||
|
const [name, setName] = useState(user?.name ?? "");
|
||||||
|
const [bio, setBio] = useState("");
|
||||||
|
const [photoUri, setPhotoUri] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function loadProfile() {
|
||||||
|
try {
|
||||||
|
// Carrega local primeiro (rápido, offline)
|
||||||
|
const [savedPhoto, savedBio] = await Promise.all([
|
||||||
|
getSetting("profilePhoto"),
|
||||||
|
getSetting("profileBio"),
|
||||||
|
]);
|
||||||
|
if (savedPhoto) setPhotoUri(savedPhoto);
|
||||||
|
if (savedBio) setBio(savedBio);
|
||||||
|
|
||||||
|
// Atualiza com dados do backend
|
||||||
|
const { data } = await api.get("/users/profile");
|
||||||
|
const profile = data.data ?? data;
|
||||||
|
if (profile.name) setName(profile.name);
|
||||||
|
if (profile.bio) setBio(profile.bio);
|
||||||
|
if (profile.profilePhoto) setPhotoUri(profile.profilePhoto);
|
||||||
|
} catch {
|
||||||
|
// fica com os dados locais se offline
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadProfile();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePickPhoto = async () => {
|
||||||
|
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||||
|
if (status !== "granted") {
|
||||||
|
Alert.alert("Permissão negada", "Permita o acesso à galeria nas configurações do celular.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await ImagePicker.launchImageLibraryAsync({
|
||||||
|
mediaTypes: "images",
|
||||||
|
allowsEditing: true,
|
||||||
|
aspect: [1, 1],
|
||||||
|
quality: 0.8,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.canceled || !result.assets[0]) return;
|
||||||
|
|
||||||
|
const picked = result.assets[0].uri;
|
||||||
|
try {
|
||||||
|
const dest = new File(Paths.document, `avatar_${user?.id ?? "user"}.jpg`);
|
||||||
|
new File(picked).copy(dest);
|
||||||
|
setPhotoUri(dest.uri);
|
||||||
|
} catch {
|
||||||
|
setPhotoUri(picked);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
const nameTrimmed = name.trim();
|
||||||
|
if (!nameTrimmed) {
|
||||||
|
Alert.alert("Nome inválido", "O nome não pode estar vazio.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSaving(true);
|
||||||
|
|
||||||
|
// Persiste localmente
|
||||||
|
await Promise.all([
|
||||||
|
updateUser({ name: nameTrimmed }),
|
||||||
|
setSetting("profileBio", bio.trim()),
|
||||||
|
photoUri ? setSetting("profilePhoto", photoUri) : Promise.resolve(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Envia ao backend
|
||||||
|
await api.patch("/users/profile", {
|
||||||
|
name: nameTrimmed,
|
||||||
|
bio: bio.trim(),
|
||||||
|
profilePhoto: photoUri ?? undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
Alert.alert("Salvo", "Perfil atualizado com sucesso.", [
|
||||||
|
{ text: "OK", onPress: () => router.back() },
|
||||||
|
]);
|
||||||
|
} catch (e: any) {
|
||||||
|
const msg = e?.response?.data?.error ?? e?.message ?? "Não foi possível salvar.";
|
||||||
|
Alert.alert("Erro", msg);
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initials = (user?.name ?? "U")
|
||||||
|
.split(" ")
|
||||||
|
.slice(0, 2)
|
||||||
|
.map((w) => w.charAt(0).toUpperCase())
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.container}>
|
||||||
|
<View style={styles.loadingContainer}>
|
||||||
|
<ActivityIndicator color={COLORS.text} size="large" />
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.container}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<TouchableOpacity onPress={() => router.back()} style={styles.backButton} activeOpacity={0.7}>
|
||||||
|
<Text style={styles.backIcon}>←</Text>
|
||||||
|
<Text style={styles.backLabel}>Voltar</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.headerTitle}>Editar Perfil</Text>
|
||||||
|
<View style={{ width: 70 }} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={styles.scroll}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
keyboardShouldPersistTaps="handled"
|
||||||
|
>
|
||||||
|
{/* Avatar */}
|
||||||
|
<View style={styles.avatarSection}>
|
||||||
|
<TouchableOpacity onPress={handlePickPhoto} activeOpacity={0.8} style={styles.avatarWrapper}>
|
||||||
|
{photoUri ? (
|
||||||
|
<Image source={{ uri: photoUri }} style={styles.avatarImage} />
|
||||||
|
) : (
|
||||||
|
<View style={styles.avatarPlaceholder}>
|
||||||
|
<Text style={styles.avatarInitials}>{initials}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View style={styles.avatarEditBadge}>
|
||||||
|
<Text style={styles.avatarEditIcon}>✏️</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={styles.avatarHint}>Toque para alterar a foto</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Campos */}
|
||||||
|
<View style={styles.form}>
|
||||||
|
<View style={styles.fieldGroup}>
|
||||||
|
<Text style={styles.fieldLabel}>Nome</Text>
|
||||||
|
<Input
|
||||||
|
value={name}
|
||||||
|
onChangeText={setName}
|
||||||
|
placeholder="Seu nome completo"
|
||||||
|
autoCapitalize="words"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.fieldGroup}>
|
||||||
|
<Text style={styles.fieldLabel}>E-mail</Text>
|
||||||
|
<View style={styles.readonlyField}>
|
||||||
|
<Text style={styles.readonlyText}>{user?.email}</Text>
|
||||||
|
<Text style={styles.lockIcon}>🔒</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.fieldHint}>O e-mail não pode ser alterado.</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.fieldGroup}>
|
||||||
|
<Text style={styles.fieldLabel}>Bio</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.bioInput}
|
||||||
|
value={bio}
|
||||||
|
onChangeText={setBio}
|
||||||
|
placeholder="Fale um pouco sobre você..."
|
||||||
|
placeholderTextColor={COLORS.inputPlaceholder}
|
||||||
|
multiline
|
||||||
|
numberOfLines={4}
|
||||||
|
textAlignVertical="top"
|
||||||
|
maxLength={200}
|
||||||
|
/>
|
||||||
|
<Text style={styles.fieldHint}>{bio.length}/200 caracteres</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.saveButton, saving && styles.saveButtonDisabled]}
|
||||||
|
onPress={handleSave}
|
||||||
|
disabled={saving}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
>
|
||||||
|
{saving ? (
|
||||||
|
<ActivityIndicator color={COLORS.background} size="small" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.saveButtonText}>Salvar Alterações</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</ScrollView>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const AVATAR_SIZE = 110;
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: { flex: 1, backgroundColor: COLORS.background },
|
||||||
|
|
||||||
|
loadingContainer: { flex: 1, justifyContent: "center", alignItems: "center" },
|
||||||
|
|
||||||
|
header: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
paddingHorizontal: SPACING.lg,
|
||||||
|
paddingVertical: SPACING.md,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: COLORS.border,
|
||||||
|
},
|
||||||
|
backButton: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: SPACING.xs,
|
||||||
|
width: 70,
|
||||||
|
},
|
||||||
|
backIcon: { fontSize: 18, color: COLORS.text },
|
||||||
|
backLabel: { fontSize: 15, color: COLORS.textSecondary, fontWeight: "500" },
|
||||||
|
headerTitle: { fontSize: 16, fontWeight: "700", color: COLORS.text },
|
||||||
|
|
||||||
|
scroll: { padding: SPACING.lg, paddingBottom: 48 },
|
||||||
|
|
||||||
|
avatarSection: { alignItems: "center", marginTop: SPACING.xl, marginBottom: SPACING.xxl },
|
||||||
|
avatarWrapper: { position: "relative" },
|
||||||
|
avatarImage: {
|
||||||
|
width: AVATAR_SIZE,
|
||||||
|
height: AVATAR_SIZE,
|
||||||
|
borderRadius: AVATAR_SIZE / 2,
|
||||||
|
borderWidth: 3,
|
||||||
|
borderColor: COLORS.borderLight,
|
||||||
|
},
|
||||||
|
avatarPlaceholder: {
|
||||||
|
width: AVATAR_SIZE,
|
||||||
|
height: AVATAR_SIZE,
|
||||||
|
borderRadius: AVATAR_SIZE / 2,
|
||||||
|
backgroundColor: COLORS.surfaceLight,
|
||||||
|
borderWidth: 3,
|
||||||
|
borderColor: COLORS.borderLight,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
avatarInitials: { fontSize: 38, fontWeight: "800", color: COLORS.text },
|
||||||
|
avatarEditBadge: {
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 4,
|
||||||
|
right: 4,
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: 16,
|
||||||
|
backgroundColor: COLORS.text,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: COLORS.background,
|
||||||
|
},
|
||||||
|
avatarEditIcon: { fontSize: 14 },
|
||||||
|
avatarHint: { marginTop: SPACING.md, fontSize: 13, color: COLORS.textTertiary },
|
||||||
|
|
||||||
|
form: { gap: SPACING.lg },
|
||||||
|
fieldGroup: { gap: SPACING.sm },
|
||||||
|
fieldLabel: {
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: "700",
|
||||||
|
color: COLORS.textTertiary,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: 0.8,
|
||||||
|
},
|
||||||
|
|
||||||
|
readonlyField: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
height: 48,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: COLORS.border,
|
||||||
|
backgroundColor: COLORS.surface,
|
||||||
|
borderRadius: BORDER_RADIUS.md,
|
||||||
|
paddingHorizontal: SPACING.md,
|
||||||
|
},
|
||||||
|
readonlyText: { fontSize: 15, color: COLORS.textSecondary, flex: 1 },
|
||||||
|
lockIcon: { fontSize: 14 },
|
||||||
|
|
||||||
|
bioInput: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: COLORS.inputBorder,
|
||||||
|
backgroundColor: COLORS.inputBackground,
|
||||||
|
borderRadius: BORDER_RADIUS.md,
|
||||||
|
color: COLORS.inputText,
|
||||||
|
fontSize: 15,
|
||||||
|
paddingHorizontal: SPACING.md,
|
||||||
|
paddingTop: SPACING.md,
|
||||||
|
minHeight: 100,
|
||||||
|
},
|
||||||
|
fieldHint: { fontSize: 11, color: COLORS.textTertiary },
|
||||||
|
|
||||||
|
saveButton: {
|
||||||
|
marginTop: SPACING.xxl,
|
||||||
|
backgroundColor: COLORS.text,
|
||||||
|
borderRadius: BORDER_RADIUS.lg,
|
||||||
|
paddingVertical: SPACING.lg,
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
saveButtonDisabled: { backgroundColor: COLORS.borderLight },
|
||||||
|
saveButtonText: { fontSize: 15, fontWeight: "700", color: COLORS.background },
|
||||||
|
});
|
||||||
|
|
@ -3,7 +3,7 @@ import { useAuth } from "@/contexts/AuthContext";
|
||||||
import {
|
import {
|
||||||
CompanyDB,
|
CompanyDB,
|
||||||
marcarCorridaComoSincronizada,
|
marcarCorridaComoSincronizada,
|
||||||
obterCorridasNaoSincronizadas,
|
obterCorridas,
|
||||||
obterEmpresas,
|
obterEmpresas,
|
||||||
upsertEmpresaLocal,
|
upsertEmpresaLocal,
|
||||||
} from "@/services/db";
|
} from "@/services/db";
|
||||||
|
|
@ -48,7 +48,7 @@ const INITIAL_ITEMS: SyncItem[] = [
|
||||||
{
|
{
|
||||||
key: "upload_corridas",
|
key: "upload_corridas",
|
||||||
label: "Corridas → Servidor",
|
label: "Corridas → Servidor",
|
||||||
description: "Enviar corridas pendentes ao servidor",
|
description: "Verificar e enviar corridas ausentes no servidor",
|
||||||
status: "pending",
|
status: "pending",
|
||||||
detail: "",
|
detail: "",
|
||||||
},
|
},
|
||||||
|
|
@ -125,21 +125,33 @@ export default function SincronizarPage() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 3. Upload Unsynced Rides ────────────────────────────────
|
// ── 3. Verificar e enviar corridas ausentes no servidor ────
|
||||||
update("upload_corridas", { status: "syncing" });
|
update("upload_corridas", { status: "syncing" });
|
||||||
try {
|
try {
|
||||||
const pendingRides = await obterCorridasNaoSincronizadas(user.id);
|
const todasCorridas = await obterCorridas(user.id);
|
||||||
if (pendingRides.length === 0) {
|
|
||||||
update("upload_corridas", { status: "success", detail: "Nenhuma corrida pendente" });
|
if (todasCorridas.length === 0) {
|
||||||
|
update("upload_corridas", { status: "success", detail: "Nenhuma corrida registrada" });
|
||||||
} else {
|
} else {
|
||||||
await api.post("/sync/rides", { rides: pendingRides });
|
const { data: resRides } = await api.get("/sync/rides");
|
||||||
for (const ride of pendingRides) {
|
const backendIds = new Set<string>(
|
||||||
await marcarCorridaComoSincronizada(ride.id);
|
(resRides.data ?? []).map((r: { id: string }) => r.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
const ausentes = todasCorridas.filter((r) => !backendIds.has(r.id));
|
||||||
|
|
||||||
|
if (ausentes.length === 0) {
|
||||||
|
update("upload_corridas", { status: "success", detail: "Todas as corridas já estão no servidor" });
|
||||||
|
} else {
|
||||||
|
await api.post("/sync/rides", { rides: ausentes });
|
||||||
|
for (const ride of ausentes) {
|
||||||
|
await marcarCorridaComoSincronizada(ride.id);
|
||||||
|
}
|
||||||
|
update("upload_corridas", {
|
||||||
|
status: "success",
|
||||||
|
detail: `${ausentes.length} corrida${ausentes.length !== 1 ? "s" : ""} enviada${ausentes.length !== 1 ? "s" : ""}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
update("upload_corridas", {
|
|
||||||
status: "success",
|
|
||||||
detail: `${pendingRides.length} corrida${pendingRides.length !== 1 ? "s" : ""} enviada${pendingRides.length !== 1 ? "s" : ""}`,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
update("upload_corridas", {
|
update("upload_corridas", {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
salvarUsuario,
|
salvarUsuario,
|
||||||
setSetting,
|
setSetting,
|
||||||
} from "@/services/db";
|
} from "@/services/db";
|
||||||
|
import { setAuthToken } from "@/server/api";
|
||||||
import { getUserIdFromToken } from "@/utils/jwt";
|
import { getUserIdFromToken } from "@/utils/jwt";
|
||||||
|
|
||||||
type User = {
|
type User = {
|
||||||
|
|
@ -32,6 +33,7 @@ type AuthContextType = {
|
||||||
token: string,
|
token: string,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
logout: () => Promise<void>;
|
logout: () => Promise<void>;
|
||||||
|
updateUser: (patch: Partial<Pick<User, "name">>) => Promise<void>;
|
||||||
isSignedIn: boolean;
|
isSignedIn: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -68,6 +70,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
await setSetting("user", JSON.stringify(parsedUser));
|
await setSetting("user", JSON.stringify(parsedUser));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setAuthToken(storedToken);
|
||||||
setToken(storedToken);
|
setToken(storedToken);
|
||||||
setUser(parsedUser);
|
setUser(parsedUser);
|
||||||
router.replace("/home");
|
router.replace("/home");
|
||||||
|
|
@ -95,6 +98,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
authUser: User,
|
authUser: User,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
|
setAuthToken(authToken);
|
||||||
setToken(authToken);
|
setToken(authToken);
|
||||||
setUser(authUser);
|
setUser(authUser);
|
||||||
await setSetting("authToken", authToken);
|
await setSetting("authToken", authToken);
|
||||||
|
|
@ -132,6 +136,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
try {
|
try {
|
||||||
await deleteSetting("authToken");
|
await deleteSetting("authToken");
|
||||||
await deleteSetting("user");
|
await deleteSetting("user");
|
||||||
|
setAuthToken(null);
|
||||||
setToken(null);
|
setToken(null);
|
||||||
setUser(null);
|
setUser(null);
|
||||||
router.replace("/");
|
router.replace("/");
|
||||||
|
|
@ -140,6 +145,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
updateUser: async (patch) => {
|
||||||
|
if (!user) return;
|
||||||
|
const updated: User = { ...user, ...patch };
|
||||||
|
setUser(updated);
|
||||||
|
await setSetting("user", JSON.stringify(updated));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
BIN
toptran-app/toptran-v1.apk
Normal file
BIN
toptran-app/toptran-v1.apk
Normal file
Binary file not shown.
Loading…
Add table
Reference in a new issue