fixed pages

This commit is contained in:
2026-08-22 08:26:27 +02:00
parent 64eade33f3
commit 770c89ca3f
4 changed files with 276 additions and 79 deletions
+4 -30
View File
@@ -221,19 +221,10 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
} }
Future<void> _openFileUrl(String url) async { Future<void> _openFileUrl(String url) async {
// url tipico: "/userarea/certificate/xxx" // L'API restituisce già "/user/document/<file>".
// fix plural errato se presente // Costruiamo l'URL assoluto senza manipolazioni.
var fixed = url.replaceFirst( final path = url.startsWith('/') ? url : '/$url';
'/userarea/certificates/', final abs = '${ApiConfig.scheme}://${ApiConfig.host}$path';
'/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';
final uri = Uri.parse(abs); final uri = Uri.parse(abs);
final ok = await launchUrl(uri, mode: LaunchMode.externalApplication); final ok = await launchUrl(uri, mode: LaunchMode.externalApplication);
@@ -388,7 +379,6 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
final filename = (c['filename'] ?? '').toString(); final filename = (c['filename'] ?? '').toString();
final uploadedAt = (c['uploaded_at'] ?? '').toString(); final uploadedAt = (c['uploaded_at'] ?? '').toString();
final expiry = (c['expiry_date'] ?? '').toString(); final expiry = (c['expiry_date'] ?? '').toString();
final notes = (c['notes'] ?? '').toString();
final fileUrl = (c['file_url'] ?? '').toString(); final fileUrl = (c['file_url'] ?? '').toString();
return Card( return Card(
@@ -489,11 +479,6 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
], ],
), ),
if (notes.trim().isNotEmpty) ...[
const SizedBox(height: 10),
Text(notes, style: const TextStyle(fontSize: 12)),
],
const SizedBox(height: 10), const SizedBox(height: 10),
Row( Row(
children: [ children: [
@@ -627,17 +612,6 @@ class _UploadCard extends StatelessWidget {
label: Text(expiryLabel), 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), const SizedBox(height: 12),
SizedBox( SizedBox(
+131 -16
View File
@@ -92,23 +92,90 @@ class _OrdersDetailPageState extends State<OrdersDetailPage> {
itemCount: orders.length, itemCount: orders.length,
itemBuilder: (_, i) { itemBuilder: (_, i) {
final o = orders[i]; final o = orders[i];
return Card( return Container(
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: 14),
child: ListTile( decoration: BoxDecoration(
title: Text( color: Colors.white,
o.serviceName.isEmpty borderRadius: BorderRadius.circular(18),
? 'Ordine #${o.orderId}' boxShadow: const [
: o.serviceName, BoxShadow(
style: const TextStyle(fontWeight: FontWeight.w800), blurRadius: 18,
), color: Color(0x12000000),
subtitle: Text( offset: Offset(0, 10),
'Ordine #${o.orderId}${o.tickets} lezioni\n' ),
'Da programmare: ${o.toSchedule} • Scadenza: ${_fmtDate(o.expireOn)}' ],
'${o.isExpired ? " (scaduto)" : ""}', ),
), child: InkWell(
isThreeLine: true, borderRadius: BorderRadius.circular(18),
trailing: const Icon(Icons.chevron_right),
onTap: () => _openDetail(o), 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)" : ""}', '${order.isExpired ? " (scaduto)" : ""}',
style: const TextStyle(fontSize: 12, color: Colors.black54), 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 Divider(height: 24),
const Text( 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,
),
),
],
),
);
}
}
+138 -31
View File
@@ -173,37 +173,10 @@ class _ReschedulePageState extends State<ReschedulePage> {
itemCount: slots.length, itemCount: slots.length,
itemBuilder: (_, i) { itemBuilder: (_, i) {
final s = slots[i]; final s = slots[i];
return Card( return _SlotCard(
margin: const EdgeInsets.only(bottom: 10), slot: s,
child: ListTile( weekday: _weekdayLabel(s.date),
title: Text( onChoose: () => _confirm(s),
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,
),
),
),
); );
}, },
), ),
@@ -262,3 +235,137 @@ class _ReschedulePageState extends State<ReschedulePage> {
} }
} }
} }
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,
),
),
);
}
}
+3 -2
View File
@@ -7,8 +7,9 @@ import 'package:mime/mime.dart';
import '../config/api_config.dart'; import '../config/api_config.dart';
class MedicalCertificatesApi { class MedicalCertificatesApi {
static Uri _u(String path, [Map<String, String>? q]) => static Uri _u(String path, [Map<String, String>? q]) => Uri.parse(
Uri.parse('${ApiConfig.phpApiBase}/$path').replace(queryParameters: q); '${ApiConfig.laravelApiBase}/$path',
).replace(queryParameters: q);
static Future<List<Map<String, dynamic>>> list({ static Future<List<Map<String, dynamic>>> list({
required String token, required String token,