feat: upgrade React Navigation to v7 and redesign appointment cards in Dashboard and Profile pages

This commit is contained in:
2026-05-06 12:44:35 +01:00
parent 99fc0a3882
commit 8f5a88788c
4 changed files with 412 additions and 144 deletions

159
package-lock.json generated
View File

@@ -9,9 +9,9 @@
"version": "1.0.0",
"dependencies": {
"@react-native-async-storage/async-storage": "2.2.0",
"@react-navigation/bottom-tabs": "^6.5.11",
"@react-navigation/native": "^6.1.9",
"@react-navigation/native-stack": "^6.9.17",
"@react-navigation/bottom-tabs": "^7.15.11",
"@react-navigation/native": "^7.2.2",
"@react-navigation/native-stack": "^7.14.12",
"@supabase/supabase-js": "^2.99.1",
"expo": "~54.0.33",
"expo-constants": "~18.0.13",
@@ -71,7 +71,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz",
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/generator": "^7.28.6",
@@ -2973,44 +2972,46 @@
"license": "MIT"
},
"node_modules/@react-navigation/bottom-tabs": {
"version": "6.6.1",
"resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-6.6.1.tgz",
"integrity": "sha512-9oD4cypEBjPuaMiu9tevWGiQ4w/d6l3HNhcJ1IjXZ24xvYDSs0mqjUcdt8SWUolCvRrYc/DmNBLlT83bk0bHTw==",
"version": "7.15.11",
"resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.15.11.tgz",
"integrity": "sha512-+WtNbd6fJgbViDNjmBUUP7eTgGH+zBtrl3jHuNnfUfXTs9YGuI5q3SiHIc9a5gY3voBOxbOXEiHJyW4xea7nAw==",
"license": "MIT",
"dependencies": {
"@react-navigation/elements": "^1.3.31",
"@react-navigation/elements": "^2.9.15",
"color": "^4.2.3",
"warn-once": "^0.1.0"
"sf-symbols-typescript": "^2.1.0"
},
"peerDependencies": {
"@react-navigation/native": "^6.0.0",
"react": "*",
"@react-navigation/native": "^7.2.2",
"react": ">= 18.2.0",
"react-native": "*",
"react-native-safe-area-context": ">= 3.0.0",
"react-native-screens": ">= 3.0.0"
"react-native-safe-area-context": ">= 4.0.0",
"react-native-screens": ">= 4.0.0"
}
},
"node_modules/@react-navigation/core": {
"version": "6.4.17",
"resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-6.4.17.tgz",
"integrity": "sha512-Nd76EpomzChWAosGqWOYE3ItayhDzIEzzZsT7PfGcRFDgW5miHV2t4MZcq9YIK4tzxZjVVpYbIynOOQQd1e0Cg==",
"version": "7.17.2",
"resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.17.2.tgz",
"integrity": "sha512-Rt2OZwcgOmjv401uLGAKaRM6xo0fiBce/A7LfRHI1oe5FV+KooWcgAoZ2XOtgKj6UzVMuQWt3b2e6rxo/mDJRA==",
"license": "MIT",
"dependencies": {
"@react-navigation/routers": "^6.1.9",
"@react-navigation/routers": "^7.5.3",
"escape-string-regexp": "^4.0.0",
"nanoid": "^3.1.23",
"fast-deep-equal": "^3.1.3",
"nanoid": "^3.3.11",
"query-string": "^7.1.3",
"react-is": "^16.13.0",
"use-latest-callback": "^0.2.1"
"react-is": "^19.1.0",
"use-latest-callback": "^0.2.4",
"use-sync-external-store": "^1.5.0"
},
"peerDependencies": {
"react": "*"
"react": ">= 18.2.0"
}
},
"node_modules/@react-navigation/core/node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"funding": [
{
"type": "github",
@@ -3026,49 +3027,62 @@
}
},
"node_modules/@react-navigation/elements": {
"version": "1.3.31",
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.31.tgz",
"integrity": "sha512-bUzP4Awlljx5RKEExw8WYtif8EuQni2glDaieYROKTnaxsu9kEIA515sXQgUDZU4Ob12VoL7+z70uO3qrlfXcQ==",
"version": "2.9.15",
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.15.tgz",
"integrity": "sha512-cyz/pPiyyC6gaTVLsGFc1g0MYgrmuCFqklAWGXMWPscr5YU3ui94vPI4vnZwcsEy0T758TQWLzmS5XudZeRKcA==",
"license": "MIT",
"dependencies": {
"color": "^4.2.3",
"use-latest-callback": "^0.2.4",
"use-sync-external-store": "^1.5.0"
},
"peerDependencies": {
"@react-navigation/native": "^6.0.0",
"react": "*",
"@react-native-masked-view/masked-view": ">= 0.2.0",
"@react-navigation/native": "^7.2.2",
"react": ">= 18.2.0",
"react-native": "*",
"react-native-safe-area-context": ">= 3.0.0"
"react-native-safe-area-context": ">= 4.0.0"
},
"peerDependenciesMeta": {
"@react-native-masked-view/masked-view": {
"optional": true
}
}
},
"node_modules/@react-navigation/native": {
"version": "6.1.18",
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-6.1.18.tgz",
"integrity": "sha512-mIT9MiL/vMm4eirLcmw2h6h/Nm5FICtnYSdohq4vTLA2FF/6PNhByM7s8ffqoVfE5L0uAa6Xda1B7oddolUiGg==",
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.2.2.tgz",
"integrity": "sha512-kem1Ko2BcbAjmbQIv66dNmr6EtfDut3QU0qjsVhMnLLhktwyXb6FzZYp8gTrUb6AvkAbaJoi+BF5Pl55pAUa5w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@react-navigation/core": "^6.4.17",
"@react-navigation/core": "^7.17.2",
"escape-string-regexp": "^4.0.0",
"fast-deep-equal": "^3.1.3",
"nanoid": "^3.1.23"
"nanoid": "^3.3.11",
"use-latest-callback": "^0.2.4"
},
"peerDependencies": {
"react": "*",
"react": ">= 18.2.0",
"react-native": "*"
}
},
"node_modules/@react-navigation/native-stack": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-6.11.0.tgz",
"integrity": "sha512-U5EcUB9Q2NQspCFwYGGNJm0h6wBCOv7T30QjndmvlawLkNt7S7KWbpWyxS9XBHSIKF57RgWjfxuJNTgTstpXxw==",
"version": "7.14.12",
"resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.14.12.tgz",
"integrity": "sha512-dUfpkrVeVKKV8iqXsmoUp3Rv0iH3YaB3eZwScru/FlcqAp/r3/qA6zEXkGX9hZK+/ziWAPFrf1frBSNbgOYSFQ==",
"license": "MIT",
"dependencies": {
"@react-navigation/elements": "^1.3.31",
"warn-once": "^0.1.0"
"@react-navigation/elements": "^2.9.15",
"color": "^4.2.3",
"sf-symbols-typescript": "^2.1.0",
"warn-once": "^0.1.1"
},
"peerDependencies": {
"@react-navigation/native": "^6.0.0",
"react": "*",
"@react-navigation/native": "^7.2.2",
"react": ">= 18.2.0",
"react-native": "*",
"react-native-safe-area-context": ">= 3.0.0",
"react-native-screens": ">= 3.0.0"
"react-native-safe-area-context": ">= 4.0.0",
"react-native-screens": ">= 4.0.0"
}
},
"node_modules/@react-navigation/native/node_modules/nanoid": {
@@ -3090,18 +3104,18 @@
}
},
"node_modules/@react-navigation/routers": {
"version": "6.1.9",
"resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-6.1.9.tgz",
"integrity": "sha512-lTM8gSFHSfkJvQkxacGM6VJtBt61ip2XO54aNfswD+KMw6eeZ4oehl7m0me3CR9hnDE4+60iAZR8sAhvCiI3NA==",
"version": "7.5.3",
"resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.3.tgz",
"integrity": "sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg==",
"license": "MIT",
"dependencies": {
"nanoid": "^3.1.23"
"nanoid": "^3.3.11"
}
},
"node_modules/@react-navigation/routers/node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"funding": [
{
"type": "github",
@@ -3334,9 +3348,8 @@
"version": "19.1.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz",
"integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -4034,7 +4047,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -4497,7 +4509,7 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/debug": {
@@ -4806,7 +4818,6 @@
"resolved": "https://registry.npmjs.org/expo/-/expo-54.0.33.tgz",
"integrity": "sha512-3yOEfAKqo+gqHcV8vKcnq0uA5zxlohnhA3fu4G43likN8ct5ZZ3LjAh9wDdKteEkoad3tFPvwxmXW711S5OHUw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.20.0",
"@expo/cli": "54.0.23",
@@ -4945,7 +4956,6 @@
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.11.tgz",
"integrity": "sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg==",
"license": "MIT",
"peer": true,
"dependencies": {
"fontfaceobserver": "^2.1.0"
},
@@ -8040,7 +8050,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -8060,7 +8069,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@@ -8081,9 +8089,9 @@
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"version": "19.2.5",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz",
"integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==",
"license": "MIT"
},
"node_modules/react-native": {
@@ -8091,7 +8099,6 @@
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz",
"integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/create-cache-key-function": "^29.7.0",
"@react-native/assets-registry": "0.81.5",
@@ -8159,7 +8166,6 @@
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
"integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"react": "*",
"react-native": "*"
@@ -8170,7 +8176,6 @@
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz",
"integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"react-freeze": "^1.0.0",
"react-native-is-edge-to-edge": "^1.2.1",
@@ -8302,7 +8307,6 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
"integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -8729,6 +8733,15 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/sf-symbols-typescript": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/sf-symbols-typescript/-/sf-symbols-typescript-2.2.0.tgz",
"integrity": "sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -9262,7 +9275,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -9488,6 +9500,15 @@
"react": ">=16.8"
}
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",

View File

@@ -10,9 +10,9 @@
},
"dependencies": {
"@react-native-async-storage/async-storage": "2.2.0",
"@react-navigation/bottom-tabs": "^6.5.11",
"@react-navigation/native": "^6.1.9",
"@react-navigation/native-stack": "^6.9.17",
"@react-navigation/bottom-tabs": "^7.15.11",
"@react-navigation/native": "^7.2.2",
"@react-navigation/native-stack": "^7.14.12",
"@supabase/supabase-js": "^2.99.1",
"expo": "~54.0.33",
"expo-constants": "~18.0.13",

View File

@@ -303,45 +303,64 @@ export default function Dashboard() {
)}
{activeTab === 'appointments' && (
<View>
<View style={styles.agendaContainer}>
{activeAppointments.length > 0 ? (
activeAppointments.map((a) => {
const svc = shop.services.find((s) => s.id === a.serviceId);
const barber = shop.barbers.find((b) => b.id === a.barberId);
const dateParts = a.date.split(' ');
const dateObj = new Date(dateParts[0]);
const time = dateParts[1] || '';
const day = dateObj.getDate();
const monthNames = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'];
const month = monthNames[dateObj.getMonth()] || '';
return (
<Card key={a.id} style={styles.itemCard}>
<View style={styles.itemHeader}>
<View>
<Text style={styles.itemName}>{svc?.name ?? 'Serviço'}</Text>
<Text style={styles.itemDesc}>{barber?.name} · {a.date}</Text>
</View>
<Badge color={a.status === 'pendente' ? 'indigo' : a.status === 'confirmado' ? 'green' : 'red'}>
{a.status}
</Badge>
</View>
<View style={styles.statusSelector}>
<Text style={styles.selectorLabel}>Alterar status:</Text>
<View style={styles.statusButtons}>
{['pendente', 'confirmado', 'concluido', 'cancelado'].map((s) => (
<Button
key={s}
onPress={() => updateAppointmentStatus(a.id, s as any)}
variant={a.status === s ? 'solid' : 'outline'}
size="sm"
style={styles.statusButton}
>
{s}
</Button>
))}
<View key={a.id} style={styles.agendaTicket}>
<View style={styles.agendaDateBox}>
<Text style={styles.agendaDay}>{day}</Text>
<Text style={styles.agendaMonth}>{month}</Text>
<View style={styles.agendaTimeWrapper}>
<Text style={styles.agendaTime}>{time}</Text>
</View>
</View>
</Card>
<View style={styles.agendaContent}>
<View style={styles.agendaHeader}>
<View style={{ flex: 1 }}>
<Text style={styles.agendaShopName}>{svc?.name ?? 'Serviço'}</Text>
<Text style={styles.itemDesc}>{barber?.name} · {currency(a.total)}</Text>
</View>
<Badge color={a.status === 'pendente' ? 'indigo' : a.status === 'confirmado' ? 'green' : 'red'}>
{a.status}
</Badge>
</View>
<View style={styles.statusSelector}>
<Text style={styles.selectorLabel}>Alterar status:</Text>
<View style={styles.statusButtons}>
{['pendente', 'confirmado', 'concluido', 'cancelado'].map((s) => (
<Button
key={s}
onPress={() => updateAppointmentStatus(a.id, s as any)}
variant={a.status === s ? 'solid' : 'outline'}
size="sm"
style={styles.statusButton}
>
{s}
</Button>
))}
</View>
</View>
</View>
</View>
);
})
) : (
<Card style={styles.emptyCard}>
<Text style={styles.emptyText}>Nenhum agendamento ativo</Text>
</Card>
<View style={styles.emptyAgendaState}>
<Text style={styles.emptyAgendaIcon}>📅</Text>
<Text style={styles.emptyText}>Nenhum agendamento ativo.</Text>
</View>
)}
</View>
)}
@@ -384,31 +403,48 @@ export default function Dashboard() {
)}
{activeTab === 'history' && (
<View>
<View style={styles.agendaContainer}>
<Text style={[styles.title, { marginBottom: 12 }]}>Histórico de Agendamentos</Text>
{historyAppointments.length > 0 ? (
historyAppointments.map((a) => {
const svc = shop.services.find((s) => s.id === a.serviceId);
const barber = shop.barbers.find((b) => b.id === a.barberId);
const dateParts = a.date.split(' ');
const dateObj = new Date(dateParts[0]);
const time = dateParts[1] || '';
const day = dateObj.getDate();
const monthNames = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'];
const month = monthNames[dateObj.getMonth()] || '';
return (
<Card key={a.id} style={styles.itemCard}>
<View style={styles.itemHeader}>
<View>
<Text style={styles.itemName}>{svc?.name ?? 'Serviço'}</Text>
<Text style={styles.itemDesc}>{barber?.name ?? 'Barbeiro'} · {a.date}</Text>
<Text style={styles.itemDesc}>{currency(a.total)}</Text>
<View key={a.id} style={[styles.agendaTicket, { opacity: 0.8 }]}>
<View style={styles.agendaDateBox}>
<Text style={styles.agendaDay}>{day}</Text>
<Text style={styles.agendaMonth}>{month}</Text>
<View style={styles.agendaTimeWrapper}>
<Text style={styles.agendaTime}>{time}</Text>
</View>
<Badge color={a.status === 'concluido' ? 'green' : 'red'}>
{a.status === 'concluido' ? 'Concluído' : 'Cancelado'}
</Badge>
</View>
</Card>
<View style={styles.agendaContent}>
<View style={styles.agendaHeader}>
<View style={{ flex: 1 }}>
<Text style={styles.agendaShopName}>{svc?.name ?? 'Serviço'}</Text>
<Text style={styles.itemDesc}>{barber?.name ?? 'Barbeiro'} · {currency(a.total)}</Text>
</View>
<Badge color={a.status === 'concluido' ? 'green' : 'red'}>
{a.status === 'concluido' ? 'Concluído' : 'Cancelado'}
</Badge>
</View>
</View>
</View>
);
})
) : (
<Card style={styles.emptyCard}>
<View style={styles.emptyAgendaState}>
<Text style={styles.emptyAgendaIcon}>📅</Text>
<Text style={styles.emptyText}>Ainda não registos concluídos ou cancelados.</Text>
</Card>
</View>
)}
</View>
)}
@@ -850,4 +886,81 @@ const styles = StyleSheet.create({
padding: 24,
alignItems: 'center',
},
agendaContainer: {
gap: 12,
},
agendaTicket: {
flexDirection: 'row',
backgroundColor: '#fff',
borderRadius: 24,
overflow: 'hidden',
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.05,
shadowRadius: 10,
elevation: 3,
marginBottom: 8,
},
agendaDateBox: {
backgroundColor: '#0f172a',
paddingVertical: 16,
paddingHorizontal: 12,
alignItems: 'center',
justifyContent: 'center',
minWidth: 85,
},
agendaDay: {
color: '#fff',
fontSize: 28,
fontWeight: '900',
lineHeight: 32,
},
agendaMonth: {
color: '#818cf8',
fontSize: 14,
fontWeight: '800',
textTransform: 'uppercase',
marginBottom: 8,
},
agendaTimeWrapper: {
backgroundColor: 'rgba(255,255,255,0.1)',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 8,
},
agendaTime: {
color: '#fff',
fontSize: 12,
fontWeight: '700',
},
agendaContent: {
flex: 1,
padding: 16,
justifyContent: 'center',
},
agendaHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 4,
},
agendaShopName: {
fontSize: 16,
fontWeight: '900',
color: '#0f172a',
marginRight: 8,
},
emptyAgendaState: {
alignItems: 'center',
padding: 40,
backgroundColor: '#fff',
borderRadius: 28,
borderWidth: 1,
borderColor: '#e2e8f0',
borderStyle: 'dashed',
gap: 16,
},
emptyAgendaIcon: {
fontSize: 48,
},
});

