fixed pages
This commit is contained in:
@@ -221,19 +221,10 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
|
||||
}
|
||||
|
||||
Future<void> _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/<file>".
|
||||
// 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<MedicalCertificatesPage> {
|
||||
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<MedicalCertificatesPage> {
|
||||
],
|
||||
),
|
||||
|
||||
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(
|
||||
|
||||
@@ -92,23 +92,90 @@ class _OrdersDetailPageState extends State<OrdersDetailPage> {
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,37 +173,10 @@ class _ReschedulePageState extends State<ReschedulePage> {
|
||||
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<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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@ import 'package:mime/mime.dart';
|
||||
import '../config/api_config.dart';
|
||||
|
||||
class MedicalCertificatesApi {
|
||||
static Uri _u(String path, [Map<String, String>? q]) =>
|
||||
Uri.parse('${ApiConfig.phpApiBase}/$path').replace(queryParameters: q);
|
||||
static Uri _u(String path, [Map<String, String>? q]) => Uri.parse(
|
||||
'${ApiConfig.laravelApiBase}/$path',
|
||||
).replace(queryParameters: q);
|
||||
|
||||
static Future<List<Map<String, dynamic>>> list({
|
||||
required String token,
|
||||
|
||||
Reference in New Issue
Block a user