This commit is contained in:
2026-05-13 12:41:07 +02:00
parent 216fd85888
commit efb82b0d4a
15 changed files with 657 additions and 50 deletions

View File

@@ -164,4 +164,8 @@ class SessionCubit extends Cubit<SessionState> {
void setIsMobileDevice(bool isMobile) {
emit(state.copyWith(isMobileDevice: isMobile));
}
void setIsSingleUserMode(bool isSingleUser) {
emit(state.copyWith(isSingleUserMode: isSingleUser));
}
}

View File

@@ -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

View File

@@ -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<StaffCubit>().loadStaffForStore(
GetIt.I.get<SessionCubit>().state.currentStore!.id!,
);
context.read<CustomersCubit>().loadCustomers();
context.read<ProductsCubit>().loadModels();
context.read<ProductsCubit>().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,

View File

@@ -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<dynamic> 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<StaffCubit, StaffState>(
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<dynamic> 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<StaffMemberModel?> getStaffMember(BuildContext context) async {
final sessionState = context.read<SessionCubit>().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);
}
}

View File

@@ -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<SessionCubit>().changeStore(store);
context.read<StaffCubit>().loadStaffForStore(store.id!);
Navigator.pop(context);
},
);

View File

@@ -13,8 +13,10 @@ class OperationFormCubit extends Cubit<OperationFormState> {
final OperationsRepository _repository = GetIt.I<OperationsRepository>();
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
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

View File

@@ -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),

View File

@@ -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<Object?> get props => [isSingleUserMode];
}
class SettingsCubit extends Cubit<SettingsState> {
final SharedPreferences _prefs = GetIt.I.get<SharedPreferences>();
SettingsCubit() : super(const SettingsState()) {
final bool isSingleUserMode = _prefs.getBool('isSingleUserMode') ?? false;
final sessionCubit = GetIt.I.get<SessionCubit>();
sessionCubit.setIsSingleUserMode(isSingleUserMode);
emit(state.copyWith(isSingleUserMode: isSingleUserMode));
}
void toggleSingleUserMode() {
final bool isSingleUserMode = !state.isSingleUserMode;
GetIt.I.get<SharedPreferences>().setBool(
'isSingleUserMode',
isSingleUserMode,
);
final sessionCubit = GetIt.I.get<SessionCubit>();
sessionCubit.setIsSingleUserMode(isSingleUserMode);
emit(state.copyWith(isSingleUserMode: !state.isSingleUserMode));
}
}

View File

@@ -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<SettingsCubit, SettingsState>(
builder: (context, state) => CheckboxListTile(
value: state.isSingleUserMode,
title: const Text('Singolo Utente'),
onChanged: (_) =>
context.read<SettingsCubit>().toggleSingleUserMode(),
),
),
_settingsTile(
title: 'Impostazioni Azienda',
icon: Icons.business,

View File

@@ -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<TicketFormState> {
final TicketRepository _repository = GetIt.I.get<TicketRepository>();
final SessionCubit _sessionCubit = GetIt.I.get<SessionCubit>();
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<TicketFormState> {
} 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<TicketFormState> {
return null;
}
}
Future<void> 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<TrackingRepository>().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',
),
);
}
}
}

View File

@@ -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<TicketFormScreen> {
ticket.id == null ? 'Nuova Scheda Assistenza' : 'Modifica Scheda',
),
actions: [
BlocBuilder<TicketFormCubit, TicketFormState>(
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<SessionCubit>()
.state
.currentStaffMember!
.id!;
final currentUserName = GetIt.I
.get<SessionCubit>()
.state
.currentStaffMember!
.name;
context.read<TicketFormCubit>().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<TicketFormScreen> {
children: [
BlocProvider(
create: (context) => TrackingCubit(
repo: GetIt.I.get<TrackingRepository>(),
parentId: ticket.id!,
parentType: TrackingParentType.ticket,
companyId: ticket.companyId,
),
child: BlocBuilder<TrackingCubit, TrackingState>(
builder: (context, state) {
@@ -826,7 +901,11 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
context.read<TrackingCubit>().addManualNote(
message,
isInternal,
// staffId: currentStaffId,
staffId: GetIt.I
.get<SessionCubit>()
.state
.currentStaffMember
?.id,
);
},
);

View File

@@ -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<TicketListScreen> {
],
),
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,
);
},
),
);
}

View File

@@ -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),
),
),
),
],
),
);
}
}

View File

@@ -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<TrackingState> {
final TrackingRepository _repo;
final TrackingRepository _repo = GetIt.I.get<TrackingRepository>();
final String parentId;
final TrackingParentType parentType;
final String companyId;
final String companyId = GetIt.I.get<SessionCubit>().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<TrackingState> {
companyId: companyId,
message: message,
type: TrackingType.manualNote,
parentId: parentId,
parentId: parentId!,
parentType: parentType,
staffId: staffId,
isInternal: isInternal,

View File

@@ -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<StoreCubit>(create: (_) => StoreCubit()),
BlocProvider<CustomersCubit>(create: (_) => CustomersCubit()),
BlocProvider<ProductsCubit>(create: (_) => ProductsCubit()),
BlocProvider<StaffCubit>(create: (_) => StaffCubit()),
BlocProvider<StaffCubit>(
create: (_) => StaffCubit()
..loadStaffForStore(
GetIt.I.get<SessionCubit>().state.currentStore!.id!,
),
),
BlocProvider<OperationListCubit>(create: (_) => OperationListCubit()),
BlocProvider<ProvidersCubit>(create: (_) => ProvidersCubit()),
BlocProvider<SettingsCubit>(create: (_) => SettingsCubit()),
],
child: const FluxApp(),
),
@@ -112,6 +120,7 @@ Future<void> setupLocator() async {
SessionCubit(getIt<CoreRepository>(), getIt<SharedPreferences>()),
);
getIt.registerLazySingleton<CompanyRepository>(() => CompanyRepository());
getIt.registerLazySingleton<TrackingRepository>(() => TrackingRepository());
}
class FluxApp extends StatefulWidget {