fix: 修复 .gitignore 误忽略 Flutter data/models/ 源码导致构建失败

问题描述:
  在其他机器上构建报错:
  "Error when reading 'lib/features/auth/data/models/auth_response.dart': 系统找不到指定的路径"
  导致 AuthUser、AuthResponse 等类型找不到,编译失败。

根本原因:
  根目录 .gitignore 第75行 "models/" 规则本意是忽略 ML 模型大文件,
  但该规则匹配了所有目录名为 models/ 的路径,包括 Flutter 项目中
  DDD 架构的 data/models/ 源码目录(共 11 个 models/ 目录、10 个 .dart 文件)。
  这些文件在本地存在但从未被 Git 追踪,其他机器 pull 后缺失这些文件。

修复内容:
  1. 修改 .gitignore: 将宽泛的 "models/" 替换为精确的规则
     - packages/services/voice-service/models/ — voice-service 下载的 ML 模型
     - *.pt, *.pth, *.safetensors — PyTorch/HuggingFace 模型二进制文件
     - 不再影响 Flutter 的 data/models/ 源码目录

  2. 提交之前被忽略的 10 个 Flutter model 文件:
     - auth/data/models/auth_response.dart — 登录响应 (accessToken, refreshToken, user)
     - chat/data/models/chat_message_model.dart — 聊天消息模型
     - chat/data/models/session_model.dart — 会话模型
     - chat/data/models/stream_event_model.dart — SSE 流事件模型
     - servers/data/models/server_model.dart — 服务器状态模型
     - approvals/data/models/approval_model.dart — 审批请求模型
     - alerts/data/models/alert_event_model.dart — 告警事件模型
     - agent_call/data/models/voice_session_model.dart — 语音会话模型
     - standing_orders/data/models/standing_order_model.dart — 常设指令模型
     - tasks/data/models/task_model.dart — 任务模型

  3. 同时提交:
     - it0_app/test/widget_test.dart — Flutter 默认测试
     - packages/services/voice-service/src/models/__init__.py — Python 模块初始化

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-22 16:29:03 -08:00
parent 913267fb9d
commit 4e1b75483d
13 changed files with 929 additions and 2 deletions

8
.gitignore vendored
View File

@ -71,8 +71,12 @@ __pycache__/
venv/ venv/
.venv/ .venv/
# Models (large files) # ML model binary files (large, downloaded at runtime)
models/ # Note: do NOT use bare "models/" — it ignores Flutter data/models/ source code
packages/services/voice-service/models/
*.pt
*.pth
*.safetensors
# Turbo # Turbo
.turbo/ .turbo/

View File

