Initial scaffold: FocusFlow shared Dart package

Models (Task, Streak, StreakEntry, Reward, User, TimeEstimate, CoworkingRoom, ApiResponse),
enums (EnergyLevel, TaskStatus, RewardType, RewardStyle),
constants (ApiConstants, ErrorCodes, AppLimits), validators, and generated .g.dart files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Oracle Public Cloud User
2026-03-04 15:50:26 +00:00
commit 1363f7d12d
32 changed files with 1197 additions and 0 deletions

26
lib/focusflow_shared.dart Normal file
View File

@@ -0,0 +1,26 @@
/// Shared models, enums, and utilities for FocusFlow ADHD app.
library;
// Models
export 'src/models/task.dart';
export 'src/models/streak.dart';
export 'src/models/streak_entry.dart';
export 'src/models/reward.dart';
export 'src/models/user.dart';
export 'src/models/time_estimate.dart';
export 'src/models/coworking_room.dart';
export 'src/models/api_response.dart';
// Enums
export 'src/enums/energy_level.dart';
export 'src/enums/task_status.dart';
export 'src/enums/reward_type.dart';
export 'src/enums/reward_style.dart';
// Constants
export 'src/constants/api_constants.dart';
export 'src/constants/error_codes.dart';
export 'src/constants/app_limits.dart';
// Validators
export 'src/validators/task_validator.dart';

View File

@@ -0,0 +1,47 @@
class ApiConstants {
ApiConstants._();
static const String apiVersion = 'v1';
static const String basePath = '/api/$apiVersion';
// Auth
static const String authRegister = '$basePath/auth/register';
static const String authLogin = '$basePath/auth/login';
static const String authRefresh = '$basePath/auth/refresh';
static const String authLogout = '$basePath/auth/logout';
// Tasks
static const String tasks = '$basePath/tasks';
static String task(String id) => '$basePath/tasks/$id';
static String completeTask(String id) => '$basePath/tasks/$id/complete';
static String skipTask(String id) => '$basePath/tasks/$id/skip';
static const String nextTask = '$basePath/tasks/next';
static const String dopamineOrdered = '$basePath/tasks/dopamine-ordered';
// Streaks
static const String streaks = '$basePath/streaks';
static String streak(String id) => '$basePath/streaks/$id';
static String forgiveStreak(String id) => '$basePath/streaks/$id/forgive';
static String streakHistory(String id) => '$basePath/streaks/$id/history';
// Rewards
static const String generateReward = '$basePath/rewards/generate';
static const String rewardHistory = '$basePath/rewards/history';
// Time
static const String timeEstimate = '$basePath/time/estimate';
static const String timeActual = '$basePath/time/actual';
static const String timeAccuracy = '$basePath/time/accuracy';
// Sync
static const String syncPush = '$basePath/sync/push';
static const String syncPull = '$basePath/sync/pull';
// Rooms (body doubling)
static const String rooms = '$basePath/rooms';
static String room(String id) => '$basePath/rooms/$id';
static String joinRoom(String id) => '$basePath/rooms/$id/join';
static String leaveRoom(String id) => '$basePath/rooms/$id/leave';
// Subscription
static const String subscriptionStatus = '$basePath/subscription/status';
}

View File

@@ -0,0 +1,17 @@
class AppLimits {
AppLimits._();
// Free tier
static const int freeMaxActiveTasks = 15;
static const int freeMaxStreaks = 3;
// Premium tier
static const int premiumMaxActiveTasks = -1; // unlimited
static const int premiumMaxStreaks = -1;
// Defaults
static const int defaultGraceDays = 2;
static const int defaultFocusMinutes = 25;
static const int defaultTaskLoad = 5;
static const int maxRoomParticipants = 10;
// Pagination
static const int defaultPageSize = 20;
static const int maxPageSize = 100;
}

View File

