Files
han-app/VM4_Expo-mobile/app/profile.tsx
T

146 lines
7.0 KiB
TypeScript

import { Feather } from "@expo/vector-icons";
import { useQuery } from "@tanstack/react-query";
import { Link, useRouter } from "expo-router";
import React, { useState } from "react";
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
import { useApp } from "../src/app-context";
import { AccordionSection } from "../src/components/AccordionSection";
import { GuestAuthGate } from "../src/components/GuestAuthGate";
import { ScreenShell } from "../src/components/ScreenShell";
import { isProduction } from "../src/config";
import { downloadAndOpen } from "../src/native-files";
import { profileApi } from "../src/services";
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 });
const [downloadError, setDownloadError] = useState<unknown>();
const download = async (id: string) => {
try {
const result = await profileApi.documentUrl(id);
const name = documents.data?.items.find((item) => item.document_id === id)?.name;
await downloadAndOpen(result.download_url, name ?? "document");
} catch (error) { setDownloadError(error); }
};
if (!enabled) {
return (
<ScreenShell>
<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>
<GuestAuthGate
icon="user"
title="Профиль доступен после входа"
description="Авторизуйтесь, чтобы видеть личные данные и документы компании."
/>
</ScreenShell>
);
}
const personal = profile.data?.profile.personal_data;
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>
<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()} />
{Boolean(downloadError) && <ErrorNotice error={downloadError} />}
</View>
</ScrollView>
</ScreenShell>
);
}
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 },
});