refinements
Some checks failed
Build and Release FLUX (Multi-Platform) / build-android (push) Successful in 1m56s
Build and Release FLUX (Multi-Platform) / build-web (push) Successful in 1m9s
Build and Release FLUX (Multi-Platform) / build-windows (push) Failing after 5m9s

This commit is contained in:
2026-05-25 14:29:48 +02:00
parent 9b5d19b926
commit b19c91a7dd
7 changed files with 165 additions and 63 deletions

View File

@@ -53,6 +53,5 @@ class LatestStoreTicketsBloc
); );
} }
}); });
// TODO: implement event handlers
} }
} }

View File

@@ -321,8 +321,9 @@ class _StaffScreenState extends State<StaffScreen> {
); );
}).toList(), }).toList(),
onChanged: (val) { onChanged: (val) {
if (val != null) if (val != null) {
setModalState(() => selectedRole = val); setModalState(() => selectedRole = val);
}
}, },
), ),
), ),

View File

@@ -138,6 +138,8 @@ class TicketFormCubit extends Cubit<TicketFormState> {
String? assignedToId, String? assignedToId,
String? assignedToName, String? assignedToName,
WarrantyType? warrantyType, WarrantyType? warrantyType,
DateTime? estimatedDeliveryAt,
bool clearEstimatedDelivery = false,
}) { }) {
emit( emit(
state.copyWith( state.copyWith(
@@ -162,6 +164,8 @@ class TicketFormCubit extends Cubit<TicketFormState> {
assignedToId: assignedToId ?? state.ticket.assignedToId, assignedToId: assignedToId ?? state.ticket.assignedToId,
assignedToName: assignedToName ?? state.ticket.assignedToName, assignedToName: assignedToName ?? state.ticket.assignedToName,
warrantyType: warrantyType ?? state.ticket.warrantyType, warrantyType: warrantyType ?? state.ticket.warrantyType,
estimatedDeliveryAt: estimatedDeliveryAt,
clearEstimatedDelivery: clearEstimatedDelivery,
), ),
), ),
); );

View File