@@ -0,0 +1,18 @@
class ErrorCodes {
ErrorCodes._();
static const String invalidCredentials = 'INVALID_CREDENTIALS';
static const String emailAlreadyExists = 'EMAIL_ALREADY_EXISTS';
static const String tokenExpired = 'TOKEN_EXPIRED';
static const String tokenInvalid = 'TOKEN_INVALID';
static const String unauthorized = 'UNAUTHORIZED';
static const String taskNotFound = 'TASK_NOT_FOUND';
static const String streakNotFound = 'STREAK_NOT_FOUND';
static const String noTasksAvailable = 'NO_TASKS_AVAILABLE';
static const String roomFull = 'ROOM_FULL';
static const String roomNotFound = 'ROOM_NOT_FOUND';
static const String validationError = 'VALIDATION_ERROR';
static const String notFound = 'NOT_FOUND';
static const String internalError = 'INTERNAL_ERROR';
static const String rateLimited = 'RATE_LIMITED';
static const String premiumRequired = 'PREMIUM_REQUIRED';
}

View File

@@ -0,0 +1,13 @@
enum EnergyLevel {
low,
medium,
high;
String get displayName {
switch (this) {
case EnergyLevel.low: return 'Low Energy';
case EnergyLevel.medium: return 'Medium Energy';
case EnergyLevel.high: return 'High Energy';
}
}
}

View File

@@ -0,0 +1,13 @@
enum RewardStyle {
playful,
minimal,
data;
String get displayName {
switch (this) {
case RewardStyle.playful: return 'Playful (animations & messages)';
case RewardStyle.minimal: return 'Minimal (subtle feedback)';
case RewardStyle.data: return 'Data-driven (stats & charts)';
}
}
}

View File

@@ -0,0 +1,19 @@
enum RewardType {
points,
badge,
animation,
message,
unlock,
surprise;
String get displayName {
switch (this) {
case RewardType.points: return 'Points';
case RewardType.badge: return 'Badge';
case RewardType.animation: return 'Celebration';
case RewardType.message: return 'Encouragement';
case RewardType.unlock: return 'Unlock';
case RewardType.surprise: return 'Surprise!';
}
}
}

View File

@@ -0,0 +1,27 @@
enum TaskStatus {
pending,
inProgress,
completed,
skipped,
archived;
String get displayName {
switch (this) {
case TaskStatus.pending: return 'To Do';
case TaskStatus.inProgress: return 'In Progress';
case TaskStatus.completed: return 'Done';
case TaskStatus.skipped: return 'Skipped';
case TaskStatus.archived: return 'Archived';
}
}
String get apiValue {
switch (this) {
case TaskStatus.pending: return 'pending';
case TaskStatus.inProgress: return 'in_progress';
case TaskStatus.completed: return 'completed';
case TaskStatus.skipped: return 'skipped';
case TaskStatus.archived: return 'archived';
}
}
}

View File

@@ -0,0 +1,44 @@
import 'package:json_annotation/json_annotation.dart';
part 'api_response.g.dart';
@JsonSerializable(genericArgumentFactories: true)
class ApiResponse<T> {
final String status;
final T? data;
final ApiError? error;
final PaginationMeta? meta;
const ApiResponse({required this.status, this.data, this.error, this.meta});
factory ApiResponse.success(T data, {PaginationMeta? meta}) =>
ApiResponse(status: 'success', data: data, meta: meta);
factory ApiResponse.error(String code, String message) =>
ApiResponse(status: 'error', error: ApiError(code: code, message: message));
factory ApiResponse.fromJson(Map<String, dynamic> json, T Function(Object? json) fromJsonT) =>
_$ApiResponseFromJson(json, fromJsonT);
Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
_$ApiResponseToJson(this, toJsonT);
}
@JsonSerializable()
class ApiError {
final String code;
final String message;
final dynamic details;
const ApiError({required this.code, required this.message, this.details});
factory ApiError.fromJson(Map<String, dynamic> json) => _$ApiErrorFromJson(json);
Map<String, dynamic> toJson() => _$ApiErrorToJson(this);
}
@JsonSerializable()
class PaginationMeta {
final int page;
final int limit;
final int total;
final bool hasMore;
const PaginationMeta({required this.page, required this.limit, required this.total, required this.hasMore});
factory PaginationMeta.fromJson(Map<String, dynamic> json) => _$PaginationMetaFromJson(json);
Map<String, dynamic> toJson() => _$PaginationMetaToJson(this);
}

