diff --git a/lib/core/blocs/session/session_cubit.dart b/lib/core/blocs/session/session_cubit.dart index a8064a3..bcf6356 100644 --- a/lib/core/blocs/session/session_cubit.dart +++ b/lib/core/blocs/session/session_cubit.dart @@ -164,4 +164,8 @@ class SessionCubit extends Cubit { void setIsMobileDevice(bool isMobile) { emit(state.copyWith(isMobileDevice: isMobile)); } + + void setIsSingleUserMode(bool isSingleUser) { + emit(state.copyWith(isSingleUserMode: isSingleUser)); + } } diff --git a/lib/core/blocs/session/session_state.dart b/lib/core/blocs/session/session_state.dart index 0bc69b9..0a95dd3 100644 --- a/lib/core/blocs/session/session_state.dart +++ b/lib/core/blocs/session/session_state.dart @@ -25,6 +25,7 @@ class SessionState extends Equatable { final StaffMemberModel? currentStaffMember; final OnboardingStep onboardingStep; final bool isMobileDevice; + final bool isSingleUserMode; const SessionState({ this.status = SessionStatus.initial, @@ -34,6 +35,7 @@ class SessionState extends Equatable { this.currentStaffMember, this.onboardingStep = OnboardingStep.none, this.isMobileDevice = false, + this.isSingleUserMode = false, }); /// Metodo per creare una copia dello stato modificando solo i campi necessari @@ -45,6 +47,7 @@ class SessionState extends Equatable { StaffMemberModel? currentStaffMember, OnboardingStep? onboardingStep, bool? isMobileDevice, + bool? isSingleUserMode, }) { return SessionState( status: status ?? this.status, @@ -54,6 +57,7 @@ class SessionState extends Equatable { currentStaffMember: currentStaffMember ?? this.currentStaffMember, onboardingStep: onboardingStep ?? this.onboardingStep, isMobileDevice: isMobileDevice ?? this.isMobileDevice, + isSingleUserMode: isSingleUserMode ?? this.isSingleUserMode, ); } @@ -66,6 +70,7 @@ class SessionState extends Equatable { currentStaffMember, onboardingStep, isMobileDevice, + isSingleUserMode, ]; // Helper rapidi per la UI diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index b356000..7bd2a95 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -23,6 +23,7 @@ import 'package:flux/features/master_data/products/ui/products_screen.dart'; import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart'; import 'package:flux/features/master_data/providers/ui/providers_master_data_screen.dart'; import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; +import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; import 'package:flux/features/master_data/staff/ui/staff_screen.dart'; import 'package:flux/features/master_data/store/ui/stores_screen.dart'; import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart'; @@ -33,14 +34,13 @@ import 'package:flux/features/operations/blocs/operation_list_cubit.dart'; import 'package:flux/features/operations/models/operation_model.dart'; import 'package:flux/features/operations/ui/operation_form_screen.dart'; import 'package:flux/features/operations/ui/operation_list_screen.dart'; -import 'package:flux/features/settings/settings_view.dart'; +import 'package:flux/features/settings/settings_screen.dart'; import 'package:flux/features/settings/theme_settings_view.dart'; import 'package:flux/features/tickets/blocs/ticket_form_cubit.dart'; import 'package:flux/features/tickets/blocs/ticket_list_cubit.dart'; import 'package:flux/features/tickets/models/ticket_model.dart'; import 'package:flux/features/tickets/ui/ticket_form_screen.dart'; import 'package:flux/features/tickets/ui/ticket_list_screen.dart'; -import 'package:flux/features/tracking/blocs/tracking_cubit.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; @@ -171,7 +171,7 @@ class AppRouter { GoRoute( path: '/settings', name: Routes.settings, - builder: (context, state) => const SettingsView(), + builder: (context, state) => const SettingsScreen(), routes: [ GoRoute( path: 'themeSettings', @@ -213,15 +213,12 @@ class AppRouter { builder: (context, state) { // 1. Leggiamo l'ID dall'URL final String pathId = state.pathParameters['id'] ?? 'new'; + final record = + state.extra + as ({StaffMemberModel createdBy, TicketModel ticket}); - // 2. Leggiamo l'oggetto dalla RAM (se arriviamo da un tap interno all'app) - final TicketModel? ticketFromExtra = state.extra as TicketModel?; - - // 3. Capiamo se è un nuovo ticket o una modifica final String? realTicketId = pathId == 'new' ? null : pathId; - context.read().loadStaffForStore( - GetIt.I.get().state.currentStore!.id!, - ); + context.read().loadCustomers(); context.read().loadModels(); context.read().loadBrands(); @@ -235,19 +232,11 @@ class AppRouter { ), ), BlocProvider(create: (context) => TicketFormCubit()), - BlocProvider( - create: (context) => TrackingCubit( - repo: repo, - parentId: parentId, - parentType: parentType, - companyId: companyId, - ), - ), ], child: TicketFormScreen( ticketId: realTicketId, - existingTicket: ticketFromExtra, + existingTicket: record.ticket, ), ); }, @@ -277,6 +266,7 @@ class AppRouter { name: Routes.operationForm, builder: (context, state) { final String pathId = state.pathParameters['id'] ?? 'new'; + final OperationModel? operationFromExtra = state.extra as OperationModel?; final String? realOperationId = pathId == 'new' ? null : pathId; @@ -301,7 +291,12 @@ class AppRouter { parentType: AttachmentParentType.operation, ), ), - BlocProvider(create: (context) => OperationFormCubit()), + BlocProvider( + create: (context) => OperationFormCubit( + createdById: createdById, + createdByName: createdByName, + ), + ), ], child: OperationFormScreen( operationId: realOperationId, diff --git a/lib/core/widgets/staff_selector_modal.dart b/lib/core/widgets/staff_selector_modal.dart new file mode 100644 index 0000000..e77c72c --- /dev/null +++ b/lib/core/widgets/staff_selector_modal.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; +import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; +import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; +// import 'package:flutter_bloc/flutter_bloc.dart'; +// Importa il tuo StaffModel + +/// Funzione helper globale per lanciare la modale ovunque ti trovi con 1 riga di codice +Future showStaffSelectorModal(BuildContext context) async { + return showModalBottomSheet( + context: context, + isScrollControlled: + true, // Permette alla modale di essere più alta se serve + backgroundColor: Colors.transparent, + builder: (context) => const StaffSelectorModal(), + ); +} + +class StaffSelectorModal extends StatelessWidget { + const StaffSelectorModal({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Container( + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + ), + padding: const EdgeInsets.all(24), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, // Occupa solo lo spazio necessario + children: [ + // --- Maniglietta superiore (UX standard dei BottomSheet) --- + Container( + width: 40, + height: 4, + margin: const EdgeInsets.only(bottom: 24), + decoration: BoxDecoration( + color: theme.dividerColor, + borderRadius: BorderRadius.circular(2), + ), + ), + + // --- Titolo --- + const Text( + 'Chi sei?', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + 'Seleziona il tuo profilo per continuare', + style: TextStyle(color: Colors.grey.shade600), + ), + const SizedBox(height: 32), + BlocBuilder( + builder: (context, state) { + if (state.status == StaffStatus.loading) { + return const CircularProgressIndicator(); + } + final staffList = state.storeStaff; + return _buildStaffGrid(context, staffList); + }, + ), + + const SizedBox(height: 16), + + // --- Tasto Annulla --- + TextButton( + onPressed: () => Navigator.of(context).pop(), // Restituisce null + child: const Text('Annulla'), + ), + ], + ), + ), + ); + } + + Widget _buildStaffGrid(BuildContext context, List staffList) { + return Wrap( + spacing: 16, + runSpacing: 16, + alignment: WrapAlignment.center, + children: staffList.map((staff) { + return InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () { + // Quando l'utente tappa il suo nome, la modale si chiude + // e restituisce il modello (o l'ID) alla schermata precedente! + Navigator.of(context).pop(staff); + }, + child: Container( + width: 100, // Pulsanti larghi e comodi + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.surfaceContainerHighest.withOpacity(0.3), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Theme.of(context).dividerColor), + ), + child: Column( + children: [ + CircleAvatar( + radius: 30, + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + child: Text( + staff['name'].substring(0, 1).toUpperCase(), + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 12), + Text( + staff['name'], + style: const TextStyle(fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ); + }).toList(), + ); + } +} + +Future getStaffMember(BuildContext context) async { + final sessionState = context.read().state; + + if (sessionState.isSingleUserMode) { + // Dispositivo personale: non rompiamo le palle. Usiamo l'utente loggato. + return sessionState.currentStaffMember; + } else { + // Dispositivo Condiviso (Kiosk Mode): Chiediamo chi è! + return await showStaffSelectorModal(context); + } +} diff --git a/lib/features/home/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart index 254e8b8..2fdd2e9 100644 --- a/lib/features/home/ui/home_screen.dart +++ b/lib/features/home/ui/home_screen.dart @@ -4,9 +4,11 @@ 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/core/utils/extensions.dart'; +import 'package:flux/core/widgets/staff_selector_modal.dart'; import 'package:flux/features/home/latest_store_operations/ui/latest_store_operations_card.dart'; import 'package:flux/features/home/ui/quick_actions_widget.dart'; import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; +import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; import 'package:go_router/go_router.dart'; class HomeScreen extends StatelessWidget { @@ -199,11 +201,13 @@ class HomeScreen extends StatelessWidget { icon: Icons.handyman, label: context.l10n.homeNewOperationTicket, color: Colors.redAccent, - onTap: () { - // Andiamo alla lista! (Da lì poi aggiungeremo il tasto "+" per il form) + onTap: () async { + StaffMemberModel? createdBy = await getStaffMember(context); + if (createdBy == null || !context.mounted) return; context.pushNamed( Routes.ticketForm, pathParameters: {'id': 'new'}, + extra: createdBy, ); }, ), @@ -383,6 +387,7 @@ class HomeScreen extends StatelessWidget { onTap: () { // Cambiamo il negozio nel SessionCubit! context.read().changeStore(store); + context.read().loadStaffForStore(store.id!); Navigator.pop(context); }, ); diff --git a/lib/features/operations/blocs/operation_form_cubit.dart b/lib/features/operations/blocs/operation_form_cubit.dart index f07dd05..e74ae3a 100644 --- a/lib/features/operations/blocs/operation_form_cubit.dart +++ b/lib/features/operations/blocs/operation_form_cubit.dart @@ -13,8 +13,10 @@ class OperationFormCubit extends Cubit { final OperationsRepository _repository = GetIt.I(); final SessionCubit _sessionCubit = GetIt.I(); final Uuid _uuid = const Uuid(); + final String createdById; + final String createdByName; - OperationFormCubit() + OperationFormCubit({required this.createdById, required this.createdByName}) : super( OperationFormState( // Inizializziamo con un modello vuoto di sicurezza diff --git a/lib/features/operations/data/operations_repository.dart b/lib/features/operations/data/operations_repository.dart index f0cd46a..9e380a7 100644 --- a/lib/features/operations/data/operations_repository.dart +++ b/lib/features/operations/data/operations_repository.dart @@ -18,7 +18,7 @@ class OperationsRepository { .from('operation') .select(''' *, - customer(name), + customer(*), store(name), staff_member(name), provider(name), @@ -47,7 +47,7 @@ class OperationsRepository { .from('operation') .select(''' *, - customer(name), + customer(*), store(name), provider(name), model(name_with_brand), diff --git a/lib/features/settings/blocs/settings_cubit.dart b/lib/features/settings/blocs/settings_cubit.dart new file mode 100644 index 0000000..1f1ee81 --- /dev/null +++ b/lib/features/settings/blocs/settings_cubit.dart @@ -0,0 +1,42 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; +import 'package:get_it/get_it.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class SettingsState extends Equatable { + final bool isSingleUserMode; + + const SettingsState({this.isSingleUserMode = false}); + + SettingsState copyWith({bool? isSingleUserMode}) { + return SettingsState( + isSingleUserMode: isSingleUserMode ?? this.isSingleUserMode, + ); + } + + @override + List get props => [isSingleUserMode]; +} + +class SettingsCubit extends Cubit { + final SharedPreferences _prefs = GetIt.I.get(); + + SettingsCubit() : super(const SettingsState()) { + final bool isSingleUserMode = _prefs.getBool('isSingleUserMode') ?? false; + final sessionCubit = GetIt.I.get(); + sessionCubit.setIsSingleUserMode(isSingleUserMode); + emit(state.copyWith(isSingleUserMode: isSingleUserMode)); + } + + void toggleSingleUserMode() { + final bool isSingleUserMode = !state.isSingleUserMode; + GetIt.I.get().setBool( + 'isSingleUserMode', + isSingleUserMode, + ); + final sessionCubit = GetIt.I.get(); + sessionCubit.setIsSingleUserMode(isSingleUserMode); + emit(state.copyWith(isSingleUserMode: !state.isSingleUserMode)); + } +} diff --git a/lib/features/settings/settings_view.dart b/lib/features/settings/settings_screen.dart similarity index 84% rename from lib/features/settings/settings_view.dart rename to lib/features/settings/settings_screen.dart index 1565164..85a13c5 100644 --- a/lib/features/settings/settings_view.dart +++ b/lib/features/settings/settings_screen.dart @@ -4,10 +4,11 @@ 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/settings/blocs/settings_cubit.dart'; import 'package:go_router/go_router.dart'; -class SettingsView extends StatelessWidget { - const SettingsView({super.key}); +class SettingsScreen extends StatelessWidget { + const SettingsScreen({super.key}); @override Widget build(BuildContext context) { @@ -24,6 +25,14 @@ class SettingsView extends StatelessWidget { context: context, onTap: () {}, ), + BlocBuilder( + builder: (context, state) => CheckboxListTile( + value: state.isSingleUserMode, + title: const Text('Singolo Utente'), + onChanged: (_) => + context.read().toggleSingleUserMode(), + ), + ), _settingsTile( title: 'Impostazioni Azienda', icon: Icons.business, diff --git a/lib/features/tickets/blocs/ticket_form_cubit.dart b/lib/features/tickets/blocs/ticket_form_cubit.dart index 8014bbb..1f735f6 100644 --- a/lib/features/tickets/blocs/ticket_form_cubit.dart +++ b/lib/features/tickets/blocs/ticket_form_cubit.dart @@ -1,19 +1,28 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/features/customers/models/customer_model.dart'; +import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; import 'package:flux/features/tickets/models/ticket_model.dart'; import 'package:flux/features/tickets/data/ticket_repository.dart'; +import 'package:flux/features/tracking/data/tracking_repository.dart'; +import 'package:flux/features/tracking/models/tracking_model.dart'; import 'package:get_it/get_it.dart'; import 'ticket_form_state.dart'; class TicketFormCubit extends Cubit { final TicketRepository _repository = GetIt.I.get(); final SessionCubit _sessionCubit = GetIt.I.get(); + final StaffMemberModel? _createdBy; - TicketFormCubit() + TicketFormCubit({StaffMemberModel? createdBy}) : super( // Inizializziamo con un ticket vuoto di default - TicketFormState(ticket: TicketModel.empty()), + TicketFormState( + ticket: TicketModel.empty().copyWith( + createdById: createdBy?.id, + createdByName: createdBy?.name, + ), + ), ); /// 1. INIZIALIZZAZIONE (Se stiamo modificando un ticket esistente) @@ -48,15 +57,14 @@ class TicketFormCubit extends Cubit { } else { // SCENARIO 3 (Nuovo Ticket): // È un nuovo ticket! Inseriamo i default base (Azienda, Negozio, Creatore) - final currentUser = _sessionCubit.state.currentStaffMember; final currentStore = _sessionCubit.state.currentStore; final companyId = _sessionCubit.state.company?.id ?? ''; final newTicket = TicketModel.empty().copyWith( companyId: companyId, storeId: currentStore?.id, - createdById: currentUser?.id, - createdByName: currentUser?.name, + createdById: createdBy.id, + createdByName: _createdBy.name, // Impostiamo lo stato iniziale ticketStatus: TicketStatus.open, ticketType: TicketType.repair, // Default @@ -219,4 +227,51 @@ class TicketFormCubit extends Cubit { return null; } } + + Future takeInCharge({ + required String staffId, + required String staffName, + }) async { + final currentTicket = state.ticket; + + // Sicurezza: non possiamo prendere in carico un ticket fantasma + if (currentTicket.id == null || currentTicket.id!.isEmpty) return; + + // 1. Prepariamo il ticket aggiornato + final updatedTicket = currentTicket.copyWith( + ticketStatus: TicketStatus + .inProgress, // Assumendo che tu abbia un enum per gli stati + assignedToId: staffId, + assignedToName: staffName, + ); + + try { + // 2. Aggiorniamo il ticket sul Database (usa il tuo metodo esistente del repo) + await _repository.updateTicket(updatedTicket); + + // 3. Spara il log automatico nella Timeline! + await GetIt.I.get().logQuickEvent( + companyId: currentTicket.companyId, + message: "Ticket preso in carico. Inizio lavorazione.", + type: TrackingType.statusChange, + parentId: currentTicket.id!, + parentType: TrackingParentType.ticket, + staffId: staffId, + // Lo mettiamo pubblico (isInternal: false) così il cliente a casa vede che + // il suo dispositivo è ufficialmente sotto i ferri! + isInternal: false, + ); + + // 4. Aggiorniamo lo stato locale del Cubit per far scattare la UI + emit(state.copyWith(ticket: updatedTicket)); + } catch (e) { + // Gestisci eventuali errori (es. mostrando una snackbar) + emit( + state.copyWith( + status: TicketFormStatus.failure, + errorMessage: 'Errore durante la presa in carico: $e', + ), + ); + } + } } diff --git a/lib/features/tickets/ui/ticket_form_screen.dart b/lib/features/tickets/ui/ticket_form_screen.dart index 3f116f5..af2fc06 100644 --- a/lib/features/tickets/ui/ticket_form_screen.dart +++ b/lib/features/tickets/ui/ticket_form_screen.dart @@ -14,9 +14,9 @@ import 'package:flux/features/tickets/models/ticket_model.dart'; import 'package:flux/core/widgets/shared_forms/staff_section.dart'; import 'package:flux/features/tickets/models/ticket_status_extension.dart'; import 'package:flux/features/tickets/ui/ticket_timeline_section.dart'; +import 'package:flux/features/tickets/ui/ticket_workspace_screen.dart'; import 'package:flux/features/tickets/utils/ticket_pdf_service.dart'; import 'package:flux/features/tracking/blocs/tracking_cubit.dart'; -import 'package:flux/features/tracking/data/tracking_repository.dart'; import 'package:flux/features/tracking/models/tracking_model.dart'; import 'package:get_it/get_it.dart'; import 'package:printing/printing.dart'; @@ -300,6 +300,83 @@ class _TicketFormScreenState extends State { ticket.id == null ? 'Nuova Scheda Assistenza' : 'Modifica Scheda', ), actions: [ + BlocBuilder( + builder: (context, state) { + final ticket = state.ticket; + + // Se il ticket non è ancora salvato, niente azioni rapide + if (ticket.id == null || ticket.id!.isEmpty) { + return const SizedBox.shrink(); + } + + // CONDIZIONE A: Da iniziare + if (ticket.ticketStatus == TicketStatus.open || + ticket.ticketStatus == TicketStatus.inProgress) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: FilledButton.icon( + style: FilledButton.styleFrom( + backgroundColor: + Colors.amber.shade700, // Colore Action + ), + onPressed: () { + final currentUserId = GetIt.I + .get() + .state + .currentStaffMember! + .id!; + final currentUserName = GetIt.I + .get() + .state + .currentStaffMember! + .name; + + context.read().takeInCharge( + staffId: currentUserId, + staffName: currentUserName, + ); + + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + TicketWorkspaceScreen(ticket: ticket), + ), + ); + }, + icon: const Icon(Icons.play_arrow, color: Colors.white), + label: const Text( + 'Prendi in Carico', + style: TextStyle(color: Colors.white), + ), + ), + ); + } + // CONDIZIONE B: Già in lavorazione + else if (ticket.ticketStatus == TicketStatus.inProgress) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: FilledButton.icon( + onPressed: () { + // Naviga direttamente alla schermata di lavorazione + // Navigator.push(context, MaterialPageRoute(builder: (_) => TicketWorkspaceScreen(ticket: ticket))); + }, + icon: const Icon(Icons.handyman), + label: const Text('Vai a Lavorazione'), + ), + ); + } + + // Se è chiuso o in altri stati strani, nascondiamo il bottone + return const SizedBox.shrink(); + }, + ), Padding( padding: const EdgeInsets.only(right: 16.0), child: Chip( @@ -807,10 +884,8 @@ class _TicketFormScreenState extends State { children: [ BlocProvider( create: (context) => TrackingCubit( - repo: GetIt.I.get(), parentId: ticket.id!, parentType: TrackingParentType.ticket, - companyId: ticket.companyId, ), child: BlocBuilder( builder: (context, state) { @@ -826,7 +901,11 @@ class _TicketFormScreenState extends State { context.read().addManualNote( message, isInternal, - // staffId: currentStaffId, + staffId: GetIt.I + .get() + .state + .currentStaffMember + ?.id, ); }, ); diff --git a/lib/features/tickets/ui/ticket_list_screen.dart b/lib/features/tickets/ui/ticket_list_screen.dart index 4df83be..b074c28 100644 --- a/lib/features/tickets/ui/ticket_list_screen.dart +++ b/lib/features/tickets/ui/ticket_list_screen.dart @@ -1,10 +1,14 @@ 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/widgets/staff_selector_modal.dart'; +import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; import 'package:flux/features/tickets/blocs/ticket_list_cubit.dart'; import 'package:flux/features/tickets/blocs/ticket_list_state.dart'; import 'package:flux/features/tickets/models/ticket_model.dart'; import 'package:flux/features/tickets/models/ticket_status_extension.dart'; +import 'package:flux/features/tickets/ui/ticket_form_screen.dart'; import 'package:go_router/go_router.dart'; class TicketListScreen extends StatefulWidget { @@ -148,11 +152,17 @@ class _TicketListScreenState extends State { ], ), floatingActionButton: FloatingActionButton.extended( - onPressed: () { - context.pushNamed(Routes.ticketForm, pathParameters: {'id': 'new'}); - }, icon: const Icon(Icons.add), label: const Text('Nuovo Ticket'), + onPressed: () async { + StaffMemberModel? createdBy = await getStaffMember(context); + if (createdBy == null || !context.mounted) return; + context.pushNamed( + Routes.ticketForm, + pathParameters: {'id': 'new'}, + extra: createdBy, + ); + }, ), ); } diff --git a/lib/features/tickets/ui/ticket_workspace_screen.dart b/lib/features/tickets/ui/ticket_workspace_screen.dart new file mode 100644 index 0000000..eb5967b --- /dev/null +++ b/lib/features/tickets/ui/ticket_workspace_screen.dart @@ -0,0 +1,251 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +// Importa il tuo TicketModel, Cubit, ecc. + +class TicketWorkspaceScreen extends StatelessWidget { + // Passiamo il ticket attuale per avere i dati (o il Cubit se preferisci) + final dynamic ticket; // Sostituisci con TicketModel + + const TicketWorkspaceScreen({super.key, required this.ticket}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Scaffold( + appBar: AppBar( + title: const Text('Banco di Lavoro'), + backgroundColor: theme.colorScheme.inversePrimary, + centerTitle: true, + ), + // SafeArea in basso per ospitare i bottoni fissi + bottomNavigationBar: SafeArea(child: _buildBottomActions(context)), + body: SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildDeviceHeader(theme), + const SizedBox(height: 24), + _buildDefectRecap(theme), + const SizedBox(height: 32), + _buildOperationsSection(theme), + ], + ), + ), + ); + } + + // --- 1. HEADER DISPOSITIVO E PASSWORD --- + Widget _buildDeviceHeader(ThemeData theme) { + return Card( + elevation: 0, + color: theme.colorScheme.primaryContainer.withValues(alpha: 0.4), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide( + color: theme.colorScheme.primary.withValues(alpha: 0.2), + ), + ), + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'DISPOSITIVO IN LAVORAZIONE', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: theme.colorScheme.primary, + letterSpacing: 1.2, + ), + ), + const SizedBox(height: 8), + Text( + ticket.deviceModel ?? + 'Modello Sconosciuto', // Es: "iPhone 13 Pro" + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.w800, + ), + ), + ], + ), + ), + // IL DATO PIÙ CERCATO DAI TECNICI: LA PASSWORD + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: theme.dividerColor), + ), + child: Column( + children: [ + const Text( + 'PIN / SBLOCCO', + style: TextStyle(fontSize: 10, color: Colors.grey), + ), + const SizedBox(height: 4), + Text( + ticket.unlockPassword?.isNotEmpty == true + ? ticket.unlockPassword! + : 'Nessuno', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + letterSpacing: 2, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + // --- 2. RECAP DIFETTO --- + Widget _buildDefectRecap(ThemeData theme) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Row( + children: [ + Icon(Icons.warning_amber_rounded, color: Colors.orange), + SizedBox(width: 8), + Text( + 'Difetto Segnalato', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ], + ), + const SizedBox(height: 12), + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest.withValues( + alpha: 0.3, + ), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: theme.dividerColor), + ), + child: Text( + ticket.defectDescription ?? 'Nessuna descrizione inserita.', + style: const TextStyle(fontSize: 16, height: 1.5), + ), + ), + ], + ); + } + + // --- 3. SEZIONE COSTI E RICAMBI (Mockup) --- + Widget _buildOperationsSection(ThemeData theme) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Ricambi e Manodopera', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + TextButton.icon( + onPressed: () { + // TODO: Apri modal per aggiungere una riga di costo + }, + icon: const Icon(Icons.add), + label: const Text('Aggiungi Voce'), + ), + ], + ), + const SizedBox(height: 12), + // Qui ci andrà un ListView.builder collegato alla tabella dei costi/operazioni + Container( + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: theme.dividerColor, + style: BorderStyle.solid, + ), + ), + child: const Center( + child: Text( + 'Nessun ricambio o costo inserito.\nClicca su "Aggiungi Voce" per iniziare.', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey), + ), + ), + ), + ], + ); + } + + // --- 4. BOTTONI AZIONE FINALI (In basso) --- + Widget _buildBottomActions(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, -5), + ), + ], + ), + child: Row( + children: [ + // Bottone Pausa / Attesa Ricambi + Expanded( + flex: 1, + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + foregroundColor: Colors.orange.shade700, + side: BorderSide(color: Colors.orange.shade700), + ), + onPressed: () { + // TODO: Logica Metti in Pausa + }, + icon: const Icon(Icons.pause), + label: const Text( + 'Metti in Pausa', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + ), + const SizedBox(width: 12), + // Bottone Completa + Expanded( + flex: 2, + child: FilledButton.icon( + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + backgroundColor: Colors.green.shade600, + ), + onPressed: () { + // TODO: Logica Completa Riparazione + }, + icon: const Icon(Icons.check_circle_outline), + label: const Text( + 'COMPLETA RIPARAZIONE', + style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/tracking/blocs/tracking_cubit.dart b/lib/features/tracking/blocs/tracking_cubit.dart index 9087265..3176152 100644 --- a/lib/features/tracking/blocs/tracking_cubit.dart +++ b/lib/features/tracking/blocs/tracking_cubit.dart @@ -1,6 +1,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/features/tracking/data/tracking_repository.dart'; import 'package:flux/features/tracking/models/tracking_model.dart'; +import 'package:get_it/get_it.dart'; // Stati base: initial, loading, loaded, error class TrackingState { @@ -11,18 +13,13 @@ class TrackingState { } class TrackingCubit extends Cubit { - final TrackingRepository _repo; + final TrackingRepository _repo = GetIt.I.get(); final String parentId; final TrackingParentType parentType; - final String companyId; + final String companyId = GetIt.I.get().state.company!.id!; - TrackingCubit({ - required TrackingRepository repo, - required this.parentId, - required this.parentType, - required this.companyId, - }) : _repo = repo, - super(TrackingState()) { + TrackingCubit({required this.parentId, required this.parentType}) + : super(TrackingState()) { loadTrackings(); } @@ -46,7 +43,7 @@ class TrackingCubit extends Cubit { companyId: companyId, message: message, type: TrackingType.manualNote, - parentId: parentId, + parentId: parentId!, parentType: parentType, staffId: staffId, isInternal: isInternal, diff --git a/lib/main.dart b/lib/main.dart index 39fb34b..06255d1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,8 +9,10 @@ import 'package:flux/features/auth/bloc/auth_cubit.dart'; import 'package:flux/features/company/data/company_repository.dart'; import 'package:flux/features/operations/blocs/operation_list_cubit.dart'; import 'package:flux/features/operations/data/operations_repository.dart'; +import 'package:flux/features/settings/blocs/settings_cubit.dart'; import 'package:flux/features/settings/document_sequence/data/document_sequence_repository.dart'; import 'package:flux/features/tickets/data/ticket_repository.dart'; +import 'package:flux/features/tracking/data/tracking_repository.dart'; import 'package:flux/l10n/app_localizations.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; @@ -56,9 +58,15 @@ void main() async { BlocProvider(create: (_) => StoreCubit()), BlocProvider(create: (_) => CustomersCubit()), BlocProvider(create: (_) => ProductsCubit()), - BlocProvider(create: (_) => StaffCubit()), + BlocProvider( + create: (_) => StaffCubit() + ..loadStaffForStore( + GetIt.I.get().state.currentStore!.id!, + ), + ), BlocProvider(create: (_) => OperationListCubit()), BlocProvider(create: (_) => ProvidersCubit()), + BlocProvider(create: (_) => SettingsCubit()), ], child: const FluxApp(), ), @@ -112,6 +120,7 @@ Future setupLocator() async { SessionCubit(getIt(), getIt()), ); getIt.registerLazySingleton(() => CompanyRepository()); + getIt.registerLazySingleton(() => TrackingRepository()); } class FluxApp extends StatefulWidget {