fixed pages

This commit is contained in:
2026-09-04 08:15:25 +02:00
parent c80535eb97
commit a1167a2d03
14 changed files with 363 additions and 174 deletions
+3
View File
@@ -43,3 +43,6 @@ app.*.map.json
/android/app/debug /android/app/debug
/android/app/profile /android/app/profile
/android/app/release /android/app/release
key.properties
*.jks
+19 -6
View File
@@ -1,3 +1,6 @@
import java.util.Properties
import java.io.FileInputStream
plugins { plugins {
id("com.android.application") id("com.android.application")
id("kotlin-android") id("kotlin-android")
@@ -5,6 +8,12 @@ plugins {
id("dev.flutter.flutter-gradle-plugin") id("dev.flutter.flutter-gradle-plugin")
} }
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android { android {
namespace = "it.yogasoul.yogibook" namespace = "it.yogasoul.yogibook"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
@@ -20,21 +29,25 @@ android {
} }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "it.yogasoul.yogibook" applicationId = "it.yogasoul.yogibook"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode versionCode = flutter.versionCode
versionName = flutter.versionName versionName = flutter.versionName
} }
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
storePassword = keystoreProperties["storePassword"] as String
}
}
buildTypes { buildTypes {
release { release {
// TODO: Add your own signing config for the release build. signingConfig = signingConfigs.getByName("release")
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
} }
} }
} }
+3 -16
View File
@@ -9,7 +9,6 @@ import 'meditation_page.dart';
import '../widgets/app_drawer.dart'; import '../widgets/app_drawer.dart';
import '../widgets/app_bottom_nav.dart'; import '../widgets/app_bottom_nav.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../widgets/yogibook_background.dart';
import 'reschedule_page.dart'; import 'reschedule_page.dart';
import 'orders_page.dart'; import 'orders_page.dart';
@@ -116,23 +115,10 @@ class _LessonsPageState extends State<LessonsPage> {
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
elevation: 0, elevation: 0,
centerTitle: true, centerTitle: true,
title: Column( title: const Text(
children: [
const Text(
'Le mie lezioni', 'Le mie lezioni',
style: TextStyle(fontWeight: FontWeight.w800, fontSize: 18), style: TextStyle(fontWeight: FontWeight.w800, fontSize: 18),
), ),
const SizedBox(height: 2),
Text(
widget.school.name,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.black54,
),
),
],
),
actions: [ actions: [
Padding( Padding(
padding: const EdgeInsets.only(right: 12), padding: const EdgeInsets.only(right: 12),
@@ -158,7 +144,8 @@ class _LessonsPageState extends State<LessonsPage> {
userFirstName: widget.userFirstName, userFirstName: widget.userFirstName,
), ),
body: YogibookBackground( body: Container(
color: const Color(0xFFE2FDFF),
child: SafeArea( child: SafeArea(
top: false, top: false,
child: Column( child: Column(
+1 -1
View File
@@ -273,7 +273,7 @@ class _MeditationPageState extends State<MeditationPage>
elevation: 0, elevation: 0,
centerTitle: true, centerTitle: true,
title: const Text( title: const Text(
'Meditazione', 'Respira',
style: TextStyle(fontWeight: FontWeight.w800, fontSize: 18), style: TextStyle(fontWeight: FontWeight.w800, fontSize: 18),
), ),
actions: [ actions: [
+19 -3
View File
@@ -81,6 +81,11 @@ class _ReschedulePageState extends State<ReschedulePage> {
return s[0].toUpperCase() + s.substring(1); return s[0].toUpperCase() + s.substring(1);
} }
String _dateLabel(String ymd) {
final dt = DateFormat('yyyy-MM-dd').parse(ymd);
return DateFormat('dd/MM/yyyy').format(dt);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const green = Color(0xFF10B981); const green = Color(0xFF10B981);
@@ -176,6 +181,7 @@ class _ReschedulePageState extends State<ReschedulePage> {
return _SlotCard( return _SlotCard(
slot: s, slot: s,
weekday: _weekdayLabel(s.date), weekday: _weekdayLabel(s.date),
dateLabel: _dateLabel(s.date),
onChoose: () => _confirm(s), onChoose: () => _confirm(s),
); );
}, },
@@ -239,11 +245,13 @@ class _ReschedulePageState extends State<ReschedulePage> {
class _SlotCard extends StatelessWidget { class _SlotCard extends StatelessWidget {
final AvailableSlot slot; final AvailableSlot slot;
final String weekday; final String weekday;
final String dateLabel;
final VoidCallback onChoose; final VoidCallback onChoose;
const _SlotCard({ const _SlotCard({
required this.slot, required this.slot,
required this.weekday, required this.weekday,
required this.dateLabel,
required this.onChoose, required this.onChoose,
}); });
@@ -316,7 +324,7 @@ class _SlotCard extends StatelessWidget {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'$weekday ${slot.date} • ${slot.time}', '$weekday $dateLabel • ${slot.time}',
style: const TextStyle( style: const TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.black54, color: Colors.black54,
@@ -325,10 +333,18 @@ class _SlotCard extends StatelessWidget {
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
Text( Text(
'Posti liberi: ${slot.freePlaces}/${slot.maxCapacity}', isBooked
? 'Già prenotato'
: isFull
? 'Non disponibile'
: 'Disponibile',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: isFull ? orange : Colors.black45, color: isFull
? const Color(0xFF9AA0A6)
: isBooked
? orange
: green,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
), ),
+8 -2
View File
@@ -18,7 +18,7 @@ class _SplashPageState extends State<SplashPage> {
static const Color kRed = Color(0xFFB01E28); static const Color kRed = Color(0xFFB01E28);
Timer? _timer; Timer? _timer;
int _seconds = 5; // countdown iniziale int _seconds = 8; // countdown iniziale
// Evita doppie navigazioni: countdown, tap e sblocco possono // Evita doppie navigazioni: countdown, tap e sblocco possono
// altrimenti innescare più push. // altrimenti innescare più push.
@@ -176,7 +176,11 @@ class _SplashPageState extends State<SplashPage> {
Positioned( Positioned(
left: 0, left: 0,
right: 0, right: 0,
bottom: 30, bottom: 0,
child: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.only(bottom: 20),
child: Column( child: Column(
children: [ children: [
Container( Container(
@@ -216,6 +220,8 @@ class _SplashPageState extends State<SplashPage> {
], ],
), ),
), ),
),
),
], ],
), ),
), ),
@@ -109,6 +109,10 @@ class _TeacherAddParticipantPageState extends State<TeacherAddParticipantPage> {
_mode = 'omaggio'; _mode = 'omaggio';
} }
}); });
// DEBUG temporaneo — rimuovere dopo diagnosi
_snack('DEBUG ordini: ${o.length} totali, ${usable.length} con residui');
// DEBUG temporaneo
_snack('DEBUG: ${o.length} ordini totali, ${usable.length} con residui');
} catch (e) { } catch (e) {
_snack('$e'); _snack('$e');
} finally { } finally {
+156 -10
View File
@@ -27,8 +27,9 @@ class _TeacherClassDetailPageState extends State<TeacherClassDetailPage> {
// copia locale modificabile dei partecipanti (per aggiornare la UI) // copia locale modificabile dei partecipanti (per aggiornare la UI)
late List<TeacherParticipant> _participants; late List<TeacherParticipant> _participants;
int _busyBookingId = int _busyBookingId = 0; // booking in aggiornamento
0; // booking in aggiornamento (per disabilitare il tasto) bool _changed =
false; // true se ci sono state modifiche (per refresh lista)/ booking in aggiornamento (per disabilitare il tasto)
@override @override
void initState() { void initState() {
@@ -100,6 +101,7 @@ class _TeacherClassDetailPageState extends State<TeacherClassDetailPage> {
); );
}); });
} }
_changed = true;
_snack(markLost ? 'Segnata come persa.' : 'Ripristinata.'); _snack(markLost ? 'Segnata come persa.' : 'Ripristinata.');
} catch (e) { } catch (e) {
_snack('$e'); _snack('$e');
@@ -143,6 +145,7 @@ class _TeacherClassDetailPageState extends State<TeacherClassDetailPage> {
setState(() { setState(() {
_participants.removeWhere((x) => x.bookingId == p.bookingId); _participants.removeWhere((x) => x.bookingId == p.bookingId);
}); });
_changed = true;
_snack('Prenotazione eliminata.'); _snack('Prenotazione eliminata.');
} catch (e) { } catch (e) {
_snack('$e'); _snack('$e');
@@ -151,6 +154,129 @@ class _TeacherClassDetailPageState extends State<TeacherClassDetailPage> {
} }
} }
Future<void> _confirmPending(TeacherParticipant p) async {
setState(() => _busyBookingId = p.bookingId);
try {
await TeacherApi.confirmPending(
token: widget.token,
bookingId: p.bookingId,
);
// da pending a confermato: aggiorna lo stato locale
final idx = _participants.indexWhere((x) => x.bookingId == p.bookingId);
if (idx != -1) {
setState(() {
_participants[idx] = TeacherParticipant(
bookingId: p.bookingId,
fullName: p.fullName,
isLost: false,
isPending: false,
);
});
}
_changed = true;
_snack('${p.fullName} confermato.');
} catch (e) {
_snack('$e');
} finally {
if (mounted) setState(() => _busyBookingId = 0);
}
}
Widget _pendingTile(TeacherParticipant p) {
final busy = _busyBookingId == p.bookingId;
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(
color: const Color(0xFFFFFBF0),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFFF5E6B8)),
),
child: Row(
children: [
Container(
width: 42,
height: 42,
alignment: Alignment.center,
decoration: const BoxDecoration(
color: Color(0xFFE0A800),
shape: BoxShape.circle,
),
child: Text(
_initials(p.fullName),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
fontSize: 15,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
p.fullName,
style: const TextStyle(
fontWeight: FontWeight.w700,
fontSize: 15,
),
),
const SizedBox(height: 2),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: const Color(0xFFFFF4D6),
borderRadius: BorderRadius.circular(20),
),
child: const Text(
'In attesa',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w800,
color: Color(0xFF8A6D00),
),
),
),
],
),
),
if (busy)
const SizedBox(
width: 36,
height: 36,
child: Padding(
padding: EdgeInsets.all(8),
child: CircularProgressIndicator(strokeWidth: 2),
),
)
else ...[
IconButton(
onPressed: () => _confirmPending(p),
tooltip: 'Conferma',
icon: const Icon(
Icons.check_circle_outline_rounded,
color: Color(0xFF1A7F52),
),
),
IconButton(
onPressed: () => _deleteBooking(p),
tooltip: 'Rimuovi',
icon: const Icon(
Icons.delete_outline_rounded,
color: Colors.black45,
),
),
],
],
),
);
}
Future<void> _reprogram(TeacherParticipant p) async { Future<void> _reprogram(TeacherParticipant p) async {
final done = await Navigator.push<bool>( final done = await Navigator.push<bool>(
context, context,
@@ -173,15 +299,27 @@ class _TeacherClassDetailPageState extends State<TeacherClassDetailPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final active = _participants.where((p) => !p.isLost).toList(); final pending = _participants.where((p) => p.isPending).toList();
final lost = _participants.where((p) => p.isLost).toList(); final active = _participants
.where((p) => !p.isPending && !p.isLost)
.toList();
final lost = _participants.where((p) => !p.isPending && p.isLost).toList();
final cls = widget.cls; final cls = widget.cls;
return Scaffold( return PopScope(
canPop: true,
onPopInvokedWithResult: (didPop, result) {
// quando si esce, se ci sono state modifiche la lista si aggiorna
},
child: Scaffold(
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
elevation: 0, elevation: 0,
centerTitle: true, centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context, _changed),
),
title: const Text( title: const Text(
'Dettaglio classe', 'Dettaglio classe',
style: TextStyle(fontWeight: FontWeight.w800, fontSize: 18), style: TextStyle(fontWeight: FontWeight.w800, fontSize: 18),
@@ -195,6 +333,12 @@ class _TeacherClassDetailPageState extends State<TeacherClassDetailPage> {
children: [ children: [
_headerCard(), _headerCard(),
const SizedBox(height: 18), const SizedBox(height: 18),
if (pending.isNotEmpty) ...[
_sectionLabel('In attesa di conferma (${pending.length})'),
const SizedBox(height: 8),
...pending.map((p) => _pendingTile(p)),
const SizedBox(height: 18),
],
_sectionLabel('Iscritti (${active.length})'), _sectionLabel('Iscritti (${active.length})'),
const SizedBox(height: 8), const SizedBox(height: 8),
if (active.isEmpty) if (active.isEmpty)
@@ -218,7 +362,10 @@ class _TeacherClassDetailPageState extends State<TeacherClassDetailPage> {
borderRadius: BorderRadius.circular(14), borderRadius: BorderRadius.circular(14),
), ),
), ),
icon: const Icon(Icons.person_add_alt_1, color: Colors.white), icon: const Icon(
Icons.person_add_alt_1,
color: Colors.white,
),
label: const Text( label: const Text(
'Aggiungi partecipante', 'Aggiungi partecipante',
style: TextStyle( style: TextStyle(
@@ -233,6 +380,7 @@ class _TeacherClassDetailPageState extends State<TeacherClassDetailPage> {
), ),
), ),
), ),
),
); );
} }
@@ -245,10 +393,8 @@ class _TeacherClassDetailPageState extends State<TeacherClassDetailPage> {
), ),
); );
if (added == true && mounted) { if (added == true && mounted) {
_snack( _changed = true;
'Partecipante aggiunto. Aggiorna il pannello per vedere la lista.', _snack('Partecipante aggiunto.');
);
// torna al pannello classi, che ricaricando mostrerà i dati aggiornati
Navigator.pop(context, true); Navigator.pop(context, true);
} }
} }
+21
View File
@@ -392,6 +392,27 @@ class _ClassCard extends StatelessWidget {
color: fillColor, color: fillColor,
), ),
), ),
if (cls.participants.any((p) => p.isPending)) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: const Color(0xFFFFF4D6),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${cls.participants.where((p) => p.isPending).length} in attesa',
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w800,
color: Color(0xFF8A6D00),
),
),
),
],
], ],
), ),
], ],
+34
View File
@@ -9,11 +9,13 @@ class TeacherParticipant {
final int bookingId; final int bookingId;
final String fullName; final String fullName;
final bool isLost; final bool isLost;
final bool isPending;
const TeacherParticipant({ const TeacherParticipant({
required this.bookingId, required this.bookingId,
required this.fullName, required this.fullName,
required this.isLost, required this.isLost,
this.isPending = false,
}); });
factory TeacherParticipant.fromMap(Map<String, dynamic> m) => factory TeacherParticipant.fromMap(Map<String, dynamic> m) =>
@@ -21,6 +23,7 @@ class TeacherParticipant {
bookingId: (m['booking_id'] as num?)?.toInt() ?? 0, bookingId: (m['booking_id'] as num?)?.toInt() ?? 0,
fullName: (m['full_name'] ?? '').toString().trim(), fullName: (m['full_name'] ?? '').toString().trim(),
isLost: m['is_lost'] == true, isLost: m['is_lost'] == true,
isPending: m['is_pending'] == true,
); );
} }
@@ -407,4 +410,35 @@ class TeacherApi {
throw Exception(data['error'] ?? data['message'] ?? 'Errore inserimento'); throw Exception(data['error'] ?? data['message'] ?? 'Errore inserimento');
} }
} }
/// Conferma una prenotazione in attesa (pending -> booked).
static Future<void> confirmPending({
required String token,
required int bookingId,
}) async {
final uri = Uri.parse(
'${ApiConfig.laravelApiBase}/api_teacher_confirm_pending.php',
);
final res = await http.post(
uri,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Bearer $token',
},
body: 'booking_id=$bookingId',
);
Map<String, dynamic> data;
try {
data = jsonDecode(res.body) as Map<String, dynamic>;
} catch (_) {
throw Exception('RAW(${res.statusCode}): ${res.body}');
}
if (res.statusCode != 200 || data['success'] != true) {
throw Exception(data['error'] ?? data['message'] ?? 'Errore conferma');
}
}
} }
+1 -1
View File
@@ -95,7 +95,7 @@ class AppBottomNav extends StatelessWidget {
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.self_improvement_rounded), icon: Icon(Icons.self_improvement_rounded),
label: 'Meditazione', label: 'Respira',
), ),
], ],
); );
+7 -40
View File
@@ -6,9 +6,9 @@ import '../config/api_config.dart';
import '../screens/login_page.dart'; import '../screens/login_page.dart';
import '../screens/medical_certificates_page.dart'; import '../screens/medical_certificates_page.dart';
import '../screens/lessons_page.dart';
import '../screens/orders_detail_page.dart'; import '../screens/orders_detail_page.dart';
import '../screens/account_page.dart';
import '../screens/settings_page.dart'; import '../screens/settings_page.dart';
import '../screens/teacher_panel_page.dart'; import '../screens/teacher_panel_page.dart';
import '../services/role_service.dart'; import '../services/role_service.dart';
@@ -159,24 +159,6 @@ class AppDrawer extends StatelessWidget {
}, },
), ),
ListTile(
leading: const Icon(Icons.event_note_rounded),
title: const Text('Lezioni'),
onTap: () {
Navigator.of(context).pop();
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => LessonsPage(
token: token,
school: school,
userFirstName: userFirstName,
),
),
);
},
),
ListTile( ListTile(
leading: const Icon(Icons.shopping_bag_rounded), leading: const Icon(Icons.shopping_bag_rounded),
title: const Text('Ordini'), title: const Text('Ordini'),
@@ -195,24 +177,6 @@ class AppDrawer extends StatelessWidget {
}, },
), ),
ListTile(
leading: const Icon(Icons.person_rounded),
title: const Text('Account'),
onTap: () {
Navigator.of(context).pop();
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => AccountPage(
token: token,
school: school,
userFirstName: userFirstName,
),
),
);
},
),
ListTile( ListTile(
leading: const Icon(Icons.settings_rounded), leading: const Icon(Icons.settings_rounded),
title: const Text('Impostazioni'), title: const Text('Impostazioni'),
@@ -235,10 +199,13 @@ class AppDrawer extends StatelessWidget {
ListTile( ListTile(
leading: const Icon(Icons.privacy_tip_outlined), leading: const Icon(Icons.privacy_tip_outlined),
title: const Text('Cookie e Privacy Policy'), title: const Text('Privacy Policy'),
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
_openUrl(context, 'https://yogasoul.it/cookie-policy/'); _openUrl(
context,
'https://yogasoul.it/privacy-policy-yogibook/',
);
}, },
), ),
+5 -13
View File
@@ -9,27 +9,19 @@ class YogibookBackground extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack( return Stack(
children: [ children: [
// --- SFONDO (gradient) // --- SFONDO (tinta piatta)
Container( Container(color: const Color(0xFFE2FDFF)),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFF4F6FA), Color(0xFFEFF7F3), Color(0xFFF4F6FA)],
),
),
),
// --- BOLLE decorative // --- BOLLE decorative
const _BgBlob(color: Color(0x3310B981), size: 280, top: -70, left: -90), const _BgBlob(color: Color(0x9962BBC1), size: 280, top: -70, left: -90),
const _BgBlob( const _BgBlob(
color: Color(0x22F59E0B), color: Color(0x99F4ECE1),
size: 230, size: 230,
bottom: 80, bottom: 80,
right: -70, right: -70,
), ),
const _BgBlob( const _BgBlob(
color: Color(0x227C3AED), color: Color(0x99E1D0B8),
size: 190, size: 190,
bottom: -55, bottom: -55,
left: 35, left: 35,
+1 -1
View File
@@ -1,5 +1,5 @@
name: yogibook_app name: yogibook_app
description: "A new Flutter project." description: "Prenotazione YogaSoul"
# The following line prevents the package from being accidentally published to # The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages. # pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: "none" # Remove this line if you wish to publish to pub.dev publish_to: "none" # Remove this line if you wish to publish to pub.dev