View File

@@ -0,0 +1,71 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'api_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ApiResponse<T> _$ApiResponseFromJson<T>(
Map<String, dynamic> json,
T Function(Object? json) fromJsonT,
) => ApiResponse<T>(
status: json['status'] as String,
data: _$nullableGenericFromJson(json['data'], fromJsonT),
error:
json['error'] == null
? null
: ApiError.fromJson(json['error'] as Map<String, dynamic>),
meta:
json['meta'] == null
? null
: PaginationMeta.fromJson(json['meta'] as Map<String, dynamic>),
);
Map<String, dynamic> _$ApiResponseToJson<T>(
ApiResponse<T> instance,
Object? Function(T value) toJsonT,
) => <String, dynamic>{
'status': instance.status,
'data': _$nullableGenericToJson(instance.data, toJsonT),
'error': instance.error,
'meta': instance.meta,
};
T? _$nullableGenericFromJson<T>(
Object? input,
T Function(Object? json) fromJson,
) => input == null ? null : fromJson(input);
Object? _$nullableGenericToJson<T>(
T? input,
Object? Function(T value) toJson,
) => input == null ? null : toJson(input);
ApiError _$ApiErrorFromJson(Map<String, dynamic> json) => ApiError(
code: json['code'] as String,
message: json['message'] as String,
details: json['details'],
);
Map<String, dynamic> _$ApiErrorToJson(ApiError instance) => <String, dynamic>{
'code': instance.code,
'message': instance.message,
'details': instance.details,
};
PaginationMeta _$PaginationMetaFromJson(Map<String, dynamic> json) =>
PaginationMeta(
page: (json['page'] as num).toInt(),
limit: (json['limit'] as num).toInt(),
total: (json['total'] as num).toInt(),
hasMore: json['hasMore'] as bool,
);
Map<String, dynamic> _$PaginationMetaToJson(PaginationMeta instance) =>
<String, dynamic>{
'page': instance.page,
'limit': instance.limit,
'total': instance.total,
'hasMore': instance.hasMore,
};

View File

@@ -0,0 +1,34 @@
import 'package:json_annotation/json_annotation.dart';
part 'coworking_room.g.dart';
@JsonSerializable()
class CoworkingRoom {
final String id;
final String hostUserId;
final String name;
final String? description;
final int maxParticipants;
final bool isPublic;
final String status; // active, ended, scheduled
final String? ambientSound; // cafe, rain, library, silent
final DateTime? scheduledAt;
final DateTime? endedAt;
final DateTime createdAt;
const CoworkingRoom({
required this.id,
required this.hostUserId,
required this.name,
this.description,
this.maxParticipants = 10,
this.isPublic = true,
this.status = 'active',
this.ambientSound,
this.scheduledAt,
this.endedAt,
required this.createdAt,
});
factory CoworkingRoom.fromJson(Map<String, dynamic> json) => _$CoworkingRoomFromJson(json);
Map<String, dynamic> toJson() => _$CoworkingRoomToJson(this);
}

View File

@@ -0,0 +1,43 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'coworking_room.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CoworkingRoom _$CoworkingRoomFromJson(Map<String, dynamic> json) =>
CoworkingRoom(
id: json['id'] as String,
hostUserId: json['hostUserId'] as String,
name: json['name'] as String,
description: json['description'] as String?,
maxParticipants: (json['maxParticipants'] as num?)?.toInt() ?? 10,
isPublic: json['isPublic'] as bool? ?? true,
status: json['status'] as String? ?? 'active',
ambientSound: json['ambientSound'] as String?,
scheduledAt:
json['scheduledAt'] == null
? null
: DateTime.parse(json['scheduledAt'] as String),
endedAt:
json['endedAt'] == null
? null
: DateTime.parse(json['endedAt'] as String),
createdAt: DateTime.parse(json['createdAt'] as String),
);
Map<String, dynamic> _$CoworkingRoomToJson(CoworkingRoom instance) =>
<String, dynamic>{
'id': instance.id,
'hostUserId': instance.hostUserId,
'name': instance.name,
'description': instance.description,
'maxParticipants': instance.maxParticipants,
'isPublic': instance.isPublic,
'status': instance.status,
'ambientSound': instance.ambientSound,
'scheduledAt': instance.scheduledAt?.toIso8601String(),
'endedAt': instance.endedAt?.toIso8601String(),
'createdAt': instance.createdAt.toIso8601String(),
};

