feat: replace age integer with date_of_birth across backend and client

Store date_of_birth (DATE) instead of a static age integer so that age
is always computed dynamically from the stored date of birth.

- Migration 011: adds date_of_birth, backfills from age, drops age
- AgeFromDOB helper computes current age from YYYY-MM-DD string
- User model, repository SQL, and service validation updated
- Flutter: User.age becomes a computed getter; profile edit screen
  uses a date picker bounded to [today-120y, today-10y]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dbastrikin
2026-03-09 23:37:58 +02:00
parent 765346d4e4
commit c9ddb708b1
13 changed files with 202 additions and 73 deletions

View File

@@ -1,6 +1,9 @@
package user package user
import "math" import (
"math"
"time"
)
// Activity level multipliers (Mifflin-St Jeor). // Activity level multipliers (Mifflin-St Jeor).
var activityMultiplier = map[string]float64{ var activityMultiplier = map[string]float64{
@@ -16,6 +19,23 @@ var goalAdjustment = map[string]float64{
"gain": 300, "gain": 300,
} }
// AgeFromDOB computes age in years from a "YYYY-MM-DD" string. Returns nil on error.
func AgeFromDOB(dob *string) *int {
if dob == nil {
return nil
}
t, err := time.Parse("2006-01-02", *dob)
if err != nil {
return nil
}
now := time.Now()
years := now.Year() - t.Year()
if now.Month() < t.Month() || (now.Month() == t.Month() && now.Day() < t.Day()) {
years--
}
return &years
}
// CalculateDailyCalories computes the daily calorie target using the // CalculateDailyCalories computes the daily calorie target using the
// Mifflin-St Jeor equation. Returns nil if any required parameter is missing. // Mifflin-St Jeor equation. Returns nil if any required parameter is missing.
func CalculateDailyCalories(heightCM *int, weightKG *float64, age *int, gender, activity, goal *string) *int { func CalculateDailyCalories(heightCM *int, weightKG *float64, age *int, gender, activity, goal *string) *int {

View File

@@ -2,10 +2,48 @@ package user
import ( import (
"testing" "testing"
"time"
) )
func ptr[T any](v T) *T { return &v } func ptr[T any](v T) *T { return &v }
func TestAgeFromDOB_Nil(t *testing.T) {
if AgeFromDOB(nil) != nil {
t.Fatal("expected nil for nil input")
}
}
func TestAgeFromDOB_InvalidFormat(t *testing.T) {
s := "not-a-date"
if AgeFromDOB(&s) != nil {
t.Fatal("expected nil for invalid format")
}
}
func TestAgeFromDOB_ExactAge(t *testing.T) {
dob := time.Now().AddDate(-30, 0, 0).Format("2006-01-02")
age := AgeFromDOB(&dob)
if age == nil {
t.Fatal("expected non-nil result")
}
if *age != 30 {
t.Errorf("expected 30, got %d", *age)
}
}
func TestAgeFromDOB_BeforeBirthday(t *testing.T) {
// Birthday is one day in the future relative to today-25y, so age should be 24
now := time.Now()
dob := time.Date(now.Year()-25, now.Month(), now.Day()+1, 0, 0, 0, 0, time.UTC).Format("2006-01-02")
age := AgeFromDOB(&dob)
if age == nil {
t.Fatal("expected non-nil result")
}
if *age != 24 {
t.Errorf("expected 24, got %d", *age)
}
}
func TestCalculateDailyCalories_MaleMaintain(t *testing.T) { func TestCalculateDailyCalories_MaleMaintain(t *testing.T) {
cal := CalculateDailyCalories(ptr(180), ptr(80.0), ptr(30), ptr("male"), ptr("moderate"), ptr("maintain")) cal := CalculateDailyCalories(ptr(180), ptr(80.0), ptr(30), ptr("male"), ptr("moderate"), ptr("maintain"))
if cal == nil { if cal == nil {

View File

@@ -13,7 +13,7 @@ type User struct {
AvatarURL *string `json:"avatar_url"` AvatarURL *string `json:"avatar_url"`
HeightCM *int `json:"height_cm"` HeightCM *int `json:"height_cm"`
WeightKG *float64 `json:"weight_kg"` WeightKG *float64 `json:"weight_kg"`
Age *int `json:"age"` DateOfBirth *string `json:"date_of_birth"`
Gender *string `json:"gender"` Gender *string `json:"gender"`
Activity *string `json:"activity"` Activity *string `json:"activity"`
Goal *string `json:"goal"` Goal *string `json:"goal"`
@@ -28,7 +28,7 @@ type UpdateProfileRequest struct {
Name *string `json:"name"` Name *string `json:"name"`
HeightCM *int `json:"height_cm"` HeightCM *int `json:"height_cm"`
WeightKG *float64 `json:"weight_kg"` WeightKG *float64 `json:"weight_kg"`
Age *int `json:"age"` DateOfBirth *string `json:"date_of_birth"`
Gender *string `json:"gender"` Gender *string `json:"gender"`
Activity *string `json:"activity"` Activity *string `json:"activity"`
Goal *string `json:"goal"` Goal *string `json:"goal"`
@@ -39,6 +39,6 @@ type UpdateProfileRequest struct {
// HasBodyParams returns true if any body parameter is being updated // HasBodyParams returns true if any body parameter is being updated
// that would require recalculation of daily calories. // that would require recalculation of daily calories.
func (r *UpdateProfileRequest) HasBodyParams() bool { func (r *UpdateProfileRequest) HasBodyParams() bool {
return r.HeightCM != nil || r.WeightKG != nil || r.Age != nil || return r.HeightCM != nil || r.WeightKG != nil || r.DateOfBirth != nil ||
r.Gender != nil || r.Activity != nil || r.Goal != nil r.Gender != nil || r.Activity != nil || r.Goal != nil
} }

View File

@@ -45,7 +45,7 @@ func (r *Repository) UpsertByFirebaseUID(ctx context.Context, uid, email, name,
avatar_url = COALESCE(EXCLUDED.avatar_url, users.avatar_url), avatar_url = COALESCE(EXCLUDED.avatar_url, users.avatar_url),
updated_at = now() updated_at = now()
RETURNING id, firebase_uid, email, name, avatar_url, RETURNING id, firebase_uid, email, name, avatar_url,
height_cm, weight_kg, age, gender, activity, goal, daily_calories, height_cm, weight_kg, date_of_birth, gender, activity, goal, daily_calories,
plan, preferences, created_at, updated_at` plan, preferences, created_at, updated_at`
return r.scanUser(r.pool.QueryRow(ctx, query, uid, email, name, avatarPtr)) return r.scanUser(r.pool.QueryRow(ctx, query, uid, email, name, avatarPtr))
@@ -54,7 +54,7 @@ func (r *Repository) UpsertByFirebaseUID(ctx context.Context, uid, email, name,
func (r *Repository) GetByID(ctx context.Context, id string) (*User, error) { func (r *Repository) GetByID(ctx context.Context, id string) (*User, error) {
query := ` query := `
SELECT id, firebase_uid, email, name, avatar_url, SELECT id, firebase_uid, email, name, avatar_url,
height_cm, weight_kg, age, gender, activity, goal, daily_calories, height_cm, weight_kg, date_of_birth, gender, activity, goal, daily_calories,
plan, preferences, created_at, updated_at plan, preferences, created_at, updated_at
FROM users WHERE id = $1` FROM users WHERE id = $1`
@@ -91,9 +91,9 @@ func buildUpdateQuery(id string, req UpdateProfileRequest) (string, []interface{
args = append(args, *req.WeightKG) args = append(args, *req.WeightKG)
argIdx++ argIdx++
} }
if req.Age != nil { if req.DateOfBirth != nil {
setClauses = append(setClauses, fmt.Sprintf("age = $%d", argIdx)) setClauses = append(setClauses, fmt.Sprintf("date_of_birth = $%d", argIdx))
args = append(args, *req.Age) args = append(args, *req.DateOfBirth)
argIdx++ argIdx++
} }
if req.Gender != nil { if req.Gender != nil {
@@ -132,7 +132,7 @@ func buildUpdateQuery(id string, req UpdateProfileRequest) (string, []interface{
UPDATE users SET %s UPDATE users SET %s
WHERE id = $%d WHERE id = $%d
RETURNING id, firebase_uid, email, name, avatar_url, RETURNING id, firebase_uid, email, name, avatar_url,
height_cm, weight_kg, age, gender, activity, goal, daily_calories, height_cm, weight_kg, date_of_birth, gender, activity, goal, daily_calories,
plan, preferences, created_at, updated_at`, plan, preferences, created_at, updated_at`,
strings.Join(setClauses, ", "), argIdx) strings.Join(setClauses, ", "), argIdx)
args = append(args, id) args = append(args, id)
@@ -184,7 +184,7 @@ func (r *Repository) SetRefreshToken(ctx context.Context, id, token string, expi
func (r *Repository) FindByRefreshToken(ctx context.Context, token string) (*User, error) { func (r *Repository) FindByRefreshToken(ctx context.Context, token string) (*User, error) {
query := ` query := `
SELECT id, firebase_uid, email, name, avatar_url, SELECT id, firebase_uid, email, name, avatar_url,
height_cm, weight_kg, age, gender, activity, goal, daily_calories, height_cm, weight_kg, date_of_birth, gender, activity, goal, daily_calories,
plan, preferences, created_at, updated_at plan, preferences, created_at, updated_at
FROM users FROM users
WHERE refresh_token = $1 AND token_expires_at > now()` WHERE refresh_token = $1 AND token_expires_at > now()`
@@ -205,14 +205,19 @@ type scannable interface {
func (r *Repository) scanUser(row scannable) (*User, error) { func (r *Repository) scanUser(row scannable) (*User, error) {
var u User var u User
var prefs []byte var prefs []byte
var dob *time.Time
err := row.Scan( err := row.Scan(
&u.ID, &u.FirebaseUID, &u.Email, &u.Name, &u.AvatarURL, &u.ID, &u.FirebaseUID, &u.Email, &u.Name, &u.AvatarURL,
&u.HeightCM, &u.WeightKG, &u.Age, &u.Gender, &u.Activity, &u.Goal, &u.DailyCalories, &u.HeightCM, &u.WeightKG, &dob, &u.Gender, &u.Activity, &u.Goal, &u.DailyCalories,
&u.Plan, &prefs, &u.CreatedAt, &u.UpdatedAt, &u.Plan, &prefs, &u.CreatedAt, &u.UpdatedAt,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("scan user: %w", err) return nil, fmt.Errorf("scan user: %w", err)
} }
if dob != nil {
s := dob.Format("2006-01-02")
u.DateOfBirth = &s
}
u.Preferences = json.RawMessage(prefs) u.Preferences = json.RawMessage(prefs)
return &u, nil return &u, nil
} }

View File

@@ -100,17 +100,17 @@ func TestRepository_Update_MultipleFields(t *testing.T) {
created, _ := repo.UpsertByFirebaseUID(ctx, "fb-multi", "multi@example.com", "Multi User", "") created, _ := repo.UpsertByFirebaseUID(ctx, "fb-multi", "multi@example.com", "Multi User", "")
height := 175 height := 175
weight := 70.5 weight := 70.5
age := 25 dob := "2001-03-09"
gender := "male" gender := "male"
activity := "moderate" activity := "moderate"
goal := "maintain" goal := "maintain"
u, err := repo.Update(ctx, created.ID, UpdateProfileRequest{ u, err := repo.Update(ctx, created.ID, UpdateProfileRequest{
HeightCM: &height, HeightCM: &height,
WeightKG: &weight, WeightKG: &weight,
Age: &age, DateOfBirth: &dob,
Gender: &gender, Gender: &gender,
Activity: &activity, Activity: &activity,
Goal: &goal, Goal: &goal,
}) })
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)

View File

@@ -3,6 +3,7 @@ package user
import ( import (
"context" "context"
"fmt" "fmt"
"time"
) )
type Service struct { type Service struct {
@@ -46,10 +47,11 @@ func (s *Service) UpdateProfile(ctx context.Context, userID string, req UpdatePr
if req.WeightKG != nil { if req.WeightKG != nil {
weight = req.WeightKG weight = req.WeightKG
} }
age := current.Age dob := current.DateOfBirth
if req.Age != nil { if req.DateOfBirth != nil {
age = req.Age dob = req.DateOfBirth
} }
age := AgeFromDOB(dob)
gender := current.Gender gender := current.Gender
if req.Gender != nil { if req.Gender != nil {
gender = req.Gender gender = req.Gender
@@ -89,9 +91,17 @@ func validateProfileRequest(req UpdateProfileRequest) error {
return fmt.Errorf("weight_kg must be between 30 and 300") return fmt.Errorf("weight_kg must be between 30 and 300")
} }
} }
if req.Age != nil { if req.DateOfBirth != nil {
if *req.Age < 10 || *req.Age > 120 { t, err := time.Parse("2006-01-02", *req.DateOfBirth)
return fmt.Errorf("age must be between 10 and 120") if err != nil {
return fmt.Errorf("date_of_birth must be in YYYY-MM-DD format")
}
if t.After(time.Now()) {
return fmt.Errorf("date_of_birth cannot be in the future")
}
age := AgeFromDOB(req.DateOfBirth)
if *age < 10 || *age > 120 {
return fmt.Errorf("date_of_birth must yield an age between 10 and 120")
} }
} }
if req.Gender != nil { if req.Gender != nil {

View File

@@ -94,19 +94,20 @@ func TestUpdateProfile_NameOnly(t *testing.T) {
} }
func TestUpdateProfile_WithCaloriesRecalculation(t *testing.T) { func TestUpdateProfile_WithCaloriesRecalculation(t *testing.T) {
dob := time.Now().AddDate(-30, 0, 0).Format("2006-01-02")
profileReq := UpdateProfileRequest{ profileReq := UpdateProfileRequest{
HeightCM: ptrInt(180), HeightCM: ptrInt(180),
WeightKG: ptrFloat(80), WeightKG: ptrFloat(80),
Age: ptrInt(30), DateOfBirth: &dob,
Gender: ptrStr("male"), Gender: ptrStr("male"),
Activity: ptrStr("moderate"), Activity: ptrStr("moderate"),
Goal: ptrStr("maintain"), Goal: ptrStr("maintain"),
} }
finalUser := &User{ finalUser := &User{
ID: "user-1", ID: "user-1",
HeightCM: ptrInt(180), HeightCM: ptrInt(180),
WeightKG: ptrFloat(80), WeightKG: ptrFloat(80),
Age: ptrInt(30), DateOfBirth: &dob,
Gender: ptrStr("male"), Gender: ptrStr("male"),
Activity: ptrStr("moderate"), Activity: ptrStr("moderate"),
Goal: ptrStr("maintain"), Goal: ptrStr("maintain"),
@@ -178,19 +179,31 @@ func TestUpdateProfile_InvalidWeight_TooHigh(t *testing.T) {
} }
} }
func TestUpdateProfile_InvalidAge_TooLow(t *testing.T) { func TestUpdateProfile_InvalidDOB_TooRecent(t *testing.T) {
svc := NewService(&mockUserRepo{}) svc := NewService(&mockUserRepo{})
dob := time.Now().AddDate(-5, 0, 0).Format("2006-01-02")
_, err := svc.UpdateProfile(context.Background(), "user-1", UpdateProfileRequest{Age: ptrInt(5)}) _, err := svc.UpdateProfile(context.Background(), "user-1", UpdateProfileRequest{DateOfBirth: &dob})
if err == nil { if err == nil {
t.Fatal("expected validation error") t.Fatal("expected validation error")
} }
} }
func TestUpdateProfile_InvalidAge_TooHigh(t *testing.T) { func TestUpdateProfile_InvalidDOB_TooOld(t *testing.T) {
svc := NewService(&mockUserRepo{}) svc := NewService(&mockUserRepo{})
dob := time.Now().AddDate(-150, 0, 0).Format("2006-01-02")
_, err := svc.UpdateProfile(context.Background(), "user-1", UpdateProfileRequest{Age: ptrInt(150)}) _, err := svc.UpdateProfile(context.Background(), "user-1", UpdateProfileRequest{DateOfBirth: &dob})
if err == nil {
t.Fatal("expected validation error")
}
}
func TestUpdateProfile_InvalidDOB_BadFormat(t *testing.T) {
svc := NewService(&mockUserRepo{})
dob := "not-a-date"
_, err := svc.UpdateProfile(context.Background(), "user-1", UpdateProfileRequest{DateOfBirth: &dob})
if err == nil { if err == nil {
t.Fatal("expected validation error") t.Fatal("expected validation error")
} }

View File

@@ -0,0 +1,13 @@
-- +goose Up
ALTER TABLE users ADD COLUMN date_of_birth DATE;
UPDATE users
SET date_of_birth = (CURRENT_DATE - (age * INTERVAL '1 year'))::DATE
WHERE age IS NOT NULL;
ALTER TABLE users DROP COLUMN age;
-- +goose Down
ALTER TABLE users ADD COLUMN age SMALLINT;
UPDATE users
SET age = EXTRACT(YEAR FROM AGE(CURRENT_DATE, date_of_birth))::SMALLINT
WHERE date_of_birth IS NOT NULL;
ALTER TABLE users DROP COLUMN date_of_birth;

View File

@@ -336,7 +336,7 @@ class _EditProfileSheetState extends ConsumerState<EditProfileSheet> {
late final TextEditingController _nameCtrl; late final TextEditingController _nameCtrl;
late final TextEditingController _heightCtrl; late final TextEditingController _heightCtrl;
late final TextEditingController _weightCtrl; late final TextEditingController _weightCtrl;
late final TextEditingController _ageCtrl; DateTime? _selectedDob;
String? _gender; String? _gender;
String? _goal; String? _goal;
String? _activity; String? _activity;
@@ -351,7 +351,8 @@ class _EditProfileSheetState extends ConsumerState<EditProfileSheet> {
_heightCtrl = TextEditingController(text: u.heightCm?.toString() ?? ''); _heightCtrl = TextEditingController(text: u.heightCm?.toString() ?? '');
_weightCtrl = TextEditingController( _weightCtrl = TextEditingController(
text: u.weightKg != null ? _fmt(u.weightKg!) : ''); text: u.weightKg != null ? _fmt(u.weightKg!) : '');
_ageCtrl = TextEditingController(text: u.age?.toString() ?? ''); _selectedDob =
u.dateOfBirth != null ? DateTime.tryParse(u.dateOfBirth!) : null;
_gender = u.gender; _gender = u.gender;
_goal = u.goal; _goal = u.goal;
_activity = u.activity; _activity = u.activity;
@@ -363,13 +364,24 @@ class _EditProfileSheetState extends ConsumerState<EditProfileSheet> {
_nameCtrl.dispose(); _nameCtrl.dispose();
_heightCtrl.dispose(); _heightCtrl.dispose();
_weightCtrl.dispose(); _weightCtrl.dispose();
_ageCtrl.dispose();
super.dispose(); super.dispose();
} }
String _fmt(double w) => String _fmt(double w) =>
w == w.truncateToDouble() ? w.toInt().toString() : w.toStringAsFixed(1); w == w.truncateToDouble() ? w.toInt().toString() : w.toStringAsFixed(1);
Future<void> _pickDob() async {
final now = DateTime.now();
final initial = _selectedDob ?? DateTime(now.year - 25, now.month, now.day);
final picked = await showDatePicker(
context: context,
initialDate: initial,
firstDate: DateTime(now.year - 120),
lastDate: DateTime(now.year - 10, now.month, now.day),
);
if (picked != null) setState(() => _selectedDob = picked);
}
Future<void> _save() async { Future<void> _save() async {
if (!_formKey.currentState!.validate()) return; if (!_formKey.currentState!.validate()) return;
setState(() => _saving = true); setState(() => _saving = true);
@@ -378,7 +390,11 @@ class _EditProfileSheetState extends ConsumerState<EditProfileSheet> {
name: _nameCtrl.text.trim(), name: _nameCtrl.text.trim(),
heightCm: int.tryParse(_heightCtrl.text), heightCm: int.tryParse(_heightCtrl.text),
weightKg: double.tryParse(_weightCtrl.text), weightKg: double.tryParse(_weightCtrl.text),
age: int.tryParse(_ageCtrl.text), dateOfBirth: _selectedDob != null
? '${_selectedDob!.year.toString().padLeft(4, '0')}-'
'${_selectedDob!.month.toString().padLeft(2, '0')}-'
'${_selectedDob!.day.toString().padLeft(2, '0')}'
: null,
gender: _gender, gender: _gender,
goal: _goal, goal: _goal,
activity: _activity, activity: _activity,
@@ -506,21 +522,21 @@ class _EditProfileSheetState extends ConsumerState<EditProfileSheet> {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// Age // Date of birth
TextFormField( InkWell(
controller: _ageCtrl, onTap: _pickDob,
decoration: child: InputDecorator(
const InputDecoration(labelText: 'Возраст (лет)'), decoration:
keyboardType: TextInputType.number, const InputDecoration(labelText: 'Дата рождения'),
inputFormatters: [ child: Text(
FilteringTextInputFormatter.digitsOnly _selectedDob != null
], ? '${_selectedDob!.day.toString().padLeft(2, '0')}.'
validator: (v) { '${_selectedDob!.month.toString().padLeft(2, '0')}.'
if (v == null || v.isEmpty) return null; '${_selectedDob!.year}'
final n = int.tryParse(v); : 'Не задано',
if (n == null || n < 10 || n > 120) return '10120'; style: Theme.of(context).textTheme.bodyMedium,
return null; ),
}, ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),

View File

@@ -12,7 +12,7 @@ class UpdateProfileRequest {
final String? name; final String? name;
final int? heightCm; final int? heightCm;
final double? weightKg; final double? weightKg;
final int? age; final String? dateOfBirth;
final String? gender; final String? gender;
final String? activity; final String? activity;
final String? goal; final String? goal;
@@ -22,7 +22,7 @@ class UpdateProfileRequest {
this.name, this.name,
this.heightCm, this.heightCm,
this.weightKg, this.weightKg,
this.age, this.dateOfBirth,
this.gender, this.gender,
this.activity, this.activity,
this.goal, this.goal,
@@ -34,7 +34,7 @@ class UpdateProfileRequest {
if (name != null) map['name'] = name; if (name != null) map['name'] = name;
if (heightCm != null) map['height_cm'] = heightCm; if (heightCm != null) map['height_cm'] = heightCm;
if (weightKg != null) map['weight_kg'] = weightKg; if (weightKg != null) map['weight_kg'] = weightKg;
if (age != null) map['age'] = age; if (dateOfBirth != null) map['date_of_birth'] = dateOfBirth;
if (gender != null) map['gender'] = gender; if (gender != null) map['gender'] = gender;
if (activity != null) map['activity'] = activity; if (activity != null) map['activity'] = activity;
if (goal != null) map['goal'] = goal; if (goal != null) map['goal'] = goal;

View File

@@ -13,7 +13,8 @@ class User {
final int? heightCm; final int? heightCm;
@JsonKey(name: 'weight_kg') @JsonKey(name: 'weight_kg')
final double? weightKg; final double? weightKg;
final int? age; @JsonKey(name: 'date_of_birth')
final String? dateOfBirth;
final String? gender; final String? gender;
final String? activity; final String? activity;
final String? goal; final String? goal;
@@ -30,7 +31,7 @@ class User {
this.avatarUrl, this.avatarUrl,
this.heightCm, this.heightCm,
this.weightKg, this.weightKg,
this.age, this.dateOfBirth,
this.gender, this.gender,
this.activity, this.activity,
this.goal, this.goal,
@@ -42,6 +43,19 @@ class User {
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json); factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this); Map<String, dynamic> toJson() => _$UserToJson(this);
int? get age {
if (dateOfBirth == null) return null;
final dob = DateTime.tryParse(dateOfBirth!);
if (dob == null) return null;
final now = DateTime.now();
int years = now.year - dob.year;
if (now.month < dob.month ||
(now.month == dob.month && now.day < dob.day)) {
years--;
}
return years;
}
bool get hasCompletedOnboarding => bool get hasCompletedOnboarding =>
heightCm != null && weightKg != null && age != null && gender != null; heightCm != null && weightKg != null && dateOfBirth != null && gender != null;
} }

View File

@@ -13,7 +13,7 @@ User _$UserFromJson(Map<String, dynamic> json) => User(
avatarUrl: json['avatar_url'] as String?, avatarUrl: json['avatar_url'] as String?,
heightCm: (json['height_cm'] as num?)?.toInt(), heightCm: (json['height_cm'] as num?)?.toInt(),
weightKg: (json['weight_kg'] as num?)?.toDouble(), weightKg: (json['weight_kg'] as num?)?.toDouble(),
age: (json['age'] as num?)?.toInt(), dateOfBirth: json['date_of_birth'] as String?,
gender: json['gender'] as String?, gender: json['gender'] as String?,
activity: json['activity'] as String?, activity: json['activity'] as String?,
goal: json['goal'] as String?, goal: json['goal'] as String?,
@@ -29,7 +29,7 @@ Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
'avatar_url': instance.avatarUrl, 'avatar_url': instance.avatarUrl,
'height_cm': instance.heightCm, 'height_cm': instance.heightCm,
'weight_kg': instance.weightKg, 'weight_kg': instance.weightKg,
'age': instance.age, 'date_of_birth': instance.dateOfBirth,
'gender': instance.gender, 'gender': instance.gender,
'activity': instance.activity, 'activity': instance.activity,
'goal': instance.goal, 'goal': instance.goal,

View File

@@ -125,10 +125,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.1" version: "1.4.0"
checked_yaml: checked_yaml:
dependency: transitive dependency: transitive
description: description:
@@ -668,26 +668,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.19" version: "0.12.17"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.13.0" version: "0.11.1"
meta: meta:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.0" version: "1.16.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@@ -993,10 +993,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.10" version: "0.7.6"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description: