import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/utils/date_utils.dart'; import 'package:tm_app/core/utils/result.dart'; import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/data/services/board_service.dart'; import 'package:tm_app/data/services/model/board_response/board_response.dart'; import 'package:tm_app/views/shared/desktop_navigation_widget.dart'; import '../../../core/constants/assets.dart'; import '../../../core/utils/app_toast.dart'; import '../../../view_models/board_view_model.dart'; import '../strings/board_strings.dart'; class BoardScreen extends StatefulWidget { const BoardScreen({super.key}); @override State createState() => _BoardScreenState(); } class _BoardScreenState extends State { late BoardViewModel _boardViewModel; @override void initState() { super.initState(); _boardViewModel = context.read(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { _boardViewModel.getBoard(); }); } Future _onCardDropped(BoardCard card, BoardColumn target) async { final source = _boardViewModel.columnForCard(card.id ?? ''); if (source == null || source.id == target.id) return; final result = await _boardViewModel.moveCard( card: card, targetColumn: target, ); if (!mounted) return; if (result is Ok) { AppToast.success( context, BoardStrings.cardMovedMessage( source.name ?? '', target.name ?? '', ), ); } else if (result is Error) { AppToast.error(context, BoardStrings.moveFailed); } } @override Widget build(BuildContext context) { SizeConfig.init(context); return Scaffold( backgroundColor: AppColors.backgroundColor, body: Column( children: [ const DesktopNavigationWidget(currentIndex: 2), Consumer( builder: (context, viewModel, child) { if (viewModel.isLoading) { return const Expanded( child: Center( child: CircularProgressIndicator( color: AppColors.secondary50, ), ), ); } if (viewModel.errorMessage != null) { return Expanded( child: Center(child: Text(viewModel.errorMessage!)), ); } if (!viewModel.hasData) { return const Expanded( child: Center(child: Text(BoardStrings.emptyListText)), ); } final columns = [...viewModel.columns]..sort( (a, b) => (a.order ?? 0).compareTo(b.order ?? 0), ); return Expanded( child: SingleChildScrollView( child: SizedBox( width: 740, height: 735, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 32), Text( viewModel.board?.name ?? BoardStrings.boardTitle, style: TextStyle( fontSize: 20.0.sp(), fontWeight: FontWeight.w600, color: AppColors.titleColor, ), ), const SizedBox(height: 20), Expanded( child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: AppColors.primary20, ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ for (int i = 0; i < columns.length; i++) ...[ if (i > 0) const SizedBox(width: 12), Expanded( child: _BoardColumnView( column: columns[i], onCardDropped: _onCardDropped, ), ), ], ], ), ), ), ], ), ), ), ); }, ), ], ), ); } } class _BoardColumnView extends StatelessWidget { final BoardColumn column; final Future Function(BoardCard, BoardColumn) onCardDropped; const _BoardColumnView({required this.column, required this.onCardDropped}); @override Widget build(BuildContext context) { final accent = _parseHexColor(column.color) ?? AppColors.textBlue; return DragTarget( onWillAcceptWithDetails: (details) => details.data.columnId != column.id, onAcceptWithDetails: (details) => onCardDropped(details.data, column), builder: (context, candidateData, rejectedData) { final isHovering = candidateData.isNotEmpty; return Container( decoration: BoxDecoration( color: isHovering ? accent.withValues(alpha: 0.15) : AppColors.primary0, borderRadius: BorderRadius.circular(12), border: Border.all( color: isHovering ? accent : AppColors.borderBlue, width: isHovering ? 2 : 1, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: double.infinity, height: 34, margin: const EdgeInsets.all(8), decoration: BoxDecoration( color: accent.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(4), ), alignment: Alignment.center, child: Text( _columnHeader(column), style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: accent, ), ), ), Expanded( child: ListView.builder( padding: const EdgeInsets.all(12), itemCount: column.cards.length, itemBuilder: (context, index) { return _TaskCard(card: column.cards[index], accent: accent); }, ), ), ], ), ); }, ); } String _columnHeader(BoardColumn column) { final name = column.name ?? ''; final limit = column.limit; if (limit == null || limit <= 0) return name; return '$name (${column.cards.length}/$limit)'; } } class _TaskCard extends StatelessWidget { final BoardCard card; final Color accent; const _TaskCard({required this.card, required this.accent}); @override Widget build(BuildContext context) { return Draggable( data: card, feedback: Material( elevation: 8, borderRadius: BorderRadius.circular(12), child: Container( width: 280, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.primary0, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.borderBlue, width: 2), ), child: _buildCardContent(context), ), ), childWhenDragging: Opacity(opacity: 0.3, child: _buildCard(context)), child: _buildCard(context), ); } Widget _buildCard(BuildContext context) { return Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.primary0, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.grey30), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: _buildCardContent(context), ); } Widget _buildCardContent(BuildContext context) { final due = card.dueDate; final labels = card.labels; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (due != null) Container( height: 24.0, padding: EdgeInsets.symmetric(horizontal: 10.0.w()), decoration: BoxDecoration( color: accent.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(4), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '${BoardStrings.deadlineLabel} :', style: TextStyle( fontSize: 14.0, fontWeight: FontWeight.w500, color: AppColors.textBlue, ), ), Text( timeConvertor(due), style: TextStyle( fontSize: 14.0, fontWeight: FontWeight.w500, color: AppColors.grey80, ), ), ], ), ), if (due != null) const SizedBox(height: 12), SelectableText( card.title ?? '', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.grey70, ), maxLines: 3, ), const SizedBox(height: 12), if (card.priority != null) _PriorityChip(priority: card.priority!), if (labels.isNotEmpty) ...[ const SizedBox(height: 8), Wrap( spacing: 6, runSpacing: 6, children: [for (final label in labels) _LabelChip(label: label)], ), ], const SizedBox(height: 12), Row( children: [ SvgPicture.asset(AssetsManager.profile), const SizedBox(width: 4), Text( card.tenderId ?? '', style: TextStyle(fontSize: 12, color: AppColors.grey60), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ], ); } } class _PriorityChip extends StatelessWidget { final String priority; const _PriorityChip({required this.priority}); @override Widget build(BuildContext context) { final (bg, fg, dot) = _styleFor(priority); return Container( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 8, height: 8, decoration: BoxDecoration(color: dot, shape: BoxShape.circle), ), const SizedBox(width: 6), Text( priority, style: TextStyle( fontSize: 12, color: fg, fontWeight: FontWeight.w400, ), ), ], ), ); } (Color bg, Color fg, Color dot) _styleFor(String priority) { switch (priority.toLowerCase()) { case 'urgent': case 'high': return (AppColors.red0, AppColors.red20, AppColors.error); case 'low': return (AppColors.primary20, AppColors.textBlue, AppColors.textBlue); case 'medium': default: return (AppColors.orange10, AppColors.orange, AppColors.orange); } } } class _LabelChip extends StatelessWidget { final String label; const _LabelChip({required this.label}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 2.0), decoration: BoxDecoration( color: AppColors.primary20, borderRadius: BorderRadius.circular(8), ), child: Text( label, style: TextStyle(fontSize: 11, color: AppColors.textBlue), ), ); } } Color? _parseHexColor(String? hex) { if (hex == null) return null; var value = hex.trim(); if (value.startsWith('#')) value = value.substring(1); if (value.length == 6) value = 'FF$value'; if (value.length != 8) return null; final parsed = int.tryParse(value, radix: 16); if (parsed == null) return null; return Color(parsed); }