edit backgroun and inmclude

This commit is contained in:
2026-01-18 21:33:01 +01:00
parent 1a41a99e20
commit 49a9d2a2a6
17 changed files with 1575 additions and 794 deletions
+114 -160
View File
@@ -9,6 +9,9 @@ import 'meditation_page.dart';
import '../services/vanguard_api.dart';
import 'login_page.dart';
import 'medical_certificates_page.dart';
import '../widgets/app_drawer.dart';
import '../widgets/app_bottom_nav.dart';
import '../widgets/yogibook_background.dart';
class HomePage extends StatefulWidget {
final String token;
@@ -251,60 +254,14 @@ class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: kBg,
drawer: Drawer(
child: SafeArea(
child: ListView(
padding: EdgeInsets.zero,
children: [
const DrawerHeader(
child: Align(
alignment: Alignment.bottomLeft,
child: Text(
'Menu',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w900),
),
),
),
ListTile(
leading: const Icon(Icons.swap_horiz),
title: const Text('Cambia scuola'),
onTap: () {
Navigator.of(context).pop();
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => SelectSchoolPage(token: widget.token),
),
);
},
),
ListTile(
leading: const Icon(Icons.medical_information),
title: const Text('Certificati medici'),
onTap: () {
Navigator.of(context).pop(); // chiude drawer
Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
MedicalCertificatesPage(token: widget.token),
),
);
},
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.logout),
title: const Text('Logout'),
onTap: _logout,
),
],
),
),
drawer: AppDrawer(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
appBar: AppBar(
backgroundColor: kBg,
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
title: const Text(
@@ -329,128 +286,125 @@ class _HomePageState extends State<HomePage> {
),
],
),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed, // con 4 icone serve
backgroundColor: Colors.white,
selectedItemColor: const Color(0xFF10B981),
unselectedItemColor: Colors.black54,
bottomNavigationBar: AppBottomNav(
currentIndex: bottomIndex,
onTap: _handleBottomNav,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_rounded),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.event_note_rounded),
label: 'Lezioni',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_rounded),
label: 'Account',
),
BottomNavigationBarItem(
icon: Icon(Icons.self_improvement_rounded),
label: 'Meditazione',
),
],
),
body: Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Ciao + nome
Text(
_name.isNotEmpty ? 'Ciao, $_name' : 'Ciao',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900),
),
const SizedBox(height: 10),
body: YogibookBackground(
child: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
// Nome scuola + indirizzo sotto
Text(
widget.school.name,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w900),
),
if (_schoolAddress.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
_schoolAddress,
style: const TextStyle(
fontSize: 12,
color: Colors.black54,
fontWeight: FontWeight.w600,
children: [
// Ciao + nome
Text(
_name.isNotEmpty ? 'Ciao, $_name' : 'Ciao',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w900,
),
),
),
],
const SizedBox(height: 10),
const SizedBox(height: 10),
// Pulsante mappe
OutlinedButton.icon(
onPressed: _openMaps,
icon: const Icon(Icons.directions, size: 18, color: kGreen),
label: const Text(
'Apri Mappe',
style: TextStyle(fontWeight: FontWeight.w800, color: kGreen),
),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: kGreen, width: 2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
// Nome scuola + indirizzo sotto
Text(
widget.school.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w900,
),
),
),
),
const SizedBox(height: 14),
// Tiles
_HomeTile(
icon: Icons.event_note_rounded,
title: 'Vedi lezioni',
subtitle: 'Prenotazioni e gestione lezioni',
onTap: _goLessons,
),
_HomeTile(
icon: Icons.shopping_bag_rounded,
title: 'Vedi ordini',
subtitle: 'Storico e stato ordini (TODO)',
onTap: () => ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('TODO: Ordini'))),
),
_HomeTile(
icon: Icons.person_rounded,
title: 'Account',
subtitle: 'Profilo e impostazioni (TODO)',
onTap: () => ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('TODO: Account'))),
),
const Spacer(),
// Logo + frase zen
Center(
child: Column(
children: [
if (_schoolAddress.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
'$zenQuote',
textAlign: TextAlign.center,
_schoolAddress,
style: const TextStyle(
fontSize: 16,
height: 1.3,
color: Colors.black87,
fontWeight: FontWeight.w700,
fontSize: 12,
color: Colors.black54,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 14),
Image.asset('assets/images/yogibook_logo.png', height: 78),
const SizedBox(height: 6),
],
),
const SizedBox(height: 10),
// Pulsante mappe
OutlinedButton.icon(
onPressed: _openMaps,
icon: const Icon(Icons.directions, size: 18, color: kGreen),
label: const Text(
'Apri Mappe',
style: TextStyle(
fontWeight: FontWeight.w800,
color: kGreen,
),
),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: kGreen, width: 2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
),
const SizedBox(height: 14),
// Tiles
_HomeTile(
icon: Icons.event_note_rounded,
title: 'Vedi lezioni',
subtitle: 'Prenotazioni e gestione lezioni',
onTap: _goLessons,
),
_HomeTile(
icon: Icons.shopping_bag_rounded,
title: 'Vedi ordini',
subtitle: 'Storico e stato ordini (TODO)',
onTap: () => ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('TODO: Ordini'))),
),
_HomeTile(
icon: Icons.person_rounded,
title: 'Account',
subtitle: 'Profilo e impostazioni (TODO)',
onTap: () => ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('TODO: Account')),
),
),
const Spacer(),
// Logo + frase zen
Center(
child: Column(
children: [
Text(
'$zenQuote',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
height: 1.3,
color: Colors.black87,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 14),
Image.asset(
'assets/images/yogibook_logo.png',
height: 78,
),
const SizedBox(height: 6),
],
),
),
],
),
],
),
),
),
);
+101 -120
View File
@@ -6,6 +6,9 @@ import '../services/vanguard_api.dart';
import 'select_school_page.dart';
import 'home_page.dart';
import 'meditation_page.dart';
import '../widgets/app_drawer.dart';
import '../widgets/app_bottom_nav.dart';
import '../widgets/yogibook_background.dart';
class LessonsPage extends StatefulWidget {
final String token;
@@ -136,18 +139,14 @@ class _LessonsPageState extends State<LessonsPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF6F6FB),
drawer: _AppDrawer(
drawer: AppDrawer(
token: widget.token,
onLogout: () {
Navigator.of(context).pop();
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Logout (TODO)')));
},
school: widget.school,
userFirstName: widget.userFirstName,
),
appBar: AppBar(
backgroundColor: const Color(0xFFF6F6FB),
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
title: Column(
@@ -185,127 +184,109 @@ class _LessonsPageState extends State<LessonsPage> {
),
],
),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed, // con 4 icone serve
backgroundColor: Colors.white,
selectedItemColor: const Color(0xFF10B981),
unselectedItemColor: Colors.black54,
bottomNavigationBar: AppBottomNav(
currentIndex: bottomIndex,
onTap: _handleBottomNav,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_rounded),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.event_note_rounded),
label: 'Lezioni',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_rounded),
label: 'Account',
),
BottomNavigationBarItem(
icon: Icon(Icons.self_improvement_rounded),
label: 'Meditazione',
),
],
),
body: SafeArea(
top: false,
child: Column(
children: [
// Address small (optional)
if (schoolAddress != null && schoolAddress!.trim().isNotEmpty)
Padding(
padding: const EdgeInsets.fromLTRB(16, 6, 16, 6),
child: Text(
schoolAddress!,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 12,
color: Colors.black45,
fontWeight: FontWeight.w500,
body: YogibookBackground(
child: SafeArea(
top: false,
child: Column(
children: [
// Address small (optional)
if (schoolAddress != null && schoolAddress!.trim().isNotEmpty)
Padding(
padding: const EdgeInsets.fromLTRB(16, 6, 16, 6),
child: Text(
schoolAddress!,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 12,
color: Colors.black45,
fontWeight: FontWeight.w500,
),
),
),
// Month selector only
Padding(
padding: const EdgeInsets.fromLTRB(16, 6, 16, 10),
child: _MonthPillCompact(
label: _monthLabel(currentMonth),
onPrev: prevMonth == null
? null
: () {
setState(() => currentMonth = prevMonth!);
_load();
},
onNext: nextMonth == null
? null
: () {
setState(() => currentMonth = nextMonth!);
_load();
},
),
),
// Month selector only
Padding(
padding: const EdgeInsets.fromLTRB(16, 6, 16, 10),
child: _MonthPillCompact(
label: _monthLabel(currentMonth),
onPrev: prevMonth == null
? null
: () {
setState(() => currentMonth = prevMonth!);
_load();
},
onNext: nextMonth == null
? null
: () {
setState(() => currentMonth = nextMonth!);
_load();
},
),
),
Expanded(
child: loading
? const Center(child: CircularProgressIndicator())
: error.isNotEmpty
? _ErrorBox(message: error, onRetry: _load)
: lessons.isEmpty
? _EmptyState(
onBrowse: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('TODO: vedi corsi')),
);
},
)
: ListView.builder(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
itemCount: lessons.length + 1,
itemBuilder: (_, i) {
if (i == lessons.length) {
return Padding(
padding: const EdgeInsets.only(top: 8, bottom: 6),
child: Center(
child: Image.asset(
'assets/images/yogibook_logo.png',
height: 56,
),
),
Expanded(
child: loading
? const Center(child: CircularProgressIndicator())
: error.isNotEmpty
? _ErrorBox(message: error, onRetry: _load)
: lessons.isEmpty
? _EmptyState(
onBrowse: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('TODO: vedi corsi')),
);
}
},
)
: ListView.builder(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
itemCount: lessons.length + 1,
itemBuilder: (_, i) {
if (i == lessons.length) {
return Padding(
padding: const EdgeInsets.only(top: 8, bottom: 6),
child: Center(
child: Image.asset(
'assets/images/yogibook_logo.png',
height: 56,
),
),
);
}
final l = lessons[i];
return _LessonGreenCardCompact(
lesson: l,
weekday: _weekdayLabel(l.date),
dayNum: _dayNum(l.date),
onReschedule: l.canModify
? () =>
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Riprogramma: API dopo'),
),
)
: null,
onCancel: l.canModify
? () =>
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Cancella: API dopo'),
),
)
: null,
);
},
),
),
],
final l = lessons[i];
return _LessonGreenCardCompact(
lesson: l,
weekday: _weekdayLabel(l.date),
dayNum: _dayNum(l.date),
onReschedule: l.canModify
? () => ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
content: Text(
'Riprogramma: API dopo',
),
),
)
: null,
onCancel: l.canModify
? () => ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
content: Text('Cancella: API dopo'),
),
)
: null,
);
},
),
),
],
),
),
),
);
+303 -204
View File
@@ -1,3 +1,4 @@
// lib/screens/medical_certificates_page.dart
import 'dart:io';
import 'package:flutter/material.dart';
@@ -5,13 +6,29 @@ import 'package:image_picker/image_picker.dart';
import 'package:file_picker/file_picker.dart';
import 'package:url_launcher/url_launcher.dart';
import '../models/school.dart';
import '../services/medical_certificates_api.dart';
import '../config/api_config.dart';
import '../widgets/app_drawer.dart';
import '../widgets/app_bottom_nav.dart';
import 'home_page.dart';
import 'lessons_page.dart';
import 'meditation_page.dart';
import '../widgets/yogibook_background.dart';
class MedicalCertificatesPage extends StatefulWidget {
final String token;
final School school;
final String? userFirstName;
const MedicalCertificatesPage({super.key, required this.token});
const MedicalCertificatesPage({
super.key,
required this.token,
required this.school,
this.userFirstName,
});
@override
State<MedicalCertificatesPage> createState() =>
@@ -19,6 +36,8 @@ class MedicalCertificatesPage extends StatefulWidget {
}
class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
static const Color kBg = Color(0xFFF6F6FB);
bool loading = true;
String error = '';
List<Map<String, dynamic>> certs = [];
@@ -31,6 +50,9 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
DateTime? expiryDate;
bool uploading = false;
// ✅ questa pagina è "sezione account"
int bottomIndex = 2;
@override
void initState() {
super.initState();
@@ -60,7 +82,6 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
}
Future<void> _pickFromChooser() async {
// 1 solo bottone -> bottom sheet scelta
final choice = await showModalBottomSheet<String>(
context: context,
showDragHandle: true,
@@ -123,7 +144,6 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
setState(() {
pickedFile = f;
pickedLabel = label;
// reset campi per nuovo upload
docNameCtrl.text = 'certificato';
notesCtrl.clear();
expiryDate = null;
@@ -182,7 +202,6 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
notes: notesCtrl.text,
);
// reset form
setState(() {
pickedFile = null;
pickedLabel = '';
@@ -205,10 +224,21 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
}
Future<void> _openFileUrl(String url) async {
// url arriva tipo "/userarea/certificate/xxx"
// per aprirlo serve url assoluto
final abs = '${ApiConfig.scheme}://${ApiConfig.host}$url';
// 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';
final uri = Uri.parse(abs);
final ok = await launchUrl(uri, mode: LaunchMode.externalApplication);
if (!ok && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -256,10 +286,67 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
bool _isPdfPath(String p) => p.toLowerCase().endsWith('.pdf');
Future<void> _goTo(Widget page) async {
if (!mounted) return;
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => page));
}
void _handleBottomNav(int i) {
if (i == bottomIndex) return;
setState(() => bottomIndex = i);
if (i == 0) {
_goTo(
HomePage(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
);
return;
}
if (i == 1) {
_goTo(
LessonsPage(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
);
return;
}
if (i == 2) {
// siamo già in "area account", non facciamo nulla (o TODO)
return;
}
if (i == 3) {
_goTo(
MeditationPage(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
);
return;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
// ✅ Drawer standard (come MeditationPage)
drawer: AppDrawer(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: const Text(
'Certificati medici',
style: TextStyle(fontWeight: FontWeight.w900),
@@ -271,206 +358,223 @@ class _MedicalCertificatesPageState extends State<MedicalCertificatesPage> {
),
],
),
body: RefreshIndicator(
onRefresh: _reload,
child: ListView(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 22),
children: [
// 1 SOLO bottone in alto
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: uploading ? null : _pickFromChooser,
icon: const Icon(Icons.cloud_upload),
label: const Text('Carica certificato'),
),
),
const SizedBox(height: 12),
// ✅ Bottom nav standard (come MeditationPage)
bottomNavigationBar: AppBottomNav(
currentIndex: bottomIndex,
onTap: _handleBottomNav,
),
// Preview + form (solo se file selezionato)
if (pickedFile != null) ...[
_UploadCard(
filename: pickedLabel.isNotEmpty
? pickedLabel
: pickedFile!.path.split(Platform.pathSeparator).last,
isImage: _isImagePath(pickedFile!.path),
isPdf: _isPdfPath(pickedFile!.path),
file: pickedFile!,
documentNameController: docNameCtrl,
notesController: notesCtrl,
expiryDate: expiryDate,
onPickExpiry: _pickExpiry,
onUpload: uploading ? null : _doUpload,
uploading: uploading,
expiryLabel: expiryDate == null
? 'Seleziona data'
: _fmtDDMMYYYY(expiryDate!),
),
const SizedBox(height: 14),
],
if (error.isNotEmpty) ...[
Text(error, style: const TextStyle(color: Colors.red)),
const SizedBox(height: 10),
],
Text(
'Certificati caricati (${certs.length})',
style: const TextStyle(fontWeight: FontWeight.w900, fontSize: 16),
),
const SizedBox(height: 10),
if (loading)
const Padding(
padding: EdgeInsets.only(top: 30),
child: Center(child: CircularProgressIndicator()),
)
else if (certs.isEmpty)
const Padding(
padding: EdgeInsets.only(top: 26),
child: Center(child: Text('Nessun certificato caricato')),
)
else
...certs.map((c) {
final expired = c['is_expired'] == true;
final docName = (c['document_name'] ?? '').toString();
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(
elevation: 0,
color: expired ? const Color(0xFFFFEBEE) : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
body: YogibookBackground(
child: SafeArea(
top: false,
child: RefreshIndicator(
onRefresh: _reload,
child: ListView(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 22),
children: [
// 1 SOLO bottone in alto
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: uploading ? null : _pickFromChooser,
icon: const Icon(Icons.cloud_upload),
label: const Text('Carica certificato'),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
),
const SizedBox(height: 12),
// Preview + form (solo se file selezionato)
if (pickedFile != null) ...[
_UploadCard(
filename: pickedLabel.isNotEmpty
? pickedLabel
: pickedFile!.path.split(Platform.pathSeparator).last,
isImage: _isImagePath(pickedFile!.path),
isPdf: _isPdfPath(pickedFile!.path),
file: pickedFile!,
documentNameController: docNameCtrl,
notesController: notesCtrl,
expiryDate: expiryDate,
onPickExpiry: _pickExpiry,
onUpload: uploading ? null : _doUpload,
uploading: uploading,
expiryLabel: expiryDate == null
? 'Seleziona data'
: _fmtDDMMYYYY(expiryDate!),
),
const SizedBox(height: 14),
],
if (error.isNotEmpty) ...[
Text(error, style: const TextStyle(color: Colors.red)),
const SizedBox(height: 10),
],
Text(
'Certificati caricati (${certs.length})',
style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 16,
),
),
const SizedBox(height: 10),
if (loading)
const Padding(
padding: EdgeInsets.only(top: 30),
child: Center(child: CircularProgressIndicator()),
)
else if (certs.isEmpty)
const Padding(
padding: EdgeInsets.only(top: 26),
child: Center(child: Text('Nessun certificato caricato')),
)
else
...certs.map((c) {
final expired = c['is_expired'] == true;
final docName = (c['document_name'] ?? '').toString();
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(
elevation: 0,
color: expired ? const Color(0xFFFFEBEE) : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
docName.isNotEmpty ? docName : 'certificato',
style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 15,
),
),
),
if (expired)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: const Color(0xFFDC3545),
borderRadius: BorderRadius.circular(999),
),
child: const Text(
'SCADUTO',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w900,
fontSize: 11,
Row(
children: [
Expanded(
child: Text(
docName.isNotEmpty
? docName
: 'certificato',
style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 15,
),
),
),
),
],
),
const SizedBox(height: 6),
Text(
filename,
style: const TextStyle(
color: Colors.black54,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
const SizedBox(height: 10),
Row(
children: [
const Icon(
Icons.event,
size: 16,
color: Colors.black54,
if (expired)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: const Color(0xFFDC3545),
borderRadius: BorderRadius.circular(999),
),
child: const Text(
'SCADUTO',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w900,
fontSize: 11,
),
),
),
],
),
const SizedBox(width: 6),
Expanded(
child: Text(
'Caricato: $uploadedAt',
style: const TextStyle(
const SizedBox(height: 6),
Text(
filename,
style: const TextStyle(
color: Colors.black54,
fontWeight: FontWeight.w600,
fontSize: 12,
),
),
const SizedBox(height: 10),
Row(
children: [
const Icon(
Icons.event,
size: 16,
color: Colors.black54,
fontSize: 12,
),
),
),
],
),
const SizedBox(height: 6),
Row(
children: [
const Icon(
Icons.timer,
size: 16,
color: Colors.black54,
),
const SizedBox(width: 6),
Expanded(
child: Text(
'Scadenza: ${expiry.isEmpty ? '' : expiry}',
style: TextStyle(
color: expired
? const Color(0xFFDC3545)
: Colors.black54,
fontSize: 12,
const SizedBox(width: 6),
Expanded(
child: Text(
'Caricato: $uploadedAt',
style: const TextStyle(
color: Colors.black54,
fontSize: 12,
),
),
),
),
],
),
const SizedBox(height: 6),
Row(
children: [
const Icon(
Icons.timer,
size: 16,
color: Colors.black54,
),
const SizedBox(width: 6),
Expanded(
child: Text(
'Scadenza: ${expiry.isEmpty ? '' : expiry}',
style: TextStyle(
color: expired
? const Color(0xFFDC3545)
: Colors.black54,
fontSize: 12,
),
),
),
],
),
if (notes.trim().isNotEmpty) ...[
const SizedBox(height: 10),
Text(notes, style: const TextStyle(fontSize: 12)),
],
const SizedBox(height: 10),
Row(
children: [
TextButton.icon(
onPressed: () => _openFileUrl(fileUrl),
icon: const Icon(Icons.open_in_new),
label: const Text('Apri'),
),
const Spacer(),
TextButton.icon(
onPressed: () =>
_deleteCert((c['id'] as num).toInt()),
icon: const Icon(
Icons.delete_outline,
color: Colors.red,
),
label: const Text(
'Elimina',
style: TextStyle(color: Colors.red),
),
),
],
),
],
),
if (notes.trim().isNotEmpty) ...[
const SizedBox(height: 10),
Text(notes, style: const TextStyle(fontSize: 12)),
],
const SizedBox(height: 10),
Row(
children: [
TextButton.icon(
onPressed: () => _openFileUrl(fileUrl),
icon: const Icon(Icons.open_in_new),
label: const Text('Apri'),
),
const Spacer(),
TextButton.icon(
onPressed: () =>
_deleteCert((c['id'] as num).toInt()),
icon: const Icon(
Icons.delete_outline,
color: Colors.red,
),
label: const Text(
'Elimina',
style: TextStyle(color: Colors.red),
),
),
],
),
],
),
),
);
}),
],
),
);
}),
],
),
),
),
),
);
@@ -567,17 +671,12 @@ class _UploadCard extends StatelessWidget {
),
const SizedBox(height: 10),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: onPickExpiry,
icon: const Icon(Icons.date_range),
label: Text(expiryLabel),
),
),
],
OutlinedButton.icon(
onPressed: onPickExpiry,
icon: const Icon(Icons.date_range),
label: Text(expiryLabel),
),
const SizedBox(height: 10),
TextField(
+182 -204
View File
@@ -8,6 +8,9 @@ import 'home_page.dart';
import 'lessons_page.dart';
import 'select_school_page.dart';
import 'login_page.dart';
import '../widgets/app_drawer.dart';
import '../widgets/app_bottom_nav.dart';
import '../widgets/yogibook_background.dart';
class MeditationPage extends StatefulWidget {
final String token;
@@ -288,41 +291,14 @@ class _MeditationPageState extends State<MeditationPage>
).animate(CurvedAnimation(parent: _pulseCtrl, curve: Curves.easeInOut));
return Scaffold(
backgroundColor: kBg,
drawer: Drawer(
child: SafeArea(
child: ListView(
padding: EdgeInsets.zero,
children: [
const DrawerHeader(
child: Align(
alignment: Alignment.bottomLeft,
child: Text(
'Menu',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w900),
),
),
),
ListTile(
leading: const Icon(Icons.swap_horiz),
title: const Text('Cambia scuola'),
onTap: () async {
Navigator.of(context).pop(); // chiude drawer
await _goTo(SelectSchoolPage(token: widget.token));
},
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.logout),
title: const Text('Logout'),
onTap: _logout,
),
],
),
),
drawer: AppDrawer(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
appBar: AppBar(
backgroundColor: kBg,
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
title: const Text(
@@ -381,203 +357,205 @@ class _MeditationPageState extends State<MeditationPage>
),
],
),
body: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 16),
child: Column(
children: [
// Top card with progress
Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(14, 12, 14, 14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
boxShadow: const [
BoxShadow(
blurRadius: 18,
color: Color(0x12000000),
offset: Offset(0, 10),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Respiro consapevole',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w900,
body: YogibookBackground(
child: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 16),
child: Column(
children: [
// Top card with progress
Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(14, 12, 14, 14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
boxShadow: const [
BoxShadow(
blurRadius: 18,
color: Color(0x12000000),
offset: Offset(0, 10),
),
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(999),
child: LinearProgressIndicator(
minHeight: 8,
value: _progress,
backgroundColor: const Color(0xFFE9E9EF),
valueColor: const AlwaysStoppedAnimation(
Color(0xFFF59E0B),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Respiro consapevole',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w900,
),
),
),
],
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(999),
child: LinearProgressIndicator(
minHeight: 8,
value: _progress,
backgroundColor: const Color(0xFFE9E9EF),
valueColor: const AlwaysStoppedAnimation(
Color(0xFFF59E0B),
),
),
),
],
),
),
),
const SizedBox(height: 20),
const SizedBox(height: 20),
// Square + countdown
Expanded(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(top: 8, bottom: 8),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedBuilder(
animation: Listenable.merge([
_squareCtrl,
bubbleScale,
]),
builder: (_, __) {
final scale = bubbleScale.value;
// Square + countdown
Expanded(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(top: 8, bottom: 8),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedBuilder(
animation: Listenable.merge([
_squareCtrl,
bubbleScale,
]),
builder: (_, __) {
final scale = bubbleScale.value;
return Transform.scale(
scale: scale,
child: CustomPaint(
painter: _SquareBreathPainter(
t: _squareCtrl.value,
color: kGreen,
),
child: SizedBox(
width: 240,
height: 240,
child: Center(
child: Text(
_breathLabel,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
fontSize: 18,
return Transform.scale(
scale: scale,
child: CustomPaint(
painter: _SquareBreathPainter(
t: _squareCtrl.value,
color: kGreen,
),
child: SizedBox(
width: 240,
height: 240,
child: Center(
child: Text(
_breathLabel,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
fontSize: 18,
),
),
),
),
),
),
);
},
),
const SizedBox(height: 24),
Text(
_mmss(remainingSeconds),
style: const TextStyle(
fontSize: 44,
fontWeight: FontWeight.w900,
color: Color(0xFF1F1F1F),
);
},
),
),
const SizedBox(height: 6),
const Text(
'Tempo rimanente',
style: TextStyle(
color: Colors.black45,
fontWeight: FontWeight.w600,
const SizedBox(height: 24),
Text(
_mmss(remainingSeconds),
style: const TextStyle(
fontSize: 44,
fontWeight: FontWeight.w900,
color: Color(0xFF1F1F1F),
),
),
),
const SizedBox(height: 18),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: 44,
child: ElevatedButton.icon(
onPressed: running ? _pause : _start,
style: ElevatedButton.styleFrom(
backgroundColor: kGreen,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
icon: Icon(
running ? Icons.pause : Icons.play_arrow,
),
label: Text(running ? 'Pausa' : 'Avvia'),
),
const SizedBox(height: 6),
const Text(
'Tempo rimanente',
style: TextStyle(
color: Colors.black45,
fontWeight: FontWeight.w600,
),
const SizedBox(width: 12),
SizedBox(
height: 44,
child: OutlinedButton(
onPressed: _reset,
style: OutlinedButton.styleFrom(
side: const BorderSide(
color: kGreen,
width: 2,
),
const SizedBox(height: 18),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: 44,
child: ElevatedButton.icon(
onPressed: running ? _pause : _start,
style: ElevatedButton.styleFrom(
backgroundColor: kGreen,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
icon: Icon(
running ? Icons.pause : Icons.play_arrow,
),
label: Text(running ? 'Pausa' : 'Avvia'),
),
child: const Text(
'Reset',
style: TextStyle(
color: Colors.black87,
fontWeight: FontWeight.w800,
),
const SizedBox(width: 12),
SizedBox(
height: 44,
child: OutlinedButton(
onPressed: _reset,
style: OutlinedButton.styleFrom(
side: const BorderSide(
color: kGreen,
width: 2,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
child: const Text(
'Reset',
style: TextStyle(
color: Colors.black87,
fontWeight: FontWeight.w800,
),
),
),
),
),
],
),
],
),
),
),
),
),
SizedBox(
width: double.infinity,
height: 52,
child: ElevatedButton(
onPressed: () async {
_pause();
await _stopMusic();
if (!mounted) return;
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => HomePage(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
],
),
],
),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: kGreen,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Text(
'TERMINA SESSIONE',
style: TextStyle(fontWeight: FontWeight.w900),
),
SizedBox(
width: double.infinity,
height: 52,
child: ElevatedButton(
onPressed: () async {
_pause();
await _stopMusic();
if (!mounted) return;
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => HomePage(
token: widget.token,
school: widget.school,
userFirstName: widget.userFirstName,
),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: kGreen,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
child: const Text(
'TERMINA SESSIONE',
style: TextStyle(fontWeight: FontWeight.w900),
),
),
),
),
],
],
),
),
),
),
+55 -105
View File
@@ -1,10 +1,14 @@
import 'package:flutter/material.dart';
import '../models/school.dart';
import '../services/vanguard_api.dart';
import 'lessons_page.dart';
import 'home_page.dart';
import 'meditation_page.dart';
import 'login_page.dart';
import 'package:provider/provider.dart';
import '../state/app_state.dart';
import '../widgets/yogibook_background.dart';
// TODO (step successivo): creeremo questi due file
// import '../services/settings_bootstrap.dart';
class SelectSchoolPage extends StatefulWidget {
final String token;
@@ -15,7 +19,6 @@ class SelectSchoolPage extends StatefulWidget {
}
class _SelectSchoolPageState extends State<SelectSchoolPage> {
static const Color kBg = Color(0xFFF6F6FB);
static const Color kGreen = Color(0xFF10B981);
bool loading = true;
@@ -23,8 +26,6 @@ class _SelectSchoolPageState extends State<SelectSchoolPage> {
String? firstName;
List<School> schools = [];
int bottomIndex = 1; // 0=Home, 1=Lezioni, 2=Account (qui siamo in "Lezioni")
@override
void initState() {
super.initState();
@@ -51,26 +52,50 @@ class _SelectSchoolPageState extends State<SelectSchoolPage> {
// Auto-select if API says so
final autoSelect = data['auto_select'] == true;
final selectedId = data['selected_school_id'];
if (autoSelect && selectedId != null && schools.isNotEmpty) {
final id = (selectedId as num).toInt();
final s = schools.firstWhere((x) => x.id == id);
_enterSchool(s);
await _enterSchool(s); // ✅ ora async
}
} catch (e) {
setState(() => error = 'Errore: $e');
} finally {
setState(() => loading = false);
if (mounted) setState(() => loading = false);
}
}
void _enterSchool(School s) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) =>
HomePage(token: widget.token, school: s, userFirstName: firstName),
),
);
Future<void> _enterSchool(School s) async {
setState(() {
loading = true;
error = '';
});
try {
// ✅ CARICA settings (user + school) nello store globale AppState
await context.read<AppState>().bootstrap(
token: widget.token,
school: s,
userFirstName: firstName,
);
if (!mounted) return;
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => HomePage(
token: widget.token,
school: s,
userFirstName: firstName,
),
),
);
} catch (e) {
if (mounted) setState(() => error = 'Errore caricando impostazioni: $e');
} finally {
if (mounted) setState(() => loading = false);
}
}
String get _name => (firstName ?? '').trim();
@@ -97,31 +122,20 @@ class _SelectSchoolPageState extends State<SelectSchoolPage> {
if (ok != true) return;
// chiude il drawer se aperto
Navigator.of(context).pop();
// chiama API logout (se fallisce usciamo lo stesso)
try {
await VanguardApi.logout(token: widget.token);
} catch (_) {}
if (!mounted) return;
// torna a Login e cancella tutta la stack
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const LoginPage()),
(route) => false,
);
}
void _handleBottomNav(int i) {
setState(() => bottomIndex = i);
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Prima seleziona la scuola')));
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -153,84 +167,20 @@ class _SelectSchoolPageState extends State<SelectSchoolPage> {
),
],
),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed, // con 4 icone serve
backgroundColor: Colors.white,
selectedItemColor: const Color(0xFF10B981),
unselectedItemColor: Colors.black54,
currentIndex: bottomIndex,
onTap: _handleBottomNav,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_rounded),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.event_note_rounded),
label: 'Lezioni',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_rounded),
label: 'Account',
),
BottomNavigationBarItem(
icon: Icon(Icons.self_improvement_rounded),
label: 'Meditazione',
),
],
),
body: Stack(
children: [
// --- SFONDO (gradient)
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFFF4F6FA),
Color(0xFFEFF7F3),
Color(0xFFF4F6FA),
],
),
),
),
// --- BOLLE decorative
const _BgBlob(
color: Color(0x3310B981),
size: 280,
top: -70,
left: -90,
),
const _BgBlob(
color: Color(0x22F59E0B),
size: 230,
bottom: 80,
right: -70,
),
const _BgBlob(
color: Color(0x227C3AED),
size: 190,
bottom: -55,
left: 35,
),
// --- CONTENUTO della pagina (quello che avevi prima)
SafeArea(
top: false,
child: loading
? const Center(child: CircularProgressIndicator())
: error.isNotEmpty
? _ErrorBox(message: error, onRetry: _load)
: _SchoolsList(
firstName: firstName,
schools: schools,
onSelect: _enterSchool,
),
),
],
body: YogibookBackground(
child: SafeArea(
top: false,
child: loading
? const Center(child: CircularProgressIndicator())
: error.isNotEmpty
? _ErrorBox(message: error, onRetry: _load)
: _SchoolsList(
firstName: firstName,
schools: schools,
onSelect: (s) => _enterSchool(s),
),
),
),
);
}
@@ -261,7 +211,7 @@ class _AppDrawer extends StatelessWidget {
ListTile(
leading: const Icon(Icons.swap_horiz),
title: const Text('Cambia scuola'),
onTap: () => Navigator.of(context).pop(), // sei già qui
onTap: () => Navigator.of(context).pop(),
),
const Divider(height: 1),
ListTile(
@@ -296,7 +246,6 @@ class _SchoolsList extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// header compatto (stesso stile “soft” delle lezioni)
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 10),
child: Column(
@@ -334,6 +283,7 @@ class _SchoolsList extends StatelessWidget {
},
),
),
const SizedBox(height: 8),
Center(
child: Padding(