View File

@@ -0,0 +1,32 @@
import 'package:json_annotation/json_annotation.dart';
part 'reward.g.dart';
@JsonSerializable()
class Reward {
final String id;
final String userId;
final String? taskId;
final String rewardType; // points, badge, animation, message, unlock, surprise
final int magnitude; // 1-100
final String? title;
final String? description;
final String? animationKey; // Lottie animation identifier
final bool isSurprise;
final DateTime createdAt;
const Reward({
required this.id,
required this.userId,
this.taskId,
required this.rewardType,
required this.magnitude,
this.title,
this.description,
this.animationKey,
this.isSurprise = false,
required this.createdAt,
});
factory Reward.fromJson(Map<String, dynamic> json) => _$RewardFromJson(json);
Map<String, dynamic> toJson() => _$RewardToJson(this);
}

View File

@@ -0,0 +1,33 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'reward.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Reward _$RewardFromJson(Map<String, dynamic> json) => Reward(
id: json['id'] as String,
userId: json['userId'] as String,
taskId: json['taskId'] as String?,
rewardType: json['rewardType'] as String,
magnitude: (json['magnitude'] as num).toInt(),
title: json['title'] as String?,
description: json['description'] as String?,
animationKey: json['animationKey'] as String?,
isSurprise: json['isSurprise'] as bool? ?? false,
createdAt: DateTime.parse(json['createdAt'] as String),
);
Map<String, dynamic> _$RewardToJson(Reward instance) => <String, dynamic>{
'id': instance.id,
'userId': instance.userId,
'taskId': instance.taskId,
'rewardType': instance.rewardType,
'magnitude': instance.magnitude,
'title': instance.title,
'description': instance.description,
'animationKey': instance.animationKey,
'isSurprise': instance.isSurprise,
'createdAt': instance.createdAt.toIso8601String(),
};

View File

@@ -0,0 +1,40 @@
import 'package:json_annotation/json_annotation.dart';
part 'streak.g.dart';
@JsonSerializable()
class Streak {
final String id;
final String userId;
final String name;
final String streakType; // daily, weekly, custom
final int currentCount;
final int longestCount;
final int graceDays;
final int graceUsed;
final DateTime? lastCompleted;
final bool isActive;
final DateTime? frozenUntil;
final bool encouragementShown;
final DateTime createdAt;
final DateTime? updatedAt;
const Streak({
required this.id,
required this.userId,
required this.name,
this.streakType = 'daily',
this.currentCount = 0,
this.longestCount = 0,
this.graceDays = 2,
this.graceUsed = 0,
this.lastCompleted,
this.isActive = true,
this.frozenUntil,
this.encouragementShown = false,
required this.createdAt,
this.updatedAt,
});
factory Streak.fromJson(Map<String, dynamic> json) => _$StreakFromJson(json);
Map<String, dynamic> toJson() => _$StreakToJson(this);
}

View File

