parent report page with two charts, Jalali birth date picker, user ID and invoices in profile

- the parents section is gone: no parent login, invite codes or "I'm a parent" button;
  "گزارشِ والدین" in the profile shows the week against the previous one (praise line,
  six tiles, minutes-per-day and right/wrong-per-day charts, a table of the numbers)
- birth date as day / month / year selects (Jalali, leap Esfand handled) instead of age;
  adults (19+) are asked which grade's math they want to learn
- user ID under the name, copyable
- "پرداخت‌ها و فاکتورها": every order with invoice number, discount, amount and reference code
- tutor: when the free daily quota runs out, a button to the subscription page instead of "retry"

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 02:38:43 +03:30
parent 0e56d9c8aa
commit de7cd0bfa8
15 changed files with 738 additions and 373 deletions

View File

@@ -61,7 +61,7 @@ export const api = {
profile: {
get: () => http.get<T.UserProfile>('/user/profile'),
update: (patch: Partial<Pick<T.UserProfile, 'name' | 'avatar' | 'birthYear' | 'parentPhone' | 'bio'>>) =>
update: (patch: Partial<Pick<T.UserProfile, 'name' | 'avatar' | 'birthYear' | 'birthDate' | 'bio'>>) =>
http.patch<T.UserProfile>('/user/profile', patch),
setGrade: (gradeId: number) =>
http.post<{ success: boolean; gradeId: number }>('/user/grade', { gradeId }),
@@ -70,6 +70,8 @@ export const api = {
summary: () => http.get<T.UserSummary>('/user/summary'),
hearts: () => http.get<T.Hearts>('/user/hearts'),
streakCalendar: () => http.get<T.StreakCalendar>('/user/streak/calendar'),
/** «گزارشِ والدین»: ۷ روزِ اخیرِ همین حساب در کنارِ ۷ روزِ قبلش. */
weeklyReport: () => http.get<T.WeeklyReport>('/user/weekly-report'),
/** بدونِ grade، پایه‌ی ذخیره‌شده‌ی خودِ کاربر ملاک است. */
home: () => http.get<T.HomeResponse>('/home'),
@@ -167,6 +169,8 @@ export const api = {
redeem: (code: string, orderId: string) =>
http.post<T.RedeemResponse>('/discount/redeem', { code, orderId }),
order: (orderId: string) => http.get<T.OrderStatus>(`/payment/orders/${orderId}`),
/** پرداخت‌ها و فاکتورهای همین کاربر، تازه‌ترین اول. */
invoices: () => http.get<{ invoices: T.Invoice[] }>('/payment/invoices').then((r) => r.invoices),
},
support: {
@@ -181,17 +185,6 @@ export const api = {
http.create<{ bugReportId: string; status: string }>('/support/bug-reports', { title, description }),
},
family: {
link: () => http.get<T.FamilyLink>('/family/link'),
invite: (childPhone: string) =>
http.post<{ familyLinkId: string; inviteCode: string; status: string }>('/family/link/invite', { childPhone }),
accept: (inviteCode: string) =>
http.post<{ familyLinkId: string; status: string }>('/family/link/accept', { inviteCode }),
weeklyReport: () => http.get<T.FamilyWeeklyReport>('/family/weekly-report'),
settings: () => http.get<T.FamilySettings>('/family/settings'),
updateSettings: (patch: Partial<T.FamilySettings>) => http.patch('/family/settings', patch),
},
affiliate: {
me: () => http.get<T.AffiliateMe>('/affiliate/me'),
/** کاربرِ تازه (با شماره، نه مهمان) را به صاحبِ کدِ معرف وصل می‌کند. */

View File

@@ -34,7 +34,8 @@ export interface UserProfile {
isGuest: boolean;
gradeId: number | null;
birthYear: number | null;
parentPhone: string;
/** تاریخِ تولدِ شمسی «1385-04-12»؛ null وقتی فقط سال داده شده یا هیچ. */
birthDate: string | null;
bio: string;
}
@@ -288,24 +289,57 @@ export interface Ticket {
messages: { id: string; sender: string; text: string }[];
}
export interface FamilyLink {
familyLinkId: string;
status: string;
childName: string;
/** یک سفارش به شکلِ فاکتور (GET /payment/invoices). مبلغ‌ها تومان‌اند. */
export interface Invoice {
orderId: string;
invoiceNumber: string;
status: 'pending' | 'paid' | 'failed';
provider: 'gateway' | 'bazaar' | string;
planTitle: string;
/** قیمتِ پایه پیش از تخفیف؛ null برای سفارش‌های قدیمی‌تر از ثبتِ فاکتور. */
listPrice: number | null;
discountAmount: number | null;
discountCode: string;
amount: number;
currency: string;
createdAt: string;
paidAt: string | null;
/** کدِ پیگیریِ بانکی/بازار؛ خالی برای پرداخت‌نشده یا کدِ تخفیفِ ۱۰۰٪. */
refId: string;
}
export interface FamilyWeeklyReport {
weekStart: string;
xpEarned: number;
lessonsCompleted: number;
islandsCompleted: number;
currentStreak: number;
/** یک روز از گزارشِ هفتگی. `minutes` تخمینِ سرور است (از فاصله‌ی جواب‌ها)، نه زمانِ باز بودنِ اپ. */
export interface ReportDay {
date: string;
minutes: number;
questions: number;
correct: number;
xp: number;
lessons: number;
islands: number;
aiQuestions: number;
}
export interface FamilySettings {
dailyLimitMinutes: number;
quietHoursStart: string;
quietHoursEnd: string;
export interface ReportTotals {
minutes: number;
activeDays: number;
questions: number;
correct: number;
xp: number;
/** یکتا: درس یا جزیره‌ی تکراری یک بار شمرده می‌شود. */
lessons: number;
islands: number;
aiQuestions: number;
}
/** GET /user/weekly-report: هفت روزِ اخیر و هفت روزِ قبلش، هر دو قدیمی به جدید. */
export interface WeeklyReport {
today: string;
days: ReportDay[];
previousDays: ReportDay[];
thisWeek: ReportTotals;
lastWeek: ReportTotals;
streak: { current: number; longest: number };
}
export interface AffiliateMe {