@hbslovely/baby-feeding 2026.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,2926 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
|
+
import * as _angular_core from '@angular/core';
|
|
3
|
+
import { AfterViewInit, EventEmitter, OnInit, OnDestroy, InjectionToken } from '@angular/core';
|
|
4
|
+
import * as _hbslovely_baby_feeding from '@hbslovely/baby-feeding';
|
|
5
|
+
import * as _angular_platform_browser from '@angular/platform-browser';
|
|
6
|
+
|
|
7
|
+
interface FeedingLog {
|
|
8
|
+
/** user id / bé (multi-account trên cùng sheet) */
|
|
9
|
+
user: string;
|
|
10
|
+
/** YYYY-MM-DD */
|
|
11
|
+
date: string;
|
|
12
|
+
/** HH:mm (24h) */
|
|
13
|
+
time: string;
|
|
14
|
+
/** ml */
|
|
15
|
+
volume: number;
|
|
16
|
+
note?: string;
|
|
17
|
+
/** row trong sheet (1-based, row đầu là header) — chỉ có khi load từ sheet */
|
|
18
|
+
rowIndex?: number;
|
|
19
|
+
}
|
|
20
|
+
interface FeedingSheetResponse {
|
|
21
|
+
success: boolean;
|
|
22
|
+
logs?: FeedingLog[];
|
|
23
|
+
rowIndex?: number;
|
|
24
|
+
error?: string;
|
|
25
|
+
}
|
|
26
|
+
/** Pha sữa đọc từ `Feeding!G1:K1` (K = ISO lúc pha, do app ghi). */
|
|
27
|
+
interface BottlePrepFromSheet {
|
|
28
|
+
volumeMl: number;
|
|
29
|
+
at: string;
|
|
30
|
+
user: string;
|
|
31
|
+
}
|
|
32
|
+
/** Cột tab Google Sheet `Settings`: ID | Tên chỉ tiêu | Giá trị | Đơn vị | Kiểu dữ liệu */
|
|
33
|
+
interface FeedingSettingRow {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
value: string;
|
|
37
|
+
unit: string;
|
|
38
|
+
dataType: string;
|
|
39
|
+
}
|
|
40
|
+
/** Giá trị đã parse — app chỉ bind theo `FEEDING_SETTING_ID`, không theo tên hiển thị. */
|
|
41
|
+
interface FeedingSettingsResolved {
|
|
42
|
+
/**
|
|
43
|
+
* Ngưỡng **H** (giờ) giữa hai cữ: cảnh báo khi khoảng cách **≥ H** — ID `FEED_TIME_WARNING`.
|
|
44
|
+
*/
|
|
45
|
+
feedTimeWarningHours: number;
|
|
46
|
+
/** Cữ có dung tích < giá trị này (ml) — `FEED_WARNING_AMOUNT`. */
|
|
47
|
+
feedWarningMl: number;
|
|
48
|
+
/**
|
|
49
|
+
* Nếu hai cữ liên tiếp cách nhau ≤ giá trị này (phút) thì gom hiển thị
|
|
50
|
+
* thành một dòng (0 = tắt) — ID `FEED_GROUP_GAP_MINUTES`.
|
|
51
|
+
*/
|
|
52
|
+
feedGroupGapMinutes: number;
|
|
53
|
+
/**
|
|
54
|
+
* Thông báo "sắp tới cữ bú" khi còn ≤ N phút — ID `FEEDING_NOTIFICATION_MINUTES`.
|
|
55
|
+
*/
|
|
56
|
+
feedingNotificationMinutes: number;
|
|
57
|
+
/**
|
|
58
|
+
* Thời gian cần bế bé vỗ ợ sau mỗi cữ bú (phút) — ID `BURP_DURATION_MINUTES`.
|
|
59
|
+
*/
|
|
60
|
+
burpDurationMinutes: number;
|
|
61
|
+
/**
|
|
62
|
+
* Bắt đầu nhắc sự kiện lịch trước N ngày — ID `EVENT_REMINDER_DAYS`.
|
|
63
|
+
*/
|
|
64
|
+
eventReminderDays: number;
|
|
65
|
+
/**
|
|
66
|
+
* Cộng thêm giờ vào cửa sổ nhắc lịch — ID `EVENT_REMINDER_HOURS`.
|
|
67
|
+
*/
|
|
68
|
+
eventReminderHours: number;
|
|
69
|
+
/**
|
|
70
|
+
* Tự động làm mới nhật ký hoạt động mỗi N phút — ID `ACTIVITY_LOG_REFRESH_MINUTES`.
|
|
71
|
+
*/
|
|
72
|
+
activityLogRefreshMinutes: number;
|
|
73
|
+
}
|
|
74
|
+
declare function parseFeedingSettingsFromRows(rows: FeedingSettingRow[]): FeedingSettingsResolved;
|
|
75
|
+
declare class FeedingLogService {
|
|
76
|
+
private http;
|
|
77
|
+
private readonly SHEET_ID;
|
|
78
|
+
private readonly SHEET_NAME;
|
|
79
|
+
private readonly SETTINGS_SHEET_NAME;
|
|
80
|
+
private readonly API_KEY;
|
|
81
|
+
private readonly BASE_URL;
|
|
82
|
+
private readonly APPS_SCRIPT_URL;
|
|
83
|
+
/**
|
|
84
|
+
* Columns:
|
|
85
|
+
* A: User (chỉ để ghi nhận ai là người log, KHÔNG dùng để filter)
|
|
86
|
+
* B: Ngày (DD/MM/YYYY)
|
|
87
|
+
* C: Giờ (HH:mm)
|
|
88
|
+
* D: Dung tích (ml)
|
|
89
|
+
* E: Ghi chú
|
|
90
|
+
*
|
|
91
|
+
* Trả về TẤT CẢ cữ bú trong sheet — mọi user share chung dữ liệu của bé.
|
|
92
|
+
*/
|
|
93
|
+
getLogs(): Observable<FeedingLog[]>;
|
|
94
|
+
/**
|
|
95
|
+
* Đọc pha sữa từ dòng 1: G nhãn | H user | I ml | J giờ | K ISO (app).
|
|
96
|
+
* Chỉ trả dữ liệu khi **H** khớp `currentUser` (`?user=`).
|
|
97
|
+
* Nếu K trống (script cũ): suy ra `at` từ J + ngày gần nhất với giờ đó.
|
|
98
|
+
*/
|
|
99
|
+
getBottlePrep(): Observable<BottlePrepFromSheet | null>;
|
|
100
|
+
/**
|
|
101
|
+
* Đọc tab `Settings` (A:E từ dòng 2). Cột A = **ID** (khóa ổn định), C = giá trị.
|
|
102
|
+
* Sheet cần public read như tab Feeding.
|
|
103
|
+
*/
|
|
104
|
+
getFeedingSettings(): Observable<FeedingSettingRow[]>;
|
|
105
|
+
/**
|
|
106
|
+
* Cập nhật **cột Giá trị** theo **ID** (cột A). POST qua Apps Script
|
|
107
|
+
* `action: 'updateFeedingSettings'`, body: `{ updates: [{ id, value }] }`.
|
|
108
|
+
*/
|
|
109
|
+
saveFeedingSettings(updates: {
|
|
110
|
+
id: string;
|
|
111
|
+
value: number;
|
|
112
|
+
/** Khi sheet chưa có dòng ID này — Apps Script sẽ append (cột B/D/E). */
|
|
113
|
+
name?: string;
|
|
114
|
+
unit?: string;
|
|
115
|
+
dataType?: string;
|
|
116
|
+
}[], meta?: {
|
|
117
|
+
user?: string;
|
|
118
|
+
}): Observable<FeedingSheetResponse>;
|
|
119
|
+
addLog(log: FeedingLog): Observable<FeedingSheetResponse>;
|
|
120
|
+
/**
|
|
121
|
+
* Cập nhật một cữ bú hiện có. **KHÔNG** cho đổi `date` (nghiệp vụ: cữ
|
|
122
|
+
* thuộc về ngày nào thì cố định, chỉ chỉnh giờ / ml / note).
|
|
123
|
+
*
|
|
124
|
+
* - `rowIndex` là số row 1-based trong sheet (header = row 1).
|
|
125
|
+
* - Server-side Apps Script chịu trách nhiệm validate + chỉ ghi 3 cột
|
|
126
|
+
* `Giờ`, `Dung tích`, `Ghi chú` để tránh nguy cơ ghi đè cột khác.
|
|
127
|
+
*/
|
|
128
|
+
updateLog(rowIndex: number, patch: {
|
|
129
|
+
time: string;
|
|
130
|
+
volume: number;
|
|
131
|
+
note?: string;
|
|
132
|
+
}): Observable<FeedingSheetResponse>;
|
|
133
|
+
deleteLog(rowIndex: number): Observable<FeedingSheetResponse>;
|
|
134
|
+
/**
|
|
135
|
+
* Ghi dòng 1 cột G–K tab Feeding: … | giờ pha | **K = ISO** (để load chính xác).
|
|
136
|
+
* Apps Script: action `setBottlePrep`.
|
|
137
|
+
*/
|
|
138
|
+
setBottlePrepOnSheet(payload: {
|
|
139
|
+
user: string;
|
|
140
|
+
volumeMl: number;
|
|
141
|
+
time: string;
|
|
142
|
+
atIso: string;
|
|
143
|
+
}): Observable<FeedingSheetResponse>;
|
|
144
|
+
/**
|
|
145
|
+
* Xoá H1:K1 trên tab Feeding. Apps Script: `clearBottlePrep`.
|
|
146
|
+
*/
|
|
147
|
+
clearBottlePrepOnSheet(): Observable<FeedingSheetResponse>;
|
|
148
|
+
/**
|
|
149
|
+
* Gửi POST tới Google Apps Script web app.
|
|
150
|
+
*
|
|
151
|
+
* Dev (proxy): dùng HttpClient bình thường, đọc JSON response.
|
|
152
|
+
*
|
|
153
|
+
* Prod (mobile-safe): dùng fetch với `mode: 'no-cors'`.
|
|
154
|
+
* - GAS `doPost` trả 302 redirect sang `script.googleusercontent.com`.
|
|
155
|
+
* XHR (HttpClient) cố gắng đọc CORS header ở endpoint cuối, và trên
|
|
156
|
+
* mobile/WebView hành vi này rất flaky ⇒ request thường fail với
|
|
157
|
+
* status 0 dù GAS đã ghi thành công.
|
|
158
|
+
* - `fetch` với `mode: 'no-cors'` gửi request như form thường, không
|
|
159
|
+
* expect đọc response, không cần CORS header ⇒ không bao giờ fail
|
|
160
|
+
* vì CORS. Opaque response cũng OK vì ta sẽ reload để verify.
|
|
161
|
+
* - Content-Type `text/plain;charset=utf-8` vẫn cần để nằm trong
|
|
162
|
+
* "CORS-safelisted" và không trigger preflight OPTIONS.
|
|
163
|
+
*/
|
|
164
|
+
private postToAppsScript;
|
|
165
|
+
/**
|
|
166
|
+
* Chuyển "07/04/2026" hoặc "2026-04-07" → "YYYY-MM-DD"
|
|
167
|
+
*/
|
|
168
|
+
private parseSheetDate;
|
|
169
|
+
private normalizeTime;
|
|
170
|
+
private parseBottlePrepRow;
|
|
171
|
+
/** Khi sheet chưa có cột K: chọn ngày (hôm qua / hôm nay / mai) sao cho giờ J gần `now` nhất. */
|
|
172
|
+
private guessIsoFromClockTime;
|
|
173
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<FeedingLogService, never>;
|
|
174
|
+
static ɵprov: _angular_core.ɵɵInjectableDeclaration<FeedingLogService>;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
interface DailySummary {
|
|
178
|
+
date: string;
|
|
179
|
+
total: number;
|
|
180
|
+
count: number;
|
|
181
|
+
avg: number;
|
|
182
|
+
}
|
|
183
|
+
interface FeedingPrediction {
|
|
184
|
+
hasData: boolean;
|
|
185
|
+
samples: number;
|
|
186
|
+
/** ISO time "HH:mm" dự đoán cữ kế tiếp */
|
|
187
|
+
nextTime?: string;
|
|
188
|
+
/** Date object đầy đủ của cữ dự đoán */
|
|
189
|
+
nextAt?: Date;
|
|
190
|
+
/** Dung tích dự đoán (ml, làm tròn 5ml) */
|
|
191
|
+
nextVolume?: number;
|
|
192
|
+
/** Khoảng dung tích tin cậy [low, high] */
|
|
193
|
+
volumeRange?: [number, number];
|
|
194
|
+
/** Phút trung vị giữa các cữ gần đây */
|
|
195
|
+
medianIntervalMinutes?: number;
|
|
196
|
+
/** 'low' | 'medium' | 'high' */
|
|
197
|
+
confidence: 'low' | 'medium' | 'high';
|
|
198
|
+
/** Giải thích ngắn gọn các yếu tố */
|
|
199
|
+
reasoning: string[];
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Predict next feeding time & volume bằng **pattern-matching theo giờ trong ngày**.
|
|
203
|
+
*
|
|
204
|
+
* Ý tưởng: cữ cuối xảy ra lúc T (vd: hôm nay 3:00). Đi tìm các cặp
|
|
205
|
+
* (cữ trước, cữ kế tiếp) trong lịch sử
|
|
206
|
+
* mà "cữ trước" có **giờ-trong-ngày gần T**. Lấy median của:
|
|
207
|
+
* - interval → dự đoán GIỜ cữ tiếp theo
|
|
208
|
+
* - volume cữ kế tiếp → dự đoán DUNG TÍCH
|
|
209
|
+
*
|
|
210
|
+
* VD: hôm qua 3:05 bú → 4:10 bú 40ml, hôm kia 2:55 bú → 4:05 bú 45ml.
|
|
211
|
+
* Hôm nay 3:00 vừa bú → pattern gợi ý ~1h5p nữa, ~40ml.
|
|
212
|
+
*/
|
|
213
|
+
declare function predictNextFeeding(logs: FeedingLog[], now?: Date): FeedingPrediction;
|
|
214
|
+
/**
|
|
215
|
+
* Tổng hợp theo ngày cho N ngày gần nhất (bao gồm hôm nay)
|
|
216
|
+
*/
|
|
217
|
+
declare function getDailySummaries(logs: FeedingLog[], days: number, now?: Date): DailySummary[];
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Nutrition target & evaluation based on baby's weight + age.
|
|
221
|
+
*
|
|
222
|
+
* References:
|
|
223
|
+
* - AAP (American Academy of Pediatrics): 150-200 ml/kg/day for <6mo (formula)
|
|
224
|
+
* - WHO: babies regulate intake themselves, but typical formula/mixed ranges
|
|
225
|
+
* - Vietnam MOH pediatric guidelines
|
|
226
|
+
*
|
|
227
|
+
* Disclaimer: These are averages for formula/mixed feeding. EBF babies
|
|
228
|
+
* self-regulate and may differ significantly. Always consult your pediatrician.
|
|
229
|
+
*/
|
|
230
|
+
interface NutritionTarget {
|
|
231
|
+
/** Min dung tích/ngày (ml) */
|
|
232
|
+
dailyMlMin: number;
|
|
233
|
+
/** Max dung tích/ngày (ml) */
|
|
234
|
+
dailyMlMax: number;
|
|
235
|
+
/** Gợi ý ml mỗi cữ (min) */
|
|
236
|
+
perFeedMin: number;
|
|
237
|
+
/** Gợi ý ml mỗi cữ (max) */
|
|
238
|
+
perFeedMax: number;
|
|
239
|
+
/** Số cữ/ngày gợi ý (min) */
|
|
240
|
+
feedsPerDayMin: number;
|
|
241
|
+
/** Số cữ/ngày gợi ý (max) */
|
|
242
|
+
feedsPerDayMax: number;
|
|
243
|
+
/** Hệ số ml/kg/ngày (trung bình) */
|
|
244
|
+
perKgFactor: number;
|
|
245
|
+
ageLabel: string;
|
|
246
|
+
note: string;
|
|
247
|
+
}
|
|
248
|
+
type NutritionStatus = 'no-data' | 'low' | 'under' | 'ok' | 'over' | 'high';
|
|
249
|
+
interface NutritionEvaluation {
|
|
250
|
+
target: NutritionTarget;
|
|
251
|
+
actualMl: number;
|
|
252
|
+
actualCount: number;
|
|
253
|
+
/** % so với trung điểm khoảng khuyến nghị */
|
|
254
|
+
percentOfTarget: number;
|
|
255
|
+
/** % so với min khuyến nghị */
|
|
256
|
+
percentOfMin: number;
|
|
257
|
+
status: NutritionStatus;
|
|
258
|
+
statusLabel: string;
|
|
259
|
+
advice: string;
|
|
260
|
+
/** Dung tích còn cần để đạt min khuyến nghị */
|
|
261
|
+
remainingToMin: number;
|
|
262
|
+
/** Dung tích còn cần để đạt max khuyến nghị */
|
|
263
|
+
remainingToMax: number;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* So sánh lượng đã bú với tiến độ đều trong ngày (theo thời điểm hiện tại).
|
|
267
|
+
* Giả định phân bổ đều từ 0h → 24h local; KN cả ngày = min–max ml.
|
|
268
|
+
*/
|
|
269
|
+
interface NutritionPaceInfo {
|
|
270
|
+
/** Phần ngày đã trôi qua (0–1], từ 0h local đến `now` */
|
|
271
|
+
dayFraction: number;
|
|
272
|
+
/** Nếu bú đều theo mức giữa KN cả ngày, đến lúc này kỳ vọng ~bao nhiêu ml */
|
|
273
|
+
expectedMidMl: number;
|
|
274
|
+
expectedMinMl: number;
|
|
275
|
+
expectedMaxMl: number;
|
|
276
|
+
/** 100% ≈ đúng mức trung bình tại thời điểm hiện tại */
|
|
277
|
+
percentOfPaceMid: number;
|
|
278
|
+
paceStatus: 'ahead' | 'on-track' | 'behind';
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Compute recommended daily milk intake based on weight (kg) and age (days).
|
|
282
|
+
*
|
|
283
|
+
* Week 1: linearly ramps up from day 1 (very small) to day 7 (~150ml/kg)
|
|
284
|
+
* - Rule of thumb: day N × 10-15ml per feed × 8-12 feeds
|
|
285
|
+
*/
|
|
286
|
+
declare function getNutritionTarget(weightKg: number, ageInDays: number): NutritionTarget | null;
|
|
287
|
+
|
|
288
|
+
interface NotificationLog {
|
|
289
|
+
user: string;
|
|
290
|
+
content: string;
|
|
291
|
+
acknowledgeUsers: string[];
|
|
292
|
+
createdAt?: string;
|
|
293
|
+
rowIndex?: number;
|
|
294
|
+
}
|
|
295
|
+
interface NotificationSheetResponse {
|
|
296
|
+
success: boolean;
|
|
297
|
+
rowIndex?: number;
|
|
298
|
+
error?: string;
|
|
299
|
+
}
|
|
300
|
+
declare class NotificationLogService {
|
|
301
|
+
private http;
|
|
302
|
+
private readonly SHEET_ID;
|
|
303
|
+
private readonly SHEET_NAME;
|
|
304
|
+
private readonly API_KEY;
|
|
305
|
+
private readonly BASE_URL;
|
|
306
|
+
private readonly APPS_SCRIPT_URL;
|
|
307
|
+
readonly notifications: _angular_core.WritableSignal<NotificationLog[]>;
|
|
308
|
+
readonly loading: _angular_core.WritableSignal<boolean>;
|
|
309
|
+
loadNotifications(): Observable<NotificationLog[]>;
|
|
310
|
+
addNotification(payload: {
|
|
311
|
+
user: string;
|
|
312
|
+
content: string;
|
|
313
|
+
}): Observable<NotificationSheetResponse>;
|
|
314
|
+
acknowledgeNotification(rowIndex: number, user: string): Observable<NotificationSheetResponse>;
|
|
315
|
+
deleteNotification(rowIndex: number, user: string): Observable<NotificationSheetResponse>;
|
|
316
|
+
private mapRows;
|
|
317
|
+
private postToAppsScript;
|
|
318
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<NotificationLogService, never>;
|
|
319
|
+
static ɵprov: _angular_core.ɵɵInjectableDeclaration<NotificationLogService>;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
interface Profile {
|
|
323
|
+
babyName: string;
|
|
324
|
+
birthDate: string;
|
|
325
|
+
gender?: 'boy' | 'girl' | '';
|
|
326
|
+
}
|
|
327
|
+
interface DayStats$1 {
|
|
328
|
+
date: string;
|
|
329
|
+
total: number;
|
|
330
|
+
count: number;
|
|
331
|
+
/** Trung bình số học (ml/cữ). */
|
|
332
|
+
avg: number;
|
|
333
|
+
/** Trung vị thể tích một cữ (ml) — ổn định hơn khi có cữ rất nhỏ. */
|
|
334
|
+
medianMl: number;
|
|
335
|
+
/**
|
|
336
|
+
* Giá trị dùng khi so khuyến nghị: trung vị nếu ≥3 cữ (ít nhạy cữ “lắt nhắt”),
|
|
337
|
+
* ngược lại trùng TB.
|
|
338
|
+
*/
|
|
339
|
+
typicalFeedMl: number;
|
|
340
|
+
max: number;
|
|
341
|
+
min: number;
|
|
342
|
+
firstTime: string;
|
|
343
|
+
lastTime: string;
|
|
344
|
+
}
|
|
345
|
+
declare class FeedingComponent {
|
|
346
|
+
private route;
|
|
347
|
+
private destroyRef;
|
|
348
|
+
private feedingLogService;
|
|
349
|
+
private weightLogService;
|
|
350
|
+
private activityLogService;
|
|
351
|
+
private eventLogService;
|
|
352
|
+
private notificationLogService;
|
|
353
|
+
readonly appInfo: _hbslovely_baby_feeding.AppInfo;
|
|
354
|
+
/** Controlled by NG_APP_ENABLE_EXPLORER env var. When false, the Documents
|
|
355
|
+
* tab is hidden and no Drive/Apps Script calls are made. */
|
|
356
|
+
readonly explorerEnabled: boolean;
|
|
357
|
+
aboutDialogOpen: _angular_core.WritableSignal<boolean>;
|
|
358
|
+
/** Vùng cuộn thật của trang (`.feeding-page`), không phải `document`. */
|
|
359
|
+
private feedingPageRef?;
|
|
360
|
+
Math: Math;
|
|
361
|
+
/** Tab nav phía dưới: feeding | weight | schedule | mom | medical | documents. */
|
|
362
|
+
bottomTab: _angular_core.WritableSignal<"weight" | "feeding" | "mom" | "medical" | "schedule" | "documents">;
|
|
363
|
+
readonly navTabs: _angular_core.Signal<({
|
|
364
|
+
id: "feeding";
|
|
365
|
+
icon: string;
|
|
366
|
+
label: string;
|
|
367
|
+
} | {
|
|
368
|
+
id: "weight";
|
|
369
|
+
icon: string;
|
|
370
|
+
label: string;
|
|
371
|
+
} | {
|
|
372
|
+
id: "schedule";
|
|
373
|
+
icon: string;
|
|
374
|
+
label: string;
|
|
375
|
+
} | {
|
|
376
|
+
id: "mom";
|
|
377
|
+
icon: string;
|
|
378
|
+
label: string;
|
|
379
|
+
} | {
|
|
380
|
+
id: "medical";
|
|
381
|
+
icon: string;
|
|
382
|
+
label: string;
|
|
383
|
+
} | {
|
|
384
|
+
id: "documents";
|
|
385
|
+
icon: string;
|
|
386
|
+
label: string;
|
|
387
|
+
})[]>;
|
|
388
|
+
readonly activeTabIndex: _angular_core.Signal<number>;
|
|
389
|
+
setBottomTab(tab: 'feeding' | 'weight' | 'schedule' | 'mom' | 'medical' | 'documents'): void;
|
|
390
|
+
/** Đưa cuộn về đầu khi đổi tab (shell `.feeding-page` + window dự phòng). */
|
|
391
|
+
private scrollFeedingShellToTop;
|
|
392
|
+
user: _angular_core.WritableSignal<string>;
|
|
393
|
+
profile: _angular_core.WritableSignal<Profile | null>;
|
|
394
|
+
draft: _angular_core.WritableSignal<Profile>;
|
|
395
|
+
/**
|
|
396
|
+
* Cân mới nhất từ sheet Weight (bản ghi ngày mới nhất toàn sheet).
|
|
397
|
+
* Cột user chỉ để biết ai log — không lọc theo user khi lấy giá trị này.
|
|
398
|
+
*/
|
|
399
|
+
latestWeightKgFromSheet: _angular_core.WritableSignal<number | undefined>;
|
|
400
|
+
editing: _angular_core.WritableSignal<boolean>;
|
|
401
|
+
now: _angular_core.WritableSignal<Date>;
|
|
402
|
+
logs: _angular_core.WritableSignal<FeedingLog[]>;
|
|
403
|
+
loadingLogs: _angular_core.WritableSignal<boolean>;
|
|
404
|
+
syncError: _angular_core.WritableSignal<string>;
|
|
405
|
+
lastContentLoadTime: _angular_core.WritableSignal<Date | null>;
|
|
406
|
+
syncMessage: _angular_core.WritableSignal<string>;
|
|
407
|
+
/** Đồng bộ từ `app-feeding-dialogs` — ẩn toast lỗi khi dialog log đang mở. */
|
|
408
|
+
logDialogOpen: _angular_core.WritableSignal<boolean>;
|
|
409
|
+
/** Bumped on `refreshAll()` so daily reloads bottle prep. */
|
|
410
|
+
dailyRefreshToken: _angular_core.WritableSignal<number>;
|
|
411
|
+
/** Đọc từ tab Sheet `Settings` — key theo `FEEDING_SETTING_ID`. */
|
|
412
|
+
feedingSettings: _angular_core.WritableSignal<FeedingSettingsResolved>;
|
|
413
|
+
/** Nhắc sự kiện lịch (toàn cục) — trong cửa sổ Settings + sheet Event */
|
|
414
|
+
eventReminderDialogOpen: _angular_core.WritableSignal<boolean>;
|
|
415
|
+
eventReminderAckSaving: _angular_core.WritableSignal<boolean>;
|
|
416
|
+
/** Đã bấm «Đã hiểu» — chỉ ẩn đến khi tải lại trang (F5 mở lại dialog). */
|
|
417
|
+
private eventReminderSessionDismissed;
|
|
418
|
+
/** Đã hiện/đóng dialog trong phiên tải trang — tránh mở lại khi `now` tick. */
|
|
419
|
+
private eventReminderHandledThisLoad;
|
|
420
|
+
/** Synced from daily + dialogs (log calc). */
|
|
421
|
+
bottlePrep: _angular_core.WritableSignal<BottlePrepFromSheet | null>;
|
|
422
|
+
notificationDraft: _angular_core.WritableSignal<string>;
|
|
423
|
+
notificationSaving: _angular_core.WritableSignal<boolean>;
|
|
424
|
+
notificationListDialogOpen: _angular_core.WritableSignal<boolean>;
|
|
425
|
+
notificationError: _angular_core.WritableSignal<string>;
|
|
426
|
+
notificationComposerDialogOpen: _angular_core.WritableSignal<boolean>;
|
|
427
|
+
acknowledgeAllConfirmDialogOpen: _angular_core.WritableSignal<boolean>;
|
|
428
|
+
/** Ngày đang xem trên card lịch sử (mặc định: hôm qua). */
|
|
429
|
+
ageInDays: _angular_core.Signal<number | null>;
|
|
430
|
+
/**
|
|
431
|
+
* Sự kiện trong cửa sổ nhắc: từ (now) đã vào khoảng [T - lead, T), chưa Acknowledge, T > now.
|
|
432
|
+
*/
|
|
433
|
+
scheduleReminderEvents: _angular_core.Signal<_hbslovely_baby_feeding.FeedingEventLog[]>;
|
|
434
|
+
visibleNotifications: _angular_core.Signal<NotificationLog[]>;
|
|
435
|
+
displayedNotifications: _angular_core.Signal<NotificationLog[]>;
|
|
436
|
+
hiddenNotificationCount: _angular_core.Signal<number>;
|
|
437
|
+
/** Hero gộp tóm tắt profile (bỏ card overview riêng). */
|
|
438
|
+
profileHeroWeightLabel: _angular_core.Signal<string | null>;
|
|
439
|
+
ageBreakdown: _angular_core.Signal<{
|
|
440
|
+
days: number;
|
|
441
|
+
weeks: number;
|
|
442
|
+
months: number;
|
|
443
|
+
remainingMonths: number;
|
|
444
|
+
years: number;
|
|
445
|
+
weeksInMonth: number;
|
|
446
|
+
remainingDays: number;
|
|
447
|
+
} | null>;
|
|
448
|
+
todayStats: _angular_core.Signal<DayStats$1>;
|
|
449
|
+
yesterdayStats: _angular_core.Signal<DayStats$1>;
|
|
450
|
+
todayLogs: _angular_core.Signal<FeedingLog[]>;
|
|
451
|
+
yesterdayLogs: _angular_core.Signal<FeedingLog[]>;
|
|
452
|
+
prediction: _angular_core.Signal<FeedingPrediction>;
|
|
453
|
+
/** Cữ bú gần nhất (toàn bộ lịch sử, không chỉ hôm nay) */
|
|
454
|
+
nutritionTarget: _angular_core.Signal<NutritionTarget | null>;
|
|
455
|
+
lastActivityTime: _angular_core.Signal<string>;
|
|
456
|
+
constructor();
|
|
457
|
+
private loadProfile;
|
|
458
|
+
startEdit(): void;
|
|
459
|
+
cancelEdit(): void;
|
|
460
|
+
save(): void;
|
|
461
|
+
clearProfile(): void;
|
|
462
|
+
/** Đóng menu bánh răng trên hero merged (dùng sau khi chọn mục). */
|
|
463
|
+
closeProfileActionsMenu(menu: HTMLDetailsElement): void;
|
|
464
|
+
openAboutDialog(): void;
|
|
465
|
+
closeAboutDialog(): void;
|
|
466
|
+
updateDraftName(value: string): void;
|
|
467
|
+
updateDraftBirth(value: string): void;
|
|
468
|
+
setDraftGender(gender: Profile['gender']): void;
|
|
469
|
+
loadLogs(): void;
|
|
470
|
+
/** Đọc tab Google Sheet `Settings` (cột A = ID) và áp dụng vào UI. */
|
|
471
|
+
loadFeedingSettings(): void;
|
|
472
|
+
onFeedingLogsChanged(): void;
|
|
473
|
+
onFeedingSettingsChanged(s: FeedingSettingsResolved): void;
|
|
474
|
+
/** Tải cân mới nhất từ sheet Weight (cùng nguồn với tab Cân nặng). */
|
|
475
|
+
loadWeightLogs(): void;
|
|
476
|
+
loadScheduleEvents(options?: {
|
|
477
|
+
checkReminder?: boolean;
|
|
478
|
+
}): void;
|
|
479
|
+
loadNotifications(): void;
|
|
480
|
+
updateNotificationDraft(value: string): void;
|
|
481
|
+
openNotificationComposerDialog(): void;
|
|
482
|
+
closeNotificationComposerDialog(): void;
|
|
483
|
+
openNotificationListDialog(): void;
|
|
484
|
+
closeNotificationListDialog(): void;
|
|
485
|
+
createNotification(): void;
|
|
486
|
+
acknowledgeNotification(item: NotificationLog): void;
|
|
487
|
+
deleteNotification(item: NotificationLog): void;
|
|
488
|
+
canDeleteNotification(item: NotificationLog): boolean;
|
|
489
|
+
openAcknowledgeAllConfirmDialog(): void;
|
|
490
|
+
closeAcknowledgeAllConfirmDialog(): void;
|
|
491
|
+
confirmAcknowledgeAll(): void;
|
|
492
|
+
acknowledgeAllNotifications(): void;
|
|
493
|
+
formatNotificationCreatedAt(value?: string): string;
|
|
494
|
+
private logNotificationAction;
|
|
495
|
+
/** Giống F5 — cho phép kiểm tra và mở lại dialog nhắc sự kiện. */
|
|
496
|
+
private resetEventReminderSessionForReload;
|
|
497
|
+
private tryOpenEventReminderAfterLoad;
|
|
498
|
+
closeEventReminderDialog(): void;
|
|
499
|
+
skipEventReminderSession(): void;
|
|
500
|
+
acknowledgeEventReminderRow(rowIndex: number): void;
|
|
501
|
+
acknowledgeAllEventReminders(): void;
|
|
502
|
+
formatReminderEventWhen(ev: {
|
|
503
|
+
date: string;
|
|
504
|
+
time: string;
|
|
505
|
+
}): string;
|
|
506
|
+
formatReminderEventTime(time: string): string;
|
|
507
|
+
private parseReminderHour;
|
|
508
|
+
private reminderTimePeriod;
|
|
509
|
+
/** Dialog log calc — clear sheet prep; daily reloads via `dailyRefreshToken`. */
|
|
510
|
+
clearBottlePrep(): void;
|
|
511
|
+
/**
|
|
512
|
+
* Tham chiếu tới `<app-documents>` đang render trong tab Documents.
|
|
513
|
+
* `*ngIf` của template khiến component có thể không tồn tại (khi đang ở
|
|
514
|
+
* tab Cữ bú) → ViewChild sẽ trả `undefined`. Với `static: false` (mặc
|
|
515
|
+
* định), Angular sẽ resolve lại sau mỗi change detection nên ViewChild
|
|
516
|
+
* tự cập nhật khi user chuyển tab.
|
|
517
|
+
*/
|
|
518
|
+
private documentsCmp?;
|
|
519
|
+
private medicalCmp?;
|
|
520
|
+
private weightCmp?;
|
|
521
|
+
/**
|
|
522
|
+
* Reload đồng thời nhật ký bú, cân nặng, tiền sử y tế **và** documents.
|
|
523
|
+
* Các child tab dùng `[hidden]` nên component có thể chưa mount —
|
|
524
|
+
* `?.refresh()` bỏ qua an toàn.
|
|
525
|
+
*/
|
|
526
|
+
refreshAll(): void;
|
|
527
|
+
/** Khi có activity mới từ user khác — reload toàn bộ dữ liệu */
|
|
528
|
+
onActivityChanged(): void;
|
|
529
|
+
/** Ảnh đính kèm trên timeline Y tế → tab Tài liệu + preview file Explorer. */
|
|
530
|
+
openMedicalAttachmentInDocuments(explorerEntryId: number): void;
|
|
531
|
+
/** True khi một nguồn dữ liệu đang load — dùng cho icon spin. */
|
|
532
|
+
isAnyLoading: _angular_core.Signal<boolean>;
|
|
533
|
+
private logTimestamp;
|
|
534
|
+
private dayLogsGroupedStats;
|
|
535
|
+
private computeDayStats;
|
|
536
|
+
/** ISO yyyy-mm-dd, `daysBefore` ngày so với «hôm nay» theo đồng hồ app. */
|
|
537
|
+
dateStrDaysAgo(daysBefore: number): string;
|
|
538
|
+
/** Tổng/cữ của 1 ngày chỉ tính tới cùng giờ hiện tại (so «ngang giờ»). */
|
|
539
|
+
private statsUpToNowForDate;
|
|
540
|
+
private statsFromList;
|
|
541
|
+
private toDateStr;
|
|
542
|
+
private toTimeStr;
|
|
543
|
+
todayDateStr(): string;
|
|
544
|
+
yesterdayDateStr(): string;
|
|
545
|
+
/** Thứ (tiếng Việt) — hero */
|
|
546
|
+
get heroWeekday(): string;
|
|
547
|
+
/** Ngày dd/mm/yyyy — hero, dễ quét */
|
|
548
|
+
get heroDateNumeric(): string;
|
|
549
|
+
/** Giờ hiện tại dạng HH:MM — dùng cho live clock ở hero */
|
|
550
|
+
get currentTimeStr(): string;
|
|
551
|
+
get maxBirthDate(): string;
|
|
552
|
+
/** Cân sheet Weight — hiển thị trong hero (cùng logic feeding-profile). */
|
|
553
|
+
formatSheetWeightKg(kg: number | undefined): string;
|
|
554
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<FeedingComponent, never>;
|
|
555
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<FeedingComponent, "app-feeding", never, {}, {}, never, never, true, never>;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** Một dòng hiển thị sau khi gom các cữ gần nhau (cùng sheet row vẫn tách). */
|
|
559
|
+
interface FeedingViewGroup {
|
|
560
|
+
members: FeedingLog[];
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Gom cữ có khoảng cách (chuỗi) ≤ `gapMinutes` phút thành một nhóm.
|
|
564
|
+
* `gapMinutes` ≤ 0 → mỗi cữ một nhóm (không gom).
|
|
565
|
+
* Trả về thứ tự **mới → cũ** (đồng bộ list log hiện tại).
|
|
566
|
+
*/
|
|
567
|
+
declare function groupLogsByProximity(logsAnyOrder: FeedingLog[], gapMinutes: number, timestampMs: (l: FeedingLog) => number): FeedingViewGroup[];
|
|
568
|
+
|
|
569
|
+
interface DayStats {
|
|
570
|
+
date: string;
|
|
571
|
+
total: number;
|
|
572
|
+
count: number;
|
|
573
|
+
avg: number;
|
|
574
|
+
medianMl: number;
|
|
575
|
+
typicalFeedMl: number;
|
|
576
|
+
max: number;
|
|
577
|
+
min: number;
|
|
578
|
+
firstTime: string;
|
|
579
|
+
lastTime: string;
|
|
580
|
+
}
|
|
581
|
+
declare class FeedingDailyComponent implements AfterViewInit {
|
|
582
|
+
private feedingLogService;
|
|
583
|
+
private activityLogService;
|
|
584
|
+
private pastDayStripScroller?;
|
|
585
|
+
Math: Math;
|
|
586
|
+
logs: _angular_core.InputSignal<FeedingLog[]>;
|
|
587
|
+
feedingSettings: _angular_core.InputSignal<FeedingSettingsResolved>;
|
|
588
|
+
now: _angular_core.InputSignal<Date>;
|
|
589
|
+
nutritionTarget: _angular_core.InputSignal<NutritionTarget | null>;
|
|
590
|
+
user: _angular_core.InputSignal<string>;
|
|
591
|
+
syncMessage: _angular_core.InputSignal<string>;
|
|
592
|
+
syncError: _angular_core.InputSignal<string>;
|
|
593
|
+
logDialogOpen: _angular_core.InputSignal<boolean>;
|
|
594
|
+
loadingLogs: _angular_core.InputSignal<boolean>;
|
|
595
|
+
/** Parent increments on `refreshAll()` to reload bottle prep. */
|
|
596
|
+
refreshToken: _angular_core.InputSignal<number>;
|
|
597
|
+
logsChanged: _angular_core.OutputEmitterRef<void>;
|
|
598
|
+
bottlePrepChange: _angular_core.OutputEmitterRef<BottlePrepFromSheet | null>;
|
|
599
|
+
editLog: _angular_core.OutputEmitterRef<FeedingLog>;
|
|
600
|
+
historyOpen: _angular_core.OutputEmitterRef<{
|
|
601
|
+
filter: "all" | "today" | "yesterday" | "date";
|
|
602
|
+
dateStr?: string;
|
|
603
|
+
}>;
|
|
604
|
+
feedRowClick: _angular_core.OutputEmitterRef<FeedingViewGroup>;
|
|
605
|
+
private static readonly PAST_DAY_STRIP_MIN_DAYS;
|
|
606
|
+
private static readonly PAST_DAY_STRIP_MAX_DAYS;
|
|
607
|
+
/** Gợi ý lý do khi xóa / đổi sữa đã pha (dialog). */
|
|
608
|
+
readonly bottlePrepReasonQuickTags: readonly ["Sữa hỏng", "Bé ói", "Pha lại", "Đổ đi", "Hết hạn", "Bé uống ít", "Đổi loại sữa", "Đo lại ml"];
|
|
609
|
+
/** Gợi ý lý do khi xóa cữ bú. */
|
|
610
|
+
readonly feedingDeleteReasonQuickTags: readonly ["Nhập nhầm", "Trùng cữ", "Sai giờ", "Sai ml", "Bé không bú", "Log thử"];
|
|
611
|
+
bottlePrep: _angular_core.WritableSignal<BottlePrepFromSheet | null>;
|
|
612
|
+
bottlePrepDraft: _angular_core.WritableSignal<string>;
|
|
613
|
+
bottlePrepEditing: _angular_core.WritableSignal<boolean>;
|
|
614
|
+
bottlePrepSaving: _angular_core.WritableSignal<boolean>;
|
|
615
|
+
pastDayViewDate: _angular_core.WritableSignal<string>;
|
|
616
|
+
feedingDeleteConfirmLog: _angular_core.WritableSignal<FeedingLog | null>;
|
|
617
|
+
feedingDeleteReasonDraft: _angular_core.WritableSignal<string>;
|
|
618
|
+
feedingDeleteSaving: _angular_core.WritableSignal<boolean>;
|
|
619
|
+
/** Ẩn banner "hoàn tất vỗ ợ" cho cữ bú hiện tại đến khi có cữ mới. */
|
|
620
|
+
private dismissedBurpDoneFeedKey;
|
|
621
|
+
bottlePrepClearConfirmOpen: _angular_core.WritableSignal<boolean>;
|
|
622
|
+
bottlePrepClearReasonDraft: _angular_core.WritableSignal<string>;
|
|
623
|
+
bottlePrepEditConfirmOpen: _angular_core.WritableSignal<boolean>;
|
|
624
|
+
bottlePrepEditReasonDraft: _angular_core.WritableSignal<string>;
|
|
625
|
+
bottlePrepEditPending: _angular_core.WritableSignal<{
|
|
626
|
+
newVolume: number;
|
|
627
|
+
time: string;
|
|
628
|
+
atIso: string;
|
|
629
|
+
oldVolume: number;
|
|
630
|
+
} | null>;
|
|
631
|
+
bottlePrepWarning: _angular_core.Signal<{
|
|
632
|
+
type: "no-prep-late";
|
|
633
|
+
minutes: number;
|
|
634
|
+
} | {
|
|
635
|
+
type: "no-prep-soon";
|
|
636
|
+
minutes: number;
|
|
637
|
+
} | {
|
|
638
|
+
type: "expired";
|
|
639
|
+
minutes: number;
|
|
640
|
+
} | {
|
|
641
|
+
type: "expiring-soon";
|
|
642
|
+
minutes: number;
|
|
643
|
+
} | null>;
|
|
644
|
+
/** % thời gian đã trôi trong cửa sổ 1h sau khi pha (0% = mới pha, 100% = hết/ quá hạn). */
|
|
645
|
+
bottlePrepBarPct: _angular_core.Signal<number>;
|
|
646
|
+
bottlePrepBarTone: _angular_core.Signal<"calm" | "soon" | "danger">;
|
|
647
|
+
bottlePrepCountdownLine: _angular_core.Signal<string>;
|
|
648
|
+
/** Dòng trái dưới progress (khi có sữa trong bình): cảnh báo / còn bao lâu hết hạn. */
|
|
649
|
+
bottlePrepFooterLeft: _angular_core.Signal<string>;
|
|
650
|
+
/** Khoảng thời gian đã trôi kể từ lúc pha — căn phải dưới progress. */
|
|
651
|
+
bottlePrepSincePrepLabel: _angular_core.Signal<string>;
|
|
652
|
+
todayStats: _angular_core.Signal<DayStats>;
|
|
653
|
+
todayLogs: _angular_core.Signal<FeedingLog[]>;
|
|
654
|
+
todayLogViewGroups: _angular_core.Signal<FeedingViewGroup[]>;
|
|
655
|
+
private logsByDateMap;
|
|
656
|
+
pastDayViewLogs: _angular_core.Signal<FeedingLog[]>;
|
|
657
|
+
pastDayViewStats: _angular_core.Signal<DayStats>;
|
|
658
|
+
pastDayViewLogViewGroups: _angular_core.Signal<FeedingViewGroup[]>;
|
|
659
|
+
pastDayStripItems: _angular_core.Signal<{
|
|
660
|
+
date: string;
|
|
661
|
+
dateLabel: string;
|
|
662
|
+
mlLabel: string;
|
|
663
|
+
total: number;
|
|
664
|
+
}[]>;
|
|
665
|
+
yesterdayUpToNowStats: _angular_core.Signal<DayStats>;
|
|
666
|
+
priorThreeDaysUpToNowAvg: _angular_core.Signal<{
|
|
667
|
+
avgTotal: number;
|
|
668
|
+
avgCount: number;
|
|
669
|
+
}>;
|
|
670
|
+
comparison: _angular_core.Signal<{
|
|
671
|
+
totalDiff: number;
|
|
672
|
+
pct: number | null;
|
|
673
|
+
}>;
|
|
674
|
+
comparisonVsPrior3Avg: _angular_core.Signal<{
|
|
675
|
+
totalDiff: number;
|
|
676
|
+
pct: number | null;
|
|
677
|
+
}>;
|
|
678
|
+
prediction: _angular_core.Signal<FeedingPrediction>;
|
|
679
|
+
lastFeeding: _angular_core.Signal<FeedingLog | null>;
|
|
680
|
+
lastFeedingAgo: _angular_core.Signal<string>;
|
|
681
|
+
burpInsight: _angular_core.Signal<{
|
|
682
|
+
state: "active";
|
|
683
|
+
remainingMinutes: number;
|
|
684
|
+
elapsedMinutes: number;
|
|
685
|
+
lastFeedTime: string;
|
|
686
|
+
lastFeedVolume: number;
|
|
687
|
+
burpDuration: number;
|
|
688
|
+
minutesSinceBurp?: undefined;
|
|
689
|
+
} | {
|
|
690
|
+
state: "done";
|
|
691
|
+
minutesSinceBurp: number;
|
|
692
|
+
lastFeedTime: string;
|
|
693
|
+
lastFeedVolume: number;
|
|
694
|
+
burpDuration: number;
|
|
695
|
+
remainingMinutes?: undefined;
|
|
696
|
+
elapsedMinutes?: undefined;
|
|
697
|
+
} | null>;
|
|
698
|
+
burpProgressPct: _angular_core.Signal<number>;
|
|
699
|
+
closeBurpDoneBanner(): void;
|
|
700
|
+
private burpFeedKey;
|
|
701
|
+
timeUntilNext: _angular_core.Signal<string>;
|
|
702
|
+
nextFeedingRecommendedMl: _angular_core.Signal<number | null>;
|
|
703
|
+
nextFeedingRecommendation: _angular_core.Signal<{
|
|
704
|
+
recommendedMl: number;
|
|
705
|
+
hasNutritionData: boolean;
|
|
706
|
+
predictedMl?: undefined;
|
|
707
|
+
remainingDaily?: undefined;
|
|
708
|
+
remainingFeeds?: undefined;
|
|
709
|
+
} | {
|
|
710
|
+
recommendedMl: number;
|
|
711
|
+
predictedMl: number | null;
|
|
712
|
+
remainingDaily: number;
|
|
713
|
+
remainingFeeds: number;
|
|
714
|
+
hasNutritionData: boolean;
|
|
715
|
+
}>;
|
|
716
|
+
predictionMedianIntervalHuman: _angular_core.Signal<string>;
|
|
717
|
+
countdownStatus: _angular_core.Signal<"late" | "now" | "waiting">;
|
|
718
|
+
countdownProgress: _angular_core.Signal<number>;
|
|
719
|
+
recentFeedViewGroups: _angular_core.Signal<FeedingViewGroup[]>;
|
|
720
|
+
todayLogsPreview: _angular_core.Signal<FeedingViewGroup[]>;
|
|
721
|
+
pastDayViewLogsPreview: _angular_core.Signal<FeedingViewGroup[]>;
|
|
722
|
+
constructor();
|
|
723
|
+
ngAfterViewInit(): void;
|
|
724
|
+
trackFeedingViewGroup: (_index: number, g: FeedingViewGroup) => string;
|
|
725
|
+
trackPastDayStripItem(_index: number, item: {
|
|
726
|
+
date: string;
|
|
727
|
+
}): string;
|
|
728
|
+
viewGroupDisplayTime(g: FeedingViewGroup): string;
|
|
729
|
+
viewGroupDisplayVolume(g: FeedingViewGroup): number;
|
|
730
|
+
viewGroupNote(g: FeedingViewGroup): string;
|
|
731
|
+
isLowVolumeViewGroup(g: FeedingViewGroup): boolean;
|
|
732
|
+
isHighVolumeViewGroup(g: FeedingViewGroup): boolean;
|
|
733
|
+
formatIntervalFromPrevViewGroup(g: FeedingViewGroup, listDesc: FeedingViewGroup[]): string;
|
|
734
|
+
isFeedTimeGapWarningViewGroup(g: FeedingViewGroup, listDesc: FeedingViewGroup[]): boolean;
|
|
735
|
+
onFeedRowClick(g: FeedingViewGroup): void;
|
|
736
|
+
openHistoryDialog(filter?: 'all' | 'today' | 'yesterday' | 'date', dateStr?: string): void;
|
|
737
|
+
openEditDialog(log: FeedingLog): void;
|
|
738
|
+
openFeedGroupDetail(g: FeedingViewGroup): void;
|
|
739
|
+
deleteLog(log: FeedingLog): void;
|
|
740
|
+
closeFeedingDeleteConfirm(): void;
|
|
741
|
+
updateFeedingDeleteReason(v: string): void;
|
|
742
|
+
toggleFeedingDeleteReasonQuickTag(tag: string): void;
|
|
743
|
+
confirmDeleteFeedingLog(): void;
|
|
744
|
+
private loadBottlePrepFromSheet;
|
|
745
|
+
editBottlePrep(): void;
|
|
746
|
+
cancelBottlePrep(): void;
|
|
747
|
+
updateBottlePrepDraft(v: string): void;
|
|
748
|
+
setBottlePrepDraft(v: number): void;
|
|
749
|
+
adjustBottlePrepDraft(delta: number): void;
|
|
750
|
+
saveBottlePrep(): void;
|
|
751
|
+
/** Lưu sheet + log pha mới / hết hạn (không qua dialog sửa ml). */
|
|
752
|
+
private commitBottlePrepSheetSave;
|
|
753
|
+
closeBottlePrepEditConfirm(): void;
|
|
754
|
+
updateBottlePrepEditReason(v: string): void;
|
|
755
|
+
confirmBottlePrepEditSave(): void;
|
|
756
|
+
openBottlePrepClearConfirm(): void;
|
|
757
|
+
closeBottlePrepClearConfirm(): void;
|
|
758
|
+
updateBottlePrepClearReason(v: string): void;
|
|
759
|
+
/** Gợi ý nhanh: chỉ 1 lý do; bấm lại cùng tag thì xóa; tag khác thì thay hết nội dung. */
|
|
760
|
+
toggleBottlePrepClearReasonQuickTag(tag: string): void;
|
|
761
|
+
toggleBottlePrepEditReasonQuickTag(tag: string): void;
|
|
762
|
+
confirmClearBottlePrep(): void;
|
|
763
|
+
formatBottleAt(iso: string): string;
|
|
764
|
+
/** Giờ:phút mốc hết hạn 1h sau khi pha — dùng cho tag. */
|
|
765
|
+
formatBottlePrepExpiryClock(iso: string): string;
|
|
766
|
+
pastDayViewShowReset(): boolean;
|
|
767
|
+
selectPastDayView(date: string): void;
|
|
768
|
+
pastDayViewCanSelectPrev(): boolean;
|
|
769
|
+
pastDayViewCanSelectNext(): boolean;
|
|
770
|
+
pastDayViewSelectPrev(): void;
|
|
771
|
+
pastDayViewSelectNext(): void;
|
|
772
|
+
pastDayViewReset(): void;
|
|
773
|
+
pastDayRelativeLabel(dateStr?: string): string;
|
|
774
|
+
formatDateDisplay(iso: string): string;
|
|
775
|
+
formatDateShort(iso: string): string;
|
|
776
|
+
formatDateStrip(iso: string): string;
|
|
777
|
+
formatDayLabel(iso: string): string;
|
|
778
|
+
todayDateStr(): string;
|
|
779
|
+
yesterdayDateStr(): string;
|
|
780
|
+
dateStrDaysAgo(daysBefore: number): string;
|
|
781
|
+
private logTimestamp;
|
|
782
|
+
private computeDayStats;
|
|
783
|
+
private statsUpToNowForDate;
|
|
784
|
+
private dayLogsGroupedStats;
|
|
785
|
+
private pseudoLogsFromViewGroups;
|
|
786
|
+
private statsFromViewGroups;
|
|
787
|
+
private statsFromList;
|
|
788
|
+
private toDateStr;
|
|
789
|
+
private toTimeStr;
|
|
790
|
+
private scrollPastDayStripIntoView;
|
|
791
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<FeedingDailyComponent, never>;
|
|
792
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<FeedingDailyComponent, "app-feeding-daily", never, { "logs": { "alias": "logs"; "required": true; "isSignal": true; }; "feedingSettings": { "alias": "feedingSettings"; "required": true; "isSignal": true; }; "now": { "alias": "now"; "required": true; "isSignal": true; }; "nutritionTarget": { "alias": "nutritionTarget"; "required": false; "isSignal": true; }; "user": { "alias": "user"; "required": true; "isSignal": true; }; "syncMessage": { "alias": "syncMessage"; "required": false; "isSignal": true; }; "syncError": { "alias": "syncError"; "required": false; "isSignal": true; }; "logDialogOpen": { "alias": "logDialogOpen"; "required": false; "isSignal": true; }; "loadingLogs": { "alias": "loadingLogs"; "required": false; "isSignal": true; }; "refreshToken": { "alias": "refreshToken"; "required": false; "isSignal": true; }; }, { "logsChanged": "logsChanged"; "bottlePrepChange": "bottlePrepChange"; "editLog": "editLog"; "historyOpen": "historyOpen"; "feedRowClick": "feedRowClick"; }, never, ["[data-feeding-charts]"], true, never>;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
interface FeedingEventLog {
|
|
796
|
+
user: string;
|
|
797
|
+
/** YYYY-MM-DD */
|
|
798
|
+
date: string;
|
|
799
|
+
/** HH:mm 24h */
|
|
800
|
+
time: string;
|
|
801
|
+
title: string;
|
|
802
|
+
note?: string;
|
|
803
|
+
place?: string;
|
|
804
|
+
acknowledged: boolean;
|
|
805
|
+
rowIndex?: number;
|
|
806
|
+
}
|
|
807
|
+
interface EventSheetResponse {
|
|
808
|
+
success: boolean;
|
|
809
|
+
rowIndex?: number;
|
|
810
|
+
error?: string;
|
|
811
|
+
}
|
|
812
|
+
declare class EventLogService {
|
|
813
|
+
private http;
|
|
814
|
+
private readonly SHEET_ID;
|
|
815
|
+
private readonly SHEET_NAME;
|
|
816
|
+
private readonly API_KEY;
|
|
817
|
+
private readonly BASE_URL;
|
|
818
|
+
private readonly APPS_SCRIPT_URL;
|
|
819
|
+
/** Trạng thái dùng chung (tab Lịch + nhắc toàn cục) */
|
|
820
|
+
readonly events: _angular_core.WritableSignal<FeedingEventLog[]>;
|
|
821
|
+
readonly loading: _angular_core.WritableSignal<boolean>;
|
|
822
|
+
/**
|
|
823
|
+
* Cột tab Event:
|
|
824
|
+
* A User | B Ngày DD/MM/YYYY | C Giờ HH:mm | D Tên sự kiện | E Ghi chú | F Vị trí | G Acknowledge
|
|
825
|
+
*/
|
|
826
|
+
loadEvents(): Observable<FeedingEventLog[]>;
|
|
827
|
+
addEvent(ev: FeedingEventLog): Observable<EventSheetResponse>;
|
|
828
|
+
updateEvent(rowIndex: number, patch: {
|
|
829
|
+
date: string;
|
|
830
|
+
time: string;
|
|
831
|
+
title: string;
|
|
832
|
+
note?: string;
|
|
833
|
+
place?: string;
|
|
834
|
+
}): Observable<EventSheetResponse>;
|
|
835
|
+
deleteEvent(rowIndex: number): Observable<EventSheetResponse>;
|
|
836
|
+
acknowledgeEvent(rowIndex: number): Observable<EventSheetResponse>;
|
|
837
|
+
/** ISO `date` + `time` → Date (giờ địa phương). */
|
|
838
|
+
static eventDateTime(ev: FeedingEventLog): Date | null;
|
|
839
|
+
private mapRows;
|
|
840
|
+
private parseAck;
|
|
841
|
+
private postToAppsScript;
|
|
842
|
+
private parseSheetDate;
|
|
843
|
+
private normalizeTime;
|
|
844
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<EventLogService, never>;
|
|
845
|
+
static ɵprov: _angular_core.ɵɵInjectableDeclaration<EventLogService>;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
interface EventDraft {
|
|
849
|
+
date: string;
|
|
850
|
+
time: string;
|
|
851
|
+
title: string;
|
|
852
|
+
note: string;
|
|
853
|
+
place: string;
|
|
854
|
+
}
|
|
855
|
+
interface ScheduleDayGroup {
|
|
856
|
+
dateIso: string;
|
|
857
|
+
dateLabel: string;
|
|
858
|
+
dateSubLabel?: string;
|
|
859
|
+
dateBadge?: string;
|
|
860
|
+
events: FeedingEventLog[];
|
|
861
|
+
}
|
|
862
|
+
interface ScheduleTimeSection {
|
|
863
|
+
id: 'today' | 'soon' | 'nextMonth' | 'later' | 'past';
|
|
864
|
+
title: string;
|
|
865
|
+
subtitle: string;
|
|
866
|
+
icon: string;
|
|
867
|
+
eventCount: number;
|
|
868
|
+
dayGroups: ScheduleDayGroup[];
|
|
869
|
+
}
|
|
870
|
+
declare class FeedingScheduleComponent {
|
|
871
|
+
private static readonly PREVIEW_EVENT_LIMIT;
|
|
872
|
+
private eventLogService;
|
|
873
|
+
private activityLogService;
|
|
874
|
+
private destroyRef;
|
|
875
|
+
private placePickerOrigin;
|
|
876
|
+
private reopenCalendarAfterAdd;
|
|
877
|
+
user: _angular_core.InputSignal<string>;
|
|
878
|
+
expandedSections: _angular_core.WritableSignal<Set<"today" | "soon" | "past" | "nextMonth" | "later">>;
|
|
879
|
+
saving: _angular_core.WritableSignal<boolean>;
|
|
880
|
+
errorMsg: _angular_core.WritableSignal<string>;
|
|
881
|
+
successMsg: _angular_core.WritableSignal<string>;
|
|
882
|
+
addOpen: _angular_core.WritableSignal<boolean>;
|
|
883
|
+
draft: _angular_core.WritableSignal<EventDraft>;
|
|
884
|
+
editing: _angular_core.WritableSignal<FeedingEventLog | null>;
|
|
885
|
+
editDraft: _angular_core.WritableSignal<EventDraft>;
|
|
886
|
+
now: _angular_core.WritableSignal<Date>;
|
|
887
|
+
calendarDialogOpen: _angular_core.WritableSignal<boolean>;
|
|
888
|
+
calendarCursor: _angular_core.WritableSignal<Date>;
|
|
889
|
+
selectedCalendarDate: _angular_core.WritableSignal<string>;
|
|
890
|
+
placePickerOpen: _angular_core.WritableSignal<"add" | "edit" | null>;
|
|
891
|
+
placeMenuRect: _angular_core.WritableSignal<{
|
|
892
|
+
top: number;
|
|
893
|
+
left: number;
|
|
894
|
+
width: number;
|
|
895
|
+
} | null>;
|
|
896
|
+
placeMenuView: _angular_core.Signal<{
|
|
897
|
+
ctx: "add" | "edit";
|
|
898
|
+
pos: {
|
|
899
|
+
top: number;
|
|
900
|
+
left: number;
|
|
901
|
+
width: number;
|
|
902
|
+
};
|
|
903
|
+
places: string[];
|
|
904
|
+
} | null>;
|
|
905
|
+
readonly calendarWeekdays: string[];
|
|
906
|
+
monthCalendarCells: _angular_core.Signal<{
|
|
907
|
+
dateIso: string;
|
|
908
|
+
day: number;
|
|
909
|
+
inMonth: boolean;
|
|
910
|
+
isToday: boolean;
|
|
911
|
+
eventCount: number;
|
|
912
|
+
hasFutureEvent: boolean;
|
|
913
|
+
}[]>;
|
|
914
|
+
/**
|
|
915
|
+
* Xác định màu sắc cho indicator số sự kiện dựa trên loại và trạng thái sự kiện
|
|
916
|
+
*/
|
|
917
|
+
getEventIndicatorColor(dateIso: string): string;
|
|
918
|
+
selectedCalendarEvents: _angular_core.Signal<FeedingEventLog[]>;
|
|
919
|
+
calendarMonthLabel: _angular_core.Signal<string>;
|
|
920
|
+
constructor();
|
|
921
|
+
onPlaceMenuViewportChange(): void;
|
|
922
|
+
events: _angular_core.Signal<FeedingEventLog[]>;
|
|
923
|
+
loading: _angular_core.Signal<boolean>;
|
|
924
|
+
/** Vị trí đã dùng — gợi ý combobox khi thêm/sửa. */
|
|
925
|
+
placeOptions: _angular_core.Signal<string[]>;
|
|
926
|
+
hasFutureEvents: _angular_core.Signal<boolean>;
|
|
927
|
+
futureSections: _angular_core.Signal<ScheduleTimeSection[]>;
|
|
928
|
+
pastSection: _angular_core.Signal<ScheduleTimeSection | null>;
|
|
929
|
+
refresh(): void;
|
|
930
|
+
openCalendarDialog(targetDateIso?: string): void;
|
|
931
|
+
closeCalendarDialog(): void;
|
|
932
|
+
calendarPrevMonth(): void;
|
|
933
|
+
calendarNextMonth(): void;
|
|
934
|
+
calendarGoToToday(): void;
|
|
935
|
+
calendarChangeMonth(monthIndex: number): void;
|
|
936
|
+
calendarChangeYear(year: number): void;
|
|
937
|
+
getCalendarYearRange(): number[];
|
|
938
|
+
selectCalendarDate(dateIso: string): void;
|
|
939
|
+
openAddFromCalendar(): void;
|
|
940
|
+
isFuture(ev: FeedingEventLog): boolean;
|
|
941
|
+
openAdd(): void;
|
|
942
|
+
closeAdd(keepCalendarReturn?: boolean): void;
|
|
943
|
+
openEdit(ev: FeedingEventLog): void;
|
|
944
|
+
cancelEdit(): void;
|
|
945
|
+
openPlacePicker(context: 'add' | 'edit', origin: HTMLInputElement): void;
|
|
946
|
+
private closePlacePicker;
|
|
947
|
+
private measurePlaceMenuRect;
|
|
948
|
+
private syncPlaceMenuPosition;
|
|
949
|
+
filteredPlaces(context: 'add' | 'edit'): string[];
|
|
950
|
+
onPlaceInput(value: string, context: 'add' | 'edit', origin: HTMLInputElement): void;
|
|
951
|
+
selectPlace(place: string, context: 'add' | 'edit'): void;
|
|
952
|
+
onPlaceBlur(context: 'add' | 'edit'): void;
|
|
953
|
+
submitAdd(): void;
|
|
954
|
+
submitEdit(): void;
|
|
955
|
+
deleteEv(ev: FeedingEventLog): void;
|
|
956
|
+
formatTime(time: string): string;
|
|
957
|
+
formatEventDate(iso: string): string;
|
|
958
|
+
formatEventCount(count: number): string;
|
|
959
|
+
isSectionExpanded(id: ScheduleTimeSection['id']): boolean;
|
|
960
|
+
toggleSectionExpand(id: ScheduleTimeSection['id']): void;
|
|
961
|
+
hasMoreEvents(section: ScheduleTimeSection): boolean;
|
|
962
|
+
visibleDayGroups(section: ScheduleTimeSection): ScheduleDayGroup[];
|
|
963
|
+
hiddenEventCount(section: ScheduleTimeSection): number;
|
|
964
|
+
trackSection(_: number, s: ScheduleTimeSection): string;
|
|
965
|
+
trackDay(_: number, d: ScheduleDayGroup): string;
|
|
966
|
+
trackEvent(_: number, ev: FeedingEventLog): string;
|
|
967
|
+
private countEventsInGroups;
|
|
968
|
+
private daysUntil;
|
|
969
|
+
private calendarDayDiff;
|
|
970
|
+
private limitDayGroups;
|
|
971
|
+
private groupByDate;
|
|
972
|
+
private formatDateHeader;
|
|
973
|
+
private formatFullDateLabel;
|
|
974
|
+
private formatScheduleRelativeLabel;
|
|
975
|
+
private formatFutureSpanLabel;
|
|
976
|
+
private formatPastSpanLabel;
|
|
977
|
+
private calendarSpan;
|
|
978
|
+
private emptyDraft;
|
|
979
|
+
private toDateStr;
|
|
980
|
+
private toTimeStr;
|
|
981
|
+
private normalizeTimeInput;
|
|
982
|
+
private parseIsoDate;
|
|
983
|
+
updateDraft(partial: Partial<EventDraft>): void;
|
|
984
|
+
updateEditDraft(partial: Partial<EventDraft>): void;
|
|
985
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<FeedingScheduleComponent, never>;
|
|
986
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<FeedingScheduleComponent, "app-feeding-schedule", never, { "user": { "alias": "user"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
interface FeedingTip {
|
|
990
|
+
category: 'feeding' | 'sleep' | 'development' | 'health' | 'mom' | 'activity';
|
|
991
|
+
title: string;
|
|
992
|
+
content: string;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Thẻ tham khảo nhanh cho mẹ — bổ sung nội dung động (tips tuần, món ăn).
|
|
997
|
+
* Mang tính giáo dục sức khỏe, không thay cho bác sĩ.
|
|
998
|
+
*
|
|
999
|
+
* Tham chiếu tổng quát: AAP/HealthyChildren (cho con bú, an toàn ngủ, sàng lọc),
|
|
1000
|
+
* WHO (dinh dưỡng cho bà mẹ cho con bú), CDC (sức khỏe tinh thần sau sinh).
|
|
1001
|
+
*/
|
|
1002
|
+
interface MomWellnessCard {
|
|
1003
|
+
id: string;
|
|
1004
|
+
icon: string;
|
|
1005
|
+
title: string;
|
|
1006
|
+
body: string;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Postpartum food suggestions by stage.
|
|
1011
|
+
*
|
|
1012
|
+
* Stage mốc theo ngày tuổi của bé (~ số tuần sau sinh của mẹ).
|
|
1013
|
+
* Nội dung tổng hợp theo tài liệu dinh dưỡng sau sinh phổ biến tại VN +
|
|
1014
|
+
* khuyến cáo WHO/AAP về cho con bú. Mang tính tham khảo, không thay thế
|
|
1015
|
+
* chỉ định của bác sĩ / chuyên gia dinh dưỡng.
|
|
1016
|
+
*/
|
|
1017
|
+
interface FoodItem {
|
|
1018
|
+
name: string;
|
|
1019
|
+
note?: string;
|
|
1020
|
+
/** Emoji minh họa */
|
|
1021
|
+
emoji?: string;
|
|
1022
|
+
}
|
|
1023
|
+
interface PostpartumStage {
|
|
1024
|
+
id: string;
|
|
1025
|
+
label: string;
|
|
1026
|
+
/** inclusive, ngày tuổi bé */
|
|
1027
|
+
ageFromDays: number;
|
|
1028
|
+
/** exclusive */
|
|
1029
|
+
ageToDays: number;
|
|
1030
|
+
headline: string;
|
|
1031
|
+
description: string;
|
|
1032
|
+
encourage: FoodItem[];
|
|
1033
|
+
avoid: FoodItem[];
|
|
1034
|
+
tip?: string;
|
|
1035
|
+
accent: 'rose' | 'peach' | 'mint' | 'lavender' | 'amber';
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
interface MomTabProfile {
|
|
1039
|
+
babyName: string;
|
|
1040
|
+
birthDate: string;
|
|
1041
|
+
gender?: 'boy' | 'girl' | '';
|
|
1042
|
+
}
|
|
1043
|
+
type MomSection = 'overview' | 'care' | 'food' | 'tips';
|
|
1044
|
+
declare class FeedingMomComponent {
|
|
1045
|
+
private destroyRef;
|
|
1046
|
+
profile: _angular_core.InputSignal<MomTabProfile | null>;
|
|
1047
|
+
editing: _angular_core.InputSignal<boolean>;
|
|
1048
|
+
goToFeeding: _angular_core.OutputEmitterRef<void>;
|
|
1049
|
+
now: _angular_core.WritableSignal<Date>;
|
|
1050
|
+
activeSection: _angular_core.WritableSignal<MomSection>;
|
|
1051
|
+
momFoodStageOverride: _angular_core.WritableSignal<number | null>;
|
|
1052
|
+
readonly momWellnessCards: MomWellnessCard[];
|
|
1053
|
+
readonly postpartumStages: PostpartumStage[];
|
|
1054
|
+
readonly momStageTotal: number;
|
|
1055
|
+
constructor();
|
|
1056
|
+
ageInDays: _angular_core.Signal<number | null>;
|
|
1057
|
+
activeMomStage: _angular_core.Signal<PostpartumStage>;
|
|
1058
|
+
activeMomStageIndex: _angular_core.Signal<number>;
|
|
1059
|
+
currentMomStage: _angular_core.Signal<PostpartumStage>;
|
|
1060
|
+
isMomStageAuto: _angular_core.Signal<boolean>;
|
|
1061
|
+
momPeriodContext: _angular_core.Signal<{
|
|
1062
|
+
label: string;
|
|
1063
|
+
summary: string;
|
|
1064
|
+
} | null>;
|
|
1065
|
+
momTipsThisPeriod: _angular_core.Signal<FeedingTip[]>;
|
|
1066
|
+
stageProgressPct: _angular_core.Signal<number>;
|
|
1067
|
+
setSection(section: MomSection): void;
|
|
1068
|
+
selectStageIndex(index: number): void;
|
|
1069
|
+
prevMomStage(): void;
|
|
1070
|
+
nextMomStage(): void;
|
|
1071
|
+
resetMomStage(): void;
|
|
1072
|
+
onGoToFeeding(): void;
|
|
1073
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<FeedingMomComponent, never>;
|
|
1074
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<FeedingMomComponent, "app-feeding-mom", never, { "profile": { "alias": "profile"; "required": false; "isSignal": true; }; "editing": { "alias": "editing"; "required": false; "isSignal": true; }; }, { "goToFeeding": "goToFeeding"; }, never, never, true, never>;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
interface NutritionTodayStats {
|
|
1078
|
+
total: number;
|
|
1079
|
+
count: number;
|
|
1080
|
+
typicalFeedMl: number;
|
|
1081
|
+
}
|
|
1082
|
+
declare class FeedingNutritionComponent {
|
|
1083
|
+
nutritionTarget: _angular_core.InputSignal<NutritionTarget | null>;
|
|
1084
|
+
todayStats: _angular_core.InputSignal<NutritionTodayStats>;
|
|
1085
|
+
now: _angular_core.InputSignal<Date>;
|
|
1086
|
+
latestWeightKg: _angular_core.InputSignal<number | undefined>;
|
|
1087
|
+
editing: _angular_core.InputSignal<boolean>;
|
|
1088
|
+
goToWeight: _angular_core.OutputEmitterRef<void>;
|
|
1089
|
+
nutritionEval: _angular_core.Signal<NutritionEvaluation | null>;
|
|
1090
|
+
nutritionPace: _angular_core.Signal<(NutritionPaceInfo & {
|
|
1091
|
+
paceLabelVi: string;
|
|
1092
|
+
}) | null>;
|
|
1093
|
+
nutritionMetricsStatus: _angular_core.Signal<{
|
|
1094
|
+
mlPerKg: "met" | "partial" | "unmet";
|
|
1095
|
+
perFeed: "met" | "partial" | "unmet";
|
|
1096
|
+
remaining: "met" | "partial" | "unmet";
|
|
1097
|
+
mlPerKgTitle: string;
|
|
1098
|
+
mlPerKgIcon: string;
|
|
1099
|
+
perFeedTitle: string;
|
|
1100
|
+
perFeedIcon: string;
|
|
1101
|
+
remainingTitle: string;
|
|
1102
|
+
remainingIcon: string;
|
|
1103
|
+
mlPerKgValue: number;
|
|
1104
|
+
mlPerKgMin: number;
|
|
1105
|
+
mlPerKgMax: number;
|
|
1106
|
+
perFeedValue: number;
|
|
1107
|
+
perFeedMin: number;
|
|
1108
|
+
perFeedMax: number;
|
|
1109
|
+
remainingPrimary: string;
|
|
1110
|
+
remainingSecondary: string;
|
|
1111
|
+
} | null>;
|
|
1112
|
+
nutritionZones: _angular_core.Signal<{
|
|
1113
|
+
minLabel: number;
|
|
1114
|
+
maxLabel: number;
|
|
1115
|
+
scaleMax: number;
|
|
1116
|
+
pointerPct: number;
|
|
1117
|
+
pointerVisPct: number;
|
|
1118
|
+
} | null>;
|
|
1119
|
+
nutritionPaceZones: _angular_core.Signal<{
|
|
1120
|
+
minLabel: number;
|
|
1121
|
+
maxLabel: number;
|
|
1122
|
+
scaleMax: number;
|
|
1123
|
+
bandLeftPct: number;
|
|
1124
|
+
bandWidthPct: number;
|
|
1125
|
+
minTickPct: number;
|
|
1126
|
+
maxTickPct: number;
|
|
1127
|
+
pointerPct: number;
|
|
1128
|
+
pointerVisPct: number;
|
|
1129
|
+
paceStatus: "ahead" | "on-track" | "behind";
|
|
1130
|
+
} | null>;
|
|
1131
|
+
formatWeightKg(kg: number | undefined): string;
|
|
1132
|
+
onGoToWeight(): void;
|
|
1133
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<FeedingNutritionComponent, never>;
|
|
1134
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<FeedingNutritionComponent, "app-feeding-nutrition", never, { "nutritionTarget": { "alias": "nutritionTarget"; "required": false; "isSignal": true; }; "todayStats": { "alias": "todayStats"; "required": false; "isSignal": true; }; "now": { "alias": "now"; "required": false; "isSignal": true; }; "latestWeightKg": { "alias": "latestWeightKg"; "required": false; "isSignal": true; }; "editing": { "alias": "editing"; "required": false; "isSignal": true; }; }, { "goToWeight": "goToWeight"; }, never, never, true, never>;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Dòng thời gian phát triển của trẻ — tổng hợp từ các nguồn y khoa:
|
|
1139
|
+
* - AAP HealthyChildren.org (developmental milestones)
|
|
1140
|
+
* - CDC "Learn the Signs. Act Early." milestones (gồm mốc ~30 tháng và ~36 tháng)
|
|
1141
|
+
* - WHO Child Growth Standards
|
|
1142
|
+
* - The Wonder Weeks (Dr. Hetty van de Rijt & Dr. Frans Plooij)
|
|
1143
|
+
* - La Leche League / AAP (stomach size by age)
|
|
1144
|
+
* - Vinmec Vietnam (bài tuần tuổi)
|
|
1145
|
+
*
|
|
1146
|
+
* Khoảng ngày tuổi (ageFromDays/ageToDays) là xấp xỉ; bé sinh non nên dùng tuổi hiệu chỉnh theo BS.
|
|
1147
|
+
*/
|
|
1148
|
+
type MoodType = 'calm' | 'happy' | 'growth' | 'leap' | 'milestone';
|
|
1149
|
+
interface StomachSize {
|
|
1150
|
+
/** Mô tả so sánh: "quả anh đào", "quả óc chó"... */
|
|
1151
|
+
compareTo: string;
|
|
1152
|
+
/** "5–7ml", "22–27ml"... */
|
|
1153
|
+
volumeMl: string;
|
|
1154
|
+
/** Emoji trực quan (hoa quả tương tự) */
|
|
1155
|
+
emoji: string;
|
|
1156
|
+
/** Scale vòng tròn visualization (0.2 → 1.5) */
|
|
1157
|
+
scale: number;
|
|
1158
|
+
}
|
|
1159
|
+
interface TimelineMilestone {
|
|
1160
|
+
id: string;
|
|
1161
|
+
/** "Tuần 1", "Tuần 5", "Tháng 4"... */
|
|
1162
|
+
periodLabel: string;
|
|
1163
|
+
/** Tóm tắt 1 dòng (hiển thị cạnh period label) */
|
|
1164
|
+
tagline: string;
|
|
1165
|
+
/** Ngày tuổi bắt đầu (inclusive) */
|
|
1166
|
+
ageFromDays: number;
|
|
1167
|
+
/** Ngày tuổi kết thúc (inclusive) */
|
|
1168
|
+
ageToDays: number;
|
|
1169
|
+
/** Emoji chủ đạo cho giai đoạn */
|
|
1170
|
+
heroEmoji: string;
|
|
1171
|
+
/** Kích thước dạ dày */
|
|
1172
|
+
stomach: StomachSize;
|
|
1173
|
+
/** Tâm trạng chung */
|
|
1174
|
+
mood: MoodType;
|
|
1175
|
+
/** Nhãn tâm trạng (hiện trên badge) */
|
|
1176
|
+
moodLabel: string;
|
|
1177
|
+
/** 2-4 sự thật thú vị */
|
|
1178
|
+
funFacts: string[];
|
|
1179
|
+
/** Kỹ năng bé có/đang học (optional) */
|
|
1180
|
+
skills?: string[];
|
|
1181
|
+
/** 2-3 lưu ý cho ba mẹ */
|
|
1182
|
+
parentNotes: string[];
|
|
1183
|
+
/** Tone màu */
|
|
1184
|
+
accent: 'rose' | 'peach' | 'mint' | 'lavender' | 'amber' | 'sky' | 'coral' | 'plum';
|
|
1185
|
+
}
|
|
1186
|
+
interface TimelineCareGuide {
|
|
1187
|
+
/** Checklist ngắn cho giai đoạn này — ưu tiên việc có thể làm ngay. */
|
|
1188
|
+
todos: string[];
|
|
1189
|
+
/** Những việc nên tránh ở đúng độ tuổi này. */
|
|
1190
|
+
notToDo: string[];
|
|
1191
|
+
/** Dấu hiệu nên hỏi bác sĩ / chuyên viên y tế, không dùng để tự chẩn đoán. */
|
|
1192
|
+
watchFor: string[];
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
type WeightGrowthSex = 'male' | 'female';
|
|
1196
|
+
type WeightGrowthStatus = 'very_low' | 'low' | 'normal' | 'high' | 'very_high' | 'unknown';
|
|
1197
|
+
interface WeightGrowthEvaluation {
|
|
1198
|
+
weeks: number;
|
|
1199
|
+
/** Tuần đầy đủ hiển thị */
|
|
1200
|
+
weeksLabel: string;
|
|
1201
|
+
medianKg: number;
|
|
1202
|
+
sdKg: number;
|
|
1203
|
+
zScore: number;
|
|
1204
|
+
status: WeightGrowthStatus;
|
|
1205
|
+
statusLabel: string;
|
|
1206
|
+
detail: string;
|
|
1207
|
+
estimate: WeightWeekEstimate;
|
|
1208
|
+
}
|
|
1209
|
+
interface WeightWeekEstimate {
|
|
1210
|
+
weekAge: number;
|
|
1211
|
+
currentWeightKg: number;
|
|
1212
|
+
medianKg: number;
|
|
1213
|
+
severeUnderweightMaxKg: number;
|
|
1214
|
+
underweightMaxKg: number;
|
|
1215
|
+
standardMinKg: number;
|
|
1216
|
+
standardMaxKg: number;
|
|
1217
|
+
overweightMinKg: number;
|
|
1218
|
+
highWeightMinKg: number;
|
|
1219
|
+
deltaFromMedianKg: number;
|
|
1220
|
+
}
|
|
1221
|
+
declare function evaluateWeightForAge(weeks: number, weightKg: number, sex: WeightGrowthSex): WeightGrowthEvaluation | null;
|
|
1222
|
+
interface WhoGirlVnReferenceRow {
|
|
1223
|
+
/** 0 = sơ sinh, 1…24 = tháng đầy đủ */
|
|
1224
|
+
monthIndex: number;
|
|
1225
|
+
monthLabelVi: string;
|
|
1226
|
+
weightMedianKg: number;
|
|
1227
|
+
heightMedianCm: number;
|
|
1228
|
+
}
|
|
1229
|
+
interface HeightGrowthEvaluation {
|
|
1230
|
+
monthsAge: number;
|
|
1231
|
+
monthsLabel: string;
|
|
1232
|
+
medianCm: number;
|
|
1233
|
+
sdCm: number;
|
|
1234
|
+
zScore: number;
|
|
1235
|
+
percentile: number;
|
|
1236
|
+
status: WeightGrowthStatus;
|
|
1237
|
+
statusLabel: string;
|
|
1238
|
+
detail: string;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
interface WeightLog {
|
|
1242
|
+
user: string;
|
|
1243
|
+
/** YYYY-MM-DD */
|
|
1244
|
+
date: string;
|
|
1245
|
+
/** kg — có thể lẻ (vd 4.25) */
|
|
1246
|
+
weightKg: number;
|
|
1247
|
+
/** cm — tuỳ chọn, thường chỉ đo khi khám định kỳ */
|
|
1248
|
+
heightCm?: number;
|
|
1249
|
+
note?: string;
|
|
1250
|
+
rowIndex?: number;
|
|
1251
|
+
}
|
|
1252
|
+
interface WeightSheetResponse {
|
|
1253
|
+
success: boolean;
|
|
1254
|
+
rowIndex?: number;
|
|
1255
|
+
error?: string;
|
|
1256
|
+
}
|
|
1257
|
+
declare class WeightLogService {
|
|
1258
|
+
private http;
|
|
1259
|
+
private readonly SHEET_ID;
|
|
1260
|
+
private readonly SHEET_NAME;
|
|
1261
|
+
private readonly API_KEY;
|
|
1262
|
+
private readonly BASE_URL;
|
|
1263
|
+
private readonly APPS_SCRIPT_URL;
|
|
1264
|
+
/**
|
|
1265
|
+
* Cột:
|
|
1266
|
+
* A = User
|
|
1267
|
+
* B = Ngày (DD/MM/YYYY)
|
|
1268
|
+
* C = Cân nặng (kg)
|
|
1269
|
+
* D = Chiều cao (cm, tuỳ chọn)
|
|
1270
|
+
* E = Ghi chú
|
|
1271
|
+
*/
|
|
1272
|
+
getLogs(): Observable<WeightLog[]>;
|
|
1273
|
+
addLog(log: WeightLog): Observable<WeightSheetResponse>;
|
|
1274
|
+
updateLog(rowIndex: number, patch: {
|
|
1275
|
+
date: string;
|
|
1276
|
+
weightKg: number;
|
|
1277
|
+
heightCm?: number | null;
|
|
1278
|
+
note?: string;
|
|
1279
|
+
}): Observable<WeightSheetResponse>;
|
|
1280
|
+
deleteLog(rowIndex: number): Observable<WeightSheetResponse>;
|
|
1281
|
+
private postToAppsScript;
|
|
1282
|
+
private parseSheetDate;
|
|
1283
|
+
/** Chuỗi "4,25" hoặc "4.25 kg" → số */
|
|
1284
|
+
private parseWeightKg;
|
|
1285
|
+
/** Chuỗi "63,5" hoặc "63.5cm" → số cm */
|
|
1286
|
+
private parseHeightCm;
|
|
1287
|
+
/**
|
|
1288
|
+
* Ưu tiên format mới: D=height, E=note.
|
|
1289
|
+
* Fallback để đọc dữ liệu cũ: D=note, E=height.
|
|
1290
|
+
*/
|
|
1291
|
+
private resolveHeightAndNote;
|
|
1292
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<WeightLogService, never>;
|
|
1293
|
+
static ɵprov: _angular_core.ɵɵInjectableDeclaration<WeightLogService>;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
declare function growthStatusClass(status: string): string;
|
|
1297
|
+
|
|
1298
|
+
interface WeightDraft {
|
|
1299
|
+
date: string;
|
|
1300
|
+
weightInput: string;
|
|
1301
|
+
heightInput: string;
|
|
1302
|
+
note: string;
|
|
1303
|
+
}
|
|
1304
|
+
interface WhoStandardRow {
|
|
1305
|
+
weeks: number;
|
|
1306
|
+
label: string;
|
|
1307
|
+
minKg: number;
|
|
1308
|
+
medianKg: number;
|
|
1309
|
+
maxKg: number;
|
|
1310
|
+
minCm: number;
|
|
1311
|
+
medianCm: number;
|
|
1312
|
+
maxCm: number;
|
|
1313
|
+
isCurrent: boolean;
|
|
1314
|
+
currentWeight: number | null;
|
|
1315
|
+
currentHeight: number | null;
|
|
1316
|
+
}
|
|
1317
|
+
declare class WeightComponent {
|
|
1318
|
+
private weightLogService;
|
|
1319
|
+
private destroyRef;
|
|
1320
|
+
private activityLogService;
|
|
1321
|
+
/** Bảng tham chiếu WHO (cân + chiều cao) theo giới tính profile */
|
|
1322
|
+
readonly whoReferenceRows: _angular_core.Signal<WhoGirlVnReferenceRow[]>;
|
|
1323
|
+
/** Cùng user với trang feeding (`?user=`) */
|
|
1324
|
+
user: _angular_core.InputSignal<string>;
|
|
1325
|
+
/** Ngày sinh (YYYY-MM-DD) — để tính tuần tuổi & timeline */
|
|
1326
|
+
birthDate: _angular_core.InputSignal<string | undefined>;
|
|
1327
|
+
gender: _angular_core.InputSignal<"" | "boy" | "girl" | undefined>;
|
|
1328
|
+
babyName: _angular_core.InputSignal<string | undefined>;
|
|
1329
|
+
BABY_TIMELINE: TimelineMilestone[];
|
|
1330
|
+
logs: _angular_core.WritableSignal<WeightLog[]>;
|
|
1331
|
+
loading: _angular_core.WritableSignal<boolean>;
|
|
1332
|
+
saving: _angular_core.WritableSignal<boolean>;
|
|
1333
|
+
errorMsg: _angular_core.WritableSignal<string>;
|
|
1334
|
+
successMsg: _angular_core.WritableSignal<string>;
|
|
1335
|
+
draft: _angular_core.WritableSignal<WeightDraft>;
|
|
1336
|
+
addDialogOpen: _angular_core.WritableSignal<boolean>;
|
|
1337
|
+
editingLog: _angular_core.WritableSignal<WeightLog | null>;
|
|
1338
|
+
editDraft: _angular_core.WritableSignal<WeightDraft>;
|
|
1339
|
+
now: _angular_core.WritableSignal<Date>;
|
|
1340
|
+
timelineDialogOpen: _angular_core.WritableSignal<boolean>;
|
|
1341
|
+
timelineView: _angular_core.WritableSignal<"list" | "chart">;
|
|
1342
|
+
selectedTimelineId: _angular_core.WritableSignal<string | null>;
|
|
1343
|
+
whoTableDialogOpen: _angular_core.WritableSignal<boolean>;
|
|
1344
|
+
/** Bảng tham chiếu WHO bé gái (VN) — dialog thông tin */
|
|
1345
|
+
whoGirlReferenceDialogOpen: _angular_core.WritableSignal<boolean>;
|
|
1346
|
+
/** Alias cho template (WHO dialog + đánh giá). */
|
|
1347
|
+
readonly growthStatusClass: typeof growthStatusClass;
|
|
1348
|
+
constructor();
|
|
1349
|
+
refresh(): void;
|
|
1350
|
+
growthSex: _angular_core.Signal<WeightGrowthSex>;
|
|
1351
|
+
ageInDays: _angular_core.Signal<number | null>;
|
|
1352
|
+
sortedLogsAsc: _angular_core.Signal<WeightLog[]>;
|
|
1353
|
+
sortedLogsDesc: _angular_core.Signal<WeightLog[]>;
|
|
1354
|
+
latestHeightLog(): WeightLog | null;
|
|
1355
|
+
/** Khoảng cách ngày giữa hai mốc ISO (YYYY-MM-DD), làm tròn theo ngày. */
|
|
1356
|
+
private diffDaysIso;
|
|
1357
|
+
/** Các đoạn giữa hai lần cân liên tiếp (theo ngày tăng dần). */
|
|
1358
|
+
weightLogIntervals: _angular_core.Signal<{
|
|
1359
|
+
fromDate: string;
|
|
1360
|
+
toDate: string;
|
|
1361
|
+
days: number;
|
|
1362
|
+
deltaKg: number;
|
|
1363
|
+
gPerDay: number | null;
|
|
1364
|
+
labelShort: string;
|
|
1365
|
+
}[]>;
|
|
1366
|
+
latestGrowthEval: _angular_core.Signal<WeightGrowthEvaluation | null>;
|
|
1367
|
+
latestHeightEval: _angular_core.Signal<HeightGrowthEvaluation | null>;
|
|
1368
|
+
/**
|
|
1369
|
+
* Bảng chuẩn WHO với min/max/median theo tuần tuổi.
|
|
1370
|
+
* Highlight dòng tuần hiện tại của bé.
|
|
1371
|
+
*/
|
|
1372
|
+
whoStandardTable: _angular_core.Signal<WhoStandardRow[]>;
|
|
1373
|
+
whoCurrentWeightLevel(row: WhoStandardRow): 'ok' | 'mid' | 'warn';
|
|
1374
|
+
whoCurrentHeightLevel(row: WhoStandardRow): 'ok' | 'mid' | 'warn';
|
|
1375
|
+
openWhoTableDialog(): void;
|
|
1376
|
+
closeWhoTableDialog(): void;
|
|
1377
|
+
readonly noteQuickTags: readonly ["Có tã", "Không tã", "Sau khi thức dậy", "Trước khi bú", "Buổi sáng", "Buổi tối"];
|
|
1378
|
+
/** Xu hướng đơn khi không có biểu đồ so sánh WHO (thiếu ngày sinh hoặc tương đương). */
|
|
1379
|
+
simpleTrendForCharts: _angular_core.Signal<boolean>;
|
|
1380
|
+
weightTrendChart: _angular_core.Signal<{
|
|
1381
|
+
W: number;
|
|
1382
|
+
H: number;
|
|
1383
|
+
PAD_L: number;
|
|
1384
|
+
PAD_R: number;
|
|
1385
|
+
PAD_T: number;
|
|
1386
|
+
PAD_B: number;
|
|
1387
|
+
bottomY: number;
|
|
1388
|
+
pathActual: string;
|
|
1389
|
+
areaPath: string;
|
|
1390
|
+
pts: {
|
|
1391
|
+
log: WeightLog;
|
|
1392
|
+
x: number;
|
|
1393
|
+
y: number;
|
|
1394
|
+
showXLabel: boolean;
|
|
1395
|
+
showKgLabel: boolean;
|
|
1396
|
+
}[];
|
|
1397
|
+
gridLines: {
|
|
1398
|
+
y: number;
|
|
1399
|
+
label: string;
|
|
1400
|
+
}[];
|
|
1401
|
+
minKg: number;
|
|
1402
|
+
maxKg: number;
|
|
1403
|
+
} | null>;
|
|
1404
|
+
/**
|
|
1405
|
+
* Biểu đồ theo tuổi (ngày): so sánh đường bé với chuẩn WHO (VN).
|
|
1406
|
+
* Cân “lúc sinh” = bản ghi cân đầu tiên (ngày sớm nhất) trong bảng.
|
|
1407
|
+
*/
|
|
1408
|
+
weightVsStandardChart: _angular_core.Signal<{
|
|
1409
|
+
regionCode: "VN";
|
|
1410
|
+
referenceCaption: string;
|
|
1411
|
+
W: number;
|
|
1412
|
+
H: number;
|
|
1413
|
+
PAD_L: number;
|
|
1414
|
+
PAD_R: number;
|
|
1415
|
+
PAD_T: number;
|
|
1416
|
+
PAD_B: number;
|
|
1417
|
+
bottomY: number;
|
|
1418
|
+
pathMedian: string;
|
|
1419
|
+
pathLow: string;
|
|
1420
|
+
pathHigh: string;
|
|
1421
|
+
bandPath: string;
|
|
1422
|
+
pathActual: string;
|
|
1423
|
+
pts: {
|
|
1424
|
+
log: WeightLog;
|
|
1425
|
+
days: number;
|
|
1426
|
+
x: number;
|
|
1427
|
+
y: number;
|
|
1428
|
+
showKgLabel: boolean;
|
|
1429
|
+
showXLabel: boolean;
|
|
1430
|
+
ageLabel: string;
|
|
1431
|
+
}[];
|
|
1432
|
+
gridLines: {
|
|
1433
|
+
y: number;
|
|
1434
|
+
label: string;
|
|
1435
|
+
}[];
|
|
1436
|
+
xTicks: {
|
|
1437
|
+
x: number;
|
|
1438
|
+
label: string;
|
|
1439
|
+
subLabel?: string;
|
|
1440
|
+
tickY: number;
|
|
1441
|
+
}[];
|
|
1442
|
+
minKg: number;
|
|
1443
|
+
maxKg: number;
|
|
1444
|
+
birthRecordLine: string;
|
|
1445
|
+
sexLabelVi: string;
|
|
1446
|
+
} | null>;
|
|
1447
|
+
heightVsStandardChart: _angular_core.Signal<{
|
|
1448
|
+
regionCode: "VN";
|
|
1449
|
+
referenceCaption: string;
|
|
1450
|
+
W: number;
|
|
1451
|
+
H: number;
|
|
1452
|
+
PAD_L: number;
|
|
1453
|
+
PAD_R: number;
|
|
1454
|
+
PAD_T: number;
|
|
1455
|
+
PAD_B: number;
|
|
1456
|
+
bottomY: number;
|
|
1457
|
+
pathMedian: string;
|
|
1458
|
+
pathLow: string;
|
|
1459
|
+
pathHigh: string;
|
|
1460
|
+
bandPath: string;
|
|
1461
|
+
pathActual: string;
|
|
1462
|
+
pts: {
|
|
1463
|
+
x: number;
|
|
1464
|
+
y: number;
|
|
1465
|
+
showLabel: boolean;
|
|
1466
|
+
showXLabel: boolean;
|
|
1467
|
+
ageLabel: string;
|
|
1468
|
+
log: WeightLog;
|
|
1469
|
+
days: number;
|
|
1470
|
+
heightCm: number;
|
|
1471
|
+
}[];
|
|
1472
|
+
gridLines: {
|
|
1473
|
+
y: number;
|
|
1474
|
+
label: string;
|
|
1475
|
+
}[];
|
|
1476
|
+
xTicks: {
|
|
1477
|
+
x: number;
|
|
1478
|
+
label: string;
|
|
1479
|
+
subLabel?: string;
|
|
1480
|
+
}[];
|
|
1481
|
+
sexLabelVi: string;
|
|
1482
|
+
} | null>;
|
|
1483
|
+
/**
|
|
1484
|
+
* Đường tốc độ tăng (g/ngày) giữa các cặp lần cân liên tiếp (chỉ khoảng ≥1 ngày).
|
|
1485
|
+
*/
|
|
1486
|
+
weightVelocityLineChart: _angular_core.Signal<{
|
|
1487
|
+
W: number;
|
|
1488
|
+
H: number;
|
|
1489
|
+
PAD_L: number;
|
|
1490
|
+
innerW: number;
|
|
1491
|
+
PAD_T: number;
|
|
1492
|
+
PAD_B: number;
|
|
1493
|
+
pathLine: string;
|
|
1494
|
+
pts: {
|
|
1495
|
+
x: number;
|
|
1496
|
+
y: number;
|
|
1497
|
+
gPerDay: number;
|
|
1498
|
+
label: string;
|
|
1499
|
+
showValue: boolean;
|
|
1500
|
+
showXLabel: boolean;
|
|
1501
|
+
}[];
|
|
1502
|
+
gridLines: {
|
|
1503
|
+
y: number;
|
|
1504
|
+
label: string;
|
|
1505
|
+
}[];
|
|
1506
|
+
zeroY: number;
|
|
1507
|
+
showZeroLine: boolean;
|
|
1508
|
+
} | null>;
|
|
1509
|
+
currentTimelineMilestone: _angular_core.Signal<TimelineMilestone | null>;
|
|
1510
|
+
timelineEntries: _angular_core.Signal<{
|
|
1511
|
+
milestone: TimelineMilestone;
|
|
1512
|
+
state: "past" | "current" | "future";
|
|
1513
|
+
}[]>;
|
|
1514
|
+
timelineStats: _angular_core.Signal<{
|
|
1515
|
+
total: number;
|
|
1516
|
+
past: number;
|
|
1517
|
+
}>;
|
|
1518
|
+
timelineChartData: _angular_core.Signal<{
|
|
1519
|
+
W: number;
|
|
1520
|
+
H: number;
|
|
1521
|
+
PAD_L: number;
|
|
1522
|
+
PAD_R: number;
|
|
1523
|
+
PAD_T: number;
|
|
1524
|
+
PAD_B: number;
|
|
1525
|
+
PLOT_H: number;
|
|
1526
|
+
points: {
|
|
1527
|
+
x: number;
|
|
1528
|
+
y: number;
|
|
1529
|
+
entry: {
|
|
1530
|
+
milestone: TimelineMilestone;
|
|
1531
|
+
state: "past" | "current" | "future";
|
|
1532
|
+
};
|
|
1533
|
+
index: number;
|
|
1534
|
+
}[];
|
|
1535
|
+
linePath: string;
|
|
1536
|
+
areaPath: string;
|
|
1537
|
+
moodGuides: {
|
|
1538
|
+
y: number;
|
|
1539
|
+
label: string;
|
|
1540
|
+
mood: MoodType;
|
|
1541
|
+
}[];
|
|
1542
|
+
}>;
|
|
1543
|
+
selectedTimelineEntry: _angular_core.Signal<{
|
|
1544
|
+
milestone: TimelineMilestone;
|
|
1545
|
+
state: "past" | "current" | "future";
|
|
1546
|
+
}>;
|
|
1547
|
+
selectedTimelineCareGuide: _angular_core.Signal<TimelineCareGuide | null>;
|
|
1548
|
+
selectedTimelineIndex: _angular_core.Signal<number>;
|
|
1549
|
+
setTimelineView(view: 'list' | 'chart'): void;
|
|
1550
|
+
selectTimelineNode(id: string): void;
|
|
1551
|
+
prevTimeline(): void;
|
|
1552
|
+
nextTimeline(): void;
|
|
1553
|
+
goToCurrentTimeline(): void;
|
|
1554
|
+
onTimelineKey(ev: KeyboardEvent): void;
|
|
1555
|
+
openTimelineDialog(): void;
|
|
1556
|
+
closeTimelineDialog(): void;
|
|
1557
|
+
openWhoGirlReferenceDialog(): void;
|
|
1558
|
+
closeWhoGirlReferenceDialog(): void;
|
|
1559
|
+
get maxBirthDate(): string;
|
|
1560
|
+
load(): void;
|
|
1561
|
+
openAddDialog(): void;
|
|
1562
|
+
closeAddDialog(): void;
|
|
1563
|
+
updateDraftDate(v: string): void;
|
|
1564
|
+
updateDraftWeight(v: string): void;
|
|
1565
|
+
updateDraftHeight(v: string): void;
|
|
1566
|
+
updateDraftNote(v: string): void;
|
|
1567
|
+
private parseDraftKg;
|
|
1568
|
+
private parseDraftHeightCm;
|
|
1569
|
+
submitAdd(): void;
|
|
1570
|
+
openEdit(log: WeightLog): void;
|
|
1571
|
+
cancelEdit(): void;
|
|
1572
|
+
updateEditDate(v: string): void;
|
|
1573
|
+
updateEditWeight(v: string): void;
|
|
1574
|
+
updateEditHeight(v: string): void;
|
|
1575
|
+
updateEditNote(v: string): void;
|
|
1576
|
+
noteHasTag(note: string, tag: string): boolean;
|
|
1577
|
+
private toggleTagInNote;
|
|
1578
|
+
toggleDraftNoteTag(tag: string): void;
|
|
1579
|
+
toggleEditNoteTag(tag: string): void;
|
|
1580
|
+
submitEdit(): void;
|
|
1581
|
+
deleteLog(log: WeightLog): void;
|
|
1582
|
+
formatDateDisplay(iso: string): string;
|
|
1583
|
+
formatDateShort(iso: string): string;
|
|
1584
|
+
formatKg(n: number): string;
|
|
1585
|
+
formatGPerDay(n: number): string;
|
|
1586
|
+
formatCm(n: number): string;
|
|
1587
|
+
private medianHeightByWeeks;
|
|
1588
|
+
private sdHeightByWeeks;
|
|
1589
|
+
private whoMetricLevel;
|
|
1590
|
+
/**
|
|
1591
|
+
* Nội suy percentile xấp xỉ từ 3 mốc WHO:
|
|
1592
|
+
* -2SD ~ P3, median ~ P50, +2SD ~ P97.
|
|
1593
|
+
*/
|
|
1594
|
+
private estimatePercentileFromBand;
|
|
1595
|
+
private clamp;
|
|
1596
|
+
private defaultDraft;
|
|
1597
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<WeightComponent, never>;
|
|
1598
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<WeightComponent, "app-weight", never, { "user": { "alias": "user"; "required": false; "isSignal": true; }; "birthDate": { "alias": "birthDate"; "required": false; "isSignal": true; }; "gender": { "alias": "gender"; "required": false; "isSignal": true; }; "babyName": { "alias": "babyName"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
type ExplorerType = 'folder' | 'file';
|
|
1602
|
+
interface ExplorerEntry {
|
|
1603
|
+
id: number;
|
|
1604
|
+
name: string;
|
|
1605
|
+
type: ExplorerType;
|
|
1606
|
+
/** null = không có cha (chỉ có root) */
|
|
1607
|
+
parentId: number | null;
|
|
1608
|
+
/** base64 data URL cho file. Folder thì để trống. */
|
|
1609
|
+
content?: string;
|
|
1610
|
+
/** Google Drive file id (nếu đã migrate/saved qua Drive). */
|
|
1611
|
+
driveFileId?: string;
|
|
1612
|
+
/** MIME type gốc (image/jpeg, application/pdf...). */
|
|
1613
|
+
mimeType?: string;
|
|
1614
|
+
/** Trạng thái lưu trữ: sheet (legacy) | drive | migrated | migration_failed. */
|
|
1615
|
+
storageStatus?: string;
|
|
1616
|
+
/** Ảnh preview đã fetch lazy qua Apps Script, ưu tiên hiển thị hơn content legacy. */
|
|
1617
|
+
previewUrl?: string;
|
|
1618
|
+
/** ISO timestamp khi entry được tạo trong sheet (server-side stamp). */
|
|
1619
|
+
createdAt?: string;
|
|
1620
|
+
/**
|
|
1621
|
+
* Kích thước file (byte) — ước lượng từ độ dài base64 sau khi đã trừ
|
|
1622
|
+
* phần header `data:image/...;base64,`. base64 gấp ~4/3 lần binary nên
|
|
1623
|
+
* `bytes ≈ encodedLen * 3/4`. Folder = 0.
|
|
1624
|
+
*/
|
|
1625
|
+
sizeBytes?: number;
|
|
1626
|
+
/** Row trong sheet (1-based, header = row 1) — chỉ có khi load từ sheet */
|
|
1627
|
+
rowIndex?: number;
|
|
1628
|
+
/** Đang loading preview/content từ Drive */
|
|
1629
|
+
isLoading?: boolean;
|
|
1630
|
+
/** Lỗi khi load preview/content */
|
|
1631
|
+
loadError?: boolean;
|
|
1632
|
+
}
|
|
1633
|
+
interface ExplorerResponse {
|
|
1634
|
+
success: boolean;
|
|
1635
|
+
entries?: ExplorerEntry[];
|
|
1636
|
+
entry?: ExplorerEntry;
|
|
1637
|
+
id?: number;
|
|
1638
|
+
error?: string;
|
|
1639
|
+
}
|
|
1640
|
+
interface ExplorerFileResponse {
|
|
1641
|
+
success: boolean;
|
|
1642
|
+
name?: string;
|
|
1643
|
+
mimeType?: string;
|
|
1644
|
+
sizeBytes?: number;
|
|
1645
|
+
dataUrl?: string;
|
|
1646
|
+
error?: string;
|
|
1647
|
+
}
|
|
1648
|
+
interface ExplorerMigrationResponse {
|
|
1649
|
+
success: boolean;
|
|
1650
|
+
migratedCount?: number;
|
|
1651
|
+
migratedFolders?: number;
|
|
1652
|
+
migratedFiles?: number;
|
|
1653
|
+
failedCount?: number;
|
|
1654
|
+
hasMore?: boolean;
|
|
1655
|
+
error?: string;
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* ExplorerService (Drive-first):
|
|
1659
|
+
* - Cây thư mục và file metadata đọc qua Apps Script action `getExplorer`.
|
|
1660
|
+
* - Binary file lưu private trong Google Drive (account B), Apps Script account A làm proxy.
|
|
1661
|
+
* - Không phụ thuộc tab `Explorer` trong Google Sheet sau khi finalize migration.
|
|
1662
|
+
*
|
|
1663
|
+
* Toàn bộ service bị vô hiệu hoá khi `environment.enableExplorer === false`.
|
|
1664
|
+
* Các method read trả về empty observable; write method trả về no-op success.
|
|
1665
|
+
* Không có request nào được gửi đi khi feature bị tắt.
|
|
1666
|
+
*/
|
|
1667
|
+
declare class ExplorerService {
|
|
1668
|
+
private http;
|
|
1669
|
+
private readonly APPS_SCRIPT_URL;
|
|
1670
|
+
/** True when the Explorer feature is active (controlled by NG_APP_ENABLE_EXPLORER). */
|
|
1671
|
+
readonly isEnabled: boolean;
|
|
1672
|
+
getEntries(): Observable<ExplorerEntry[]>;
|
|
1673
|
+
private parseDriveFileId;
|
|
1674
|
+
private parseMimeType;
|
|
1675
|
+
private parseSize;
|
|
1676
|
+
private parseStorageStatus;
|
|
1677
|
+
private normalizeEntry;
|
|
1678
|
+
private toParentId;
|
|
1679
|
+
addEntry(payload: {
|
|
1680
|
+
name: string;
|
|
1681
|
+
type: ExplorerType;
|
|
1682
|
+
parentId: number;
|
|
1683
|
+
content?: string;
|
|
1684
|
+
mimeType?: string;
|
|
1685
|
+
sizeBytes?: number;
|
|
1686
|
+
}): Observable<ExplorerResponse>;
|
|
1687
|
+
/**
|
|
1688
|
+
* Update tên (folder/file) hoặc nội dung (file). KHÔNG cho đổi `type` hoặc
|
|
1689
|
+
* `parent_id` — nếu cần thì xoá rồi thêm mới.
|
|
1690
|
+
*/
|
|
1691
|
+
updateEntry(id: number, patch: {
|
|
1692
|
+
name?: string;
|
|
1693
|
+
content?: string;
|
|
1694
|
+
mimeType?: string;
|
|
1695
|
+
sizeBytes?: number;
|
|
1696
|
+
}): Observable<ExplorerResponse>;
|
|
1697
|
+
/**
|
|
1698
|
+
* **Soft delete** entry. Server set cột `isDeleted=TRUE` thay vì xoá row
|
|
1699
|
+
* vật lý → có thể restore thủ công bằng cách đổi cột G về `FALSE` trong
|
|
1700
|
+
* Google Sheets. Nếu là folder, server cascade soft-delete toàn bộ con
|
|
1701
|
+
* cháu trong cùng một call.
|
|
1702
|
+
*
|
|
1703
|
+
* Phía client không cần biết khác biệt: getEntries() đã filter sẵn các
|
|
1704
|
+
* row đã xoá nên UI thấy biến mất ngay.
|
|
1705
|
+
*/
|
|
1706
|
+
deleteEntry(id: number): Observable<ExplorerResponse>;
|
|
1707
|
+
/**
|
|
1708
|
+
* Chuyển một mảng file (chỉ file, folder sẽ bị server bỏ qua) sang
|
|
1709
|
+
* folder cha mới. Dùng cho thao tác cut & paste nhiều file 1 lần.
|
|
1710
|
+
*
|
|
1711
|
+
* `parentId` phải là folder đã tồn tại. Server validate.
|
|
1712
|
+
*/
|
|
1713
|
+
moveEntries(ids: number[], parentId: number): Observable<ExplorerResponse>;
|
|
1714
|
+
/**
|
|
1715
|
+
* Upload ảnh medical lên Google Drive thông qua addExplorer.
|
|
1716
|
+
* Trả về thông tin file bao gồm driveFileId.
|
|
1717
|
+
*/
|
|
1718
|
+
uploadMedicalImageToDrive(payload: {
|
|
1719
|
+
fileName: string;
|
|
1720
|
+
dataUrl: string;
|
|
1721
|
+
mimeType: string;
|
|
1722
|
+
sizeBytes: number;
|
|
1723
|
+
parentId: number;
|
|
1724
|
+
}): Observable<ExplorerResponse & {
|
|
1725
|
+
driveFileId?: string;
|
|
1726
|
+
}>;
|
|
1727
|
+
/**
|
|
1728
|
+
* Lấy nội dung file (data URL) qua Apps Script action `getExplorerFile`.
|
|
1729
|
+
* Dùng cho preview/download khi file đã chuyển sang Drive private.
|
|
1730
|
+
* Bây giờ có thể nhận cả Explorer ID hoặc Drive file ID.
|
|
1731
|
+
*/
|
|
1732
|
+
getFileDataUrl(id: number | string): Observable<ExplorerFileResponse>;
|
|
1733
|
+
/**
|
|
1734
|
+
* Lấy nội dung nhiều file song song để tối ưu hiệu suất.
|
|
1735
|
+
* Trả về Map<id, ExplorerFileResponse> để dễ dàng map với entry tương ứng.
|
|
1736
|
+
*/
|
|
1737
|
+
getMultipleFileDataUrls(ids: number[]): Observable<Map<number, ExplorerFileResponse>>;
|
|
1738
|
+
migrateExplorerToDrive(payload?: {
|
|
1739
|
+
destinationFolderId?: string;
|
|
1740
|
+
limit?: number;
|
|
1741
|
+
startAfterId?: number;
|
|
1742
|
+
}): Observable<ExplorerMigrationResponse>;
|
|
1743
|
+
deleteExplorerSheetAfterMigration(): Observable<ExplorerResponse>;
|
|
1744
|
+
/**
|
|
1745
|
+
* Gửi POST tới Google Apps Script (cùng pattern với `FeedingLogService`).
|
|
1746
|
+
*
|
|
1747
|
+
* **Lưu ý quan trọng**: Apps Script web app trả `302 Found` redirect sang
|
|
1748
|
+
* `script.googleusercontent.com` cho POST, kèm CORS thoáng cho GET nhưng
|
|
1749
|
+
* **không phản hồi CORS đúng cho POST response**. Hệ quả:
|
|
1750
|
+
* - Apps Script `doPost` chạy thành công, **dữ liệu đã được ghi vào sheet**.
|
|
1751
|
+
* - Browser/HttpClient cố theo redirect → đụng CORS / non-JSON → throw
|
|
1752
|
+
* error mặc dù request đã có hiệu lực.
|
|
1753
|
+
*
|
|
1754
|
+
* Vì vậy: swallow lỗi response và **luôn coi như thành công**. Component
|
|
1755
|
+
* sẽ tự `loadEntries()` sau ~800-1000ms để verify state thật. Cách này
|
|
1756
|
+
* đồng nhất với `FeedingLogService.addLog/updateLog/deleteLog` trong cùng
|
|
1757
|
+
* codebase.
|
|
1758
|
+
*
|
|
1759
|
+
* Trade-off: nếu Apps Script thực sự fail (quota, sai action…), UI báo
|
|
1760
|
+
* thành công nhưng reload sẽ không thấy thay đổi → user tự nhận biết.
|
|
1761
|
+
* Đổi lại: không bao giờ báo "lỗi" khi backend đã ghi thành công (nguyên
|
|
1762
|
+
* nhân của bug "upload báo lỗi nhưng vẫn upload được").
|
|
1763
|
+
*/
|
|
1764
|
+
private postToAppsScript;
|
|
1765
|
+
/**
|
|
1766
|
+
* Dùng cho action bắt buộc cần đọc JSON response (vd getExplorerFile).
|
|
1767
|
+
* Nếu không đi qua proxy tương thích CORS thì throw lỗi để user cấu hình lại.
|
|
1768
|
+
*/
|
|
1769
|
+
private postToAppsScriptExpectResponse;
|
|
1770
|
+
private shouldUseJsonProxy;
|
|
1771
|
+
/**
|
|
1772
|
+
* Đọc 1 file ảnh (File hoặc Blob) → resize/compress xuống tối đa
|
|
1773
|
+
* `maxSize`px (cạnh dài) ở chất lượng JPEG `quality`, trả về data URL
|
|
1774
|
+
* base64. Mục đích: né giới hạn 50.000 ký tự / cell của Google Sheets.
|
|
1775
|
+
*
|
|
1776
|
+
* Khoảng an toàn để ảnh JPEG ~30–35 KB sau base64: maxSize 720, quality 0.7.
|
|
1777
|
+
*/
|
|
1778
|
+
fileToCompressedBase64(file: File, maxSize?: number, quality?: number): Promise<string>;
|
|
1779
|
+
/** Đọc file bất kỳ thành data URL (phục vụ upload file non-image). */
|
|
1780
|
+
fileToDataUrl(file: File): Promise<string>;
|
|
1781
|
+
private readAsDataURL;
|
|
1782
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<ExplorerService, never>;
|
|
1783
|
+
static ɵprov: _angular_core.ɵɵInjectableDeclaration<ExplorerService>;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
/** Metadữ loại sự kiện — nguồn duy nhất cho union `MedicalEventKind` */
|
|
1787
|
+
declare const MEDICAL_KINDS: readonly [{
|
|
1788
|
+
readonly id: "vaccine";
|
|
1789
|
+
readonly label: "Tiêm chủng / vaccine";
|
|
1790
|
+
readonly shortLabel: "Tiêm";
|
|
1791
|
+
readonly icon: "pi pi-shield";
|
|
1792
|
+
readonly color: "#0ea5e9";
|
|
1793
|
+
}, {
|
|
1794
|
+
readonly id: "checkup";
|
|
1795
|
+
readonly label: "Khám định kỳ / soi chiếu";
|
|
1796
|
+
readonly shortLabel: "Khám";
|
|
1797
|
+
readonly icon: "pi pi-user-plus";
|
|
1798
|
+
readonly color: "#22c55e";
|
|
1799
|
+
}, {
|
|
1800
|
+
readonly id: "medication";
|
|
1801
|
+
readonly label: "Thuốc / kê đơn";
|
|
1802
|
+
readonly shortLabel: "Thuốc";
|
|
1803
|
+
readonly icon: "pi pi-tablet";
|
|
1804
|
+
readonly color: "#a855f7";
|
|
1805
|
+
}, {
|
|
1806
|
+
readonly id: "illness";
|
|
1807
|
+
readonly label: "Ốm / sốt / triệu chứng";
|
|
1808
|
+
readonly shortLabel: "Ốm";
|
|
1809
|
+
readonly icon: "pi pi-exclamation-circle";
|
|
1810
|
+
readonly color: "#f97316";
|
|
1811
|
+
}, {
|
|
1812
|
+
readonly id: "lab";
|
|
1813
|
+
readonly label: "Xét nghiệm / kết quả";
|
|
1814
|
+
readonly shortLabel: "Xét nghiệm";
|
|
1815
|
+
readonly icon: "pi pi-chart-bar";
|
|
1816
|
+
readonly color: "#6366f1";
|
|
1817
|
+
}, {
|
|
1818
|
+
readonly id: "allergy";
|
|
1819
|
+
readonly label: "Dị ứng / phản ứng thuốc";
|
|
1820
|
+
readonly shortLabel: "Dị ứng";
|
|
1821
|
+
readonly icon: "pi pi-ban";
|
|
1822
|
+
readonly color: "#dc2626";
|
|
1823
|
+
}, {
|
|
1824
|
+
readonly id: "dental";
|
|
1825
|
+
readonly label: "Răng miệng / nha khoa";
|
|
1826
|
+
readonly shortLabel: "Nha";
|
|
1827
|
+
readonly icon: "pi pi-circle-fill";
|
|
1828
|
+
readonly color: "#14b8a6";
|
|
1829
|
+
}, {
|
|
1830
|
+
readonly id: "ent";
|
|
1831
|
+
readonly label: "Tai — Mũi — Họng";
|
|
1832
|
+
readonly shortLabel: "TMH";
|
|
1833
|
+
readonly icon: "pi pi-volume-up";
|
|
1834
|
+
readonly color: "#0891b2";
|
|
1835
|
+
}, {
|
|
1836
|
+
readonly id: "dermatology";
|
|
1837
|
+
readonly label: "Da liễu";
|
|
1838
|
+
readonly shortLabel: "Da";
|
|
1839
|
+
readonly icon: "pi pi-heart-fill";
|
|
1840
|
+
readonly color: "#db2777";
|
|
1841
|
+
}, {
|
|
1842
|
+
readonly id: "vision";
|
|
1843
|
+
readonly label: "Mắt / nhãn khoa";
|
|
1844
|
+
readonly shortLabel: "Mắt";
|
|
1845
|
+
readonly icon: "pi pi-eye";
|
|
1846
|
+
readonly color: "#2563eb";
|
|
1847
|
+
}, {
|
|
1848
|
+
readonly id: "hearing";
|
|
1849
|
+
readonly label: "Thính lực";
|
|
1850
|
+
readonly shortLabel: "Thính";
|
|
1851
|
+
readonly icon: "pi pi-volume-down";
|
|
1852
|
+
readonly color: "#7c3aed";
|
|
1853
|
+
}, {
|
|
1854
|
+
readonly id: "emergency";
|
|
1855
|
+
readonly label: "Cấp cứu / ER";
|
|
1856
|
+
readonly shortLabel: "Cấp cứu";
|
|
1857
|
+
readonly icon: "pi pi-bolt";
|
|
1858
|
+
readonly color: "#b91c1c";
|
|
1859
|
+
}, {
|
|
1860
|
+
readonly id: "surgery";
|
|
1861
|
+
readonly label: "Phẫu thuật / tiểu phẫu";
|
|
1862
|
+
readonly shortLabel: "PT";
|
|
1863
|
+
readonly icon: "pi pi-cog";
|
|
1864
|
+
readonly color: "#475569";
|
|
1865
|
+
}, {
|
|
1866
|
+
readonly id: "therapy";
|
|
1867
|
+
readonly label: "Vật lý trị liệu / PHCN";
|
|
1868
|
+
readonly shortLabel: "VLTL";
|
|
1869
|
+
readonly icon: "pi pi-replay";
|
|
1870
|
+
readonly color: "#059669";
|
|
1871
|
+
}, {
|
|
1872
|
+
readonly id: "nutrition";
|
|
1873
|
+
readonly label: "Dinh dưỡng / tư vấn sữa";
|
|
1874
|
+
readonly shortLabel: "Dinh dưỡng";
|
|
1875
|
+
readonly icon: "pi pi-apple";
|
|
1876
|
+
readonly color: "#65a30d";
|
|
1877
|
+
}, {
|
|
1878
|
+
readonly id: "screening";
|
|
1879
|
+
readonly label: "Sàng lọc / khám lưới";
|
|
1880
|
+
readonly shortLabel: "Sàng lọc";
|
|
1881
|
+
readonly icon: "pi pi-list-check";
|
|
1882
|
+
readonly color: "#0284c7";
|
|
1883
|
+
}, {
|
|
1884
|
+
readonly id: "mental";
|
|
1885
|
+
readonly label: "Tâm lý / thần kinh";
|
|
1886
|
+
readonly shortLabel: "Tâm lý";
|
|
1887
|
+
readonly icon: "pi pi-comments";
|
|
1888
|
+
readonly color: "#8b5cf6";
|
|
1889
|
+
}, {
|
|
1890
|
+
readonly id: "home_care";
|
|
1891
|
+
readonly label: "Chăm sóc tại nhà / điều dưỡng";
|
|
1892
|
+
readonly shortLabel: "Tại nhà";
|
|
1893
|
+
readonly icon: "pi pi-home";
|
|
1894
|
+
readonly color: "#ca8a04";
|
|
1895
|
+
}, {
|
|
1896
|
+
readonly id: "follow_up";
|
|
1897
|
+
readonly label: "Tái khám / theo dõi";
|
|
1898
|
+
readonly shortLabel: "Tái khám";
|
|
1899
|
+
readonly icon: "pi pi-calendar-plus";
|
|
1900
|
+
readonly color: "#ea580c";
|
|
1901
|
+
}, {
|
|
1902
|
+
readonly id: "other";
|
|
1903
|
+
readonly label: "Khác";
|
|
1904
|
+
readonly shortLabel: "Khác";
|
|
1905
|
+
readonly icon: "pi pi-bookmark";
|
|
1906
|
+
readonly color: "#64748b";
|
|
1907
|
+
}];
|
|
1908
|
+
type MedicalEventKind = (typeof MEDICAL_KINDS)[number]['id'];
|
|
1909
|
+
type MedicalKindMeta = (typeof MEDICAL_KINDS)[number];
|
|
1910
|
+
|
|
1911
|
+
interface MedicalHistoryEntry {
|
|
1912
|
+
user: string;
|
|
1913
|
+
/** YYYY-MM-DD */
|
|
1914
|
+
date: string;
|
|
1915
|
+
kind: MedicalEventKind;
|
|
1916
|
+
title: string;
|
|
1917
|
+
detail: string;
|
|
1918
|
+
place?: string;
|
|
1919
|
+
/** Google Drive file ID for medical image attachments */
|
|
1920
|
+
driveFileId?: string;
|
|
1921
|
+
rowIndex?: number;
|
|
1922
|
+
}
|
|
1923
|
+
interface MedicalSheetResponse {
|
|
1924
|
+
success: boolean;
|
|
1925
|
+
rowIndex?: number;
|
|
1926
|
+
error?: string;
|
|
1927
|
+
}
|
|
1928
|
+
declare class MedicalHistoryService {
|
|
1929
|
+
private http;
|
|
1930
|
+
private googleSheets;
|
|
1931
|
+
private dataMapper;
|
|
1932
|
+
private errorHandler;
|
|
1933
|
+
private readonly SHEET_ID;
|
|
1934
|
+
private readonly SHEET_GID;
|
|
1935
|
+
private readonly SHEET_NAME;
|
|
1936
|
+
private readonly APPS_SCRIPT_URL;
|
|
1937
|
+
/**
|
|
1938
|
+
* Cột:
|
|
1939
|
+
* A = User
|
|
1940
|
+
* B = Ngày (DD/MM/YYYY)
|
|
1941
|
+
* C = Loại (slug)
|
|
1942
|
+
* D = Tiêu đề
|
|
1943
|
+
* E = Chi tiết
|
|
1944
|
+
* F = Nơi khám / ghi chú ngắn
|
|
1945
|
+
* G = Google Drive file ID cho ảnh đính kèm, có thể để trống
|
|
1946
|
+
*/
|
|
1947
|
+
getEntries(): Observable<MedicalHistoryEntry[]>;
|
|
1948
|
+
/**
|
|
1949
|
+
* Test method to verify Google Sheets connection and data format
|
|
1950
|
+
*/
|
|
1951
|
+
testConnection(): Observable<{
|
|
1952
|
+
success: boolean;
|
|
1953
|
+
message: string;
|
|
1954
|
+
rowCount: number;
|
|
1955
|
+
}>;
|
|
1956
|
+
addEntry(entry: MedicalHistoryEntry): Observable<MedicalSheetResponse>;
|
|
1957
|
+
updateEntry(rowIndex: number, patch: {
|
|
1958
|
+
date?: string;
|
|
1959
|
+
kind?: MedicalEventKind;
|
|
1960
|
+
title?: string;
|
|
1961
|
+
detail?: string;
|
|
1962
|
+
place?: string;
|
|
1963
|
+
/** Gửi `null` hoặc `''` để xoá đính kèm */
|
|
1964
|
+
driveFileId?: string | null | '';
|
|
1965
|
+
}): Observable<MedicalSheetResponse>;
|
|
1966
|
+
deleteEntry(rowIndex: number): Observable<MedicalSheetResponse>;
|
|
1967
|
+
private postToAppsScript;
|
|
1968
|
+
private parseSheetDate;
|
|
1969
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<MedicalHistoryService, never>;
|
|
1970
|
+
static ɵprov: _angular_core.ɵɵInjectableDeclaration<MedicalHistoryService>;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
interface MedicalImagePreviewState {
|
|
1974
|
+
src: string;
|
|
1975
|
+
title: string;
|
|
1976
|
+
driveFileId?: string;
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
declare class MedicalHistoryComponent {
|
|
1980
|
+
private medicalService;
|
|
1981
|
+
private explorerService;
|
|
1982
|
+
readonly explorerEnabled: boolean;
|
|
1983
|
+
private destroyRef;
|
|
1984
|
+
private activityLogService;
|
|
1985
|
+
private cdr;
|
|
1986
|
+
/** Cùng `?user=` với trang feeding */
|
|
1987
|
+
user: _angular_core.InputSignal<string>;
|
|
1988
|
+
/** Cha (`feeding`) chuyển tab Tài liệu + `DocumentsComponent.revealFileEntry`. */
|
|
1989
|
+
openExplorerFile: EventEmitter<number>;
|
|
1990
|
+
entries: _angular_core.WritableSignal<MedicalHistoryEntry[]>;
|
|
1991
|
+
explorerEntries: _angular_core.WritableSignal<ExplorerEntry[]>;
|
|
1992
|
+
medicalFolderId: _angular_core.WritableSignal<number | null>;
|
|
1993
|
+
loading: _angular_core.WritableSignal<boolean>;
|
|
1994
|
+
errorMsg: _angular_core.WritableSignal<string>;
|
|
1995
|
+
successMsg: _angular_core.WritableSignal<string>;
|
|
1996
|
+
kindFilterKinds: _angular_core.WritableSignal<("mental" | "screening" | "vaccine" | "checkup" | "medication" | "illness" | "lab" | "allergy" | "dental" | "ent" | "dermatology" | "vision" | "hearing" | "emergency" | "surgery" | "therapy" | "nutrition" | "home_care" | "follow_up" | "other")[]>;
|
|
1997
|
+
filterPanelOpen: _angular_core.WritableSignal<boolean>;
|
|
1998
|
+
filterPanelKindSearch: _angular_core.WritableSignal<string>;
|
|
1999
|
+
timelineSort: _angular_core.WritableSignal<"newest" | "oldest">;
|
|
2000
|
+
kindListSort: _angular_core.WritableSignal<"count" | "alpha">;
|
|
2001
|
+
textSearchQuery: _angular_core.WritableSignal<string>;
|
|
2002
|
+
imagePreview: _angular_core.WritableSignal<MedicalImagePreviewState | null>;
|
|
2003
|
+
readonly kindCatalog: readonly MedicalKindMeta[];
|
|
2004
|
+
explorerById: _angular_core.Signal<Map<number, ExplorerEntry>>;
|
|
2005
|
+
kindsInFilterPanel: _angular_core.Signal<({
|
|
2006
|
+
readonly id: "vaccine";
|
|
2007
|
+
readonly label: "Tiêm chủng / vaccine";
|
|
2008
|
+
readonly shortLabel: "Tiêm";
|
|
2009
|
+
readonly icon: "pi pi-shield";
|
|
2010
|
+
readonly color: "#0ea5e9";
|
|
2011
|
+
} | {
|
|
2012
|
+
readonly id: "checkup";
|
|
2013
|
+
readonly label: "Khám định kỳ / soi chiếu";
|
|
2014
|
+
readonly shortLabel: "Khám";
|
|
2015
|
+
readonly icon: "pi pi-user-plus";
|
|
2016
|
+
readonly color: "#22c55e";
|
|
2017
|
+
} | {
|
|
2018
|
+
readonly id: "medication";
|
|
2019
|
+
readonly label: "Thuốc / kê đơn";
|
|
2020
|
+
readonly shortLabel: "Thuốc";
|
|
2021
|
+
readonly icon: "pi pi-tablet";
|
|
2022
|
+
readonly color: "#a855f7";
|
|
2023
|
+
} | {
|
|
2024
|
+
readonly id: "illness";
|
|
2025
|
+
readonly label: "Ốm / sốt / triệu chứng";
|
|
2026
|
+
readonly shortLabel: "Ốm";
|
|
2027
|
+
readonly icon: "pi pi-exclamation-circle";
|
|
2028
|
+
readonly color: "#f97316";
|
|
2029
|
+
} | {
|
|
2030
|
+
readonly id: "lab";
|
|
2031
|
+
readonly label: "Xét nghiệm / kết quả";
|
|
2032
|
+
readonly shortLabel: "Xét nghiệm";
|
|
2033
|
+
readonly icon: "pi pi-chart-bar";
|
|
2034
|
+
readonly color: "#6366f1";
|
|
2035
|
+
} | {
|
|
2036
|
+
readonly id: "allergy";
|
|
2037
|
+
readonly label: "Dị ứng / phản ứng thuốc";
|
|
2038
|
+
readonly shortLabel: "Dị ứng";
|
|
2039
|
+
readonly icon: "pi pi-ban";
|
|
2040
|
+
readonly color: "#dc2626";
|
|
2041
|
+
} | {
|
|
2042
|
+
readonly id: "dental";
|
|
2043
|
+
readonly label: "Răng miệng / nha khoa";
|
|
2044
|
+
readonly shortLabel: "Nha";
|
|
2045
|
+
readonly icon: "pi pi-circle-fill";
|
|
2046
|
+
readonly color: "#14b8a6";
|
|
2047
|
+
} | {
|
|
2048
|
+
readonly id: "ent";
|
|
2049
|
+
readonly label: "Tai — Mũi — Họng";
|
|
2050
|
+
readonly shortLabel: "TMH";
|
|
2051
|
+
readonly icon: "pi pi-volume-up";
|
|
2052
|
+
readonly color: "#0891b2";
|
|
2053
|
+
} | {
|
|
2054
|
+
readonly id: "dermatology";
|
|
2055
|
+
readonly label: "Da liễu";
|
|
2056
|
+
readonly shortLabel: "Da";
|
|
2057
|
+
readonly icon: "pi pi-heart-fill";
|
|
2058
|
+
readonly color: "#db2777";
|
|
2059
|
+
} | {
|
|
2060
|
+
readonly id: "vision";
|
|
2061
|
+
readonly label: "Mắt / nhãn khoa";
|
|
2062
|
+
readonly shortLabel: "Mắt";
|
|
2063
|
+
readonly icon: "pi pi-eye";
|
|
2064
|
+
readonly color: "#2563eb";
|
|
2065
|
+
} | {
|
|
2066
|
+
readonly id: "hearing";
|
|
2067
|
+
readonly label: "Thính lực";
|
|
2068
|
+
readonly shortLabel: "Thính";
|
|
2069
|
+
readonly icon: "pi pi-volume-down";
|
|
2070
|
+
readonly color: "#7c3aed";
|
|
2071
|
+
} | {
|
|
2072
|
+
readonly id: "emergency";
|
|
2073
|
+
readonly label: "Cấp cứu / ER";
|
|
2074
|
+
readonly shortLabel: "Cấp cứu";
|
|
2075
|
+
readonly icon: "pi pi-bolt";
|
|
2076
|
+
readonly color: "#b91c1c";
|
|
2077
|
+
} | {
|
|
2078
|
+
readonly id: "surgery";
|
|
2079
|
+
readonly label: "Phẫu thuật / tiểu phẫu";
|
|
2080
|
+
readonly shortLabel: "PT";
|
|
2081
|
+
readonly icon: "pi pi-cog";
|
|
2082
|
+
readonly color: "#475569";
|
|
2083
|
+
} | {
|
|
2084
|
+
readonly id: "therapy";
|
|
2085
|
+
readonly label: "Vật lý trị liệu / PHCN";
|
|
2086
|
+
readonly shortLabel: "VLTL";
|
|
2087
|
+
readonly icon: "pi pi-replay";
|
|
2088
|
+
readonly color: "#059669";
|
|
2089
|
+
} | {
|
|
2090
|
+
readonly id: "nutrition";
|
|
2091
|
+
readonly label: "Dinh dưỡng / tư vấn sữa";
|
|
2092
|
+
readonly shortLabel: "Dinh dưỡng";
|
|
2093
|
+
readonly icon: "pi pi-apple";
|
|
2094
|
+
readonly color: "#65a30d";
|
|
2095
|
+
} | {
|
|
2096
|
+
readonly id: "screening";
|
|
2097
|
+
readonly label: "Sàng lọc / khám lưới";
|
|
2098
|
+
readonly shortLabel: "Sàng lọc";
|
|
2099
|
+
readonly icon: "pi pi-list-check";
|
|
2100
|
+
readonly color: "#0284c7";
|
|
2101
|
+
} | {
|
|
2102
|
+
readonly id: "mental";
|
|
2103
|
+
readonly label: "Tâm lý / thần kinh";
|
|
2104
|
+
readonly shortLabel: "Tâm lý";
|
|
2105
|
+
readonly icon: "pi pi-comments";
|
|
2106
|
+
readonly color: "#8b5cf6";
|
|
2107
|
+
} | {
|
|
2108
|
+
readonly id: "home_care";
|
|
2109
|
+
readonly label: "Chăm sóc tại nhà / điều dưỡng";
|
|
2110
|
+
readonly shortLabel: "Tại nhà";
|
|
2111
|
+
readonly icon: "pi pi-home";
|
|
2112
|
+
readonly color: "#ca8a04";
|
|
2113
|
+
} | {
|
|
2114
|
+
readonly id: "follow_up";
|
|
2115
|
+
readonly label: "Tái khám / theo dõi";
|
|
2116
|
+
readonly shortLabel: "Tái khám";
|
|
2117
|
+
readonly icon: "pi pi-calendar-plus";
|
|
2118
|
+
readonly color: "#ea580c";
|
|
2119
|
+
} | {
|
|
2120
|
+
readonly id: "other";
|
|
2121
|
+
readonly label: "Khác";
|
|
2122
|
+
readonly shortLabel: "Khác";
|
|
2123
|
+
readonly icon: "pi pi-bookmark";
|
|
2124
|
+
readonly color: "#64748b";
|
|
2125
|
+
})[]>;
|
|
2126
|
+
kindFilterKindSet: _angular_core.Signal<Set<"mental" | "screening" | "vaccine" | "checkup" | "medication" | "illness" | "lab" | "allergy" | "dental" | "ent" | "dermatology" | "vision" | "hearing" | "emergency" | "surgery" | "therapy" | "nutrition" | "home_care" | "follow_up" | "other">>;
|
|
2127
|
+
kindFilterSummary: _angular_core.Signal<string>;
|
|
2128
|
+
constructor();
|
|
2129
|
+
onDocumentClick(ev: Event): void;
|
|
2130
|
+
refresh(): void;
|
|
2131
|
+
userNorm: _angular_core.Signal<string>;
|
|
2132
|
+
myEntries: _angular_core.Signal<MedicalHistoryEntry[]>;
|
|
2133
|
+
filteredEntries: _angular_core.Signal<MedicalHistoryEntry[]>;
|
|
2134
|
+
visibleEntries: _angular_core.Signal<MedicalHistoryEntry[]>;
|
|
2135
|
+
timelineGroups: _angular_core.Signal<{
|
|
2136
|
+
key: string;
|
|
2137
|
+
monthLabel: string;
|
|
2138
|
+
entries: MedicalHistoryEntry[];
|
|
2139
|
+
}[]>;
|
|
2140
|
+
kindCounts: _angular_core.Signal<Record<"mental" | "screening" | "vaccine" | "checkup" | "medication" | "illness" | "lab" | "allergy" | "dental" | "ent" | "dermatology" | "vision" | "hearing" | "emergency" | "surgery" | "therapy" | "nutrition" | "home_care" | "follow_up" | "other", number>>;
|
|
2141
|
+
load(): void;
|
|
2142
|
+
onDialogSaved(): void;
|
|
2143
|
+
private attachmentCache;
|
|
2144
|
+
attachmentFor(entry: MedicalHistoryEntry): ExplorerEntry | undefined;
|
|
2145
|
+
readonly resolveAttachment: (entry: MedicalHistoryEntry) => ExplorerEntry | undefined;
|
|
2146
|
+
private preloadAttachments;
|
|
2147
|
+
openAttachmentInDocuments(entry: MedicalHistoryEntry): void;
|
|
2148
|
+
openImagePreview(entry: MedicalHistoryEntry): void;
|
|
2149
|
+
closeImagePreview(): void;
|
|
2150
|
+
openInDocumentsFromPreview(driveFileId: string): void;
|
|
2151
|
+
toggleFilterPanel(): void;
|
|
2152
|
+
clearKindFilters(): void;
|
|
2153
|
+
resetFilterPanel(): void;
|
|
2154
|
+
clearTextSearch(): void;
|
|
2155
|
+
private entryMatchesFreeText;
|
|
2156
|
+
toggleKindFilter(kind: MedicalEventKind): void;
|
|
2157
|
+
metaFor(kind: MedicalEventKind): MedicalKindMeta;
|
|
2158
|
+
deleteEntry(entry: MedicalHistoryEntry): void;
|
|
2159
|
+
formatDateDisplay(iso: string): string;
|
|
2160
|
+
private formatMonthHeading;
|
|
2161
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<MedicalHistoryComponent, never>;
|
|
2162
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<MedicalHistoryComponent, "app-medical-history", never, { "user": { "alias": "user"; "required": false; "isSignal": true; }; }, { "openExplorerFile": "openExplorerFile"; }, never, never, true, never>;
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
interface Crumb {
|
|
2166
|
+
id: number;
|
|
2167
|
+
name: string;
|
|
2168
|
+
}
|
|
2169
|
+
/**
|
|
2170
|
+
* Draft cho modal Add / Rename.
|
|
2171
|
+
*
|
|
2172
|
+
* Dùng chung cho cả thư mục **và** file (rename). `entryType` xác định
|
|
2173
|
+
* UI string + icon. Mode `add` hiện chỉ áp dụng cho folder (file thêm qua
|
|
2174
|
+
* upload, không qua modal text).
|
|
2175
|
+
*/
|
|
2176
|
+
interface EntryDraft {
|
|
2177
|
+
mode: 'add' | 'rename';
|
|
2178
|
+
/** Loại entry đang đổi: folder hoặc file. Mặc định 'folder'. */
|
|
2179
|
+
entryType: 'folder' | 'file';
|
|
2180
|
+
/** id entry đang rename. Khi `mode === 'add'` → undefined. */
|
|
2181
|
+
targetId?: number;
|
|
2182
|
+
/**
|
|
2183
|
+
* Phần tên USER nhìn thấy + chỉnh trong input. Với file đang rename,
|
|
2184
|
+
* đây là **base name** (đã tách extension) — user khỏi phải gõ `.jpg`.
|
|
2185
|
+
*/
|
|
2186
|
+
name: string;
|
|
2187
|
+
/**
|
|
2188
|
+
* Phần extension (bao gồm dấu chấm, vd `.jpg`). Chỉ set khi rename file.
|
|
2189
|
+
* Lúc submit sẽ được nối lại trừ khi user tự gõ extension khác.
|
|
2190
|
+
*/
|
|
2191
|
+
extension?: string;
|
|
2192
|
+
}
|
|
2193
|
+
type ViewMode = 'large' | 'icons' | 'detail';
|
|
2194
|
+
declare class DocumentsComponent {
|
|
2195
|
+
private explorerService;
|
|
2196
|
+
private activityLogService;
|
|
2197
|
+
private destroyRef;
|
|
2198
|
+
/** id của root folder. App giả định row đầu tiên (id=1) là root. */
|
|
2199
|
+
private readonly ROOT_ID;
|
|
2200
|
+
entries: _angular_core.WritableSignal<ExplorerEntry[]>;
|
|
2201
|
+
loading: _angular_core.WritableSignal<boolean>;
|
|
2202
|
+
saving: _angular_core.WritableSignal<boolean>;
|
|
2203
|
+
errorMsg: _angular_core.WritableSignal<string>;
|
|
2204
|
+
successMsg: _angular_core.WritableSignal<string>;
|
|
2205
|
+
/** Folder hiện tại đang xem. Mặc định = root. */
|
|
2206
|
+
currentFolderId: _angular_core.WritableSignal<number>;
|
|
2207
|
+
/**
|
|
2208
|
+
* Draft cho modal: add folder, rename folder, hoặc rename file.
|
|
2209
|
+
* Tên giữ là `folderDraft` để tránh churn template, nhưng kiểu giờ là
|
|
2210
|
+
* `EntryDraft` chứa `entryType` để phân biệt.
|
|
2211
|
+
*/
|
|
2212
|
+
folderDraft: _angular_core.WritableSignal<EntryDraft | null>;
|
|
2213
|
+
/** File preview fullscreen */
|
|
2214
|
+
previewEntry: _angular_core.WritableSignal<ExplorerEntry | null>;
|
|
2215
|
+
/** Chế độ xem: large icons / small icons / detail list. Persist qua localStorage. */
|
|
2216
|
+
viewMode: _angular_core.WritableSignal<ViewMode>;
|
|
2217
|
+
/** Dropdown menu thư mục (Thêm thư mục / Đổi tên thư mục hiện tại) */
|
|
2218
|
+
folderMenuOpen: _angular_core.WritableSignal<boolean>;
|
|
2219
|
+
/**
|
|
2220
|
+
* Dropdown 3-chấm trong selection action bar — gom Tải xuống / Cắt /
|
|
2221
|
+
* Xoá vào 1 menu để bar đỡ rối khi nhiều nút.
|
|
2222
|
+
*/
|
|
2223
|
+
selectActionsMenuOpen: _angular_core.WritableSignal<boolean>;
|
|
2224
|
+
/** Entry id whose item action dropdown is open. */
|
|
2225
|
+
itemActionsMenuOpen: _angular_core.WritableSignal<number | null>;
|
|
2226
|
+
/**
|
|
2227
|
+
* Toggle thanh search trong toolbar. Khi true, breadcrumb sẽ ẩn đi và
|
|
2228
|
+
* input chiếm chỗ; phím tắt `/` (không trong input) cũng mở search.
|
|
2229
|
+
*/
|
|
2230
|
+
searchOpen: _angular_core.WritableSignal<boolean>;
|
|
2231
|
+
/** Query hiện tại — trim/lowercase được tính ở `searchResults` & highlight. */
|
|
2232
|
+
searchQuery: _angular_core.WritableSignal<string>;
|
|
2233
|
+
/**
|
|
2234
|
+
* id entry vừa được click trong search result — để pulse highlight ~1.5s
|
|
2235
|
+
* sau khi navigate, giúp user tìm thấy item trong thư mục đông đúc.
|
|
2236
|
+
*/
|
|
2237
|
+
highlightedEntryId: _angular_core.WritableSignal<number | null>;
|
|
2238
|
+
private highlightTimer;
|
|
2239
|
+
/** Token để hủy preload cũ khi có lần loadEntries mới hơn. */
|
|
2240
|
+
private preloadRunId;
|
|
2241
|
+
/** Khi true: click vào file/folder = toggle chọn (không mở preview/navigate). */
|
|
2242
|
+
selectMode: _angular_core.WritableSignal<boolean>;
|
|
2243
|
+
/** Tập id (file HOẶC folder) đang được tích chọn. Root không bao giờ vào. */
|
|
2244
|
+
selectedIds: _angular_core.WritableSignal<Set<number>>;
|
|
2245
|
+
/**
|
|
2246
|
+
* Clipboard cho cut & paste. Chứa cả file lẫn folder ids. Server kiểm
|
|
2247
|
+
* tra chu kỳ (folder không paste được vào chính nó hoặc descendant)
|
|
2248
|
+
* — client cũng check sớm để báo lỗi rõ ràng trước khi gọi API.
|
|
2249
|
+
*/
|
|
2250
|
+
clipboard: _angular_core.WritableSignal<{
|
|
2251
|
+
ids: number[];
|
|
2252
|
+
} | null>;
|
|
2253
|
+
/** Tiến độ upload nhiều file: hiện "Đang tải 3/8…". */
|
|
2254
|
+
uploadProgress: _angular_core.WritableSignal<{
|
|
2255
|
+
done: number;
|
|
2256
|
+
total: number;
|
|
2257
|
+
failed: number;
|
|
2258
|
+
} | null>;
|
|
2259
|
+
/**
|
|
2260
|
+
* Trạng thái zoom/pan của ảnh preview:
|
|
2261
|
+
* - `previewScale = 1`: ảnh fit, không cho pan.
|
|
2262
|
+
* - `> 1`: ảnh phóng to, có thể kéo (1 ngón / chuột) hoặc zoom toàn cục
|
|
2263
|
+
* (wheel / pinch / nút).
|
|
2264
|
+
*
|
|
2265
|
+
* Click vào nền KHÔNG đóng preview — user phải bấm nút × hoặc Esc. Tránh
|
|
2266
|
+
* tình trạng vô tình tap ra ngoài (nhất là trên mobile) làm mất ảnh.
|
|
2267
|
+
*
|
|
2268
|
+
* Transform áp dụng lên `<img>`: `translate(tx, ty) scale(s)`. Reference
|
|
2269
|
+
* point = tâm của stage (DIV chứa ảnh, không bị transform). Khi zoom vào
|
|
2270
|
+
* 1 toạ độ cụ thể, dịch chuyển translate sao cho điểm đó đứng yên trong
|
|
2271
|
+
* viewport (xem `applyZoom` bên dưới).
|
|
2272
|
+
*/
|
|
2273
|
+
previewScale: _angular_core.WritableSignal<number>;
|
|
2274
|
+
previewTx: _angular_core.WritableSignal<number>;
|
|
2275
|
+
previewTy: _angular_core.WritableSignal<number>;
|
|
2276
|
+
/** True khi đang drag/pinch — tắt CSS transition để response tức thì. */
|
|
2277
|
+
previewInteracting: _angular_core.WritableSignal<boolean>;
|
|
2278
|
+
private readonly MIN_ZOOM;
|
|
2279
|
+
private readonly MAX_ZOOM;
|
|
2280
|
+
/** Hệ số zoom mỗi lần bấm nút +/-. Zoom đôi mỗi 2 lần bấm là vừa phải. */
|
|
2281
|
+
private readonly ZOOM_STEP;
|
|
2282
|
+
/** Toạ độ con trỏ đang active (cho pinch & pan). pointerId → {x, y}. */
|
|
2283
|
+
private pointers;
|
|
2284
|
+
/** Snapshot khi bắt đầu pan 1 ngón / chuột. */
|
|
2285
|
+
private panStart;
|
|
2286
|
+
/** Snapshot khi bắt đầu pinch 2 ngón. */
|
|
2287
|
+
private pinchStart;
|
|
2288
|
+
/** True nếu trong 1 lượt tương tác đã có drag/pinch — chặn click backdrop. */
|
|
2289
|
+
private didPan;
|
|
2290
|
+
/** CSS transform string áp lên `<img class="docs-preview__img">`. */
|
|
2291
|
+
previewTransform: _angular_core.Signal<string>;
|
|
2292
|
+
previewZoomPercent: _angular_core.Signal<number>;
|
|
2293
|
+
canZoomIn: _angular_core.Signal<boolean>;
|
|
2294
|
+
canZoomOut: _angular_core.Signal<boolean>;
|
|
2295
|
+
/**
|
|
2296
|
+
* Danh sách file ảnh ANH/EM trong cùng thư mục cha của entry đang preview.
|
|
2297
|
+
* Dùng để chuyển ảnh trước/sau bằng nút mũi tên hoặc phím ←/→ mà không
|
|
2298
|
+
* phải đóng preview rồi mở lại.
|
|
2299
|
+
*
|
|
2300
|
+
* Sort theo cùng thứ tự lưới hiển thị (alphabet vi locale) để index khớp.
|
|
2301
|
+
* Folder bị loại — chỉ điều hướng giữa các file. Soft-deleted entries đã
|
|
2302
|
+
* được service filter trước.
|
|
2303
|
+
*/
|
|
2304
|
+
previewSiblings: _angular_core.Signal<ExplorerEntry[]>;
|
|
2305
|
+
previewIndex: _angular_core.Signal<number>;
|
|
2306
|
+
/** Tổng số ảnh trong thư mục — dùng cho counter "3/12". */
|
|
2307
|
+
previewTotal: _angular_core.Signal<number>;
|
|
2308
|
+
canPreviewPrev: _angular_core.Signal<boolean>;
|
|
2309
|
+
canPreviewNext: _angular_core.Signal<boolean>;
|
|
2310
|
+
entriesById: _angular_core.Signal<Map<number, ExplorerEntry>>;
|
|
2311
|
+
/** Children của folder hiện tại, sort: folder trước, file sau, theo tên. */
|
|
2312
|
+
currentChildren: _angular_core.Signal<ExplorerEntry[]>;
|
|
2313
|
+
currentFolder: _angular_core.Signal<ExplorerEntry | null>;
|
|
2314
|
+
/**
|
|
2315
|
+
* Breadcrumb từ root → folder hiện tại. **KHÔNG bao gồm root** theo yêu
|
|
2316
|
+
* cầu: chỉ hiện đường dẫn dưới root.
|
|
2317
|
+
*/
|
|
2318
|
+
breadcrumbs: _angular_core.Signal<Crumb[]>;
|
|
2319
|
+
isAtRoot: _angular_core.Signal<boolean>;
|
|
2320
|
+
folderCount: _angular_core.Signal<number>;
|
|
2321
|
+
fileCount: _angular_core.Signal<number>;
|
|
2322
|
+
selectedCount: _angular_core.Signal<number>;
|
|
2323
|
+
/**
|
|
2324
|
+
* True khi mọi entry CON ở folder hiện tại đều đang được chọn (cả folder
|
|
2325
|
+
* lẫn file, trừ root). Dùng cho nút "Chọn tất cả".
|
|
2326
|
+
*/
|
|
2327
|
+
allFilesSelected: _angular_core.Signal<boolean>;
|
|
2328
|
+
/** True nếu trong selection có ít nhất 1 file — dùng enable nút Download. */
|
|
2329
|
+
selectionHasFile: _angular_core.Signal<boolean>;
|
|
2330
|
+
/**
|
|
2331
|
+
* Search result: filter mọi entry (folder + file) khắp cây theo
|
|
2332
|
+
* `searchQuery`. Loại root và soft-deleted (đã filter ở service).
|
|
2333
|
+
* Sort: folder trước, sau đó theo tên (vi locale).
|
|
2334
|
+
*
|
|
2335
|
+
* Empty query → mảng rỗng (UI sẽ hiện hint "Gõ để tìm…").
|
|
2336
|
+
*/
|
|
2337
|
+
searchResults: _angular_core.Signal<ExplorerEntry[]>;
|
|
2338
|
+
constructor();
|
|
2339
|
+
loadEntries(): void;
|
|
2340
|
+
/**
|
|
2341
|
+
* Sau khi có metadata entry, preload dataUrl cho các file previewable
|
|
2342
|
+
* chưa có `content/previewUrl` để UI hiển thị ảnh ngay, không cần click từng file.
|
|
2343
|
+
*
|
|
2344
|
+
* Chạy song song để tăng tốc độ tải file.
|
|
2345
|
+
*/
|
|
2346
|
+
private preloadFileContents;
|
|
2347
|
+
refresh(): void;
|
|
2348
|
+
openFolder(id: number): void;
|
|
2349
|
+
/**
|
|
2350
|
+
* Gọi từ tab khác (vd Y tế): chuyển sang thư mục chứa file và mở preview.
|
|
2351
|
+
*/
|
|
2352
|
+
revealFileEntry(entryId: number): void;
|
|
2353
|
+
goToCrumb(id: number): void;
|
|
2354
|
+
goRoot(): void;
|
|
2355
|
+
setViewMode(mode: ViewMode): void;
|
|
2356
|
+
private loadViewMode;
|
|
2357
|
+
toggleFolderMenu(ev?: Event): void;
|
|
2358
|
+
closeFolderMenu(): void;
|
|
2359
|
+
toggleSelectActionsMenu(ev?: Event): void;
|
|
2360
|
+
closeSelectActionsMenu(): void;
|
|
2361
|
+
toggleItemActionsMenu(entryId: number, ev?: Event): void;
|
|
2362
|
+
closeItemActionsMenu(): void;
|
|
2363
|
+
/**
|
|
2364
|
+
* Mở thanh search (hoặc đóng nếu đang mở). Khi đóng → clear query.
|
|
2365
|
+
* Khi mở → focus input ở chu kỳ tick kế tiếp (tránh template chưa render).
|
|
2366
|
+
*/
|
|
2367
|
+
toggleSearch(ev?: Event): void;
|
|
2368
|
+
closeSearch(): void;
|
|
2369
|
+
/** Hai-way binding cho input search (dùng `[ngModel]` + change). */
|
|
2370
|
+
setSearchQuery(v: string): void;
|
|
2371
|
+
/**
|
|
2372
|
+
* Click 1 result trong list:
|
|
2373
|
+
* - Folder → navigate vào.
|
|
2374
|
+
* - File → set folder hiện tại = parent của file (để breadcrumb +
|
|
2375
|
+
* siblings preview đúng), sau đó mở preview.
|
|
2376
|
+
* Đóng search và pulse highlight trên item ~1.5s.
|
|
2377
|
+
*/
|
|
2378
|
+
clickSearchResult(entry: ExplorerEntry, ev?: Event): void;
|
|
2379
|
+
/**
|
|
2380
|
+
* Bật highlight ngắn hạn cho 1 entry — UI bind class `is-flash` để pulse.
|
|
2381
|
+
* Tự tắt sau 1.5s. Cancel timer cũ nếu user click liên tiếp nhiều result.
|
|
2382
|
+
*/
|
|
2383
|
+
private pulseHighlight;
|
|
2384
|
+
/**
|
|
2385
|
+
* Build path string của 1 entry (vd "docs / reports / 2026-04") — không
|
|
2386
|
+
* gồm tên entry, chỉ ancestor chain (loại root). Dùng trong search result
|
|
2387
|
+
* để user biết item nằm ở đâu.
|
|
2388
|
+
*/
|
|
2389
|
+
getEntryPath(entry: ExplorerEntry): string;
|
|
2390
|
+
/**
|
|
2391
|
+
* Cắt 1 string thành các segment {text, match} để render <mark> highlight
|
|
2392
|
+
* cho phần khớp query. Case-insensitive. Empty query → nguyên chuỗi.
|
|
2393
|
+
*
|
|
2394
|
+
* Vẫn giữ được chữ hoa/thường gốc (chỉ dùng lower để tìm vị trí). Không
|
|
2395
|
+
* normalize accent — user gõ có dấu hay không thì cứ match nguyên dạng,
|
|
2396
|
+
* giữ logic đơn giản và dự đoán được.
|
|
2397
|
+
*/
|
|
2398
|
+
highlightSegments(name: string, query: string): {
|
|
2399
|
+
text: string;
|
|
2400
|
+
match: boolean;
|
|
2401
|
+
}[];
|
|
2402
|
+
/** Đóng tất cả popover khi click ra ngoài (bỏ qua click trong `.docs-menu`). */
|
|
2403
|
+
onDocumentClick(ev: Event): void;
|
|
2404
|
+
openAddFolder(): void;
|
|
2405
|
+
/** Đổi tên folder HIỆN TẠI (folder đang đứng trong). Disabled ở root. */
|
|
2406
|
+
openRenameCurrentFolder(): void;
|
|
2407
|
+
openRenameFolder(entry: ExplorerEntry): void;
|
|
2408
|
+
/**
|
|
2409
|
+
* Đổi tên 1 file. Tách base name khỏi extension trước khi đặt vào input
|
|
2410
|
+
* — user không phải lo chuyện gõ `.jpg`. Lúc submit sẽ tự ghép lại trừ
|
|
2411
|
+
* khi user gõ extension khác (ví dụ chuyển `.png`).
|
|
2412
|
+
*/
|
|
2413
|
+
openRenameFile(entry: ExplorerEntry, ev?: Event): void;
|
|
2414
|
+
/**
|
|
2415
|
+
* Trích phần extension cuối tên file (bao gồm dấu chấm). Chấp nhận
|
|
2416
|
+
* 2-5 ký tự alphanum sau dấu chấm cuối cùng để tránh nhầm với tên có
|
|
2417
|
+
* dấu chấm ở giữa (vd `2026.04.29-cat.jpg` → `.jpg`).
|
|
2418
|
+
*/
|
|
2419
|
+
private extractExtension;
|
|
2420
|
+
closeFolderDraft(): void;
|
|
2421
|
+
updateFolderDraftName(v: string): void;
|
|
2422
|
+
submitFolderDraft(): void;
|
|
2423
|
+
deleteEntry(entry: ExplorerEntry): void;
|
|
2424
|
+
/**
|
|
2425
|
+
* Upload nhiều file ảnh tuần tự. Mỗi file:
|
|
2426
|
+
* 1) Compress xuống ≤ MAX_CELL_CHARS (~49.000 ký tự / cell Sheets).
|
|
2427
|
+
* 2) Gọi `addEntry`. Apps Script tự stamp `created_at`.
|
|
2428
|
+
*
|
|
2429
|
+
* Tuần tự (chứ không Promise.all) vì Apps Script web app chạy đơn nhiệm
|
|
2430
|
+
* — gửi parallel chỉ tốn time-out chứ không nhanh hơn.
|
|
2431
|
+
*/
|
|
2432
|
+
onFileSelected(input: HTMLInputElement): Promise<void>;
|
|
2433
|
+
/** Safari/iOS: ảnh chụp có thể không có MIME hoặc là HEIC. */
|
|
2434
|
+
private isImageFile;
|
|
2435
|
+
private resolveMimeType;
|
|
2436
|
+
private extractSizeFromDataUrl;
|
|
2437
|
+
/**
|
|
2438
|
+
* Resize-compress ảnh thử nhiều bậc tới khi <= MAX_CELL_CHARS hoặc fail.
|
|
2439
|
+
*
|
|
2440
|
+
* Server cho phép tổng ~4.9M ký tự (chia 100 cells), nên các bậc đầu giữ
|
|
2441
|
+
* chất lượng rất cao (2048/0.92). Chỉ rớt xuống bậc thấp khi ảnh quá khổ
|
|
2442
|
+
* (chụp DSLR / panorama). Bậc thấp cuối cùng (480/0.55) thường ~30-40KB
|
|
2443
|
+
* — gần như chắc chắn fit dù ảnh ban đầu là gì.
|
|
2444
|
+
*/
|
|
2445
|
+
private compressUntilFits;
|
|
2446
|
+
/**
|
|
2447
|
+
* Click vào file (file card riêng — không phải detail row):
|
|
2448
|
+
* - Nếu đang `selectMode` → toggle chọn.
|
|
2449
|
+
* - Bình thường → mở preview.
|
|
2450
|
+
*/
|
|
2451
|
+
onFileClick(entry: ExplorerEntry): void;
|
|
2452
|
+
/**
|
|
2453
|
+
* Click cho cả folder + file (detail row + grid):
|
|
2454
|
+
* - Trong `selectMode`: toggle chọn entry (folder + file đều được).
|
|
2455
|
+
* - Bình thường: folder → navigate, file → preview.
|
|
2456
|
+
*
|
|
2457
|
+
* Lưu ý: vì selectMode hijack click trên folder để toggle, user muốn
|
|
2458
|
+
* chuyển sang folder khác trong lúc đang chọn thì phải dùng breadcrumb.
|
|
2459
|
+
*/
|
|
2460
|
+
onEntryClick(entry: ExplorerEntry): void;
|
|
2461
|
+
private openPreview;
|
|
2462
|
+
closePreview(): void;
|
|
2463
|
+
/**
|
|
2464
|
+
* Chuyển sang ảnh trước trong cùng thư mục. Reset zoom/pan trước khi
|
|
2465
|
+
* đổi để ảnh mới luôn fit tự nhiên. No-op khi đã ở ảnh đầu.
|
|
2466
|
+
*/
|
|
2467
|
+
prevImage(ev?: Event): Promise<void>;
|
|
2468
|
+
nextImage(ev?: Event): Promise<void>;
|
|
2469
|
+
/**
|
|
2470
|
+
* Đặt lại scale=1, tx=ty=0 và bỏ "interacting". Gọi khi mở/đóng preview
|
|
2471
|
+
* hoặc khi user bấm vào label "100%".
|
|
2472
|
+
*/
|
|
2473
|
+
resetPreviewZoom(): void;
|
|
2474
|
+
zoomInPreview(ev?: Event): void;
|
|
2475
|
+
zoomOutPreview(ev?: Event): void;
|
|
2476
|
+
/**
|
|
2477
|
+
* Lõi của zoom: đặt scale mới (đã clamp), điều chỉnh translate để giữ
|
|
2478
|
+
* `anchor` (toạ độ viewport — ví dụ vị trí chuột / midpoint pinch) đứng
|
|
2479
|
+
* yên. Nếu không có anchor → zoom quanh tâm stage (giảm tỉ lệ translate
|
|
2480
|
+
* theo cùng `ratio` để hiệu ứng zoom-from-center).
|
|
2481
|
+
*
|
|
2482
|
+
* Công thức: với `centerX/Y` = tâm stage và điểm anchor `P`, để giữ P
|
|
2483
|
+
* đứng yên trong viewport sau khi đổi scale theo tỉ lệ r = newScale/oldScale:
|
|
2484
|
+
* newTx = (P.x - centerX) * (1 - r) + tx * r
|
|
2485
|
+
* newTy = (P.y - centerY) * (1 - r) + ty * r
|
|
2486
|
+
* Khi scale chạm về 1.0 thì snap luôn translate về 0 để ảnh tự fit.
|
|
2487
|
+
*/
|
|
2488
|
+
private applyZoom;
|
|
2489
|
+
/**
|
|
2490
|
+
* Wheel zoom: lăn lên = zoom in, lăn xuống = zoom out. Anchor = vị trí
|
|
2491
|
+
* con trỏ → "zoom toward cursor" trải nghiệm tự nhiên.
|
|
2492
|
+
*
|
|
2493
|
+
* `passive: false` cần thiết để gọi `preventDefault()` ngăn page scroll.
|
|
2494
|
+
* Angular template binding mặc định không passive nên `preventDefault`
|
|
2495
|
+
* có hiệu lực.
|
|
2496
|
+
*/
|
|
2497
|
+
onPreviewWheel(ev: WheelEvent): void;
|
|
2498
|
+
/**
|
|
2499
|
+
* Pointerdown: track pointer + bắt đầu pan (1 pointer) hoặc pinch (2 pointer).
|
|
2500
|
+
* Sau khi có 2 pointer → huỷ panStart, ưu tiên pinch.
|
|
2501
|
+
*/
|
|
2502
|
+
onPreviewPointerDown(ev: PointerEvent): void;
|
|
2503
|
+
onPreviewPointerMove(ev: PointerEvent): void;
|
|
2504
|
+
onPreviewPointerUp(ev: PointerEvent): void;
|
|
2505
|
+
/**
|
|
2506
|
+
* Double-click / double-tap: toggle giữa 1x ↔ 2.5x quanh điểm bấm.
|
|
2507
|
+
* Nhanh hơn click nhiều lần nút +/-, đặc biệt trên touch.
|
|
2508
|
+
*/
|
|
2509
|
+
onPreviewDblClick(ev: MouseEvent): void;
|
|
2510
|
+
/**
|
|
2511
|
+
* Phím tắt:
|
|
2512
|
+
* Khi search đang mở:
|
|
2513
|
+
* Esc → đóng search
|
|
2514
|
+
* Khi đang xem preview:
|
|
2515
|
+
* Esc → đóng
|
|
2516
|
+
* +/- → zoom in / out
|
|
2517
|
+
* 0 → reset zoom
|
|
2518
|
+
* ← / → → ảnh trước / sau trong cùng thư mục
|
|
2519
|
+
* Khi không có overlay nào:
|
|
2520
|
+
* / → mở search (focus input)
|
|
2521
|
+
*
|
|
2522
|
+
* Bỏ qua nếu user đang gõ trong input/textarea/contenteditable — TRỪ khi
|
|
2523
|
+
* input đó là search box (Esc vẫn phải đóng được search).
|
|
2524
|
+
*/
|
|
2525
|
+
onDocumentKeydown(ev: KeyboardEvent): void;
|
|
2526
|
+
private startPinch;
|
|
2527
|
+
private updatePinch;
|
|
2528
|
+
toggleSelectMode(): void;
|
|
2529
|
+
/** Bật select mode rồi tích sẵn 1 entry (long-press / nút trên card). */
|
|
2530
|
+
startSelectionWith(id: number): void;
|
|
2531
|
+
/** Toggle 1 entry (file hoặc folder). Không cho thêm root. */
|
|
2532
|
+
toggleSelectEntry(id: number): void;
|
|
2533
|
+
isSelected(id: number): boolean;
|
|
2534
|
+
/** Chọn / bỏ chọn tất cả entry ở folder hiện tại (cả folder + file, trừ root). */
|
|
2535
|
+
toggleSelectAllInCurrent(): void;
|
|
2536
|
+
clearSelection(): void;
|
|
2537
|
+
exitSelectMode(): void;
|
|
2538
|
+
/**
|
|
2539
|
+
* Cắt các entry đang được tích chọn (file + folder) vào clipboard. Loại
|
|
2540
|
+
* root nếu lỡ lọt. Server sẽ check chu kỳ ở paste, client cũng check
|
|
2541
|
+
* trước để có UX tốt hơn.
|
|
2542
|
+
*/
|
|
2543
|
+
cutSelection(): void;
|
|
2544
|
+
cancelClipboard(): void;
|
|
2545
|
+
/**
|
|
2546
|
+
* Paste tại folder hiện tại. Trước khi gọi server, validate:
|
|
2547
|
+
* 1. Bỏ các entry đã ở target.
|
|
2548
|
+
* 2. Nếu trong clipboard có folder F, target không được là F hoặc bất
|
|
2549
|
+
* kỳ descendant nào của F (sẽ tạo chu kỳ vô hạn).
|
|
2550
|
+
* Server cũng check lần nữa để đảm bảo dữ liệu nhất quán.
|
|
2551
|
+
*/
|
|
2552
|
+
pasteHere(): void;
|
|
2553
|
+
/**
|
|
2554
|
+
* Trả về Set chứa id của tất cả ancestor (loại trừ chính nó). Dùng cho
|
|
2555
|
+
* cycle check khi paste folder.
|
|
2556
|
+
*/
|
|
2557
|
+
private getAncestorIds;
|
|
2558
|
+
/**
|
|
2559
|
+
* Tải 1 ảnh về máy. Dùng cho cả trường hợp click nút download trên card,
|
|
2560
|
+
* trong detail row, và trong preview. Convert `data:image/...;base64,...`
|
|
2561
|
+
* → Blob → ObjectURL → trigger anchor click.
|
|
2562
|
+
*
|
|
2563
|
+
* Lý do dùng Blob thay vì set thẳng `<a href="data:...">`: Safari + một
|
|
2564
|
+
* số mobile browser từ chối download data URL dài (>~2MB). Blob ổn hơn.
|
|
2565
|
+
*/
|
|
2566
|
+
downloadEntry(entry: ExplorerEntry, ev?: Event): Promise<void>;
|
|
2567
|
+
/**
|
|
2568
|
+
* Tải nhiều ảnh đã chọn (multi-select). Trigger lần lượt với một
|
|
2569
|
+
* khoảng delay nhỏ giữa các lần để tránh browser chặn pop-up "site này
|
|
2570
|
+
* muốn tải nhiều file" — đa số trình duyệt cho phép sau khi user accept
|
|
2571
|
+
* 1 lần đầu trong session.
|
|
2572
|
+
*/
|
|
2573
|
+
bulkDownloadSelection(): Promise<void>;
|
|
2574
|
+
/** Trigger tải xuống 1 file qua anchor + Blob URL. */
|
|
2575
|
+
private triggerDownload;
|
|
2576
|
+
/**
|
|
2577
|
+
* Convert `data:<mime>;base64,<payload>` → Blob. Nếu không phải base64
|
|
2578
|
+
* data URL thì throw → caller hiển thị error.
|
|
2579
|
+
*/
|
|
2580
|
+
private dataUrlToBlob;
|
|
2581
|
+
/**
|
|
2582
|
+
* Lọc bỏ ký tự không hợp lệ trong filename trên các OS chính (Windows
|
|
2583
|
+
* khắt khe nhất). Đảm bảo có extension; nếu thiếu thì append `.jpg`
|
|
2584
|
+
* vì uploads của app đều convert sang JPEG.
|
|
2585
|
+
*/
|
|
2586
|
+
private sanitizeFilename;
|
|
2587
|
+
isPreviewableFile(entry: ExplorerEntry): boolean;
|
|
2588
|
+
private hydrateFileContent;
|
|
2589
|
+
/**
|
|
2590
|
+
* Xoá hàng loạt: cả folder lẫn file. Server soft-delete folder cascading
|
|
2591
|
+
* sang descendant. Confirm message phân biệt số lượng folder/file để
|
|
2592
|
+
* user nhận thức được phạm vi.
|
|
2593
|
+
*/
|
|
2594
|
+
bulkDeleteSelection(): void;
|
|
2595
|
+
/**
|
|
2596
|
+
* Xoá tuần tự nhiều entry. Apps Script đơn nhiệm/sequential nên gọi
|
|
2597
|
+
* tuần tự an toàn hơn parallel.
|
|
2598
|
+
*/
|
|
2599
|
+
private deleteSequentially;
|
|
2600
|
+
private flashSuccess;
|
|
2601
|
+
isEmpty(): boolean;
|
|
2602
|
+
/** Số ảnh trong 1 folder (đếm con trực tiếp + con cháu type=file). */
|
|
2603
|
+
countFilesInFolder(folderId: number): number;
|
|
2604
|
+
/** "12 KB", "1.4 MB", v.v. */
|
|
2605
|
+
formatSize(bytes?: number): string;
|
|
2606
|
+
/** "29/04/2026" hoặc "29/04/2026 14:30" tuỳ độ dài input. */
|
|
2607
|
+
formatDate(iso?: string): string;
|
|
2608
|
+
/** Label tóm tắt cho folder trong detail view: "5 mục" / "trống". */
|
|
2609
|
+
describeFolderContent(folderId: number): string;
|
|
2610
|
+
/** Label viewMode (cho aria/title) */
|
|
2611
|
+
viewModeLabel(mode: ViewMode): string;
|
|
2612
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<DocumentsComponent, never>;
|
|
2613
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<DocumentsComponent, "app-documents", never, {}, {}, never, never, true, never>;
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
/** Event types cho logging */
|
|
2617
|
+
declare const ACTIVITY_EVENT: {
|
|
2618
|
+
readonly FEEDING_ADDED: "FEEDING_ADDED";
|
|
2619
|
+
readonly FEEDING_UPDATED: "FEEDING_UPDATED";
|
|
2620
|
+
readonly FEEDING_DELETED: "FEEDING_DELETED";
|
|
2621
|
+
readonly BOTTLE_PREP_ADDED: "BOTTLE_PREP_ADDED";
|
|
2622
|
+
/** Đổi dung tích sữa đã pha (không phải pha mới). */
|
|
2623
|
+
readonly BOTTLE_PREP_UPDATED: "BOTTLE_PREP_UPDATED";
|
|
2624
|
+
/** Ghi nhận cữ bú từ sữa đã pha — sheet đã xóa bình (lý do cố định). */
|
|
2625
|
+
readonly BOTTLE_PREP_CLEARED_FEEDING: "BOTTLE_PREP_CLEARED_FEEDING";
|
|
2626
|
+
/** Người dùng xóa thủ công lượng sữa đã pha. */
|
|
2627
|
+
readonly BOTTLE_PREP_MANUAL_CLEARED: "BOTTLE_PREP_MANUAL_CLEARED";
|
|
2628
|
+
/** Thay bình khi cảnh báo hết hạn / sắp hết hạn (1 giờ) — ghi trước log pha mới. */
|
|
2629
|
+
readonly BOTTLE_PREP_CLEARED_EXPIRY: "BOTTLE_PREP_CLEARED_EXPIRY";
|
|
2630
|
+
readonly WEIGHT_ADDED: "WEIGHT_ADDED";
|
|
2631
|
+
readonly WEIGHT_UPDATED: "WEIGHT_UPDATED";
|
|
2632
|
+
readonly WEIGHT_DELETED: "WEIGHT_DELETED";
|
|
2633
|
+
readonly MEDICAL_ADDED: "MEDICAL_ADDED";
|
|
2634
|
+
readonly MEDICAL_UPDATED: "MEDICAL_UPDATED";
|
|
2635
|
+
readonly MEDICAL_DELETED: "MEDICAL_DELETED";
|
|
2636
|
+
readonly SCHEDULE_ADDED: "SCHEDULE_ADDED";
|
|
2637
|
+
readonly SCHEDULE_UPDATED: "SCHEDULE_UPDATED";
|
|
2638
|
+
readonly SCHEDULE_DELETED: "SCHEDULE_DELETED";
|
|
2639
|
+
readonly SETTINGS_UPDATED: "SETTINGS_UPDATED";
|
|
2640
|
+
readonly PROFILE_UPDATED: "PROFILE_UPDATED";
|
|
2641
|
+
readonly NOTIFICATION_CREATED: "NOTIFICATION_CREATED";
|
|
2642
|
+
readonly NOTIFICATION_ACKNOWLEDGED: "NOTIFICATION_ACKNOWLEDGED";
|
|
2643
|
+
readonly NOTIFICATION_DELETED: "NOTIFICATION_DELETED";
|
|
2644
|
+
readonly FILE_ADDED: "FILE_ADDED";
|
|
2645
|
+
readonly FILE_DELETED: "FILE_DELETED";
|
|
2646
|
+
readonly FILE_MOVED: "FILE_MOVED";
|
|
2647
|
+
readonly FILE_RENAMED: "FILE_RENAMED";
|
|
2648
|
+
readonly FOLDER_ADDED: "FOLDER_ADDED";
|
|
2649
|
+
readonly FOLDER_DELETED: "FOLDER_DELETED";
|
|
2650
|
+
readonly FOLDER_RENAMED: "FOLDER_RENAMED";
|
|
2651
|
+
};
|
|
2652
|
+
type ActivityEventType = (typeof ACTIVITY_EVENT)[keyof typeof ACTIVITY_EVENT];
|
|
2653
|
+
/** Row data từ sheet Log */
|
|
2654
|
+
interface ActivityLogRow {
|
|
2655
|
+
id: string;
|
|
2656
|
+
user: string;
|
|
2657
|
+
eventType: ActivityEventType;
|
|
2658
|
+
content: string;
|
|
2659
|
+
timestamp: string;
|
|
2660
|
+
rowIndex?: number;
|
|
2661
|
+
}
|
|
2662
|
+
/** Dữ liệu để tạo log entry mới */
|
|
2663
|
+
interface ActivityLogEntry {
|
|
2664
|
+
user: string;
|
|
2665
|
+
eventType: ActivityEventType;
|
|
2666
|
+
content: string;
|
|
2667
|
+
}
|
|
2668
|
+
/** Helper để format content user-friendly */
|
|
2669
|
+
declare function formatLogContent(eventType: ActivityEventType, details: Record<string, string | number | undefined>): string;
|
|
2670
|
+
/**
|
|
2671
|
+
* HTML (text đã escape) cho phần sau `<user> đã` — bind với DomSanitizer + [innerHTML].
|
|
2672
|
+
* Phần trong cặp '...' (formatLogContent) được bọc <strong class="activity-item__hl">.
|
|
2673
|
+
*/
|
|
2674
|
+
declare function buildActivityLogMessageHtml(content: string): string;
|
|
2675
|
+
declare class ActivityLogService {
|
|
2676
|
+
private http;
|
|
2677
|
+
private readonly SHEET_ID;
|
|
2678
|
+
private readonly SHEET_NAME;
|
|
2679
|
+
private readonly API_KEY;
|
|
2680
|
+
private readonly BASE_URL;
|
|
2681
|
+
private readonly STORAGE_KEY;
|
|
2682
|
+
private readonly APPS_SCRIPT_URL;
|
|
2683
|
+
/** Refresh interval in minutes (configurable from settings) */
|
|
2684
|
+
refreshIntervalMinutes: _angular_core.WritableSignal<number>;
|
|
2685
|
+
private autoRefreshSub?;
|
|
2686
|
+
private logs;
|
|
2687
|
+
private lastLogId;
|
|
2688
|
+
private lastReadLogId;
|
|
2689
|
+
private loading;
|
|
2690
|
+
private hasNewActivity;
|
|
2691
|
+
private refreshTrigger$;
|
|
2692
|
+
readonly logs$: _angular_core.Signal<ActivityLogRow[]>;
|
|
2693
|
+
readonly loading$: _angular_core.Signal<boolean>;
|
|
2694
|
+
readonly hasNewActivity$: _angular_core.Signal<boolean>;
|
|
2695
|
+
readonly lastReadLogId$: _angular_core.Signal<string | null>;
|
|
2696
|
+
/** Số lượng log chưa xem (mới) */
|
|
2697
|
+
unreadCount: _angular_core.WritableSignal<number>;
|
|
2698
|
+
constructor();
|
|
2699
|
+
/** Load last read log ID from localStorage */
|
|
2700
|
+
private loadLastReadLogId;
|
|
2701
|
+
/** Save last read log ID to localStorage */
|
|
2702
|
+
private saveLastReadLogId;
|
|
2703
|
+
/** Calculate unread count based on last read log ID */
|
|
2704
|
+
private calculateUnreadCount;
|
|
2705
|
+
/** Update refresh interval and restart auto-refresh */
|
|
2706
|
+
setRefreshInterval(minutes: number): void;
|
|
2707
|
+
private restartAutoRefresh;
|
|
2708
|
+
private startAutoRefresh;
|
|
2709
|
+
/** Trigger manual refresh */
|
|
2710
|
+
refresh(): void;
|
|
2711
|
+
/** Mark all as read - save last read ID to localStorage */
|
|
2712
|
+
markAsRead(): void;
|
|
2713
|
+
/** Check if a specific log is unread */
|
|
2714
|
+
isLogUnread(logId: string): boolean;
|
|
2715
|
+
/** Fetch logs from sheet */
|
|
2716
|
+
private fetchLogs;
|
|
2717
|
+
/** Add a new log entry */
|
|
2718
|
+
addLog(entry: ActivityLogEntry): Observable<boolean>;
|
|
2719
|
+
/**
|
|
2720
|
+
* Gửi POST tới Google Apps Script web app.
|
|
2721
|
+
*
|
|
2722
|
+
* Dev (proxy): dùng HttpClient bình thường, đọc JSON response.
|
|
2723
|
+
*
|
|
2724
|
+
* Prod (mobile-safe): dùng fetch với `mode: 'no-cors'`.
|
|
2725
|
+
* - GAS `doPost` trả 302 redirect sang `script.googleusercontent.com`.
|
|
2726
|
+
* XHR (HttpClient) cố gắng đọc CORS header ở endpoint cuối, và trên
|
|
2727
|
+
* mobile/WebView hành vi này rất flaky ⇒ request thường fail với
|
|
2728
|
+
* status 0 dù GAS đã ghi thành công.
|
|
2729
|
+
* - `fetch` với `mode: 'no-cors'` gửi request như form thường, không
|
|
2730
|
+
* expect đọc response, không cần CORS header ⇒ không bao giờ fail
|
|
2731
|
+
* vì CORS. Opaque response cũng OK vì ta sẽ reload để verify.
|
|
2732
|
+
* - Content-Type `text/plain;charset=utf-8` vẫn cần để nằm trong
|
|
2733
|
+
* "CORS-safelisted" và không trigger preflight OPTIONS.
|
|
2734
|
+
*/
|
|
2735
|
+
private postToAppsScript;
|
|
2736
|
+
/** Helper: Log a feeding event */
|
|
2737
|
+
logFeeding(user: string, eventType: 'FEEDING_ADDED' | 'FEEDING_UPDATED' | 'FEEDING_DELETED', details: {
|
|
2738
|
+
time?: string;
|
|
2739
|
+
oldTime?: string;
|
|
2740
|
+
newTime?: string;
|
|
2741
|
+
volume?: number;
|
|
2742
|
+
oldVolume?: number;
|
|
2743
|
+
newVolume?: number;
|
|
2744
|
+
reason?: string;
|
|
2745
|
+
}): Observable<boolean>;
|
|
2746
|
+
/** Helper: Log a weight event */
|
|
2747
|
+
logWeight(user: string, eventType: 'WEIGHT_ADDED' | 'WEIGHT_UPDATED' | 'WEIGHT_DELETED', details: {
|
|
2748
|
+
date?: string;
|
|
2749
|
+
weight?: number;
|
|
2750
|
+
oldWeight?: number;
|
|
2751
|
+
newWeight?: number;
|
|
2752
|
+
}): Observable<boolean>;
|
|
2753
|
+
/** Helper: Log a medical event */
|
|
2754
|
+
logMedical(user: string, eventType: 'MEDICAL_ADDED' | 'MEDICAL_UPDATED' | 'MEDICAL_DELETED', details: {
|
|
2755
|
+
title?: string;
|
|
2756
|
+
location?: string;
|
|
2757
|
+
}): Observable<boolean>;
|
|
2758
|
+
/** Helper: Log a schedule event */
|
|
2759
|
+
logSchedule(user: string, eventType: 'SCHEDULE_ADDED' | 'SCHEDULE_UPDATED' | 'SCHEDULE_DELETED', details: {
|
|
2760
|
+
title?: string;
|
|
2761
|
+
date?: string;
|
|
2762
|
+
}): Observable<boolean>;
|
|
2763
|
+
/** Helper: Log settings update */
|
|
2764
|
+
logSettings(user: string, settingName: string): Observable<boolean>;
|
|
2765
|
+
/** Helper: Log a document/file event */
|
|
2766
|
+
logDocument(user: string, eventType: 'FILE_ADDED' | 'FILE_DELETED' | 'FILE_MOVED' | 'FILE_RENAMED' | 'FOLDER_ADDED' | 'FOLDER_DELETED' | 'FOLDER_RENAMED', details: {
|
|
2767
|
+
name?: string;
|
|
2768
|
+
folder?: string;
|
|
2769
|
+
toFolder?: string;
|
|
2770
|
+
oldName?: string;
|
|
2771
|
+
newName?: string;
|
|
2772
|
+
}): Observable<boolean>;
|
|
2773
|
+
/** Sau khi ghi cữ bú từ sữa đã pha (tính còn lại trong bình → lưu cữ). */
|
|
2774
|
+
logBottlePrepClearedAfterFeed(volumeMl: number, consumedMl: number, remainingMl: number): Observable<boolean>;
|
|
2775
|
+
/** Người dùng xóa thủ công lượng sữa đã pha trên sheet. */
|
|
2776
|
+
logBottlePrepManualClear(actor: string, volumeMl: number, reason?: string): Observable<boolean>;
|
|
2777
|
+
/**
|
|
2778
|
+
* Khi pha sữa mới từ cảnh báo hết hạn / sắp hết hạn (1 giờ): ghi xóa lô cũ trước log pha mới.
|
|
2779
|
+
* @param variant `expired` = đã quá hạn `minutes` phút; `expiring` = còn `minutes` phút trong khung 1 giờ.
|
|
2780
|
+
*/
|
|
2781
|
+
logBottlePrepClearedForExpiry(volumeMl: number, minutes: number, variant: 'expired' | 'expiring'): Observable<boolean>;
|
|
2782
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<ActivityLogService, never>;
|
|
2783
|
+
static ɵprov: _angular_core.ɵɵInjectableDeclaration<ActivityLogService>;
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
type SortOrder = 'desc' | 'asc';
|
|
2787
|
+
type FilterCategory = 'all' | 'feeding' | 'weight' | 'medical' | 'schedule' | 'document' | 'settings';
|
|
2788
|
+
declare class ActivityLogComponent implements OnInit, OnDestroy {
|
|
2789
|
+
private activityLogService;
|
|
2790
|
+
private sanitizer;
|
|
2791
|
+
private checkSub?;
|
|
2792
|
+
/** Emit when new activity detected - parent can reload data */
|
|
2793
|
+
activityChanged: _angular_core.OutputEmitterRef<void>;
|
|
2794
|
+
dialogOpen: _angular_core.WritableSignal<boolean>;
|
|
2795
|
+
lastRefreshTime: _angular_core.WritableSignal<Date>;
|
|
2796
|
+
private nowTick;
|
|
2797
|
+
private rawLogs;
|
|
2798
|
+
loading: _angular_core.Signal<boolean>;
|
|
2799
|
+
unreadCount: _angular_core.WritableSignal<number>;
|
|
2800
|
+
hasNewActivity: _angular_core.Signal<boolean>;
|
|
2801
|
+
lastReadLogId: _angular_core.Signal<string | null>;
|
|
2802
|
+
/** Pagination */
|
|
2803
|
+
private readonly PAGE_SIZE;
|
|
2804
|
+
displayCount: _angular_core.WritableSignal<number>;
|
|
2805
|
+
/** Sort order */
|
|
2806
|
+
sortOrder: SortOrder;
|
|
2807
|
+
private sortOrderSignal;
|
|
2808
|
+
/** Filter category */
|
|
2809
|
+
filterCategory: FilterCategory;
|
|
2810
|
+
private filterCategorySignal;
|
|
2811
|
+
/** Tìm kiếm tự do (user, nội dung, loại sự kiện). */
|
|
2812
|
+
textSearchQuery: _angular_core.WritableSignal<string>;
|
|
2813
|
+
/** Dialog activity: hiển thị giờ chính xác cho toàn bộ list. */
|
|
2814
|
+
private activityShowExactTime;
|
|
2815
|
+
/** Category mapping for filter */
|
|
2816
|
+
private readonly categoryPrefixes;
|
|
2817
|
+
/** All logs (sorted) */
|
|
2818
|
+
allLogs: _angular_core.Signal<ActivityLogRow[]>;
|
|
2819
|
+
/** Filtered logs (danh mục + text). */
|
|
2820
|
+
filteredLogs: _angular_core.Signal<ActivityLogRow[]>;
|
|
2821
|
+
/** Displayed logs (limited by displayCount) */
|
|
2822
|
+
displayedLogs: _angular_core.Signal<ActivityLogRow[]>;
|
|
2823
|
+
/**
|
|
2824
|
+
* Một dòng list: log + HTML đã build (parse + escape một lần khi logs / slice đổi).
|
|
2825
|
+
* Render bằng [innerHTML] thay vì *ngFor từng part.
|
|
2826
|
+
*/
|
|
2827
|
+
parsedDisplayRows: _angular_core.Signal<{
|
|
2828
|
+
log: ActivityLogRow;
|
|
2829
|
+
contentHtml: _angular_platform_browser.SafeHtml;
|
|
2830
|
+
}[]>;
|
|
2831
|
+
/** Has more logs to load? */
|
|
2832
|
+
hasMore: _angular_core.Signal<boolean>;
|
|
2833
|
+
/** Remaining count */
|
|
2834
|
+
remainingCount: _angular_core.Signal<number>;
|
|
2835
|
+
/** Có chuỗi tìm kiếm (sau trim) — dùng cho empty state. */
|
|
2836
|
+
hasTextFilter: _angular_core.Signal<boolean>;
|
|
2837
|
+
/** Có bộ lọc / sắp xếp khác mặc định — bật nút reset. */
|
|
2838
|
+
hasActiveFilters: _angular_core.Signal<boolean>;
|
|
2839
|
+
private lastCheckHasNew;
|
|
2840
|
+
ngOnInit(): void;
|
|
2841
|
+
ngOnDestroy(): void;
|
|
2842
|
+
openDialog(): void;
|
|
2843
|
+
closeDialog(): void;
|
|
2844
|
+
refreshLogs(): void;
|
|
2845
|
+
loadMore(): void;
|
|
2846
|
+
onSortChange(): void;
|
|
2847
|
+
toggleSortOrder(): void;
|
|
2848
|
+
sortOrderToggleTitle(): string;
|
|
2849
|
+
onFilterChange(): void;
|
|
2850
|
+
onTextSearchChange(value: string): void;
|
|
2851
|
+
resetFilters(): void;
|
|
2852
|
+
/** So khớp tìm kiếm: bỏ dấu + lowercase để dễ gõ tiếng Việt. */
|
|
2853
|
+
private normalizeSearchText;
|
|
2854
|
+
trackByParsedRow(_: number, row: {
|
|
2855
|
+
log: ActivityLogRow;
|
|
2856
|
+
}): string;
|
|
2857
|
+
getEventLabel(eventType: string): string;
|
|
2858
|
+
getEventIcon(eventType: string): string;
|
|
2859
|
+
getEventColor(eventType: string): string;
|
|
2860
|
+
/** Sheet cũ có thể còn chữ «Hệ thống» — hiển thị thống nhất «System». */
|
|
2861
|
+
displayActivityUser(user: string): string;
|
|
2862
|
+
isDeleteActivity(eventType: string): boolean;
|
|
2863
|
+
isSystemActor(user: string): boolean;
|
|
2864
|
+
/** Check if a log entry is unread */
|
|
2865
|
+
isUnread(logId: string): boolean;
|
|
2866
|
+
activityTimeText(timestamp: string): string;
|
|
2867
|
+
activityTimeToggleTitle(): string;
|
|
2868
|
+
activityTimeToggleAriaLabel(): string;
|
|
2869
|
+
toggleActivityTimeDisplay(event?: Event): void;
|
|
2870
|
+
private isActivityTimeExact;
|
|
2871
|
+
private formatRelativeTime;
|
|
2872
|
+
private formatExactTime;
|
|
2873
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<ActivityLogComponent, never>;
|
|
2874
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<ActivityLogComponent, "app-activity-log", never, {}, { "activityChanged": "activityChanged"; }, never, never, true, never>;
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
interface GoogleSheetConfig {
|
|
2878
|
+
spreadsheetId: string;
|
|
2879
|
+
sheetName?: string;
|
|
2880
|
+
sheetGid?: string;
|
|
2881
|
+
range?: string;
|
|
2882
|
+
}
|
|
2883
|
+
interface GoogleSheetMetadata {
|
|
2884
|
+
sheets: Array<{
|
|
2885
|
+
properties: {
|
|
2886
|
+
sheetId: number;
|
|
2887
|
+
title: string;
|
|
2888
|
+
index: number;
|
|
2889
|
+
};
|
|
2890
|
+
}>;
|
|
2891
|
+
}
|
|
2892
|
+
declare class GoogleSheetsService {
|
|
2893
|
+
private http;
|
|
2894
|
+
private readonly API_KEY;
|
|
2895
|
+
/**
|
|
2896
|
+
* Get values from a Google Sheet with flexible configuration
|
|
2897
|
+
*/
|
|
2898
|
+
getSheetValues(config: GoogleSheetConfig): Observable<string[][]>;
|
|
2899
|
+
/**
|
|
2900
|
+
* Get sheet metadata including all sheet names and IDs
|
|
2901
|
+
*/
|
|
2902
|
+
getSheetMetadata(spreadsheetId: string): Observable<GoogleSheetMetadata>;
|
|
2903
|
+
/**
|
|
2904
|
+
* Find sheet name by GID
|
|
2905
|
+
*/
|
|
2906
|
+
private getSheetNameByGid;
|
|
2907
|
+
/**
|
|
2908
|
+
* Fetch values from a specific range
|
|
2909
|
+
*/
|
|
2910
|
+
private fetchValues;
|
|
2911
|
+
/**
|
|
2912
|
+
* Handle HTTP errors with user-friendly messages
|
|
2913
|
+
*/
|
|
2914
|
+
private handleError;
|
|
2915
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<GoogleSheetsService, never>;
|
|
2916
|
+
static ɵprov: _angular_core.ɵɵInjectableDeclaration<GoogleSheetsService>;
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
interface AppInfo {
|
|
2920
|
+
name: string;
|
|
2921
|
+
version: string;
|
|
2922
|
+
}
|
|
2923
|
+
declare const APP_INFO_TOKEN: InjectionToken<AppInfo>;
|
|
2924
|
+
|
|
2925
|
+
export { ACTIVITY_EVENT, APP_INFO_TOKEN, ActivityLogComponent, ActivityLogService, DocumentsComponent, EventLogService, ExplorerService, FeedingComponent, FeedingDailyComponent, FeedingLogService, FeedingMomComponent, FeedingNutritionComponent, FeedingScheduleComponent, GoogleSheetsService, MedicalHistoryComponent, MedicalHistoryService, NotificationLogService, WeightComponent, WeightLogService, buildActivityLogMessageHtml, evaluateWeightForAge, formatLogContent, getDailySummaries, getNutritionTarget, groupLogsByProximity, parseFeedingSettingsFromRows, predictNextFeeding };
|
|
2926
|
+
export type { ActivityLogEntry, ActivityLogRow, AppInfo, BottlePrepFromSheet, DailySummary, FeedingEventLog, FeedingLog, FeedingSettingRow, FeedingSettingsResolved, FeedingViewGroup, MedicalHistoryEntry, NotificationLog, NutritionTarget, WeightLog };
|