syncra_addons/creche_app/lib/features/menu/menu_screen.dart

452 lines
19 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:intl/intl.dart';
import '/core/auth_provider.dart';
const _bg = Color(0xFF0D1117);
const _card = Color(0xFF161B22);
const _blue = Color(0xFF4FC3F7);
const _green = Color(0xFF2ECC71);
const _amber = Color(0xFFFFB300);
const _red = Color(0xFFE74C3C);
const _mealNames = ['Pequeno Almoço', 'Almoço', 'Lanche da Tarde', 'Jantar'];
const _mealIcons = [Icons.free_breakfast, Icons.lunch_dining, Icons.icecream, Icons.dinner_dining];
const _weekDays = ['Segunda', 'Terça', 'Quarta', 'Quinta', 'Sexta'];
class MenuScreen extends ConsumerStatefulWidget {
const MenuScreen({super.key});
@override
ConsumerState<MenuScreen> createState() => _State();
}
class _State extends ConsumerState<MenuScreen> with SingleTickerProviderStateMixin {
late TabController _tabs;
DateTime _selectedWeek = _startOfWeek(DateTime.now());
bool _isAdmin = false;
@override
void initState() {
super.initState();
_tabs = TabController(length: 2, vsync: this);
_checkRole();
}
@override
void dispose() { _tabs.dispose(); super.dispose(); }
Future<void> _checkRole() async {
final p = await ref.read(currentProfileProvider.future);
if (mounted) setState(() => _isAdmin = p?.role == 'principal' || p?.role == 'admin');
}
static DateTime _startOfWeek(DateTime d) {
final diff = d.weekday - 1;
return DateTime(d.year, d.month, d.day - diff);
}
String get _weekLabel {
final end = _selectedWeek.add(const Duration(days: 4));
final fmt = DateFormat('d MMM', 'pt_PT');
return '${fmt.format(_selectedWeek)} ${fmt.format(end)}';
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: _bg,
appBar: AppBar(
backgroundColor: _card, elevation: 0,
title: const Text('Cardápio', style: TextStyle(color: _blue, fontWeight: FontWeight.bold)),
bottom: TabBar(
controller: _tabs, indicatorColor: _blue, labelColor: _blue,
unselectedLabelColor: Colors.white38,
tabs: const [Tab(text: '📅 Semanal'), Tab(text: '📋 Mensal')],
),
actions: [
if (_isAdmin)
IconButton(
icon: const Icon(Icons.add_circle_outline, color: _amber),
tooltip: 'Publicar cardápio',
onPressed: () => _showPublishDialog(context),
),
],
),
body: TabBarView(controller: _tabs, children: [
_WeeklyMenu(week: _selectedWeek, weekLabel: _weekLabel,
onPrev: () => setState(() => _selectedWeek = _selectedWeek.subtract(const Duration(days: 7))),
onNext: () => setState(() => _selectedWeek = _selectedWeek.add(const Duration(days: 7)))),
const _MonthlyMenu(),
]),
);
}
void _showPublishDialog(BuildContext ctx) {
showModalBottomSheet(
context: ctx, isScrollControlled: true,
backgroundColor: _card,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20))),
builder: (_) => _PublishMenuForm(week: _selectedWeek),
);
}
}
// ── Cardápio Semanal ──────────────────────────────────────────────
class _WeeklyMenu extends StatelessWidget {
final DateTime week;
final String weekLabel;
final VoidCallback onPrev, onNext;
const _WeeklyMenu({required this.week, required this.weekLabel, required this.onPrev, required this.onNext});
@override
Widget build(BuildContext context) {
final sb = Supabase.instance.client;
final weekStr = DateFormat('yyyy-MM-dd').format(week);
return Column(children: [
// Navegação de semana
Container(
color: _card,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(children: [
IconButton(onPressed: onPrev, icon: const Icon(Icons.chevron_left, color: _blue)),
Expanded(child: Text(weekLabel,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14))),
IconButton(onPressed: onNext, icon: const Icon(Icons.chevron_right, color: _blue)),
]),
),
Expanded(
child: FutureBuilder<List<Map<String, dynamic>>>(
future: sb.from('menu_items').select()
.eq('week_start', weekStr)
.order('day_index').order('meal_index'),
builder: (ctx, snap) {
if (snap.hasError) return _err('Erro: ${snap.error}');
if (!snap.hasData) return const Center(child: CircularProgressIndicator(color: _blue));
final items = snap.data!;
if (items.isEmpty) return _emptyMenu();
// Agrupar por dia
final Map<int, List<Map<String, dynamic>>> byDay = {};
for (final item in items) {
final day = (item['day_index'] as int?) ?? 0;
byDay.putIfAbsent(day, () => []).add(item);
}
return ListView.builder(
padding: const EdgeInsets.all(14),
itemCount: 5,
itemBuilder: (_, i) {
final date = week.add(Duration(days: i));
final dayMeals = byDay[i] ?? [];
return _DayCard(
dayName: _weekDays[i],
date: DateFormat('d/MM').format(date),
meals: dayMeals,
isToday: DateFormat('yyyy-MM-dd').format(DateTime.now()) ==
DateFormat('yyyy-MM-dd').format(date),
);
},
);
},
),
),
]);
}
}
class _DayCard extends StatelessWidget {
final String dayName, date;
final List<Map<String, dynamic>> meals;
final bool isToday;
const _DayCard({required this.dayName, required this.date, required this.meals, required this.isToday});
@override
Widget build(BuildContext context) => Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: _card, borderRadius: BorderRadius.circular(16),
border: Border.all(color: isToday ? _blue.withOpacity(0.5) : Colors.white.withOpacity(0.07)),
boxShadow: isToday ? [BoxShadow(color: _blue.withOpacity(0.08), blurRadius: 12)] : null,
),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
// Header do dia
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: isToday ? _blue.withOpacity(0.12) : Colors.white.withOpacity(0.03),
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
),
child: Row(children: [
Text(dayName, style: TextStyle(
color: isToday ? _blue : Colors.white,
fontWeight: FontWeight.bold, fontSize: 14)),
const SizedBox(width: 8),
Text(date, style: TextStyle(color: Colors.white.withOpacity(0.4), fontSize: 12)),
if (isToday) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(color: _blue.withOpacity(0.2), borderRadius: BorderRadius.circular(10)),
child: const Text('Hoje', style: TextStyle(color: _blue, fontSize: 10, fontWeight: FontWeight.bold)),
),
],
]),
),
if (meals.isEmpty)
Padding(
padding: const EdgeInsets.all(14),
child: Text('Sem ementa publicada', style: TextStyle(color: Colors.white.withOpacity(0.25), fontSize: 12)),
)
else
...meals.map((m) {
final mealIdx = (m['meal_index'] as int?) ?? 0;
final name = mealIdx < _mealIcons.length ? _mealNames[mealIdx] : 'Refeição';
final icon = mealIdx < _mealIcons.length ? _mealIcons[mealIdx] : Icons.restaurant;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
child: Row(children: [
Icon(icon, size: 16, color: _amber),
const SizedBox(width: 10),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(name, style: const TextStyle(color: Color(0xFF888888), fontSize: 10)),
Text(m['description'] ?? '', style: const TextStyle(color: Colors.white, fontSize: 13)),
]),
]),
);
}),
const SizedBox(height: 4),
]),
);
}
// ── Cardápio Mensal ───────────────────────────────────────────────
class _MonthlyMenu extends StatefulWidget {
const _MonthlyMenu();
@override
State<_MonthlyMenu> createState() => _MonthlyState();
}
class _MonthlyState extends State<_MonthlyMenu> {
DateTime _month = DateTime(DateTime.now().year, DateTime.now().month);
@override
Widget build(BuildContext context) {
final sb = Supabase.instance.client;
final monthStr = DateFormat('yyyy-MM').format(_month);
final monthName = DateFormat('MMMM yyyy', 'pt_PT').format(_month);
return Column(children: [
Container(
color: _card,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(children: [
IconButton(
onPressed: () => setState(() => _month = DateTime(_month.year, _month.month - 1)),
icon: const Icon(Icons.chevron_left, color: _blue)),
Expanded(child: Text(monthName.toUpperCase(),
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13, letterSpacing: 1))),
IconButton(
onPressed: () => setState(() => _month = DateTime(_month.year, _month.month + 1)),
icon: const Icon(Icons.chevron_right, color: _blue)),
]),
),
Expanded(
child: FutureBuilder<List<Map<String, dynamic>>>(
future: sb.from('menu_items').select()
.like('week_start', '$monthStr%')
.order('week_start').order('day_index'),
builder: (ctx, snap) {
if (!snap.hasData) return const Center(child: CircularProgressIndicator(color: _blue));
if (snap.data!.isEmpty) return _emptyMenu();
// Agrupa por semana
final Map<String, List<Map<String, dynamic>>> byWeek = {};
for (final item in snap.data!) {
final w = item['week_start'] as String;
byWeek.putIfAbsent(w, () => []).add(item);
}
return ListView(
padding: const EdgeInsets.all(14),
children: byWeek.entries.map((e) {
final weekDt = DateTime.parse(e.key);
final end = weekDt.add(const Duration(days: 4));
return _WeekSummaryCard(
label: '${DateFormat('d', 'pt_PT').format(weekDt)}${DateFormat('d MMM', 'pt_PT').format(end)}',
items: e.value,
);
}).toList(),
);
},
),
),
]);
}
}
class _WeekSummaryCard extends StatelessWidget {
final String label;
final List<Map<String, dynamic>> items;
const _WeekSummaryCard({required this.label, required this.items});
@override
Widget build(BuildContext context) => Container(
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: _card, borderRadius: BorderRadius.circular(14),
border: Border.all(color: Colors.white.withOpacity(0.07))),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [
const Icon(Icons.calendar_view_week, color: _blue, size: 16),
const SizedBox(width: 6),
Text('Semana de $label', style: const TextStyle(color: _blue, fontWeight: FontWeight.bold, fontSize: 13)),
]),
const SizedBox(height: 8),
...items.take(6).map((m) {
final day = (m['day_index'] as int?) ?? 0;
final meal = (m['meal_index'] as int?) ?? 0;
final dayName = day < _weekDays.length ? _weekDays[day] : '';
final mealName = meal < _mealNames.length ? _mealNames[meal] : '';
return Padding(
padding: const EdgeInsets.only(bottom: 3),
child: Text('$dayName $mealName: ${m['description'] ?? ''}',
style: const TextStyle(color: Color(0xFF888888), fontSize: 12)),
);
}),
if (items.length > 6)
Text('+${items.length - 6} itens', style: TextStyle(color: Colors.white.withOpacity(0.2), fontSize: 11)),
]),
);
}
// ── Formulário publicar cardápio (admin) ──────────────────────────
class _PublishMenuForm extends ConsumerStatefulWidget {
final DateTime week;
const _PublishMenuForm({required this.week});
@override
ConsumerState<_PublishMenuForm> createState() => _PublishState();
}
class _PublishState extends ConsumerState<_PublishMenuForm> {
int _day = 0, _meal = 0;
final _descCtrl = TextEditingController();
bool _saving = false;
@override
void dispose() { _descCtrl.dispose(); super.dispose(); }
Future<void> _save() async {
if (_descCtrl.text.trim().isEmpty) return;
setState(() => _saving = true);
try {
final sb = Supabase.instance.client;
final weekStr = DateFormat('yyyy-MM-dd').format(widget.week);
await sb.from('menu_items').upsert({
'week_start': weekStr,
'day_index': _day,
'meal_index': _meal,
'description': _descCtrl.text.trim(),
}, onConflict: 'week_start,day_index,meal_index');
_descCtrl.clear();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Publicado! ✓', style: TextStyle(color: Colors.white)),
backgroundColor: _green, behavior: SnackBarBehavior.floating));
}
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Erro: $e'), backgroundColor: _red, behavior: SnackBarBehavior.floating));
} finally { if (mounted) setState(() => _saving = false); }
}
@override
Widget build(BuildContext context) => Padding(
padding: EdgeInsets.only(left: 20, right: 20, top: 20, bottom: MediaQuery.of(context).viewInsets.bottom + 20),
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
const Text('Publicar Ementa', style: TextStyle(color: Colors.white, fontSize: 17, fontWeight: FontWeight.bold)),
Text('Semana: ${DateFormat('d MMM', 'pt_PT').format(widget.week)}',
style: const TextStyle(color: Color(0xFF888888), fontSize: 12)),
const SizedBox(height: 18),
// Dia
const Text('Dia', style: TextStyle(color: Color(0xFF888888), fontSize: 12)),
const SizedBox(height: 6),
SingleChildScrollView(scrollDirection: Axis.horizontal,
child: Row(children: List.generate(5, (i) => GestureDetector(
onTap: () => setState(() => _day = i),
child: Container(
margin: const EdgeInsets.only(right: 6),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: _day == i ? _blue.withOpacity(0.2) : Colors.white.withOpacity(0.05),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: _day == i ? _blue : Colors.white.withOpacity(0.1))),
child: Text(_weekDays[i], style: TextStyle(color: _day == i ? _blue : Colors.white60, fontSize: 12)),
),
))),
),
const SizedBox(height: 12),
// Refeição
const Text('Refeição', style: TextStyle(color: Color(0xFF888888), fontSize: 12)),
const SizedBox(height: 6),
SingleChildScrollView(scrollDirection: Axis.horizontal,
child: Row(children: List.generate(_mealNames.length, (i) => GestureDetector(
onTap: () => setState(() => _meal = i),
child: Container(
margin: const EdgeInsets.only(right: 6),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: _meal == i ? _amber.withOpacity(0.2) : Colors.white.withOpacity(0.05),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: _meal == i ? _amber : Colors.white.withOpacity(0.1))),
child: Row(children: [
Icon(_mealIcons[i], size: 14, color: _meal == i ? _amber : Colors.white38),
const SizedBox(width: 4),
Text(_mealNames[i], style: TextStyle(color: _meal == i ? _amber : Colors.white60, fontSize: 12)),
]),
),
))),
),
const SizedBox(height: 14),
TextField(
controller: _descCtrl, style: const TextStyle(color: Colors.white),
maxLines: 2,
decoration: InputDecoration(
hintText: 'Ex: Arroz com frango e legumes, sumo natural',
hintStyle: const TextStyle(color: Color(0xFF555555), fontSize: 13),
filled: true, fillColor: Colors.white.withOpacity(0.04),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.white.withOpacity(0.09))),
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
),
),
const SizedBox(height: 16),
GestureDetector(
onTap: _saving ? null : _save,
child: Container(
height: 50, width: double.infinity,
decoration: BoxDecoration(
gradient: const LinearGradient(colors: [_blue, Color(0xFF0288D1)]),
borderRadius: BorderRadius.circular(12)),
child: Center(child: _saving
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
: const Text('Publicar', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 15))),
),
),
]),
);
}
Widget _emptyMenu() => Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.restaurant_menu, size: 60, color: Colors.white.withOpacity(0.08)),
const SizedBox(height: 12),
const Text('Sem ementa publicada para esta semana', style: TextStyle(color: Color(0xFF888888), fontSize: 13)),
const SizedBox(height: 4),
const Text('A diretora ainda não publicou o cardápio.', style: TextStyle(color: Color(0xFF555555), fontSize: 11)),
]));
Widget _err(String msg) => Center(child: Text(msg, style: const TextStyle(color: _red)));