fixed pages
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user