lavorazione dei ticket
This commit is contained in:
@@ -318,4 +318,49 @@ class TicketFormCubit extends Cubit<TicketFormState> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> completeTicket({
|
||||
required TicketResult result,
|
||||
required double finalPrice,
|
||||
}) async {
|
||||
final currentTicket = state.ticket;
|
||||
|
||||
if (currentTicket.id == null || currentTicket.id!.isEmpty) return;
|
||||
|
||||
// 1. Aggiorniamo il ticket con il nuovo status, l'esito e il prezzo!
|
||||
final updatedTicket = currentTicket.copyWith(
|
||||
ticketStatus: TicketStatus.ready,
|
||||
ticketResult: result,
|
||||
customerPrice: finalPrice,
|
||||
);
|
||||
|
||||
try {
|
||||
await _repository.updateTicket(updatedTicket);
|
||||
|
||||
// 2. Timeline personalizzata in base all'esito
|
||||
final esitoTesto = result == TicketResult.success
|
||||
? "Riparato con successo"
|
||||
: "Non riparabile/Preventivo rifiutato";
|
||||
|
||||
await GetIt.I.get<TrackingRepository>().logQuickEvent(
|
||||
companyId: currentTicket.companyId,
|
||||
message:
|
||||
"Lavorazione completata. Esito: $esitoTesto. Il dispositivo è pronto per il ritiro.",
|
||||
type: TrackingType.statusChange,
|
||||
parentId: currentTicket.id!,
|
||||
parentType: TrackingParentType.ticket,
|
||||
staffId: currentTicket.assignedToId ?? '',
|
||||
isInternal: false,
|
||||
);
|
||||
|
||||
emit(state.copyWith(ticket: updatedTicket));
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: TicketFormStatus.failure,
|
||||
errorMessage: 'Errore durante la chiusura: $e',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:io' show Platform;
|
||||
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/shared_forms/customer_section.dart';
|
||||
import 'package:flux/core/widgets/shared_forms/model_section.dart';
|
||||
import 'package:flux/core/widgets/shared_forms/shared_files_section.dart';
|
||||
@@ -22,6 +23,7 @@ import 'package:flux/features/tickets/utils/ticket_pdf_service.dart';
|
||||
import 'package:flux/features/tracking/blocs/tracking_cubit.dart';
|
||||
import 'package:flux/features/tracking/models/tracking_model.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:printing/printing.dart';
|
||||
|
||||
class TicketFormScreen extends StatefulWidget {
|
||||
@@ -278,6 +280,19 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _navigateToWorkspace(String ticketId) async {
|
||||
final formCubit = context.read<TicketFormCubit>();
|
||||
final trackingCubit = context.read<TrackingCubit>();
|
||||
_flushControllersToCubit();
|
||||
await context.pushNamed(
|
||||
Routes.ticketWorkspace, // Assicurati di aver definito questo nome!
|
||||
pathParameters: {'id': ticketId},
|
||||
extra: formCubit, // Passiamo l'intero Cubit come extra!
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
trackingCubit.loadTrackings(ticketId, TrackingParentType.ticket);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
@@ -345,24 +360,11 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
|
||||
context,
|
||||
);
|
||||
if (takenBy == null || !context.mounted) return;
|
||||
final formCubit = context.read<TicketFormCubit>();
|
||||
_flushControllersToCubit();
|
||||
context.read<TicketFormCubit>().takeInCharge(
|
||||
staffId: takenBy.id!,
|
||||
staffName: takenBy.name,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => BlocProvider.value(
|
||||
value:
|
||||
formCubit, // Magia! La nuova schermata userà lo stesso Cubit!
|
||||
child:
|
||||
const TicketWorkspaceScreen(), // Non passiamo più il ticket
|
||||
),
|
||||
),
|
||||
);
|
||||
_navigateToWorkspace(ticket.id!);
|
||||
},
|
||||
icon: const Icon(Icons.play_arrow, color: Colors.white),
|
||||
label: const Text(
|
||||
@@ -380,23 +382,7 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: FilledButton.icon(
|
||||
onPressed: () {
|
||||
final formCubit = context.read<TicketFormCubit>();
|
||||
// Naviga direttamente alla schermata di lavorazione
|
||||
_flushControllersToCubit();
|
||||
if (!context.mounted) return;
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => BlocProvider.value(
|
||||
value:
|
||||
formCubit, // Magia! La nuova schermata userà lo stesso Cubit!
|
||||
child:
|
||||
const TicketWorkspaceScreen(), // Non passiamo più il ticket
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onPressed: () => _navigateToWorkspace(ticket.id!),
|
||||
icon: const Icon(Icons.handyman),
|
||||
label: const Text('Vai a Lavorazione'),
|
||||
),
|
||||
@@ -971,38 +957,7 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
|
||||
title: 'Timeline & Note',
|
||||
icon: Icons.history,
|
||||
themeColor: Colors.blueGrey,
|
||||
children: [
|
||||
BlocProvider(
|
||||
create: (context) => TrackingCubit(
|
||||
parentId: ticket.id!,
|
||||
parentType: TrackingParentType.ticket,
|
||||
),
|
||||
child: BlocBuilder<TrackingCubit, TrackingState>(
|
||||
builder: (context, state) {
|
||||
if (state.isLoading && state.logs.isEmpty) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return TicketTimelineSection(
|
||||
logs: state.logs,
|
||||
onAddNote: (message, isInternal) {
|
||||
// Recupera l'ID dello staff loggato dal tuo auth state
|
||||
// final currentStaffId = ...
|
||||
context.read<TrackingCubit>().addManualNote(
|
||||
message,
|
||||
isInternal,
|
||||
staffId: GetIt.I
|
||||
.get<SessionCubit>()
|
||||
.state
|
||||
.currentStaffMember
|
||||
?.id,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
children: [TicketTimelineSection(ticketId: ticket.id!)],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -155,11 +155,13 @@ class _TicketListScreenState extends State<TicketListScreen> {
|
||||
onPressed: () async {
|
||||
StaffMemberModel? createdBy = await getStaffMember(context);
|
||||
if (createdBy == null || !context.mounted) return;
|
||||
context.pushNamed(
|
||||
await context.pushNamed(
|
||||
Routes.ticketForm,
|
||||
pathParameters: {'id': 'new'},
|
||||
extra: (createdBy: createdBy, ticket: null),
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
context.read<TicketListCubit>().fetchTickets(reset: true);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/features/tracking/blocs/tracking_cubit.dart';
|
||||
import 'package:flux/features/tracking/models/tracking_model.dart';
|
||||
|
||||
class TicketTimelineSection extends StatefulWidget {
|
||||
final List<TrackingModel> logs;
|
||||
final void Function(String message, bool isInternal) onAddNote;
|
||||
final String ticketId;
|
||||
|
||||
const TicketTimelineSection({
|
||||
super.key,
|
||||
required this.logs,
|
||||
required this.onAddNote,
|
||||
});
|
||||
const TicketTimelineSection({super.key, required this.ticketId});
|
||||
|
||||
@override
|
||||
State<TicketTimelineSection> createState() => _TicketTimelineSectionState();
|
||||
@@ -28,7 +25,12 @@ class _TicketTimelineSectionState extends State<TicketTimelineSection> {
|
||||
void _submitNote() {
|
||||
final text = _textController.text.trim();
|
||||
if (text.isNotEmpty) {
|
||||
widget.onAddNote(text, _isInternal);
|
||||
context.read<TrackingCubit>().addTimelineEvent(
|
||||
parentId: widget.ticketId,
|
||||
parentType: TrackingParentType.ticket,
|
||||
message: text,
|
||||
isInternal: _isInternal,
|
||||
);
|
||||
_textController.clear();
|
||||
// Chiudiamo la tastiera se siamo su mobile
|
||||
FocusScope.of(context).unfocus();
|
||||
@@ -124,129 +126,137 @@ class _TicketTimelineSectionState extends State<TicketTimelineSection> {
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// --- TIMELINE SCROLLABILE ---
|
||||
if (widget.logs.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(32.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Nessun evento registrato.',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 400,
|
||||
), // Limite di altezza
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: widget.logs.length,
|
||||
itemBuilder: (context, index) {
|
||||
final log = widget.logs[index];
|
||||
final isLast = index == widget.logs.length - 1;
|
||||
BlocBuilder<TrackingCubit, TrackingState>(
|
||||
builder: (context, state) {
|
||||
if (state.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (state.logs.isEmpty) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(32.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Nessun evento registrato.',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 400,
|
||||
), // Limite di altezza
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: state.logs.length,
|
||||
itemBuilder: (context, index) {
|
||||
final log = state.logs[index];
|
||||
final isLast = index == state.logs.length - 1;
|
||||
|
||||
return IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// --- LINEA E PALLINO ---
|
||||
SizedBox(
|
||||
width: 30,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 4),
|
||||
width: 14,
|
||||
height: 14,
|
||||
decoration: BoxDecoration(
|
||||
color: _getEventColor(log.eventType),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: theme.scaffoldBackgroundColor,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isLast)
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: 2,
|
||||
color: theme.dividerColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// --- CONTENUTO EVENTO ---
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24.0),
|
||||
return IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// --- LINEA E PALLINO ---
|
||||
SizedBox(
|
||||
width: 30,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
log.staffName ?? 'Sistema',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 4),
|
||||
width: 14,
|
||||
height: 14,
|
||||
decoration: BoxDecoration(
|
||||
color: _getEventColor(log.eventType),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: theme.scaffoldBackgroundColor,
|
||||
width: 2,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
_formatDate(log.createdAt),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!isLast)
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: 2,
|
||||
color: theme.dividerColor,
|
||||
),
|
||||
if (!log.isInternal) ...[
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withValues(
|
||||
alpha: 0.1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: Colors.green.withValues(
|
||||
alpha: 0.3,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"PUBBLICO",
|
||||
style: TextStyle(
|
||||
color: Colors.green,
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
log.message,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// --- CONTENUTO EVENTO ---
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
log.staffName ?? 'Sistema',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
_formatDate(log.createdAt),
|
||||
style: theme.textTheme.bodySmall
|
||||
?.copyWith(color: Colors.grey),
|
||||
),
|
||||
if (!log.isInternal) ...[
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withValues(
|
||||
alpha: 0.1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
4,
|
||||
),
|
||||
border: Border.all(
|
||||
color: Colors.green.withValues(
|
||||
alpha: 0.3,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"PUBBLICO",
|
||||
style: TextStyle(
|
||||
color: Colors.green,
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
log.message,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flux/features/tickets/models/ticket_model.dart';
|
||||
|
||||
Future<({TicketResult result, double finalPrice})?> showCompletionTicketDialog(
|
||||
BuildContext context, {
|
||||
required double calculatedTotal,
|
||||
}) async {
|
||||
return showDialog<({TicketResult result, double finalPrice})>(
|
||||
context: context,
|
||||
barrierDismissible: false, // Obbliga l'utente a premere un bottone
|
||||
builder: (context) => CompletionTicketDialog(initialTotal: calculatedTotal),
|
||||
);
|
||||
}
|
||||
|
||||
class CompletionTicketDialog extends StatefulWidget {
|
||||
final double initialTotal;
|
||||
|
||||
const CompletionTicketDialog({super.key, required this.initialTotal});
|
||||
|
||||
@override
|
||||
State<CompletionTicketDialog> createState() => _CompletionTicketDialogState();
|
||||
}
|
||||
|
||||
class _CompletionTicketDialogState extends State<CompletionTicketDialog> {
|
||||
late final TextEditingController _priceCtrl;
|
||||
TicketResult _selectedResult = TicketResult.success; // Default: Riparato!
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Inizializziamo il campo testuale con il totale calcolato
|
||||
_priceCtrl = TextEditingController(
|
||||
text: widget.initialTotal.toStringAsFixed(2),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_priceCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return AlertDialog(
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.check_circle_outline, color: Colors.green),
|
||||
SizedBox(width: 8),
|
||||
Text('Completa Lavorazione'),
|
||||
],
|
||||
),
|
||||
content: SizedBox(
|
||||
width: double.maxFinite, // Per allargare bene la modale
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Specifica l\'esito della lavorazione e il totale da incassare.',
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// --- ESITO LAVORAZIONE (Choice Chips) ---
|
||||
const Text('Esito:', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ChoiceChip(
|
||||
label: const Center(child: Text('Riparato')),
|
||||
selectedColor: Colors.green.withValues(alpha: 0.2),
|
||||
selected: _selectedResult == TicketResult.success,
|
||||
onSelected: (selected) {
|
||||
if (selected) {
|
||||
setState(() => _selectedResult = TicketResult.success);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ChoiceChip(
|
||||
label: const Center(child: Text('Non Riparato')),
|
||||
selectedColor: Colors.red.withValues(alpha: 0.2),
|
||||
selected: _selectedResult == TicketResult.failure,
|
||||
onSelected: (selected) {
|
||||
if (selected) {
|
||||
setState(() => _selectedResult = TicketResult.failure);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// --- PREZZO FINALE ---
|
||||
const Text(
|
||||
'Totale da pagare al ritiro:',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _priceCtrl,
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(
|
||||
RegExp(r'[0-9.,]'),
|
||||
), // Solo numeri, punti e virgole
|
||||
],
|
||||
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.euro),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: theme.colorScheme.surfaceContainerHighest.withValues(
|
||||
alpha: 0.3,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Annulla'),
|
||||
),
|
||||
FilledButton.icon(
|
||||
style: FilledButton.styleFrom(backgroundColor: Colors.green.shade600),
|
||||
onPressed: () {
|
||||
// Conversione sicura: cambiamo un'eventuale virgola italiana in punto
|
||||
final safePriceText = _priceCtrl.text.replaceAll(',', '.');
|
||||
final finalPrice = double.tryParse(safePriceText) ?? 0.0;
|
||||
|
||||
Navigator.of(
|
||||
context,
|
||||
).pop((result: _selectedResult, finalPrice: finalPrice));
|
||||
},
|
||||
icon: const Icon(Icons.save),
|
||||
label: const Text('Conferma Completamento'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/core/routes/routes.dart';
|
||||
import 'package:flux/features/tickets/blocs/ticket_form_cubit.dart';
|
||||
import 'package:flux/features/tickets/blocs/ticket_form_state.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_workspace/completion_ticket_dialog.dart';
|
||||
import 'package:flux/features/tickets/ui/ticket_workspace/pause_ticket_dialog.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class TicketWorkspaceScreen extends StatelessWidget {
|
||||
const TicketWorkspaceScreen({super.key});
|
||||
@@ -285,8 +289,30 @@ class TicketWorkspaceScreen extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
backgroundColor: Colors.green.shade600,
|
||||
),
|
||||
onPressed: () {
|
||||
// TODO: Logica Completa Riparazione
|
||||
onPressed: () async {
|
||||
// 1. Apriamo la nuova modale avanzata e aspettiamo il Record.
|
||||
// (Per ora passiamo 0.0, poi lo sostituiremo con la somma dei costi)
|
||||
final completionData = await showCompletionTicketDialog(
|
||||
context,
|
||||
calculatedTotal: 0.0,
|
||||
);
|
||||
|
||||
// 2. Se l'utente ha premuto "Conferma" e non "Annulla"...
|
||||
if (completionData != null && context.mounted) {
|
||||
// 3. Lanciamo l'azione nel Cubit
|
||||
await context.read<TicketFormCubit>().completeTicket(
|
||||
result: completionData.result,
|
||||
finalPrice: completionData.finalPrice,
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
// 4. Avvisiamo il "Vigile Urbano" di ricaricare la lista
|
||||
context.read<TicketListCubit>().fetchTickets(reset: true);
|
||||
|
||||
// 5. Teletrasporto alla Base
|
||||
context.goNamed(Routes.home);
|
||||
}
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.check_circle_outline),
|
||||
label: const Text(
|
||||
|
||||
@@ -14,17 +14,15 @@ class TrackingState {
|
||||
|
||||
class TrackingCubit extends Cubit<TrackingState> {
|
||||
final TrackingRepository _repo = GetIt.I.get<TrackingRepository>();
|
||||
final String parentId;
|
||||
final TrackingParentType parentType;
|
||||
final String companyId = GetIt.I.get<SessionCubit>().state.company!.id!;
|
||||
|
||||
TrackingCubit({required this.parentId, required this.parentType})
|
||||
: super(TrackingState()) {
|
||||
loadTrackings();
|
||||
}
|
||||
TrackingCubit() : super(TrackingState());
|
||||
|
||||
Future<void> loadTrackings() async {
|
||||
emit(TrackingState(isLoading: true, logs: state.logs));
|
||||
Future<void> loadTrackings(
|
||||
String parentId,
|
||||
TrackingParentType parentType,
|
||||
) async {
|
||||
emit(TrackingState(isLoading: true, logs: []));
|
||||
final trackings = await _repo.getTrackingsByParent(
|
||||
parentId: parentId,
|
||||
parentType: parentType,
|
||||
@@ -32,9 +30,11 @@ class TrackingCubit extends Cubit<TrackingState> {
|
||||
emit(TrackingState(isLoading: false, logs: trackings));
|
||||
}
|
||||
|
||||
Future<void> addManualNote(
|
||||
String message,
|
||||
bool isInternal, {
|
||||
Future<void> addTimelineEvent({
|
||||
required String parentId,
|
||||
required TrackingParentType parentType,
|
||||
required String message,
|
||||
required bool isInternal,
|
||||
String? staffId,
|
||||
}) async {
|
||||
// Aggiungiamo un feedback visivo immediato (Optimistic UI) se vogliamo,
|
||||
@@ -49,6 +49,6 @@ class TrackingCubit extends Cubit<TrackingState> {
|
||||
isInternal: isInternal,
|
||||
);
|
||||
// Ricarichiamo la lista fresca dal server
|
||||
await loadTrackings();
|
||||
await loadTrackings(parentId, parentType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,7 @@ class TrackingRepository {
|
||||
.select('*, staff_member(name)')
|
||||
.eq('parent_id', parentId)
|
||||
.eq('parent_type', parentType.name)
|
||||
.order(
|
||||
'created_at',
|
||||
ascending: true,
|
||||
); // ascending: true per avere la timeline dall'alto (vecchi) al basso (nuovi)
|
||||
.order('created_at', ascending: false);
|
||||
|
||||
return response.map((map) => TrackingModel.fromMap(map)).toList();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user