@@ -0,0 +1,50 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'streak.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Streak _$StreakFromJson(Map<String, dynamic> json) => Streak(
id: json['id'] as String,
userId: json['userId'] as String,
name: json['name'] as String,
streakType: json['streakType'] as String? ?? 'daily',
currentCount: (json['currentCount'] as num?)?.toInt() ?? 0,
longestCount: (json['longestCount'] as num?)?.toInt() ?? 0,
graceDays: (json['graceDays'] as num?)?.toInt() ?? 2,
graceUsed: (json['graceUsed'] as num?)?.toInt() ?? 0,
lastCompleted:
json['lastCompleted'] == null
? null
: DateTime.parse(json['lastCompleted'] as String),
isActive: json['isActive'] as bool? ?? true,
frozenUntil:
json['frozenUntil'] == null
? null
: DateTime.parse(json['frozenUntil'] as String),
encouragementShown: json['encouragementShown'] as bool? ?? false,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt:
json['updatedAt'] == null
? null
: DateTime.parse(json['updatedAt'] as String),
);
Map<String, dynamic> _$StreakToJson(Streak instance) => <String, dynamic>{
'id': instance.id,
'userId': instance.userId,
'name': instance.name,
'streakType': instance.streakType,
'currentCount': instance.currentCount,
'longestCount': instance.longestCount,
'graceDays': instance.graceDays,
'graceUsed': instance.graceUsed,
'lastCompleted': instance.lastCompleted?.toIso8601String(),
'isActive': instance.isActive,
'frozenUntil': instance.frozenUntil?.toIso8601String(),
'encouragementShown': instance.encouragementShown,
'createdAt': instance.createdAt.toIso8601String(),
'updatedAt': instance.updatedAt?.toIso8601String(),
};

View File

@@ -0,0 +1,24 @@
import 'package:json_annotation/json_annotation.dart';
part 'streak_entry.g.dart';
@JsonSerializable()
class StreakEntry {
final String id;
final String streakId;
final DateTime completedDate;
final bool wasForgiven;
final String? note;
final DateTime createdAt;
const StreakEntry({
required this.id,
required this.streakId,
required this.completedDate,
this.wasForgiven = false,
this.note,
required this.createdAt,
});
factory StreakEntry.fromJson(Map<String, dynamic> json) => _$StreakEntryFromJson(json);
Map<String, dynamic> toJson() => _$StreakEntryToJson(this);
}

View File

@@ -0,0 +1,26 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'streak_entry.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
StreakEntry _$StreakEntryFromJson(Map<String, dynamic> json) => StreakEntry(
id: json['id'] as String,
streakId: json['streakId'] as String,
completedDate: DateTime.parse(json['completedDate'] as String),
wasForgiven: json['wasForgiven'] as bool? ?? false,
note: json['note'] as String?,
createdAt: DateTime.parse(json['createdAt'] as String),
);
Map<String, dynamic> _$StreakEntryToJson(StreakEntry instance) =>
<String, dynamic>{
'id': instance.id,
'streakId': instance.streakId,
'completedDate': instance.completedDate.toIso8601String(),
'wasForgiven': instance.wasForgiven,
'note': instance.note,
'createdAt': instance.createdAt.toIso8601String(),
};

56
lib/src/models/task.dart Normal file
View File

@@ -0,0 +1,56 @@
import 'package:json_annotation/json_annotation.dart';
part 'task.g.dart';
@JsonSerializable()
class Task {
final String id;
final String userId;
final String title;
final String? description;
final String status; // pending, in_progress, completed, skipped, archived
final int priority; // 0-100, ML-adjusted
final String energyLevel; // low, medium, high
final int? estimatedMinutes;
final int? actualMinutes;
final DateTime? dueAt;
final DateTime? completedAt;
final String? parentTaskId;
final double? dopamineScore; // ML-computed 0-100
final double noveltyFactor; // decays over time
final DateTime? lastInteracted;
final int timesPostponed;
final List<String> tags;
final String? category;
final String? color;
final int version;
final DateTime createdAt;
final DateTime? updatedAt;
const Task({
required this.id,
required this.userId,
required this.title,
this.description,
this.status = 'pending',
this.priority = 0,
this.energyLevel = 'medium',
this.estimatedMinutes,
this.actualMinutes,
this.dueAt,
this.completedAt,
this.parentTaskId,
this.dopamineScore,
this.noveltyFactor = 1.0,
this.lastInteracted,
this.timesPostponed = 0,
this.tags = const [],
this.category,
this.color,
this.version = 1,
required this.createdAt,
this.updatedAt,
});
factory Task.fromJson(Map<String, dynamic> json) => _$TaskFromJson(json);
Map<String, dynamic> toJson() => _$TaskToJson(this);
}

