added pages to project
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../models/lesson.dart';
|
||||
import '../services/lessons_api.dart';
|
||||
import '../widgets/yogibook_background.dart';
|
||||
|
||||
/// Schermata "I miei ordini": lista ordini con conteggi.
|
||||
/// Toccando un ordine si apre il dettaglio (box + lezioni).
|
||||
class OrdersDetailPage extends StatefulWidget {
|
||||
final String token;
|
||||
|
||||
const OrdersDetailPage({super.key, required this.token});
|
||||
|
||||
@override
|
||||
State<OrdersDetailPage> createState() => _OrdersDetailPageState();
|
||||
}
|
||||
|
||||
class _OrdersDetailPageState extends State<OrdersDetailPage> {
|
||||
bool loading = true;
|
||||
String error = '';
|
||||
List<OrderDetail> orders = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
setState(() {
|
||||
loading = true;
|
||||
error = '';
|
||||
});
|
||||
try {
|
||||
final data = await LessonsApi.fetchOrdersDetail(token: widget.token);
|
||||
setState(() => orders = data);
|
||||
} catch (e) {
|
||||
setState(() => error = 'Errore: $e');
|
||||
} finally {
|
||||
setState(() => loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
String _fmtDate(String? raw) {
|
||||
if (raw == null || raw.isEmpty) return '-';
|
||||
try {
|
||||
final dt = DateTime.parse(raw);
|
||||
return DateFormat('dd-MM-yyyy').format(dt);
|
||||
} catch (_) {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
title: const Text(
|
||||
'I miei ordini',
|
||||
style: TextStyle(fontWeight: FontWeight.w800, fontSize: 18),
|
||||
),
|
||||
),
|
||||
body: YogibookBackground(
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: error.isNotEmpty
|
||||
? Center(child: Text(error))
|
||||
: orders.isEmpty
|
||||
? const Center(child: Text('Nessun ordine trovato'))
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
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),
|
||||
onTap: () => _openDetail(o),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openDetail(OrderDetail o) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.white,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (_) => _OrderDetailSheet(order: o, fmtDate: _fmtDate),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OrderDetailSheet extends StatelessWidget {
|
||||
final OrderDetail order;
|
||||
final String Function(String?) fmtDate;
|
||||
|
||||
const _OrderDetailSheet({required this.order, required this.fmtDate});
|
||||
|
||||
String _statusLabel(String s) {
|
||||
switch (s) {
|
||||
case 'completed':
|
||||
return 'Completata';
|
||||
case 'booked':
|
||||
return 'Programmata';
|
||||
case 'lost':
|
||||
return 'Persa';
|
||||
case 'expired':
|
||||
return 'Scaduta';
|
||||
case 'pending':
|
||||
return 'Da confermare';
|
||||
default:
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
Color _statusColor(String s) {
|
||||
switch (s) {
|
||||
case 'completed':
|
||||
return const Color(0xFF28A745);
|
||||
case 'booked':
|
||||
return const Color(0xFF007BFF);
|
||||
case 'lost':
|
||||
return const Color(0xFFDC3545);
|
||||
case 'expired':
|
||||
return const Color(0xFFFF8C00);
|
||||
case 'pending':
|
||||
return const Color(0xFFB8860B);
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
String _fmtDateTime(String? raw) {
|
||||
if (raw == null || raw.isEmpty) return '-';
|
||||
try {
|
||||
final dt = DateTime.parse(raw);
|
||||
return DateFormat('dd-MM-yyyy HH:mm').format(dt);
|
||||
} catch (_) {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: 0.75,
|
||||
maxChildSize: 0.95,
|
||||
minChildSize: 0.5,
|
||||
builder: (_, scrollCtrl) => Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: ListView(
|
||||
controller: scrollCtrl,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black26,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Ordine #${order.orderId}',
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900),
|
||||
),
|
||||
Text(
|
||||
order.serviceName,
|
||||
style: const TextStyle(color: Colors.black54),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Box conteggi
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_statBox('Totale', order.total, const Color(0xFFD1E7DD)),
|
||||
_statBox('Praticate', order.completed, const Color(0xFFD4EDDA)),
|
||||
_statBox('Perse', order.lost, const Color(0xFFF8D7DA)),
|
||||
_statBox('Scadute', order.expired, const Color(0xFFFFF3CD)),
|
||||
_statBox('Programmate', order.booked, const Color(0xFFCCE5FF)),
|
||||
_statBox(
|
||||
'Da confermare',
|
||||
order.pending,
|
||||
const Color(0xFFFDE7C4),
|
||||
),
|
||||
_statBox(
|
||||
'Da programmare',
|
||||
order.toSchedule,
|
||||
const Color(0xFFE2D3F5),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Scadenza: ${fmtDate(order.expireOn)}'
|
||||
'${order.isExpired ? " (scaduto)" : ""}',
|
||||
style: const TextStyle(fontSize: 12, color: Colors.black54),
|
||||
),
|
||||
const Divider(height: 24),
|
||||
|
||||
const Text(
|
||||
'Lezioni',
|
||||
style: TextStyle(fontWeight: FontWeight.w800),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
if (order.lessons.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12),
|
||||
child: Text('Nessuna lezione per questo ordine.'),
|
||||
)
|
||||
else
|
||||
...order.lessons.map(
|
||||
(l) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l.className,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_fmtDateTime(l.datetime),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _statusColor(l.status),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(
|
||||
_statusLabel(l.status),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _statBox(String label, int value, Color bg) {
|
||||
return Container(
|
||||
width: 100,
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'$value',
|
||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w900),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user