Первая версия мобильного приложения
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
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 },
|
||||
});
|
||||
Reference in New Issue
Block a user