View File

@@ -0,0 +1,68 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'task.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Task _$TaskFromJson(Map<String, dynamic> json) => Task(
id: json['id'] as String,
userId: json['userId'] as String,
title: json['title'] as String,
description: json['description'] as String?,
status: json['status'] as String? ?? 'pending',
priority: (json['priority'] as num?)?.toInt() ?? 0,
energyLevel: json['energyLevel'] as String? ?? 'medium',
estimatedMinutes: (json['estimatedMinutes'] as num?)?.toInt(),
actualMinutes: (json['actualMinutes'] as num?)?.toInt(),
dueAt: json['dueAt'] == null ? null : DateTime.parse(json['dueAt'] as String),
completedAt:
json['completedAt'] == null
? null
: DateTime.parse(json['completedAt'] as String),
parentTaskId: json['parentTaskId'] as String?,
dopamineScore: (json['dopamineScore'] as num?)?.toDouble(),
noveltyFactor: (json['noveltyFactor'] as num?)?.toDouble() ?? 1.0,
lastInteracted:
json['lastInteracted'] == null
? null
: DateTime.parse(json['lastInteracted'] as String),
timesPostponed: (json['timesPostponed'] as num?)?.toInt() ?? 0,
tags:
(json['tags'] as List<dynamic>?)?.map((e) => e as String).toList() ??
const [],
category: json['category'] as String?,
color: json['color'] as String?,
version: (json['version'] as num?)?.toInt() ?? 1,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt:
json['updatedAt'] == null
? null
: DateTime.parse(json['updatedAt'] as String),
);
Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{
'id': instance.id,
'userId': instance.userId,
'title': instance.title,
'description': instance.description,
'status': instance.status,
'priority': instance.priority,
'energyLevel': instance.energyLevel,
'estimatedMinutes': instance.estimatedMinutes,
'actualMinutes': instance.actualMinutes,
'dueAt': instance.dueAt?.toIso8601String(),
'completedAt': instance.completedAt?.toIso8601String(),
'parentTaskId': instance.parentTaskId,
'dopamineScore': instance.dopamineScore,
'noveltyFactor': instance.noveltyFactor,
'lastInteracted': instance.lastInteracted?.toIso8601String(),
'timesPostponed': instance.timesPostponed,
'tags': instance.tags,
'category': instance.category,
'color': instance.color,
'version': instance.version,
'createdAt': instance.createdAt.toIso8601String(),
'updatedAt': instance.updatedAt?.toIso8601String(),
};

View File

@@ -0,0 +1,26 @@
import 'package:json_annotation/json_annotation.dart';
part 'time_estimate.g.dart';
@JsonSerializable()
class TimeEstimate {
final String id;
final String userId;
final String taskId;
final int estimatedMinutes;
final int? actualMinutes;
final double? accuracyRatio;
final DateTime createdAt;
const TimeEstimate({
required this.id,
required this.userId,
required this.taskId,
required this.estimatedMinutes,
this.actualMinutes,
this.accuracyRatio,
required this.createdAt,
});
factory TimeEstimate.fromJson(Map<String, dynamic> json) => _$TimeEstimateFromJson(json);
Map<String, dynamic> toJson() => _$TimeEstimateToJson(this);
}

View File

@@ -0,0 +1,28 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'time_estimate.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
TimeEstimate _$TimeEstimateFromJson(Map<String, dynamic> json) => TimeEstimate(
id: json['id'] as String,
userId: json['userId'] as String,
taskId: json['taskId'] as String,
estimatedMinutes: (json['estimatedMinutes'] as num).toInt(),
actualMinutes: (json['actualMinutes'] as num?)?.toInt(),
accuracyRatio: (json['accuracyRatio'] as num?)?.toDouble(),
createdAt: DateTime.parse(json['createdAt'] as String),
);
Map<String, dynamic> _$TimeEstimateToJson(TimeEstimate instance) =>
<String, dynamic>{
'id': instance.id,
'userId': instance.userId,
'taskId': instance.taskId,
'estimatedMinutes': instance.estimatedMinutes,
'actualMinutes': instance.actualMinutes,
'accuracyRatio': instance.accuracyRatio,
'createdAt': instance.createdAt.toIso8601String(),
};

