This commit is contained in:
2026-05-26 12:28:12 +02:00
parent 2afe97c6db
commit 45455a16c4
12 changed files with 851 additions and 8 deletions

View File

@@ -0,0 +1,71 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/features/tasks/data/task_repository.dart';
import 'package:flux/features/tasks/models/task_model.dart';
import 'package:flux/features/tasks/models/task_status.dart';
import 'package:get_it/get_it.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
part 'dashboard_task_list_state.dart';
class DashboardTaskListCubit extends Cubit<DashboardTaskListState> {
final TaskRepository _repository = GetIt.I.get<TaskRepository>();
final SupabaseClient _supabase = GetIt.I.get<SupabaseClient>();
RealtimeChannel? _taskChannel;
DashboardTaskListCubit() : super(DashboardTaskListState());
void startListening({required String staffId}) {
emit(state.copyWith(status: DashboardTaskListStatus.loading));
_loadTasks(staffId: staffId);
_taskChannel?.unsubscribe();
_taskChannel = _supabase
.channel('public:tasks_staff_$staffId')
.onPostgresChanges(
event: PostgresChangeEvent.all,
schema: 'public',
table: 'tasks',
filter: PostgresChangeFilter(
type: PostgresChangeFilterType.eq,
column: 'staff_id',
value: staffId,
),
callback: (payload) {
_loadTasks(staffId: staffId);
},
);
_taskChannel?.subscribe();
}
Future<void> _loadTasks({required String staffId}) async {
try {
final tasks = await _repository.getTasks(
companyId: GetIt.I.get<SessionCubit>().state.company!.id!,
staffId: staffId,
statuses: [TaskStatus.open, TaskStatus.inProgress],
);
emit(
state.copyWith(
status: DashboardTaskListStatus.success,
tasks: tasks,
errorMessage: null,
),
);
} catch (e) {
emit(
state.copyWith(
status: DashboardTaskListStatus.failure,
errorMessage: e.toString(),
),
);
}
}
@override
Future<void> close() {
_taskChannel?.unsubscribe();
return super.close();
}
}

View File

@@ -0,0 +1,30 @@
part of 'dashboard_task_list_cubit.dart';
enum DashboardTaskListStatus { initial, loading, success, failure }
class DashboardTaskListState extends Equatable {
final DashboardTaskListStatus status;
final List<TaskModel> tasks;
final String? errorMessage;
const DashboardTaskListState({
this.status = DashboardTaskListStatus.initial,
this.tasks = const [],
this.errorMessage,
});
DashboardTaskListState copyWith({
DashboardTaskListStatus? status,
List<TaskModel>? tasks,
String? errorMessage,
}) {
return DashboardTaskListState(
status: status ?? this.status,
tasks: tasks ?? this.tasks,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [status, tasks, errorMessage];
}

View File

@@ -0,0 +1,211 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/core/routes/routes.dart';
import 'package:flux/core/theme/theme.dart';
import 'package:flux/features/home/dashboard_task_list/blocs/dashboard_task_list_cubit.dart';
import 'package:go_router/go_router.dart';
import 'package:flux/features/tasks/models/task_status.dart';
class DashboardTasksCard extends StatelessWidget {
const DashboardTasksCard({super.key});
@override
Widget build(BuildContext context) {
// Recuperiamo lo staff (o l'utente) loggato
// Adatta il getter in base a come è strutturato il tuo SessionState
final currentStaffId = context
.read<SessionCubit>()
.state
.currentStaffMember
?.id;
if (currentStaffId == null) {
return const SizedBox.shrink(); // Sicurezza se lo stato non è pronto
}
return BlocProvider(
create: (context) =>
DashboardTaskListCubit()..startListening(staffId: currentStaffId),
child: const _DashboardTasksCardContent(),
);
}
}
class _DashboardTasksCardContent extends StatelessWidget {
const _DashboardTasksCardContent();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
const color =
Colors.orange; // Colore arancione per distinguerla dai Ticket blu
return Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: theme.dividerColor.withValues(alpha: 0.5)),
),
child: InkWell(
onTap: () => context.pushNamed(
Routes.tasks,
), // Porta alla lista completa (TaskListScreen)
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// --- HEADER DELLA CARD ---
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.assignment_outlined, // Icona a tema ToDo
color: color,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
"I Miei Task",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: context.primaryText,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 12),
// --- CORPO DELLA CARD (LA LISTA REAL-TIME) ---
Expanded(
child: BlocBuilder<DashboardTaskListCubit, DashboardTaskListState>(
builder: (context, state) {
if (state.status == DashboardTaskListStatus.loading ||
state.status == DashboardTaskListStatus.initial) {
return const Center(child: CircularProgressIndicator());
}
if (state.status == DashboardTaskListStatus.failure) {
return Center(
child: Text(
"Errore di caricamento",
style: TextStyle(color: theme.colorScheme.error),
),
);
}
if (state.tasks.isEmpty) {
return Center(
child: Text(
"Nessun task in sospeso. Ottimo lavoro!",
style: TextStyle(
color: context.secondaryText,
fontStyle: FontStyle.italic,
),
),
);
}
return ListView.separated(
itemCount: state.tasks.length,
separatorBuilder: (context, index) => Divider(
height: 1,
color: theme.dividerColor.withValues(alpha: 0.3),
),
itemBuilder: (context, index) {
final task = state.tasks[index];
// Definisci il colore in base allo stato del task
final statusColor = task.status == TaskStatus.inProgress
? Colors.blue
: Colors.grey.shade400;
// Formattiamo la data (o indichiamo se non c'è)
final dueDateString = task.dueDate != null
? "${task.dueDate!.day}/${task.dueDate!.month}"
: "Nessuna";
// Controllo Ninja: Il task è già scaduto rispetto a oggi?
final isOverdue =
task.dueDate != null &&
task.dueDate!.isBefore(DateTime.now());
return InkWell(
onTap: () => context.pushNamed(
Routes.taskForm,
extra:
task, // Passiamo direttamente il modello intero se il tuo router lo accetta!
pathParameters: {'id': task.id ?? 'new'},
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: 8,
height: 30,
decoration: BoxDecoration(
color: statusColor,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(width: 8),
Expanded(
flex: 7,
child: Text(
task.title,
style: TextStyle(
fontWeight: FontWeight.w600,
color: context.primaryText,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
Expanded(
flex: 3,
child: Align(
alignment: Alignment.centerRight,
child: Text(
dueDateString,
style: TextStyle(
color: isOverdue
? theme.colorScheme.error
: context.secondaryText,
fontSize: 12,
fontWeight: isOverdue
? FontWeight.bold
: FontWeight.normal,
),
),
),
),
],
),
),
);
},
);
},
),
),
],
),
),
),
);
}
}