@@ -203,6 +203,7 @@ class TicketModel extends Equatable {
TicketType? ticketType, TicketType? ticketType,
TicketStatus? ticketStatus, TicketStatus? ticketStatus,
DateTime? estimatedDeliveryAt, DateTime? estimatedDeliveryAt,
bool clearEstimatedDelivery = false,
TicketResult? ticketResult, TicketResult? ticketResult,
String? resolutionNotes, String? resolutionNotes,
String? shippingDocumentId, String? shippingDocumentId,
@@ -242,7 +243,9 @@ class TicketModel extends Equatable {
hasCourtesyDevice: hasCourtesyDevice ?? this.hasCourtesyDevice, hasCourtesyDevice: hasCourtesyDevice ?? this.hasCourtesyDevice,
ticketType: ticketType ?? this.ticketType, ticketType: ticketType ?? this.ticketType,
ticketStatus: ticketStatus ?? this.ticketStatus, ticketStatus: ticketStatus ?? this.ticketStatus,
estimatedDeliveryAt: estimatedDeliveryAt ?? this.estimatedDeliveryAt, estimatedDeliveryAt: clearEstimatedDelivery
? null
: (estimatedDeliveryAt ?? this.estimatedDeliveryAt),
ticketResult: ticketResult ?? this.ticketResult, ticketResult: ticketResult ?? this.ticketResult,
resolutionNotes: resolutionNotes ?? this.resolutionNotes, resolutionNotes: resolutionNotes ?? this.resolutionNotes,
shippingDocumentId: shippingDocumentId ?? this.shippingDocumentId, shippingDocumentId: shippingDocumentId ?? this.shippingDocumentId,

View File

@@ -141,6 +141,55 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
} }
} }
// Formatta in "GG/MM/AAAA HH:MM"
String _formatDateTime(DateTime dt) {
return "${dt.day.toString().padLeft(2, '0')}/${dt.month.toString().padLeft(2, '0')}/${dt.year} ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}";
}
// Lancia i popup di Data e poi di Ora
Future<void> _selectDeliveryDate(
BuildContext context,
TicketModel ticket,
) async {
final initialDate = ticket.estimatedDeliveryAt ?? DateTime.now();
// 1. Chiediamo la Data
final pickedDate = await showDatePicker(
context: context,
initialDate: initialDate,
firstDate: DateTime(
2020,
), // Oppure DateTime.now() se non vuoi date passate
lastDate: DateTime(2100),
);
if (pickedDate == null) return; // L'utente ha annullato
// 2. Chiediamo l'Ora
if (!context.mounted) return;
final pickedTime = await showTimePicker(
context: context,
initialTime: TimeOfDay.fromDateTime(initialDate),
);
if (pickedTime == null) return; // L'utente ha annullato
// 3. Fondiamo Data e Ora in un unico DateTime
final finalDateTime = DateTime(
pickedDate.year,
pickedDate.month,
pickedDate.day,
pickedTime.hour,
pickedTime.minute,
);
// 4. Aggiorniamo il Cubit
if (!context.mounted) return;
context.read<TicketFormCubit>().updateFields(
estimatedDeliveryAt: finalDateTime,
);
}
Future<String?> _generateIdForQr() async { Future<String?> _generateIdForQr() async {
// 1. Validiamo i campi obbligatori (es. il cliente) // 1. Validiamo i campi obbligatori (es. il cliente)
if (!_formKey.currentState!.validate()) return null; if (!_formKey.currentState!.validate()) return null;
@@ -817,6 +866,37 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
), ),
], ],
), ),
const SizedBox(height: 16),
TextFormField(
readOnly: true, // MAGIA: Impedisce l'apertura della tastiera
// Creiamo un controller "al volo" solo per mostrargli la stringa
controller: TextEditingController(
text: ticket.estimatedDeliveryAt != null
? _formatDateTime(ticket.estimatedDeliveryAt!)
: '',
),
decoration: InputDecoration(
labelText: 'Riconsegna prevista (Data e Ora)',
prefixIcon: const Icon(Icons.event_available),
// Bottone con la X per rimuovere la data se il cliente ti dice "fai con calma"
suffixIcon: ticket.estimatedDeliveryAt != null
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
// NOTA: Dovrai assicurarti che il tuo Cubit gestisca il reset.
// O passi un flag come clearEstimatedDelivery: true,
// o gestisci il null se il tuo updateFields lo permette.
context.read<TicketFormCubit>().updateFields(
clearEstimatedDelivery:
true, // Esempio di flag da aggiungere nel Cubit
);
},
)
: null,
),
// Quando tappi il campo di testo, partono i calendari
onTap: () => _selectDeliveryDate(context, ticket),
),
if (ticket.ticketType == TicketType.repair) ...[ if (ticket.ticketType == TicketType.repair) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
DropdownButtonFormField<WarrantyType>( DropdownButtonFormField<WarrantyType>(

View File

@@ -117,64 +117,76 @@ class TicketList extends StatelessWidget {
AnimatedPositioned( AnimatedPositioned(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut, curve: Curves.easeInOut,
bottom: state.selectedTickets.isNotEmpty bottom: state.selectedTickets.isNotEmpty ? 90 : -100,
? 90 // Mettiamo left e right a 0 per far occupare tutta la larghezza invisibile
: -100, // Nasconde o mostra left: 0,
left: 16, right: 0,
right: 16, child: Align(
alignment: Alignment.bottomCenter,
// 1. IL LIMITE MASSIMO: Su desktop non supererà mai i 600px, su mobile si restringe da solo
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Card( child: Card(
elevation: 8, elevation: 8,
color: Theme.of(context).colorScheme.inverseSurface, color: Theme.of(context).colorScheme.inverseSurface,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(
16,
), // Qui possiamo giocare coi bordi
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16.0, horizontal: 16.0,
vertical: 8.0, vertical: 8.0,
), ),
// ECCO LA MAGIA: Wrap invece di Row! // 2. LA ROW PRINCIPALE: Spinge tutto ai due estremi del nostro "dock"
child: Wrap( child: Row(
alignment: WrapAlignment.spaceBetween, // Sostituisce lo Spacer! mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 8.0, // Spazio orizzontale
runSpacing: 8.0, // Spazio verticale se va a capo
children: [ children: [
// BLOCCO 1: Icona e Contatore // BLOCCO SINISTRO: Chiusura e Contatore
Row( Row(
mainAxisSize: MainAxisSize mainAxisSize: MainAxisSize.min,
.min, // Fondamentale per non occupare tutto il Wrap
children: [ children: [
IconButton( IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: () => color: Theme.of(
context.read<TicketListCubit>().clearSelection(), context,
).colorScheme.onInverseSurface,
onPressed: () => context
.read<TicketListCubit>()
.clearSelection(),
), ),
const SizedBox(width: 8),
Text( Text(
'${state.selectedTickets.length} selezionati', '${state.selectedTickets.length} selezionati',
style: const TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 16, fontSize: 16,
color: Theme.of(
context,
).colorScheme.onInverseSurface,
), ),
), ),
], ],
), ),
// BLOCCO 2: I Bottoni (Un altro Wrap per farli andare a capo tra loro se serve!) // BLOCCO DESTRO: Wrap confinato solo ai bottoni
Wrap( Wrap(
spacing: 8.0, spacing: 8.0,
runSpacing: 8.0, runSpacing: 8.0,
alignment: WrapAlignment.end, alignment: WrapAlignment.end,
children: [ children: [
FilledButton.icon( IconButton.filled(
tooltip: 'Riconsegna',
onPressed: () => _setStatusClosed(context), onPressed: () => _setStatusClosed(context),
icon: const Icon(Icons.approval), icon: const Icon(Icons.approval),
label: const Text('Riconsegna'),
), ),
FilledButton.icon( IconButton.filled(
tooltip: 'Spedisci',
onPressed: () => _showShippingModal(context), onPressed: () => _showShippingModal(context),
icon: const Icon(Icons.local_shipping), icon: const Icon(Icons.local_shipping),
label: const Text('Spedisci'),
), ),
], ],
), ),
@@ -183,6 +195,9 @@ class TicketList extends StatelessWidget {
), ),
), ),
), ),
),
),
),
], ],
); );
} }

View File

@@ -1,7 +1,7 @@
name: flux name: flux
description: "Gestione attività negozio di telefonia" description: "Gestione attività negozio di telefonia"
publish_to: 'none' publish_to: 'none'
version: 1.0.14+14 version: 1.0.15+15
environment: environment:
sdk: ^3.11.3 sdk: ^3.11.3