@ -0,0 +1,72 @@
import '../../domain/entities/voice_session.dart';
class VoiceSessionModel {
final String id;
final String? websocketUrl;
final String status;
final String? startedAt;
final String? endedAt;
const VoiceSessionModel({
required this.id,
this.websocketUrl,
required this.status,
this.startedAt,
this.endedAt,
});
factory VoiceSessionModel.fromJson(Map<String, dynamic> json) {
return VoiceSessionModel(
id: json['id'] as String? ?? json['sessionId'] as String? ?? '',
websocketUrl: json['websocket_url'] as String? ??
json['ws_url'] as String? ??
json['websocketUrl'] as String?,
status: json['status'] as String? ?? 'active',
startedAt: json['startedAt'] as String? ??
json['started_at'] as String? ??
json['createdAt'] as String? ??
json['created_at'] as String?,
endedAt: json['endedAt'] as String? ?? json['ended_at'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
if (websocketUrl != null) 'websocket_url': websocketUrl,
'status': status,
if (startedAt != null) 'startedAt': startedAt,
if (endedAt != null) 'endedAt': endedAt,
};
}
/// Converts this model to a domain [VoiceSession] entity.
VoiceSession toEntity() {
return VoiceSession(
id: id,
websocketUrl: websocketUrl,
status: _parseStatus(status),
startedAt: DateTime.tryParse(startedAt ?? '') ?? DateTime.now(),
endedAt: endedAt != null ? DateTime.tryParse(endedAt!) : null,
);
}
static VoiceSessionStatus _parseStatus(String status) {
switch (status.toLowerCase()) {
case 'ringing':
return VoiceSessionStatus.ringing;
case 'connecting':
return VoiceSessionStatus.connecting;
case 'active':
case 'connected':
return VoiceSessionStatus.active;
case 'ended':
case 'closed':
return VoiceSessionStatus.ended;
case 'error':
return VoiceSessionStatus.error;
default:
return VoiceSessionStatus.connecting;
}
}
}

View File

@ -0,0 +1,77 @@
import '../../domain/entities/alert_event.dart';
class AlertEventModel {
final String id;
final String? ruleId;
final String? serverId;
final String severity;
final String? message;
final bool acknowledged;
final String? createdAt;
const AlertEventModel({
required this.id,
this.ruleId,
this.serverId,
required this.severity,
this.message,
required this.acknowledged,
this.createdAt,
});
factory AlertEventModel.fromJson(Map<String, dynamic> json) {
return AlertEventModel(
id: json['id']?.toString() ?? '',
ruleId: json['ruleId'] as String? ?? json['rule_id'] as String?,
serverId:
json['serverId'] as String? ?? json['server_id'] as String?,
severity: json['severity'] as String? ?? 'info',
message: json['message'] as String?,
acknowledged: json['acknowledged'] as bool? ?? false,
createdAt:
json['createdAt'] as String? ?? json['created_at'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
if (ruleId != null) 'ruleId': ruleId,
if (serverId != null) 'serverId': serverId,
'severity': severity,
if (message != null) 'message': message,
'acknowledged': acknowledged,
};
}
/// Converts this model to a domain [AlertEvent] entity.
AlertEvent toEntity() {
return AlertEvent(
id: id,
ruleId: ruleId,
serverId: serverId,
severity: severity,
message: message,
acknowledged: acknowledged,
createdAt: _parseDateTime(createdAt),
);
}
/// Creates a model from a domain [AlertEvent] entity.
factory AlertEventModel.fromEntity(AlertEvent entity) {
return AlertEventModel(
id: entity.id,
ruleId: entity.ruleId,
serverId: entity.serverId,
severity: entity.severity,
message: entity.message,
acknowledged: entity.acknowledged,
createdAt: entity.createdAt?.toIso8601String(),
);
}
static DateTime? _parseDateTime(String? value) {
if (value == null) return null;
return DateTime.tryParse(value);
}
}

View File

@ -0,0 +1,91 @@
import '../../domain/entities/approval.dart';
class ApprovalModel {
final String id;
final String? taskId;
final String? taskDescription;
final String? command;
final String? riskLevel;
final String? requestedBy;
final String status;
final String? expiresAt;
final String? createdAt;
const ApprovalModel({
required this.id,
this.taskId,
this.taskDescription,
this.command,
this.riskLevel,
this.requestedBy,
required this.status,
this.expiresAt,
this.createdAt,
});
factory ApprovalModel.fromJson(Map<String, dynamic> json) {
return ApprovalModel(
id: json['id']?.toString() ?? '',
taskId: json['taskId'] as String? ?? json['task_id'] as String?,
taskDescription: json['taskDescription'] as String? ??
json['task_description'] as String?,
command: json['command'] as String?,
riskLevel:
json['riskLevel'] as String? ?? json['risk_level'] as String?,
requestedBy: json['requestedBy'] as String? ??
json['requested_by'] as String?,
status: json['status'] as String? ?? 'pending',
expiresAt:
json['expiresAt'] as String? ?? json['expires_at'] as String?,
createdAt:
json['createdAt'] as String? ?? json['created_at'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
if (taskId != null) 'taskId': taskId,
if (taskDescription != null) 'taskDescription': taskDescription,
if (command != null) 'command': command,
if (riskLevel != null) 'riskLevel': riskLevel,
if (requestedBy != null) 'requestedBy': requestedBy,
'status': status,
};
}
/// Converts this model to a domain [Approval] entity.
Approval toEntity() {
return Approval(
id: id,
taskId: taskId,
taskDescription: taskDescription,
command: command,
riskLevel: riskLevel,
requestedBy: requestedBy,
status: status,
expiresAt: _parseDateTime(expiresAt),
createdAt: _parseDateTime(createdAt),
);
}
/// Creates a model from a domain [Approval] entity.
factory ApprovalModel.fromEntity(Approval entity) {
return ApprovalModel(
id: entity.id,
taskId: entity.taskId,
taskDescription: entity.taskDescription,
command: entity.command,
riskLevel: entity.riskLevel,
requestedBy: entity.requestedBy,
status: entity.status,
expiresAt: entity.expiresAt?.toIso8601String(),
createdAt: entity.createdAt?.toIso8601String(),
);
}
static DateTime? _parseDateTime(String? value) {
if (value == null) return null;
return DateTime.tryParse(value);
}
}

View File

@ -0,0 +1,42 @@
class AuthResponse {
final String accessToken;
final String refreshToken;
final AuthUser user;
const AuthResponse({
required this.accessToken,
required this.refreshToken,
required this.user,
});
factory AuthResponse.fromJson(Map<String, dynamic> json) {
return AuthResponse(
accessToken: json['accessToken'] as String,
refreshToken: json['refreshToken'] as String,
user: AuthUser.fromJson(json['user'] as Map<String, dynamic>),
);
}
}
class AuthUser {
final String id;
final String email;
final String name;
final List<String> roles;
const AuthUser({
required this.id,
required this.email,
required this.name,
required this.roles,
});
factory AuthUser.fromJson(Map<String, dynamic> json) {
return AuthUser(
id: json['id'] as String,
email: json['email'] as String,
name: json['name'] as String,
roles: (json['roles'] as List).cast<String>(),
);
}
}

View File

@ -0,0 +1,174 @@
import '../../domain/entities/chat_message.dart';
class ChatMessageModel {
final String id;
final String role;
final String content;
final String timestamp;
final String? type;
final Map<String, dynamic>? toolExecution;
final Map<String, dynamic>? approvalRequest;
final Map<String, dynamic>? metadata;
const ChatMessageModel({
required this.id,
required this.role,
required this.content,
required this.timestamp,
this.type,
this.toolExecution,
this.approvalRequest,
this.metadata,
});
factory ChatMessageModel.fromJson(Map<String, dynamic> json) {
return ChatMessageModel(
id: json['id'] as String? ?? '',
role: json['role'] as String? ?? 'assistant',
content: json['content'] as String? ?? json['text'] as String? ?? '',
timestamp: json['timestamp'] as String? ??
json['created_at'] as String? ??
json['createdAt'] as String? ??
DateTime.now().toIso8601String(),
type: json['type'] as String?,
toolExecution: json['tool_execution'] as Map<String, dynamic>? ??
json['toolExecution'] as Map<String, dynamic>?,
approvalRequest: json['approval_request'] as Map<String, dynamic>? ??
json['approvalRequest'] as Map<String, dynamic>?,
metadata: json['metadata'] as Map<String, dynamic>?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'role': role,
'content': content,
'timestamp': timestamp,
if (type != null) 'type': type,
if (toolExecution != null) 'toolExecution': toolExecution,
if (approvalRequest != null) 'approvalRequest': approvalRequest,
if (metadata != null) 'metadata': metadata,
};
}
/// Converts this model to a domain [ChatMessage] entity.
ChatMessage toEntity() {
return ChatMessage(
id: id,
role: _parseRole(role),
content: content,
timestamp: DateTime.tryParse(timestamp) ?? DateTime.now(),
type: _parseType(type),
toolExecution: toolExecution != null ? _parseToolExecution(toolExecution!) : null,
approvalRequest: approvalRequest != null ? _parseApprovalRequest(approvalRequest!) : null,
metadata: metadata,
);
}
/// Creates a model from a domain [ChatMessage] entity.
factory ChatMessageModel.fromEntity(ChatMessage entity) {
return ChatMessageModel(
id: entity.id,
role: entity.role.name,
content: entity.content,
timestamp: entity.timestamp.toIso8601String(),
type: entity.type?.name,
toolExecution: entity.toolExecution != null
? {
'toolName': entity.toolExecution!.toolName,
'input': entity.toolExecution!.input,
'output': entity.toolExecution!.output,
'riskLevel': entity.toolExecution!.riskLevel,
'status': entity.toolExecution!.status.name,
}
: null,
approvalRequest: entity.approvalRequest != null
? {
'taskId': entity.approvalRequest!.taskId,
'command': entity.approvalRequest!.command,
'riskLevel': entity.approvalRequest!.riskLevel,
'targetServer': entity.approvalRequest!.targetServer,
'expiresAt': entity.approvalRequest!.expiresAt.toIso8601String(),
'status': entity.approvalRequest!.status,
}
: null,
metadata: entity.metadata,
);
}
static MessageRole _parseRole(String role) {
switch (role.toLowerCase()) {
case 'user':
return MessageRole.user;
case 'system':
return MessageRole.system;
case 'assistant':
default:
return MessageRole.assistant;
}
}
static MessageType? _parseType(String? type) {
if (type == null) return null;
switch (type.toLowerCase()) {
case 'tool_use':
case 'tooluse':
return MessageType.toolUse;
case 'tool_result':
case 'toolresult':
return MessageType.toolResult;
case 'approval':
return MessageType.approval;
case 'thinking':
return MessageType.thinking;
case 'standing_order_draft':
case 'standingorderdraft':
return MessageType.standingOrderDraft;
case 'text':
default:
return MessageType.text;
}
}
static ToolExecution _parseToolExecution(Map<String, dynamic> json) {
return ToolExecution(
toolName: json['toolName'] as String? ?? json['tool_name'] as String? ?? '',
input: json['input'] as String? ?? '',
output: json['output'] as String?,
riskLevel: json['riskLevel'] as int? ?? json['risk_level'] as int? ?? 0,
status: _parseToolStatus(json['status'] as String? ?? 'executing'),
);
}
static ToolStatus _parseToolStatus(String status) {
switch (status.toLowerCase()) {
case 'completed':
return ToolStatus.completed;
case 'error':
return ToolStatus.error;
case 'blocked':
return ToolStatus.blocked;
case 'awaiting_approval':
case 'awaitingapproval':
return ToolStatus.awaitingApproval;
case 'executing':
default:
return ToolStatus.executing;
}
}
static ApprovalRequest _parseApprovalRequest(Map<String, dynamic> json) {
return ApprovalRequest(
taskId: json['taskId'] as String? ?? json['task_id'] as String? ?? '',
command: json['command'] as String? ?? '',
riskLevel: json['riskLevel'] as int? ?? json['risk_level'] as int? ?? 0,
targetServer: json['targetServer'] as String? ?? json['target_server'] as String?,
expiresAt: DateTime.tryParse(
json['expiresAt'] as String? ?? json['expires_at'] as String? ?? '',
) ??
DateTime.now().add(const Duration(minutes: 5)),
status: json['status'] as String?,
);
}
}

View File

@ -0,0 +1,71 @@
import '../../domain/entities/chat_session.dart';
class SessionModel {
final String id;
final String? tenantId;
final String status;
final String startedAt;
final String? endedAt;
final int messageCount;
const SessionModel({
required this.id,
this.tenantId,
required this.status,
required this.startedAt,
this.endedAt,
this.messageCount = 0,
});
factory SessionModel.fromJson(Map<String, dynamic> json) {
return SessionModel(
id: json['id'] as String? ?? '',
tenantId: json['tenantId'] as String? ?? json['tenant_id'] as String?,
status: json['status'] as String? ?? 'active',
startedAt: json['startedAt'] as String? ??
json['started_at'] as String? ??
json['created_at'] as String? ??
json['createdAt'] as String? ??
DateTime.now().toIso8601String(),
endedAt: json['endedAt'] as String? ?? json['ended_at'] as String?,
messageCount: json['messageCount'] as int? ??
json['message_count'] as int? ??
0,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
if (tenantId != null) 'tenantId': tenantId,
'status': status,
'startedAt': startedAt,
if (endedAt != null) 'endedAt': endedAt,
'messageCount': messageCount,
};
}
/// Converts this model to a domain [ChatSession] entity.
ChatSession toEntity() {
return ChatSession(
id: id,
tenantId: tenantId,
status: status,
startedAt: DateTime.tryParse(startedAt) ?? DateTime.now(),
endedAt: endedAt != null ? DateTime.tryParse(endedAt!) : null,
messageCount: messageCount,
);
}
/// Creates a model from a domain [ChatSession] entity.
factory SessionModel.fromEntity(ChatSession entity) {
return SessionModel(
id: entity.id,
tenantId: entity.tenantId,
status: entity.status,
startedAt: entity.startedAt.toIso8601String(),
endedAt: entity.endedAt?.toIso8601String(),
messageCount: entity.messageCount,
);
}
}

View File

@ -0,0 +1,90 @@
import '../../domain/entities/stream_event.dart';
class StreamEventModel {
final String type;
final Map<String, dynamic> data;
const StreamEventModel({
required this.type,
required this.data,
});
factory StreamEventModel.fromJson(Map<String, dynamic> json) {
final type = json['type'] as String? ??
json['event'] as String? ??
'text';
// The data payload may be nested under 'data' or at the top level
final data = json['data'] as Map<String, dynamic>? ?? json;
return StreamEventModel(type: type, data: data);
}
/// Converts this model to a domain [StreamEvent].
StreamEvent toEntity() {
switch (type.toLowerCase()) {
case 'thinking':
return ThinkingEvent(
data['content'] as String? ?? data['text'] as String? ?? '',
);
case 'text':
case 'message':
case 'stream_event':
return TextEvent(
data['content'] as String? ?? data['text'] as String? ?? '',
);
case 'tool_use':
case 'tool_call':
return ToolUseEvent(
data['toolName'] as String? ?? data['tool_name'] as String? ?? data['name'] as String? ?? '',
data['input'] as Map<String, dynamic>? ?? {},
);
case 'tool_result':
return ToolResultEvent(
data['toolName'] as String? ?? data['tool_name'] as String? ?? '',
data['output'] as String? ?? data['content'] as String? ?? '',
data['isError'] as bool? ?? data['is_error'] as bool? ?? false,
);
case 'approval_required':
case 'approval':
return ApprovalRequiredEvent(
data['taskId'] as String? ?? data['task_id'] as String? ?? '',
data['command'] as String? ?? '',
data['riskLevel'] as int? ?? data['risk_level'] as int? ?? 0,
);
case 'completed':
case 'done':
case 'stream_end':
return CompletedEvent(
data['summary'] as String? ?? data['content'] as String? ?? '',
);
case 'error':
return ErrorEvent(
data['message'] as String? ?? data['error'] as String? ?? 'Unknown error',
);
case 'standing_order_draft':
return StandingOrderDraftEvent(
data['draft'] as Map<String, dynamic>? ?? data,
);
case 'standing_order_confirmed':
return StandingOrderConfirmedEvent(
data['orderId'] as String? ?? data['order_id'] as String? ?? '',
data['orderName'] as String? ?? data['order_name'] as String? ?? '',
);
default:
// Fall back to text event for unknown types
return TextEvent(
data['content'] as String? ?? data['text'] as String? ?? '',
);
}
}
}

View File

@ -0,0 +1,85 @@
import '../../domain/entities/server.dart';
class ServerModel {
final String id;
final String hostname;
final String? ip;
final String? environment;
final String? role;
final String status;
final String? lastCheckedAt;
final List<String>? tags;
const ServerModel({
required this.id,
required this.hostname,
this.ip,
this.environment,
this.role,
required this.status,
this.lastCheckedAt,
this.tags,
});
factory ServerModel.fromJson(Map<String, dynamic> json) {
return ServerModel(
id: json['id']?.toString() ?? '',
hostname: json['hostname'] as String? ?? 'unknown',
ip: json['ip'] as String? ?? json['ipAddress'] as String? ??
json['ip_address'] as String?,
environment: json['environment'] as String?,
role: json['role'] as String?,
status: json['status'] as String? ?? 'unknown',
lastCheckedAt: json['lastCheckedAt'] as String? ??
json['last_checked_at'] as String?,
tags: (json['tags'] as List<dynamic>?)
?.map((t) => t.toString())
.toList(),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'hostname': hostname,
if (ip != null) 'ip': ip,
if (environment != null) 'environment': environment,
if (role != null) 'role': role,
'status': status,
if (tags != null) 'tags': tags,
};
}
/// Converts this model to a domain [Server] entity.
Server toEntity() {
return Server(
id: id,
hostname: hostname,
ip: ip,
environment: environment,
role: role,
status: status,
lastCheckedAt: _parseDateTime(lastCheckedAt),
tags: tags,
);
}
/// Creates a model from a domain [Server] entity.
factory ServerModel.fromEntity(Server entity) {
return ServerModel(
id: entity.id,
hostname: entity.hostname,
ip: entity.ip,
environment: entity.environment,
role: entity.role,
status: entity.status,
lastCheckedAt: entity.lastCheckedAt?.toIso8601String(),
tags: entity.tags,
);
}
static DateTime? _parseDateTime(String? value) {
if (value == null) return null;
return DateTime.tryParse(value);
}
}

View File

@ -0,0 +1,94 @@
import '../../domain/entities/standing_order.dart';
class StandingOrderModel {
final String id;
final String name;
final String triggerType;
final String? schedule;
final String? agentInstructions;
final String? decisionBoundary;
final bool isActive;
final String? lastExecutedAt;
final String? createdAt;
const StandingOrderModel({
required this.id,
required this.name,
required this.triggerType,
this.schedule,
this.agentInstructions,
this.decisionBoundary,
required this.isActive,
this.lastExecutedAt,
this.createdAt,
});
factory StandingOrderModel.fromJson(Map<String, dynamic> json) {
return StandingOrderModel(
id: json['id']?.toString() ?? '',
name: json['name'] as String? ?? 'Untitled',
triggerType: json['triggerType'] as String? ??
json['trigger_type'] as String? ??
'cron',
schedule: json['schedule'] as String?,
agentInstructions: json['agentInstructions'] as String? ??
json['agent_instructions'] as String?,
decisionBoundary: json['decisionBoundary'] as String? ??
json['decision_boundary'] as String?,
isActive: json['isActive'] as bool? ??
json['is_active'] as bool? ??
false,
lastExecutedAt: json['lastExecutedAt'] as String? ??
json['last_executed_at'] as String?,
createdAt: json['createdAt'] as String? ??
json['created_at'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'triggerType': triggerType,
if (schedule != null) 'schedule': schedule,
if (agentInstructions != null) 'agentInstructions': agentInstructions,
if (decisionBoundary != null) 'decisionBoundary': decisionBoundary,
'isActive': isActive,
};
}
/// Converts this model to a domain [StandingOrder] entity.
StandingOrder toEntity() {
return StandingOrder(
id: id,
name: name,
triggerType: triggerType,
schedule: schedule,
agentInstructions: agentInstructions,
decisionBoundary: decisionBoundary,
isActive: isActive,
lastExecutedAt: _parseDateTime(lastExecutedAt),
createdAt: _parseDateTime(createdAt),
);
}
/// Creates a model from a domain [StandingOrder] entity.
factory StandingOrderModel.fromEntity(StandingOrder entity) {
return StandingOrderModel(
id: entity.id,
name: entity.name,
triggerType: entity.triggerType,
schedule: entity.schedule,
agentInstructions: entity.agentInstructions,
decisionBoundary: entity.decisionBoundary,
isActive: entity.isActive,
lastExecutedAt: entity.lastExecutedAt?.toIso8601String(),
createdAt: entity.createdAt?.toIso8601String(),
);
}
static DateTime? _parseDateTime(String? value) {
if (value == null) return null;
return DateTime.tryParse(value);
}
}

View File

@ -0,0 +1,96 @@
import '../../domain/entities/task.dart';
class TaskModel {
final String id;
final String title;
final String? description;
final String status;
final String? priority;
final String? serverId;
final String? serverName;
final String? assignedTo;
final String? createdAt;
final String? updatedAt;
final String? completedAt;
const TaskModel({
required this.id,
required this.title,
this.description,
required this.status,
this.priority,
this.serverId,
this.serverName,
this.assignedTo,
this.createdAt,
this.updatedAt,
this.completedAt,
});
factory TaskModel.fromJson(Map<String, dynamic> json) {
return TaskModel(
id: json['id']?.toString() ?? '',
title: json['title'] as String? ?? json['name'] as String? ?? 'Untitled',
description: json['description'] as String?,
status: json['status'] as String? ?? 'unknown',
priority: json['priority'] as String?,
serverId: json['serverId'] as String? ?? json['server_id'] as String?,
serverName: json['serverName'] as String? ?? json['server_name'] as String?,
assignedTo: json['assignedTo'] as String? ?? json['assigned_to'] as String?,
createdAt: json['createdAt'] as String? ?? json['created_at'] as String?,
updatedAt: json['updatedAt'] as String? ?? json['updated_at'] as String?,
completedAt: json['completedAt'] as String? ?? json['completed_at'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
if (description != null) 'description': description,
'status': status,
if (priority != null) 'priority': priority,
if (serverId != null) 'serverId': serverId,
if (assignedTo != null) 'assignedTo': assignedTo,
};
}
/// Converts this model to a domain [Task] entity.
Task toEntity() {
return Task(
id: id,
title: title,
description: description,
status: status,
priority: priority,
serverId: serverId,
serverName: serverName,
assignedTo: assignedTo,
createdAt: _parseDateTime(createdAt),
updatedAt: _parseDateTime(updatedAt),
completedAt: _parseDateTime(completedAt),
);
}
/// Creates a model from a domain [Task] entity.
factory TaskModel.fromEntity(Task entity) {
return TaskModel(
id: entity.id,
title: entity.title,
description: entity.description,
status: entity.status,
priority: entity.priority,
serverId: entity.serverId,
serverName: entity.serverName,
assignedTo: entity.assignedTo,
createdAt: entity.createdAt?.toIso8601String(),
updatedAt: entity.updatedAt?.toIso8601String(),
completedAt: entity.completedAt?.toIso8601String(),
);
}
static DateTime? _parseDateTime(String? value) {
if (value == null) return null;
return DateTime.tryParse(value);
}
}

View File

@ -0,0 +1,30 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:it0_app/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}

View File

@ -0,0 +1 @@