w
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user