From 770c89ca3fd5cdbdb5c30eff83ac6475e2b1bcb6 Mon Sep 17 00:00:00 2001 From: solocla Date: Sat, 22 Aug 2026 08:26:27 +0200 Subject: [PATCH] fixed pages --- lib/screens/medical_certificates_page.dart | 34 +---- lib/screens/orders_detail_page.dart | 147 ++++++++++++++++-- lib/screens/reschedule_page.dart | 169 +++++++++++++++++---- lib/services/medical_certificates_api.dart | 5 +- 4 files changed, 276 insertions(+), 79 deletions(-) diff --git a/lib/screens/medical_certificates_page.dart b/lib/screens/medical_certificates_page.dart index cbd467e..45f2153 100644 --- a/lib/screens/medical_certificates_page.dart +++ b/lib/screens/medical_certificates_page.dart @@ -221,19 +221,10 @@ class _MedicalCertificatesPageState extends State { } Future _openFileUrl(String url) async { - // url tipico: "/userarea/certificate/xxx" - // fix plural errato se presente - var fixed = url.replaceFirst( - '/userarea/certificates/', - '/userarea/certificate/', - ); - - // se non ha /public davanti, lo aggiungiamo - if (!fixed.startsWith('/public/')) { - fixed = '/public${fixed.startsWith('/') ? '' : '/'}$fixed'; - } - - final abs = '${ApiConfig.scheme}://${ApiConfig.host}$fixed'; + // L'API restituisce già "/user/document/". + // Costruiamo l'URL assoluto senza manipolazioni. + final path = url.startsWith('/') ? url : '/$url'; + final abs = '${ApiConfig.scheme}://${ApiConfig.host}$path'; final uri = Uri.parse(abs); final ok = await launchUrl(uri, mode: LaunchMode.externalApplication); @@ -388,7 +379,6 @@ class _MedicalCertificatesPageState extends State { final filename = (c['filename'] ?? '').toString(); final uploadedAt = (c['uploaded_at'] ?? '').toString(); final expiry = (c['expiry_date'] ?? '').toString(); - final notes = (c['notes'] ?? '').toString(); final fileUrl = (c['file_url'] ?? '').toString(); return Card( @@ -489,11 +479,6 @@ class _MedicalCertificatesPageState extends State { ], ), - if (notes.trim().isNotEmpty) ...[ - const SizedBox(height: 10), - Text(notes, style: const TextStyle(fontSize: 12)), - ], - const SizedBox(height: 10), Row( children: [ @@ -627,17 +612,6 @@ class _UploadCard extends StatelessWidget { label: Text(expiryLabel), ), - const SizedBox(height: 10), - - TextField( - controller: notesController, - minLines: 2, - maxLines: 4, - decoration: const InputDecoration( - labelText: 'Note (opzionale)', - border: OutlineInputBorder(), - ), - ), const SizedBox(height: 12), SizedBox( diff --git a/lib/screens/orders_detail_page.dart b/lib/screens/orders_detail_page.dart index 2e03fab..b243901 100644 --- a/lib/screens/orders_detail_page.dart +++ b/lib/screens/orders_detail_page.dart @@ -92,23 +92,90 @@ class _OrdersDetailPageState extends State { itemCount: orders.length, itemBuilder: (_, i) { final o = orders[i]; - return Card( - margin: const EdgeInsets.only(bottom: 12), - child: ListTile( - title: Text( - o.serviceName.isEmpty - ? 'Ordine #${o.orderId}' - : o.serviceName, - style: const TextStyle(fontWeight: FontWeight.w800), - ), - subtitle: Text( - 'Ordine #${o.orderId} • ${o.tickets} lezioni\n' - 'Da programmare: ${o.toSchedule} • Scadenza: ${_fmtDate(o.expireOn)}' - '${o.isExpired ? " (scaduto)" : ""}', - ), - isThreeLine: true, - trailing: const Icon(Icons.chevron_right), + return Container( + margin: const EdgeInsets.only(bottom: 14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(18), + boxShadow: const [ + BoxShadow( + blurRadius: 18, + color: Color(0x12000000), + offset: Offset(0, 10), + ), + ], + ), + child: InkWell( + borderRadius: BorderRadius.circular(18), onTap: () => _openDetail(o), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 14, 12, 14), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + o.serviceName.isEmpty + ? 'Ordine #${o.orderId}' + : o.serviceName, + style: const TextStyle( + fontWeight: FontWeight.w900, + fontSize: 16, + ), + ), + const SizedBox(height: 6), + Text( + 'Ordine #${o.orderId} • ${o.tickets} lezioni', + style: const TextStyle( + fontSize: 12, + color: Colors.black54, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + 'Da programmare: ${o.toSchedule}', + style: const TextStyle( + fontSize: 12, + color: Colors.black54, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + // Riga riprogrammazioni + scadenza (chip) + Wrap( + spacing: 8, + runSpacing: 6, + children: [ + _MiniChip( + icon: Icons.autorenew, + label: + 'Riprog. ${o.reprogrammed}/${o.maxReschedule}', + color: const Color(0xFF10B981), + ), + _MiniChip( + icon: Icons.event_busy, + label: + 'Scad. ${_fmtDate(o.expireOn)}', + color: o.isExpired + ? const Color(0xFFDC3545) + : const Color(0xFFF59E0B), + ), + ], + ), + ], + ), + ), + const SizedBox(width: 6), + const Icon( + Icons.chevron_right, + color: Colors.black38, + ), + ], + ), + ), ), ); }, @@ -242,6 +309,15 @@ class _OrderDetailSheet extends StatelessWidget { '${order.isExpired ? " (scaduto)" : ""}', style: const TextStyle(fontSize: 12, color: Colors.black54), ), + const SizedBox(height: 4), + Text( + 'Riprogrammazioni usate: ${order.reprogrammed} / ${order.maxReschedule}', + style: const TextStyle( + fontSize: 12, + color: Colors.black54, + fontWeight: FontWeight.w600, + ), + ), const Divider(height: 24), const Text( @@ -335,3 +411,42 @@ class _OrderDetailSheet extends StatelessWidget { ); } } + +class _MiniChip extends StatelessWidget { + final IconData icon; + final String label; + final Color color; + + const _MiniChip({ + required this.icon, + required this.label, + required this.color, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(999), + border: Border.all(color: color.withValues(alpha: 0.5), width: 1), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 14, color: color), + const SizedBox(width: 5), + Text( + label, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w800, + color: color, + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/reschedule_page.dart b/lib/screens/reschedule_page.dart index 0a8426f..1eaf983 100644 --- a/lib/screens/reschedule_page.dart +++ b/lib/screens/reschedule_page.dart @@ -173,37 +173,10 @@ class _ReschedulePageState extends State { itemCount: slots.length, itemBuilder: (_, i) { final s = slots[i]; - return Card( - margin: const EdgeInsets.only(bottom: 10), - child: ListTile( - title: Text( - s.className, - style: const TextStyle( - fontWeight: FontWeight.w800, - ), - ), - subtitle: Text( - '${_weekdayLabel(s.date)} ${s.date} • ${s.time}\n' - 'Posti liberi: ${s.freePlaces}/${s.maxCapacity}' - '${s.alreadyBooked ? " • Sei già prenotato" : ""}', - ), - isThreeLine: true, - trailing: s.bookable - ? ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: green, - ), - onPressed: () => _confirm(s), - child: const Text('Scegli'), - ) - : const Text( - 'Non disp.', - style: TextStyle( - color: Colors.black38, - fontWeight: FontWeight.w700, - ), - ), - ), + return _SlotCard( + slot: s, + weekday: _weekdayLabel(s.date), + onChoose: () => _confirm(s), ); }, ), @@ -262,3 +235,137 @@ class _ReschedulePageState extends State { } } } + +class _SlotCard extends StatelessWidget { + final AvailableSlot slot; + final String weekday; + final VoidCallback onChoose; + + const _SlotCard({ + required this.slot, + required this.weekday, + required this.onChoose, + }); + + @override + Widget build(BuildContext context) { + const green = Color(0xFF10B981); + const orange = Color(0xFFF59E0B); + + // Determina lo stato dello slot + final bool isBooked = slot.alreadyBooked; + final bool isFull = !slot.bookable && !isBooked; + final bool isSelectable = slot.bookable && !isBooked; + + // Colori in base allo stato + Color bgColor; + Color borderColor; + if (isBooked) { + bgColor = const Color(0xFFFFF4E5); // arancio chiaro + borderColor = orange; + } else if (isFull) { + bgColor = const Color(0xFFF3F3F5); // grigio chiaro + borderColor = const Color(0xFFD9D9E0); + } else { + bgColor = Colors.white; + borderColor = const Color(0x14000000); + } + + // Etichetta / badge a destra + Widget trailing; + if (isSelectable) { + trailing = ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: green, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: onChoose, + child: const Text('Scegli'), + ); + } else if (isBooked) { + trailing = _StatusBadge(text: 'Già prenotato', color: orange); + } else { + trailing = _StatusBadge( + text: 'Al completo', + color: const Color(0xFF9AA0A6), + ); + } + + return Container( + margin: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.fromLTRB(14, 12, 12, 12), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: borderColor, width: 1.5), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + slot.className, + style: const TextStyle( + fontWeight: FontWeight.w800, + fontSize: 15, + ), + ), + const SizedBox(height: 4), + Text( + '$weekday ${slot.date} • ${slot.time}', + style: const TextStyle( + fontSize: 12, + color: Colors.black54, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + 'Posti liberi: ${slot.freePlaces}/${slot.maxCapacity}', + style: TextStyle( + fontSize: 12, + color: isFull ? orange : Colors.black45, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + const SizedBox(width: 10), + trailing, + ], + ), + ); + } +} + +class _StatusBadge extends StatelessWidget { + final String text; + final Color color; + + const _StatusBadge({required this.text, required this.color}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(999), + border: Border.all(color: color, width: 1.5), + ), + child: Text( + text, + style: TextStyle( + color: color, + fontWeight: FontWeight.w800, + fontSize: 12, + ), + ), + ); + } +} diff --git a/lib/services/medical_certificates_api.dart b/lib/services/medical_certificates_api.dart index 8d9397b..7d6ed43 100644 --- a/lib/services/medical_certificates_api.dart +++ b/lib/services/medical_certificates_api.dart @@ -7,8 +7,9 @@ import 'package:mime/mime.dart'; import '../config/api_config.dart'; class MedicalCertificatesApi { - static Uri _u(String path, [Map? q]) => - Uri.parse('${ApiConfig.phpApiBase}/$path').replace(queryParameters: q); + static Uri _u(String path, [Map? q]) => Uri.parse( + '${ApiConfig.laravelApiBase}/$path', + ).replace(queryParameters: q); static Future>> list({ required String token,