Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/app/(auth)/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { useT } from '@/i18n';
import { useAuth } from '@/lib/auth';
import { isValidContact } from '@/lib/auth-validate';
import { useBack } from '@/lib/nav';
import { useTheme } from '@/theme';
import { Icon } from '@/ui';

export default function LoginScreen() {
const router = useRouter();
const goBack = useBack();
const t = useT();
const { colors, radius } = useTheme();
const { signIn } = useAuth();
Expand All @@ -33,12 +35,17 @@ export default function LoginScreen() {
setContactErr(cErr);
setPasswordErr(pErr);
if (cErr || pErr) return;
signIn('email'); // Phase-0 stub: a valid pair signs in
// Phase-0 stub: a valid pair signs in. The display name comes from the saved account
// profile — the single `profiles` row, which is this build's account store and now
// survives sign-out and reloads (TP-FIX-0719, пп. 4/6). An account that never went
// through the wizard simply has no name yet: the greeting drops the comma and Settings
// shows «Добавьте имя» instead of a dash. A real lookup arrives with GoTrue in Phase 4.
signIn('email');
};

return (
<SafeAreaView edges={['top']} style={[styles.fill, { backgroundColor: colors.bg }]}>
<Header title={t('auth.loginTitle')} onBack={() => router.back()} />
<Header title={t('auth.loginTitle')} onBack={() => goBack()} />

<View style={styles.content}>
{/* Email или телефон */}
Expand Down
4 changes: 3 additions & 1 deletion src/app/(auth)/recover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';

import { useT } from '@/i18n';
import { isStrongPassword, isValidCode, isValidContact, passwordChecks } from '@/lib/auth-validate';
import { useBack } from '@/lib/nav';
import { useTheme } from '@/theme';
import { Icon } from '@/ui';

Expand All @@ -23,6 +24,7 @@ type Step = 'contact' | 'code' | 'password' | 'done';

export default function RecoverScreen() {
const router = useRouter();
const goBack = useBack();
const t = useT();
const { colors, radius } = useTheme();

Expand Down Expand Up @@ -73,7 +75,7 @@ export default function RecoverScreen() {
<SafeAreaView edges={['top']} style={[styles.fill, { backgroundColor: colors.bg }]}>
<Header
title={step === 'code' ? t('auth.codeTitle') : step === 'password' ? t('auth.newPasswordTitle') : t('auth.recoverTitle')}
onBack={() => (step === 'contact' || step === 'done' ? router.back() : setStep(step === 'code' ? 'contact' : 'code'))}
onBack={() => (step === 'contact' || step === 'done' ? goBack() : setStep(step === 'code' ? 'contact' : 'code'))}
/>

<View style={styles.content}>
Expand Down
8 changes: 4 additions & 4 deletions src/app/(auth)/register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@
* The finish applies everything to the single profile row (context + DB, ADR-0013 C) and
* signs in against the Phase-0 stub.
*/
import { useRouter } from 'expo-router';
import { useState } from 'react';
import { Pressable, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';

import { useProfile } from '@/db/hooks';
import { updateProfile } from '@/db/mutations';
import { DURATIONS, type Duration, type LessonFormat } from '@/domain/types';
import { activityDefaultClientType, useMode, useT, type Activity, type ClientType, type StringKey } from '@/i18n';
import { useAuth } from '@/lib/auth';
import { isStrongPassword, isValidContact, passwordChecks } from '@/lib/auth-validate';
import { DURATIONS, type Duration, type LessonFormat } from '@/domain/types';
import { useBack } from '@/lib/nav';
import { useTheme } from '@/theme';
import { Icon, Segmented } from '@/ui';

Expand All @@ -45,7 +45,7 @@ const LEADS: { key: number; label: StringKey }[] = [
];

export default function RegisterScreen() {
const router = useRouter();
const goBack = useBack();
const t = useT();
const { colors, radius } = useTheme();
const { signIn } = useAuth();
Expand Down Expand Up @@ -126,7 +126,7 @@ export default function RegisterScreen() {
<Header
title={stepTitle}
subtitle={`${t('auth.stepOf')} ${step} ${t('common.of')} ${TOTAL_STEPS}`}
onBack={() => (step === 1 ? router.back() : setStep((s) => s - 1))}
onBack={() => (step === 1 ? goBack() : setStep((s) => s - 1))}
/>

<ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled" showsVerticalScrollIndicator={false}>
Expand Down
16 changes: 15 additions & 1 deletion src/app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,17 @@ function YourDayCard({
{i === nextIdx ? (
// No numberOfLines: on web it ellipsizes to the 20px slot width («с...»);
// the fixed-width caption is MEANT to overhang the slot, centred on the dot.
<Text style={[styles.tlCaption, { color: colors.muted }]}>
// At the ENDS of the lane it may only overhang INWARDS: the first and last
// dots sit flush with the card, so a centred caption hung 50px past the edge
// and got cut off by the screen — which is every morning, before the first
// lesson of the day, when the next dot IS the first one.
<Text
style={[
styles.tlCaption,
{ color: colors.muted },
i === lessons.length - 1 && i !== 0 ? styles.tlCaptionEnd : null,
i === 0 ? styles.tlCaptionStart : null,
]}>
{t('today.nextAt')} {hhmm(s.nextAt as number)}
</Text>
) : null}
Expand Down Expand Up @@ -457,6 +467,10 @@ const styles = StyleSheet.create({
tlDotFuture: { width: 12, height: 12, borderRadius: 6, borderWidth: 1.5 },
tlCaptionRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 6, height: 15 },
tlCaption: { fontSize: 12, fontWeight: '500', fontVariant: ['tabular-nums'], width: 120, textAlign: 'center' },
// Half the overhang ((120 − 20) / 2) pushed back inwards, so an end caption starts/ends
// flush with its dot instead of hanging outside the card.
tlCaptionStart: { textAlign: 'left', transform: [{ translateX: 50 }] },
tlCaptionEnd: { textAlign: 'right', transform: [{ translateX: -50 }] },
allLink: { fontSize: 14, fontWeight: '600' },

nearest: {
Expand Down
2 changes: 1 addition & 1 deletion src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default function RootLayout() {
{(initial) => (
<TutorThemeProvider initial={initial.theme}>
<DualModeProvider initial={initial.clientType}>
<AuthProvider>
<AuthProvider initialSession={initial.signedIn}>
<NavigationRoot />
</AuthProvider>
</DualModeProvider>
Expand Down
9 changes: 5 additions & 4 deletions src/app/finance/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useLocalSearchParams, useRouter } from 'expo-router';
import { useLocalSearchParams } from 'expo-router';
import { type ReactNode, useMemo } from 'react';
import { Linking, Pressable, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
Expand All @@ -9,6 +9,7 @@ import { createTransaction, settleExpectationPaid } from '@/db/mutations';
import type { PayMethod, PayStatus } from '@/domain/types';
import { useT } from '@/i18n';
import { formatRub } from '@/lib/format';
import { useBack } from '@/lib/nav';
import { hhmm } from '@/lib/time';
import { useTheme } from '@/theme';
import { Card, Chip, type ChipTone, Icon } from '@/ui';
Expand Down Expand Up @@ -46,7 +47,7 @@ function kindTone(kind: PayStatus): ChipTone {

export default function OperationDetailScreen() {
const { id, kind } = useLocalSearchParams<{ id: string; kind?: string }>();
const router = useRouter();
const goBack = useBack();
const t = useT();
const { colors, radius } = useTheme();
const dateLabel = useDateLabel();
Expand Down Expand Up @@ -113,7 +114,7 @@ export default function OperationDetailScreen() {
subjectId: txn.subjectId,
});
}
router.back();
goBack();
};

/** Reach out to the student/client via the device dialer (paid-operation convenience). */
Expand All @@ -123,7 +124,7 @@ export default function OperationDetailScreen() {

return (
<SafeAreaView edges={['top']} style={[styles.fill, { backgroundColor: colors.bg }]}>
<Header title={t('finance.opTitle')} onBack={() => router.back()} />
<Header title={t('finance.opTitle')} onBack={() => goBack()} />

{!view ? (
<EmptyState icon="wallet" text={t('common.none')} />
Expand Down
8 changes: 4 additions & 4 deletions src/app/finance/new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
* reuse the shared kit Sheet (student/subject) and DateTimePickerSheet (date) — no new sheets.
* All strings via i18n (dual-mode resolves inside t()); all colours via theme tokens.
*/
import { useRouter } from 'expo-router';
import { useState } from 'react';
import { Pressable, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
Expand All @@ -21,6 +20,7 @@ import { createExpectation, createTransaction } from '@/db/mutations';
import type { PayMethod } from '@/domain/types';
import { useT, type StringKey } from '@/i18n';
import { formatNumberRu } from '@/lib/format';
import { useBack } from '@/lib/nav';
import { nowMs } from '@/lib/time';
import { useTheme } from '@/theme';
import { Card, Chip, type ChipTone, Icon, SectionLabel, Sheet } from '@/ui';
Expand Down Expand Up @@ -56,7 +56,7 @@ const METHODS: { key: PayMethod; label: StringKey }[] = [
];

export default function NewOperationScreen() {
const router = useRouter();
const goBack = useBack();
const t = useT();
const { colors, radius } = useTheme();
const dateLabel = useDateLabel();
Expand Down Expand Up @@ -105,12 +105,12 @@ export default function NewOperationScreen() {
comment: comment || null,
});
}
router.back();
goBack();
};

return (
<SafeAreaView edges={['top']} style={[styles.fill, { backgroundColor: colors.bg }]}>
<Header title={t('finance.newOp')} onBack={() => router.back()} />
<Header title={t('finance.newOp')} onBack={() => goBack()} />

<ScrollView
contentContainerStyle={styles.content}
Expand Down
51 changes: 39 additions & 12 deletions src/app/lesson/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useLocalSearchParams, useRouter } from 'expo-router';
import { useLocalSearchParams } from 'expo-router';
import { type ReactNode, useState } from 'react';
import { Linking, Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
Expand All @@ -21,8 +21,9 @@ import type { Scope } from '@/domain/scope';
import { type PayStatus, type TxnType } from '@/domain/types';
import { lifecycleSnapshot } from '@/domain/undo';
import { useT } from '@/i18n';
import { useSnack } from '@/lib/snack';
import { formatRub } from '@/lib/format';
import { useBack } from '@/lib/nav';
import { useSnack } from '@/lib/snack';
import { hhmm } from '@/lib/time';
import { useTheme } from '@/theme';
import { Card, Dot, Icon, Sheet, type DotTone } from '@/ui';
Expand All @@ -41,7 +42,7 @@ const PAY_TONE: Record<PayStatus, DotTone> = { paid: 'green', debt: 'red', expec

export default function LessonCardScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const router = useRouter();
const goBack = useBack();
const t = useT();
const { colors, radius } = useTheme();
const dateLabel = useDateLabel();
Expand All @@ -64,6 +65,7 @@ export default function LessonCardScreen() {

const series = lesson ? isSeriesLesson(lesson) : false;
const conducted = lesson?.lifecycleStatus === 'done';
const cancelled = lesson?.lifecycleStatus === 'cancelled';

// «Готово» + undo: snapshot BEFORE the mutation, «Вернуть» restores it.
const conductWithUndo = () => {
Expand Down Expand Up @@ -93,15 +95,32 @@ export default function LessonCardScreen() {
setCancelScopeOpen(false);
setReasonOpen(true);
};
/**
* A scope edit can touch NOTHING: `domain/scope` protects an occurrence that carries a
* money operation or is already done/cancelled, and «all» can land on an empty window
* (ADR-0016 §4). Reporting success there is a lie (the lesson stays put), and reporting
* the wrong reason is only marginally better — so name the one that actually applies.
* `txns` is the same reversal-filtered ledger the protection is computed from.
*/
const refusalText = () => {
if (txns.length > 0) return t('snack.protectedByMoney');
if (lesson?.lifecycleStatus === 'done') return t('snack.protectedDone');
return t('snack.noChanges');
};

const confirmCancel = () => {
if (!lesson) return;
setReasonOpen(false);
void scopeCancel(lesson, pendingCancelScope, reason.trim()).then(({ undo }) => {
void scopeCancel(lesson, pendingCancelScope, reason.trim()).then(({ affected, undo }) => {
if (affected === 0) {
snack.show(refusalText());
return;
}
snack.show(t('snack.lessonCancelled'), { actionLabel: t('action.undo'), onAction: () => void undo() });
// Return to the schedule after the action (prototype pattern) — the cancelled lesson
// leaves the timeline, and the detail is a transient action screen.
goBack();
});
// Return to the schedule after the action (prototype pattern) — the cancelled lesson
// leaves the timeline, and the detail is a transient action screen.
router.back();
};

// Reschedule: pick the new time, then a series lesson asks the scope; a standalone one
Expand All @@ -113,17 +132,21 @@ export default function LessonCardScreen() {
setRescheduleScopeOpen(true);
} else {
void rescheduleLesson(lesson, ms);
router.back();
goBack();
}
};
const onRescheduleScopePick = (scope: Scope) => {
setRescheduleScopeOpen(false);
if (!lesson || pendingStartsAt === null) return;
void scopeReschedule(lesson, scope, pendingStartsAt).then(({ undo }) => {
void scopeReschedule(lesson, scope, pendingStartsAt).then(({ affected, undo }) => {
if (affected === 0) {
snack.show(refusalText()); // same protection rules as the cancel path
return;
}
snack.show(t('snack.rescheduled'), { actionLabel: t('action.undo'), onAction: () => void undo() });
// Return to the schedule, which reflects the new time (prototype pattern).
goBack();
});
// Return to the schedule, which reflects the new time (prototype pattern).
router.back();
};
// Money undo (ADR-0002): «Отменить» appends the COMPENSATING row — never deletes.
const recordPaymentWithUndo = (type: Exclude<TxnType, 'expected'>) => {
Expand All @@ -140,7 +163,7 @@ export default function LessonCardScreen() {

return (
<SafeAreaView edges={['top']} style={[styles.fill, { backgroundColor: colors.bg }]}>
<Header title={t('lesson.nom')} onBack={() => router.back()} />
<Header title={t('lesson.nom')} onBack={() => goBack()} />

{!lesson ? (
<EmptyState icon="calendar" text={t('common.none')} />
Expand Down Expand Up @@ -253,12 +276,15 @@ export default function LessonCardScreen() {
<Text style={[styles.actionLabel, { color: colors.body }]}>{t('action.reschedule')}</Text>
</Pressable>

{/* Already cancelled → nothing left to cancel; the domain would refuse anyway. */}
<Pressable
onPress={onCancelPress}
disabled={cancelled}
style={({ pressed }) => [
styles.action,
styles.actionGhost,
{ backgroundColor: colors.dangerLight, borderRadius: radius.field },
cancelled && styles.disabled,
pressed && styles.pressed,
]}>
<Icon name="close" size={17} sw={2} stroke={colors.danger} />
Expand Down Expand Up @@ -432,4 +458,5 @@ const styles = StyleSheet.create({
},
payChoiceLabel: { fontSize: 15, fontWeight: '600' },
pressed: { opacity: 0.85 },
disabled: { opacity: 0.45 },
});
9 changes: 5 additions & 4 deletions src/app/lesson/new.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useLocalSearchParams, useRouter } from 'expo-router';
import { useLocalSearchParams } from 'expo-router';
import { useState, type ReactNode } from 'react';
import { Pressable, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
Expand All @@ -8,6 +8,7 @@ import { useProfile, useStudents, useSubjects } from '@/db/hooks';
import { createLesson, recordLessonPayment } from '@/db/mutations';
import { DURATIONS, type Duration, type LessonFormat, type PayStatus } from '@/domain/types';
import { useT } from '@/i18n';
import { useBack } from '@/lib/nav';
import { dayBounds, hhmm, nowMs } from '@/lib/time';
import { useTheme } from '@/theme';
import { CatAvatar, Icon, Segmented, Sheet } from '@/ui';
Expand All @@ -22,7 +23,7 @@ function defaultStartsAt(): number {
export default function LessonFormScreen() {
// `at` prefills date/time (tap on a free window, spec 05 §5.2); `studentId` presets the student.
const { studentId: preselect, at } = useLocalSearchParams<{ studentId?: string; at?: string }>();
const router = useRouter();
const goBack = useBack();
const t = useT();
const { colors, radius } = useTheme();

Expand Down Expand Up @@ -90,7 +91,7 @@ export default function LessonFormScreen() {
if (payStatus !== 'expected') {
await recordLessonPayment(lesson, { type: payStatus });
}
router.back();
goBack();
};

const payLabels: Record<PayStatus, string> = {
Expand All @@ -109,7 +110,7 @@ export default function LessonFormScreen() {
<SafeAreaView edges={['top']} style={[styles.fill, { backgroundColor: colors.bg }]}>
<View style={styles.header}>
<Pressable
onPress={() => router.back()}
onPress={() => goBack()}
hitSlop={8}
style={({ pressed }) => [styles.backBtn, { backgroundColor: colors.stoneLight }, pressed && styles.pressed]}
accessibilityRole="button"
Expand Down
Loading
Loading