Meal Entry¶
Overview¶
The Meal Entry feature allows staff and carers to log food intake for care recipients, capturing the food item name, portion size, carbohydrates consumed, and carb level category. The primary purpose is bolus calculation support for diabetes management. Each entry can be later edited or deleted. A history view with client-side search provides an audit trail, and Excel export is available for reporting. CareRecipients themselves cannot edit or delete entries — only create them — preserving data accuracy.
Role Access¶
| Role | Create | View | Edit | Delete | Export |
|---|---|---|---|---|---|
| SuperAdmin | ✓ | All | ✓ | ✓ | ✓ |
| Administrator | ✓ | All | ✓ | ✓ | ✓ |
| Carer | ✓ | Linked CRs | ✓ | ✓ | ✓ |
| SupportWorker | ✓ | Linked CRs | ✓ | ✓ | ✓ |
| CareRecipient | ✓ (own) | Own | — | — | ✓ |
| HealthCareProvider | — | Linked CRs | — | — | — |
Backend¶
Controller: MealEntryController — /api/mealentry¶
| Method | Route | Auth | Description |
|---|---|---|---|
| POST | /api/mealentry/entries |
Authorized | Create a meal entry |
| GET | /api/mealentry/entries |
Authorized | List entries (role-scoped) |
| GET | /api/mealentry/entries/{id} |
Anonymous | Get a single entry |
| PUT | /api/mealentry/entries/{id} |
Authorized (non-CR) | Update an entry |
| DELETE | /api/mealentry/entries/{id} |
Authorized (non-CR) | Delete an entry |
| GET | /api/mealentry/export/weekly |
Anonymous | Weekly entries export with optional date range |
Business Logic¶
POST /api/mealentry/entries
- CareRecipient role: CareRecipientId is automatically set to self (from JWT id claim); the request's careRecipientId field is ignored.
- Other roles: CareRecipientId may be specified or null (entry attributed to the user themselves).
PUT / DELETE /api/mealentry/entries/{id}
- CareRecipient role: returns 403 Forbidden. Only Carer/SupportWorker/Admin roles may modify or delete entries.
GET /api/mealentry/entries
- CareRecipient: returns entries where CareRecipientId == self OR UserId == self.
- Other roles: accepts optional ?userId=N query param to filter by user.
GET /api/mealentry/export/weekly
- Returns entries optionally filtered by ?startDate and ?endDate query params.
Key DTOs¶
CreateMealEntryRequest
foodItem string Max 200 characters (required)
portionSize decimal Numeric portion amount (required)
portionUnit PortionUnit Enum (required) — see values below
carbsConsumed decimal Carbs in grams for the listed portion (required)
comments string? Optional free text
clientTimestamp DateTime? Client-supplied timestamp for offline entries
UpdateMealEntryRequest
MealEntryResponse (response)
id int
foodItem string
portionSize decimal
portionUnit PortionUnit
carbsConsumed decimal
carbLevel CarbLevel Auto-classified: Low / Medium / High
comments string?
timestamp DateTime
userId int
userName string
lastUpdatedByName string?
lastUpdatedAt DateTime?
Model: MealEntry¶
| Field | Type | Notes |
|---|---|---|
Id |
int | PK |
FoodItem |
string (200) | Free-text food name |
PortionSize |
decimal (7,2) | Numeric quantity (column: portion_size) |
PortionUnit |
PortionUnit enum | Unit of measurement (column: portion_unit) |
CarbsConsumed |
decimal (7,2) | Carbs in grams for the recorded portion |
CarbLevel |
CarbLevel enum | Auto-set by service: Low (≤15g) / Medium (16–70g) / High (>70g) |
Comments |
string? | |
Timestamp |
DateTime | Defaults to NOW() AT TIME ZONE 'UTC' |
UserId |
int | FK → User (creator) |
CareRecipientId |
int? | FK → User (care recipient the entry belongs to) |
LastUpdatedById |
int? | FK → User (last editor) |
LastUpdatedAt |
DateTime? |
Enums¶
PortionUnit
| Value | Label |
|---|---|
| Grams | g |
| Millilitres | ml |
| Cup | cup |
| Tablespoon | tbsp |
| Teaspoon | tsp |
| Medium | medium |
| Large | large |
| Small | small |
| Slice | slice |
| Piece | piece |
| Bar | bar |
| Tub | tub |
| Packet | packet |
| Plate | plate |
CarbLevel (auto-classified by MealEntryService.ClassifyCarbLevel())
| Value | Threshold |
|---|---|
| Low | ≤ 15g |
| Medium | 16–70g |
| High | > 70g |
MealEntryService¶
File: server/src/Vitara.Api/Services/MealEntryService.cs
Dependencies: ApplicationDbContext
Key methods:
- AddMealEntryAsync(request, userId, careRecipientId?) — creates entry; auto-classifies CarbLevel; uses ClientTimestamp if provided (offline support), otherwise DateTime.UtcNow.
- UpdateMealEntryAsync(id, request, updatedByUserId) — updates all fields; recalculates CarbLevel; sets LastUpdatedById and LastUpdatedAt.
- DeleteMealEntryAsync(id) — hard delete.
- GetMealEntriesAsync(userId?, termId?, careRecipientId?) — returns all entries ordered by timestamp descending; includes User and LastUpdatedByUser navigation.
- GetMealEntriesForCareRecipientAsync(userId) — returns entries where CareRecipientId == userId OR UserId == userId.
- GetWeeklyMealEntriesAsync(startDate?, endDate?) — date-range filter; defaults to last 7 days.
- ClassifyCarbLevel(carbsConsumed) — private helper: ≤15g → Low, 16–70g → Medium, >70g → High.
Frontend¶
Routes¶
| Path | Component | Notes |
|---|---|---|
/meal-entry/entry |
MealEntryComponent |
Create or edit a single entry |
/meal-entry/history |
MealEntryHistoryComponent |
View and manage all entries |
MealEntryComponent — /meal-entry/entry¶
File: client/src/app/features/meal-entry/components/meal-entry.component.ts
Modes:
- Create mode (default): form is empty; submission calls MealEntryService.createEntry(dto).
- Edit mode: activated when ?id=N query param is present. On init, loads the entry via MealEntryService.getEntry(id), pre-fills all fields, and submission calls MealEntryService.updateEntry(id, dto).
Form fields:
- foodItem — required text input (max 200).
- portionSize — required decimal number input.
- portionUnit — required dropdown: Grams / Millilitres / Cup / Tablespoon / Teaspoon / Medium / Large / Small / Slice / Piece.
- carbsConsumed — required decimal input (the total carbs for the specified portion).
- comments — optional textarea.
Offline fallback: MealEntryService.createEntry() enqueues to OfflineQueueService (type 'meal-entry') on network failure.
Navigation: On successful create, navigates to /meal-entry/history. On successful edit, navigates back with a success snackbar.
MealEntryHistoryComponent — /meal-entry/history¶
File: client/src/app/features/meal-entry/components/meal-entry-history.component.ts
On init: Calls MealEntryService.getEntries().
Table columns: Food Item | Portion Size | Carbs (g) | Comments | Last Updated | Actions
Carbs (g) column: Displays the gram value followed by a colour-coded CarbLevel badge — green Low, blue Medium, orange High — matching the badge style used in the entry form's live feedback.
Client-side search: Filters across food item, portion unit, portion size, carbs, and comments fields.
Edit action: Navigates to /meal-entry/entry?id=N.
Delete action: Triggers DialogService.confirmDelete(), then calls MealEntryService.deleteMealEntry(id). Hidden for CareRecipient role.
Excel export: Downloads Bolus_History_Report_YYYY-MM-DD.xlsx via GET /api/export/mealentry-report.
Single-Item Carb Calculator (inline, per row)¶
A calculate icon button in each row's Actions cell opens an inline calculator panel beneath that row.
Formula: (inputAmount / portionSize) × carbsConsumed — proportional scaling.
Example: Logged entry is 100g fish = 20g carbs. User enters 150g → result: 30g.
Unit behaviour:
- For Grams and Millilitres (proportional units): label shows "Amount (g)" or "Amount (ml)".
- For non-proportional units (Cup, Slice, Piece, etc.): label shows the unit name; calculation still applies the same ratio.
Result is rounded to 1 decimal place. If the result has a fractional part, a rounded whole-gram figure is shown alongside with an asterisk note for bolus purposes.
State: expandedEntry, calcInput, calcResult on MealEntryHistoryComponent. Only one row can be expanded at a time.
Multi-Item Carb Calculator (modal)¶
Trigger: Purple calculate icon button in the toolbar between "+ Add New Entry" and the Excel export button.
Tooltip: "Meal Carb Calculator — add multiple foods, set quantities and see per-item carbs plus a grand total"
Component: MealCarbCalculatorModalComponent
File: client/src/app/features/meal-entry/components/meal-carb-calculator-modal/
How it works:
1. Opens as a MatDialog receiving { entries: MealEntry[] } — no extra API call; uses already-loaded history data.
2. Add Items section: search box filters the user's food history by name; each result row shows the food name and recorded portion info with a + button to add it.
3. Your Meal section: each added item shows the food name, an editable quantity input (pre-filled with the original portionSize), the unit label, and the calculated carbs for that quantity.
4. Footer: running total in grams + a colour-coded CarbLevel badge (green = Low, amber = Medium, red = High).
5. Items can be removed with the × button; the total updates live.
6. Calculator only — nothing is saved.
Carb calculation (per item): same formula as single-item: (quantity / portionSize) × carbsConsumed, rounded to 1 dp.
CarbLevel thresholds (total): ≤15g → Low, 16–70g → Medium, >70g → High.
Responsive: maxWidth: 95vw; stacks to single column below 480px; 44px minimum tap targets.
Feature Service: MealEntryService¶
File: client/src/app/features/meal-entry/services/meal-entry.service.ts
| Method | HTTP | Endpoint | Notes |
|---|---|---|---|
addMealEntry(dto) |
POST | /mealentry/entries |
Offline fallback via OfflineQueueService |
getMealEntries(userId?) |
GET | /mealentry/entries |
Optional userId filter |
getMealEntry(id) |
GET | /mealentry/entries/{id} |
For edit pre-fill |
updateMealEntry(id, dto) |
PUT | /mealentry/entries/{id} |
|
deleteMealEntry(id) |
DELETE | /mealentry/entries/{id} |
|
exportToExcel() |
GET | /export/mealentry-report |
Returns Blob |
getWeeklyMealEntries(start?, end?) |
GET | /mealentry/export/weekly |
Optional date range |
Offline queue: On network error (status 0 or 5xx), addMealEntry() enqueues to OfflineQueueService (type 'meal-entry') with label "{foodItem} – {carbsConsumed}g carbs". Replayed by SyncService on reconnection.
Data Validation Rules¶
| Field | Rule |
|---|---|
foodItem |
Required, max 200 characters |
portionSize |
Required, decimal ≥ 0 |
portionUnit |
Required, must be a valid PortionUnit enum value |
carbsConsumed |
Required, decimal ≥ 0 |
carbLevel |
Auto-set by server — not submitted by client |
| CareRecipient edit/delete | 403 Forbidden at server; edit/delete buttons hidden in UI |
| HealthCareProvider edit/delete | 403 Forbidden at server; edit/delete buttons hidden in UI |
End-to-End Data Flow¶
Create Entry¶
Carer Angular API DB
| | | |
| Navigate /meal-entry/entry| | |
| Fill form: Fish, 200g, | | |
| Grams, 30g carbs | | |
| | | |
| Submit |-- POST /mealentry/entries -->| |
| | |-- INSERT MealEntry ->|
| |<-- MealEntryResponse ---------| |
|<-- navigate /history | | |
Edit Entry¶
Carer Angular API DB
| | | |
| Click Edit on row | | |
|<-- navigate /entry?id=5 | | |
| |-- GET /mealentry/entries/5 -->| |
| |<-- MealEntryResponse ---------| |
|<-- form pre-filled | | |
| Change portion to 250g | | |
| Submit |-- PUT /mealentry/entries/5 -->| |
| |<-- Updated MealEntryResponse -| |
|<-- snackbar + navigate | | |
Multi-Item Carb Calculator¶
User Angular (modal) No API call
| | |
| Click purple calc btn | |
| | Opens MealCarbCalculatorModal |
| | (passes mealEntries already |
| | loaded on history page) |
| Search "yoghurt" | filteredEntries getter |
| Click + Add | pushes CarbCalcItem |
| Change qty to 150g | onQuantityChange() |
| | item.calculatedCarbs = |
| | (150/100) × 8.5 = 12.8g |
| Add apple (120g/13g) | totalCarbs = 12.8 + 13 = 25.8g|
| | totalCarbLevel = "Medium" |
| Click Close | dialogRef.close() |
| | history page unchanged |