refinements
This commit is contained in:
@@ -53,6 +53,5 @@ class LatestStoreTicketsBloc
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// TODO: implement event handlers
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>(
|
||||||
|
|||||||
@@ -117,68 +117,83 @@ 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(
|
||||||
child: Card(
|
alignment: Alignment.bottomCenter,
|
||||||
elevation: 8,
|
// 1. IL LIMITE MASSIMO: Su desktop non supererà mai i 600px, su mobile si restringe da solo
|
||||||
color: Theme.of(context).colorScheme.inverseSurface,
|
child: ConstrainedBox(
|
||||||
shape: RoundedRectangleBorder(
|
constraints: const BoxConstraints(maxWidth: 600),
|
||||||
borderRadius: BorderRadius.circular(16),
|
child: Padding(
|
||||||
),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
child: Padding(
|
child: Card(
|
||||||
padding: const EdgeInsets.symmetric(
|
elevation: 8,
|
||||||
horizontal: 16.0,
|
color: Theme.of(context).colorScheme.inverseSurface,
|
||||||
vertical: 8.0,
|
shape: RoundedRectangleBorder(
|
||||||
),
|
borderRadius: BorderRadius.circular(
|
||||||
// ECCO LA MAGIA: Wrap invece di Row!
|
16,
|
||||||
child: Wrap(
|
), // Qui possiamo giocare coi bordi
|
||||||
alignment: WrapAlignment.spaceBetween, // Sostituisce lo Spacer!
|
),
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
child: Padding(
|
||||||
spacing: 8.0, // Spazio orizzontale
|
padding: const EdgeInsets.symmetric(
|
||||||
runSpacing: 8.0, // Spazio verticale se va a capo
|
horizontal: 16.0,
|
||||||
children: [
|
vertical: 8.0,
|
||||||
// BLOCCO 1: Icona e Contatore
|
),
|
||||||
Row(
|
// 2. LA ROW PRINCIPALE: Spinge tutto ai due estremi del nostro "dock"
|
||||||
mainAxisSize: MainAxisSize
|
child: Row(
|
||||||
.min, // Fondamentale per non occupare tutto il Wrap
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
// BLOCCO SINISTRO: Chiusura e Contatore
|
||||||
icon: const Icon(Icons.close),
|
Row(
|
||||||
onPressed: () =>
|
mainAxisSize: MainAxisSize.min,
|
||||||
context.read<TicketListCubit>().clearSelection(),
|
children: [
|
||||||
),
|
IconButton(
|
||||||
Text(
|
icon: const Icon(Icons.close),
|
||||||
'${state.selectedTickets.length} selezionati',
|
color: Theme.of(
|
||||||
style: const TextStyle(
|
context,
|
||||||
fontWeight: FontWeight.bold,
|
).colorScheme.onInverseSurface,
|
||||||
fontSize: 16,
|
onPressed: () => context
|
||||||
|
.read<TicketListCubit>()
|
||||||
|
.clearSelection(),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'${state.selectedTickets.length} selezionati',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
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(
|
||||||
onPressed: () => _setStatusClosed(context),
|
tooltip: 'Riconsegna',
|
||||||
icon: const Icon(Icons.approval),
|
onPressed: () => _setStatusClosed(context),
|
||||||
label: const Text('Riconsegna'),
|
icon: const Icon(Icons.approval),
|
||||||
),
|
),
|
||||||
FilledButton.icon(
|
IconButton.filled(
|
||||||
onPressed: () => _showShippingModal(context),
|
tooltip: 'Spedisci',
|
||||||
icon: const Icon(Icons.local_shipping),
|
onPressed: () => _showShippingModal(context),
|
||||||
label: const Text('Spedisci'),
|
icon: const Icon(Icons.local_shipping),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user