View File

@@ -5,6 +5,7 @@ import 'package:flux/core/routes/routes.dart';
import 'package:flux/core/theme/theme.dart';
import 'package:flux/core/utils/extensions.dart';
import 'package:flux/core/widgets/staff_selector_modal.dart';
import 'package:flux/features/home/dashboard_task_list/ui/dashboard_tasks_card.dart';
import 'package:flux/features/home/latest_store_operations/ui/latest_store_operations_card.dart';
import 'package:flux/features/home/latest_store_tickets/ui/latest_store_tickets_card.dart';
import 'package:flux/features/home/ui/quick_actions_widget.dart';
@@ -13,6 +14,8 @@ import 'package:flux/features/master_data/staff/models/staff_member_model.dart';
import 'package:flux/features/notes/data/notes_repository.dart';
import 'package:flux/features/notes/models/note_model.dart';
import 'package:flux/features/notes/ui/dashboard_notes_widget.dart';
import 'package:flux/features/tasks/data/task_repository.dart';
import 'package:flux/features/tasks/models/task_model.dart';
import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';
@@ -75,12 +78,7 @@ class HomeScreen extends StatelessWidget {
context: context,
),
DashboardNotesWidget(),
_buildDashboardWidget(
title: context.l10n.homeMyTasks,
icon: Icons.check_box_outlined,
color: Colors.green,
context: context,
),
DashboardTasksCard(),
]),
),
),
@@ -238,7 +236,24 @@ class HomeScreen extends StatelessWidget {
label: context.l10n.commonTask,
color: Colors.teal,
onTap: () {
// TODO: Quando faremo i task
final companyId = context.read<SessionCubit>().state.company!.id!;
final currentStaffId = context
.read<SessionCubit>()
.state
.currentStaffMember!
.id!;
final emptyTask = TaskModel.empty(
companyId: companyId,
createdById: currentStaffId,
);
final savedTask = GetIt.I.get<TaskRepository>().createTask(
emptyTask,
);
context.pushNamed(
Routes.taskForm,
pathParameters: {'id': 'new'},
extra: savedTask,
);
},
),
],