View File

@@ -232,44 +232,70 @@ export default function Profile() {
)}
{activeTab === 'agenda' && (
<View>
<Text style={styles.sectionTitle}>Minha Agenda</Text>
<View style={styles.agendaContainer}>
<Text style={styles.sectionTitle}>Próximos Agendamentos</Text>
{myAppointments.length ? myAppointments.map((appointment) => {
const shop = shops.find((s) => s.id === appointment.shopId);
const service = shop?.services.find((s) => s.id === appointment.serviceId);
const canReview = appointment.status === 'concluido' && !reviewedAppointments.has(appointment.id);
const dateParts = appointment.date.split(' ');
const dateObj = new Date(dateParts[0]);
const time = dateParts[1] || '';
const day = dateObj.getDate();
const monthNames = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'];
const month = monthNames[dateObj.getMonth()] || '';
return (
<Card key={appointment.id} style={styles.itemCard}>
<View style={styles.itemHeader}>
<Text style={styles.itemName}>{shop?.name || 'Barbearia'}</Text>
<Badge color={statusColor[appointment.status]}>{statusLabel[appointment.status]}</Badge>
<View key={appointment.id} style={styles.agendaTicket}>
<View style={styles.agendaDateBox}>
<Text style={styles.agendaDay}>{day}</Text>
<Text style={styles.agendaMonth}>{month}</Text>
<View style={styles.agendaTimeWrapper}>
<Text style={styles.agendaTime}>{time}</Text>
</View>
</View>
<Text style={styles.itemDate}>{appointment.date}</Text>
{!!service && <Text style={styles.itemDate}>{service.name} · {service.duration} min</Text>}
<Text style={styles.itemTotal}>{currency(appointment.total)}</Text>
{canReview ? (
<Button
style={styles.smallAction}
onPress={() => {
setReviewTarget({
appointmentId: appointment.id,
shopId: appointment.shopId,
shopName: shop?.name || 'Barbearia',
});
}}
>
Avaliar agora
</Button>
) : appointment.status === 'concluido' ? (
<Text style={styles.reviewedText}>Avaliado</Text>
) : null}
</Card>
<View style={styles.agendaContent}>
<View style={styles.agendaHeader}>
<Text style={styles.agendaShopName}>{shop?.name || 'Barbearia'}</Text>
<Badge color={statusColor[appointment.status]} style={styles.agendaBadge}>
{statusLabel[appointment.status]}
</Badge>
</View>
{!!service && <Text style={styles.agendaService}>{service.name}</Text>}
<View style={styles.agendaFooter}>
<Text style={styles.agendaTotal}>{currency(appointment.total)}</Text>
{canReview ? (
<TouchableOpacity
style={styles.reviewMiniButton}
onPress={() => {
setReviewTarget({
appointmentId: appointment.id,
shopId: appointment.shopId,
shopName: shop?.name || 'Barbearia',
});
}}
>
<Text style={styles.reviewMiniButtonText}>Avaliar</Text>
</TouchableOpacity>
) : appointment.status === 'concluido' ? (
<Text style={styles.reviewedText}> Avaliado</Text>
) : null}
</View>
</View>
</View>
);
}) : (
<Card style={styles.emptyCard}>
<Text style={styles.emptyText}>Sem agendamentos futuros.</Text>
</Card>
<View style={styles.emptyAgendaState}>
<Text style={styles.emptyAgendaIcon}>📅</Text>
<Text style={styles.emptyText}>Não tens marcações agendadas.</Text>
<Button
style={styles.darkButton}
onPress={() => navigation.navigate('Explore')}
>
Procurar Barbearias
</Button>
</View>
)}
</View>
)}
@@ -521,6 +547,114 @@ const styles = StyleSheet.create({
fontSize: 12,
fontWeight: '900',
textTransform: 'uppercase',
marginTop: 10,
},
agendaContainer: {
gap: 12,
},
agendaTicket: {
flexDirection: 'row',
backgroundColor: '#fff',
borderRadius: 24,
overflow: 'hidden',
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.05,
shadowRadius: 10,
elevation: 3,
marginBottom: 8,
},
agendaDateBox: {
backgroundColor: '#0f172a',
paddingVertical: 16,
paddingHorizontal: 12,
alignItems: 'center',
justifyContent: 'center',
minWidth: 85,
},
agendaDay: {
color: '#fff',
fontSize: 28,
fontWeight: '900',
lineHeight: 32,
},
agendaMonth: {
color: '#818cf8',
fontSize: 14,
fontWeight: '800',
textTransform: 'uppercase',
marginBottom: 8,
},
agendaTimeWrapper: {
backgroundColor: 'rgba(255,255,255,0.1)',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 8,
},
agendaTime: {
color: '#fff',
fontSize: 12,
fontWeight: '700',
},
agendaContent: {
flex: 1,
padding: 16,
justifyContent: 'center',
},
agendaHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 4,
},
agendaShopName: {
fontSize: 16,
fontWeight: '900',
color: '#0f172a',
flex: 1,
marginRight: 8,
},
agendaBadge: {
transform: [{ scale: 0.9 }],
},
agendaService: {
fontSize: 14,
color: '#64748b',
fontWeight: '600',
marginBottom: 12,
},
agendaFooter: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
agendaTotal: {
fontSize: 16,
fontWeight: '900',
color: '#6366f1',
},
reviewMiniButton: {
backgroundColor: '#818cf8',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 12,
},
reviewMiniButtonText: {
color: '#fff',
fontSize: 11,
fontWeight: '900',
textTransform: 'uppercase',
},
emptyAgendaState: {
alignItems: 'center',
padding: 40,
backgroundColor: '#fff',
borderRadius: 28,
borderWidth: 1,
borderColor: '#e2e8f0',
borderStyle: 'dashed',
gap: 16,
},
emptyAgendaIcon: {
fontSize: 48,
},
});