42
lib/src/models/user.dart Normal file
View File

@@ -0,0 +1,42 @@
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';
@JsonSerializable()
class User {
final String id;
final String email;
final String displayName;
final String? avatarUrl;
final String timezone;
final bool onboardingCompleted;
final int preferredTaskLoad;
final int focusSessionMinutes;
final String rewardStyle; // playful, minimal, data
final bool forgivenessEnabled;
final String subscriptionTier; // free, premium, enterprise
final DateTime? subscriptionExpires;
final DateTime? lastActiveAt;
final DateTime createdAt;
final DateTime? updatedAt;
const User({
required this.id,
required this.email,
required this.displayName,
this.avatarUrl,
this.timezone = 'UTC',
this.onboardingCompleted = false,
this.preferredTaskLoad = 5,
this.focusSessionMinutes = 25,
this.rewardStyle = 'playful',
this.forgivenessEnabled = true,
this.subscriptionTier = 'free',
this.subscriptionExpires,
this.lastActiveAt,
required this.createdAt,
this.updatedAt,
});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}

View File

@@ -0,0 +1,52 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
User _$UserFromJson(Map<String, dynamic> json) => User(
id: json['id'] as String,
email: json['email'] as String,
displayName: json['displayName'] as String,
avatarUrl: json['avatarUrl'] as String?,
timezone: json['timezone'] as String? ?? 'UTC',
onboardingCompleted: json['onboardingCompleted'] as bool? ?? false,
preferredTaskLoad: (json['preferredTaskLoad'] as num?)?.toInt() ?? 5,
focusSessionMinutes: (json['focusSessionMinutes'] as num?)?.toInt() ?? 25,
rewardStyle: json['rewardStyle'] as String? ?? 'playful',
forgivenessEnabled: json['forgivenessEnabled'] as bool? ?? true,
subscriptionTier: json['subscriptionTier'] as String? ?? 'free',
subscriptionExpires:
json['subscriptionExpires'] == null
? null
: DateTime.parse(json['subscriptionExpires'] as String),
lastActiveAt:
json['lastActiveAt'] == null
? null
: DateTime.parse(json['lastActiveAt'] as String),
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt:
json['updatedAt'] == null
? null
: DateTime.parse(json['updatedAt'] as String),
);
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
'id': instance.id,
'email': instance.email,
'displayName': instance.displayName,
'avatarUrl': instance.avatarUrl,
'timezone': instance.timezone,
'onboardingCompleted': instance.onboardingCompleted,
'preferredTaskLoad': instance.preferredTaskLoad,
'focusSessionMinutes': instance.focusSessionMinutes,
'rewardStyle': instance.rewardStyle,
'forgivenessEnabled': instance.forgivenessEnabled,
'subscriptionTier': instance.subscriptionTier,
'subscriptionExpires': instance.subscriptionExpires?.toIso8601String(),
'lastActiveAt': instance.lastActiveAt?.toIso8601String(),
'createdAt': instance.createdAt.toIso8601String(),
'updatedAt': instance.updatedAt?.toIso8601String(),
};

View File

@@ -0,0 +1,17 @@
class TaskValidator {
TaskValidator._();
static String? validateTitle(String? title) {
if (title == null || title.trim().isEmpty) return 'Title is required';
if (title.length > 500) return 'Title must be 500 characters or less';
return null;
}
static String? validateEstimatedMinutes(int? minutes) {
if (minutes != null && minutes < 1) return 'Estimate must be at least 1 minute';
if (minutes != null && minutes > 480) return 'Estimate cannot exceed 8 hours';
return null;
}
static String? validatePriority(int? priority) {
if (priority != null && (priority < 0 || priority > 100)) return 'Priority must be 0-100';
return null;
}
}