Накатил человеческий дизайн
This commit is contained in:
@@ -1,13 +1,20 @@
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Link } from "expo-router";
|
||||
import { Link, useRouter } from "expo-router";
|
||||
import React, { useState } from "react";
|
||||
import { ScrollView, Text, View } from "react-native";
|
||||
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
|
||||
import { useApp } from "../src/app-context";
|
||||
import { AccordionSection } from "../src/components/AccordionSection";
|
||||
import { AppHeader } from "../src/components/AppHeader";
|
||||
import { ScreenShell } from "../src/components/ScreenShell";
|
||||
import { isProduction } from "../src/config";
|
||||
import { profileApi } from "../src/services";
|
||||
import { Button, ErrorNotice, Header, Loading, styles } from "../src/ui";
|
||||
import { Button, ErrorNotice, Loading, styles } from "../src/ui";
|
||||
import { colors, radii, spacing } from "../src/theme";
|
||||
|
||||
export default function ProfileScreen() {
|
||||
const app = useApp();
|
||||
const router = useRouter();
|
||||
const enabled = app.authStatus === "authenticated";
|
||||
const profile = useQuery({ queryKey: ["profile"], queryFn: profileApi.me, enabled });
|
||||
const documents = useQuery({ queryKey: ["documents"], queryFn: profileApi.documents, enabled });
|
||||
@@ -20,44 +27,112 @@ export default function ProfileScreen() {
|
||||
} catch (error) { setDownloadError(error); }
|
||||
};
|
||||
|
||||
if (!enabled) return <ScrollView contentContainerStyle={styles.page}>
|
||||
<Header status={app.authStatus} realtime={app.realtimeState} />
|
||||
<Text accessibilityRole="header" style={styles.title}>Профиль</Text>
|
||||
<Text style={styles.text}>Профиль доступен после авторизации.</Text>
|
||||
<Link href="/" style={styles.link}>Перейти в чат для входа</Link>
|
||||
</ScrollView>;
|
||||
if (!enabled) {
|
||||
return (
|
||||
<ScreenShell>
|
||||
<AppHeader guestLabel="Гость" />
|
||||
<View style={{ flex: 1, padding: spacing.lg, justifyContent: "center", gap: spacing.md }}>
|
||||
<Text accessibilityRole="header" style={styles.title}>Профиль</Text>
|
||||
<Text style={styles.text}>Профиль доступен после авторизации.</Text>
|
||||
<Link href="/" style={styles.link}>Перейти на главную для входа</Link>
|
||||
</View>
|
||||
</ScreenShell>
|
||||
);
|
||||
}
|
||||
|
||||
const personal = profile.data?.profile.personal_data;
|
||||
return <ScrollView contentContainerStyle={styles.page}>
|
||||
<Header status={app.authStatus} realtime={app.realtimeState} onLogout={() => void app.signOut()} />
|
||||
<Text accessibilityRole="header" style={styles.title}>Профиль</Text>
|
||||
{(profile.isLoading || documents.isLoading) && <Loading />}
|
||||
{(profile.error || documents.error) && <ErrorNotice error={profile.error ?? documents.error} retry={() => { void profile.refetch(); void documents.refetch(); }} />}
|
||||
<View style={styles.card}>
|
||||
<Text accessibilityRole="header" style={styles.heading}>Личные данные</Text>
|
||||
<Row label="ФИО" value={personal?.full_name} />
|
||||
<Row label="Гражданство" value={personal?.citizenship} />
|
||||
<Row label="Телефон в РФ" value={personal?.russian_phone} />
|
||||
<Row label="Зарубежный телефон" value={personal?.foreign_phone} />
|
||||
<Row label="Email" value={personal?.email} />
|
||||
<Text style={styles.muted}>Редактирование профиля недоступно. Для изменения данных напишите оператору.</Text>
|
||||
<Link href="/" style={styles.link}>Написать оператору</Link>
|
||||
</View>
|
||||
<View style={styles.card}>
|
||||
<Text accessibilityRole="header" style={styles.heading}>Документы</Text>
|
||||
{!documents.data?.items.length && <Text style={styles.muted}>Документов пока нет.</Text>}
|
||||
{documents.data?.items.map((document) => <View key={document.document_id} style={styles.row}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.text}>{document.name}</Text>
|
||||
<Text style={styles.muted}>{new Date(document.sent_at).toLocaleDateString("ru-RU")}</Text>
|
||||
const fullName = personal?.full_name ?? "Пользователь";
|
||||
|
||||
return (
|
||||
<ScreenShell>
|
||||
<ScrollView contentContainerStyle={{ paddingBottom: spacing.xl }}>
|
||||
<View style={stylesLocal.topBar}>
|
||||
<Pressable accessibilityRole="button" accessibilityLabel="Назад" onPress={() => router.back()} style={({ pressed }) => [stylesLocal.backButton, pressed && stylesLocal.pressed]}>
|
||||
<Feather name="arrow-left" size={20} color={colors.foreground} />
|
||||
</Pressable>
|
||||
<Text accessibilityRole="header" style={styles.title}>Профиль</Text>
|
||||
</View>
|
||||
<Button title="Скачать" secondary onPress={() => void download(document.document_id)} />
|
||||
</View>)}
|
||||
{downloadError && <ErrorNotice error={downloadError} />}
|
||||
</View>
|
||||
</ScrollView>;
|
||||
|
||||
<View style={stylesLocal.avatarBlock}>
|
||||
<View style={stylesLocal.avatar}>
|
||||
<Feather name="user" size={40} color={colors.primaryForeground} />
|
||||
</View>
|
||||
<Text style={stylesLocal.name}>{fullName}</Text>
|
||||
<Text style={styles.muted}>{personal?.citizenship ? `Гражданство: ${personal.citizenship}` : "Мигрант"}</Text>
|
||||
</View>
|
||||
|
||||
{(profile.isLoading || documents.isLoading) && <View style={{ padding: spacing.lg }}><Loading /></View>}
|
||||
{(profile.error || documents.error) && (
|
||||
<View style={{ paddingHorizontal: spacing.lg }}>
|
||||
<ErrorNotice error={profile.error ?? documents.error} retry={() => { void profile.refetch(); void documents.refetch(); }} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={{ paddingHorizontal: spacing.lg }}>
|
||||
<AccordionSection
|
||||
defaultOpen
|
||||
title="Личная информация"
|
||||
items={[
|
||||
{ title: "Имя", value: personal?.full_name ?? "Не указано", icon: "user" },
|
||||
{ title: "Телефон в РФ", value: personal?.russian_phone ?? "Не указано", icon: "phone" },
|
||||
{ title: "Зарубежный телефон", value: personal?.foreign_phone ?? "Не указано", icon: "phone" },
|
||||
{ title: "Email", value: personal?.email ?? "Не указано", icon: "mail" },
|
||||
]}
|
||||
/>
|
||||
|
||||
<AccordionSection
|
||||
title="Готовые документы"
|
||||
items={documents.data?.items.length
|
||||
? documents.data.items.map((doc) => ({
|
||||
title: doc.name,
|
||||
value: new Date(doc.sent_at).toLocaleDateString("ru-RU"),
|
||||
icon: "file-text" as const,
|
||||
action: "download",
|
||||
}))
|
||||
: [{ title: "Документов пока нет", value: "Оператор отправит их в этот раздел", icon: "file-text" }]}
|
||||
onItemPress={(index) => {
|
||||
const doc = documents.data?.items[index];
|
||||
if (doc) void download(doc.document_id);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Text style={[styles.muted, { marginBottom: spacing.md }]}>
|
||||
Редактирование профиля недоступно. Для изменения данных напишите оператору.
|
||||
</Text>
|
||||
<Link href="/" style={styles.link}>Написать оператору</Link>
|
||||
|
||||
{!isProduction && (
|
||||
<Pressable onPress={() => router.push("/diagnostics")} style={({ pressed }) => [stylesLocal.menuItem, pressed && stylesLocal.pressed]}>
|
||||
<Text style={stylesLocal.menuText}>Диагностика (dev)</Text>
|
||||
<Feather name="chevron-right" size={20} color={colors.mutedForeground} />
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
<Button title="Выйти из аккаунта" danger onPress={() => void app.signOut()} />
|
||||
{downloadError && <ErrorNotice error={downloadError} />}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</ScreenShell>
|
||||
);
|
||||
}
|
||||
|
||||
function Row({ label, value }: { label: string; value: string | null | undefined }) {
|
||||
return <View><Text style={styles.muted}>{label}</Text><Text style={styles.text}>{value || "Не указано"}</Text></View>;
|
||||
}
|
||||
const stylesLocal = StyleSheet.create({
|
||||
topBar: { flexDirection: "row", alignItems: "center", gap: spacing.md, paddingHorizontal: spacing.lg, paddingVertical: spacing.lg },
|
||||
backButton: { width: 36, height: 36, borderRadius: radii.full, alignItems: "center", justifyContent: "center" },
|
||||
avatarBlock: { alignItems: "center", paddingVertical: spacing.lg, marginBottom: spacing.sm },
|
||||
avatar: { width: 80, height: 80, borderRadius: radii.full, backgroundColor: colors.primary, alignItems: "center", justifyContent: "center", marginBottom: spacing.md },
|
||||
name: { fontSize: 18, fontWeight: "500", color: colors.foreground, marginBottom: 4 },
|
||||
menuItem: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
backgroundColor: colors.card,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderRadius: radii.lg,
|
||||
padding: spacing.lg,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
menuText: { fontSize: 14, fontWeight: "500", color: colors.foreground },
|
||||
pressed: { backgroundColor: colors.accent },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user