diff --git a/.gitignore b/.gitignore index eb1d9ed..8d87f18 100644 --- a/.gitignore +++ b/.gitignore @@ -71,8 +71,12 @@ __pycache__/ venv/ .venv/ -# Models (large files) -models/ +# ML model binary files (large, downloaded at runtime) +# Note: do NOT use bare "models/" — it ignores Flutter data/models/ source code +packages/services/voice-service/models/ +*.pt +*.pth +*.safetensors # Turbo .turbo/ diff --git a/it0_app/lib/features/agent_call/data/models/voice_session_model.dart b/it0_app/lib/features/agent_call/data/models/voice_session_model.dart new file mode 100644 index 0000000..8817b1e --- /dev/null +++ b/it0_app/lib/features/agent_call/data/models/voice_session_model.dart @@ -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 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 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; + } + } +} diff --git a/it0_app/lib/features/alerts/data/models/alert_event_model.dart b/it0_app/lib/features/alerts/data/models/alert_event_model.dart new file mode 100644 index 0000000..f16319b --- /dev/null +++ b/it0_app/lib/features/alerts/data/models/alert_event_model.dart @@ -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 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 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); + } +} diff --git a/it0_app/lib/features/approvals/data/models/approval_model.dart b/it0_app/lib/features/approvals/data/models/approval_model.dart new file mode 100644 index 0000000..ea273da --- /dev/null +++ b/it0_app/lib/features/approvals/data/models/approval_model.dart @@ -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 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 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); + } +} diff --git a/it0_app/lib/features/auth/data/models/auth_response.dart b/it0_app/lib/features/auth/data/models/auth_response.dart new file mode 100644 index 0000000..a7a8dc5 --- /dev/null +++ b/it0_app/lib/features/auth/data/models/auth_response.dart @@ -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 json) { + return AuthResponse( + accessToken: json['accessToken'] as String, + refreshToken: json['refreshToken'] as String, + user: AuthUser.fromJson(json['user'] as Map), + ); + } +} + +class AuthUser { + final String id; + final String email; + final String name; + final List roles; + + const AuthUser({ + required this.id, + required this.email, + required this.name, + required this.roles, + }); + + factory AuthUser.fromJson(Map json) { + return AuthUser( + id: json['id'] as String, + email: json['email'] as String, + name: json['name'] as String, + roles: (json['roles'] as List).cast(), + ); + } +} diff --git a/it0_app/lib/features/chat/data/models/chat_message_model.dart b/it0_app/lib/features/chat/data/models/chat_message_model.dart new file mode 100644 index 0000000..1db270f --- /dev/null +++ b/it0_app/lib/features/chat/data/models/chat_message_model.dart @@ -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? toolExecution; + final Map? approvalRequest; + final Map? 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 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? ?? + json['toolExecution'] as Map?, + approvalRequest: json['approval_request'] as Map? ?? + json['approvalRequest'] as Map?, + metadata: json['metadata'] as Map?, + ); + } + + Map 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 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 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?, + ); + } +} diff --git a/it0_app/lib/features/chat/data/models/session_model.dart b/it0_app/lib/features/chat/data/models/session_model.dart new file mode 100644 index 0000000..89ecfd2 --- /dev/null +++ b/it0_app/lib/features/chat/data/models/session_model.dart @@ -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 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 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, + ); + } +} diff --git a/it0_app/lib/features/chat/data/models/stream_event_model.dart b/it0_app/lib/features/chat/data/models/stream_event_model.dart new file mode 100644 index 0000000..cbec253 --- /dev/null +++ b/it0_app/lib/features/chat/data/models/stream_event_model.dart @@ -0,0 +1,90 @@ +import '../../domain/entities/stream_event.dart'; + +class StreamEventModel { + final String type; + final Map data; + + const StreamEventModel({ + required this.type, + required this.data, + }); + + factory StreamEventModel.fromJson(Map 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? ?? 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? ?? {}, + ); + + 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? ?? 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? ?? '', + ); + } + } +} diff --git a/it0_app/lib/features/servers/data/models/server_model.dart b/it0_app/lib/features/servers/data/models/server_model.dart new file mode 100644 index 0000000..79a8a7c --- /dev/null +++ b/it0_app/lib/features/servers/data/models/server_model.dart @@ -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? 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 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?) + ?.map((t) => t.toString()) + .toList(), + ); + } + + Map 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); + } +} diff --git a/it0_app/lib/features/standing_orders/data/models/standing_order_model.dart b/it0_app/lib/features/standing_orders/data/models/standing_order_model.dart new file mode 100644 index 0000000..a5c4577 --- /dev/null +++ b/it0_app/lib/features/standing_orders/data/models/standing_order_model.dart @@ -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 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 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); + } +} diff --git a/it0_app/lib/features/tasks/data/models/task_model.dart b/it0_app/lib/features/tasks/data/models/task_model.dart new file mode 100644 index 0000000..e7a27f0 --- /dev/null +++ b/it0_app/lib/features/tasks/data/models/task_model.dart @@ -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 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 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); + } +} diff --git a/it0_app/test/widget_test.dart b/it0_app/test/widget_test.dart new file mode 100644 index 0000000..b3234ba --- /dev/null +++ b/it0_app/test/widget_test.dart @@ -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); + }); +} diff --git a/packages/services/voice-service/src/models/__init__.py b/packages/services/voice-service/src/models/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/services/voice-service/src/models/__init__.py @@ -0,0 +1 @@ +