tontine_calendar 1.2.0 copy "tontine_calendar: ^1.2.0" to clipboard
tontine_calendar: ^1.2.0 copied to clipboard

A customizable Flutter calendar widget designed for tontine applications with paginated months, configurable days, and contribution tracking.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:tontine_calendar/tontine_calendar.dart';

void main() {
  runApp(const TontineCalendarExampleApp());
}

class TontineCalendarExampleApp extends StatelessWidget {
  const TontineCalendarExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Tontine Calendar Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xff0d7abc)),
        useMaterial3: true,
      ),
      home: const ExampleHomePage(),
    );
  }
}

class ExampleHomePage extends StatefulWidget {
  const ExampleHomePage({super.key});

  @override
  State<ExampleHomePage> createState() => _ExampleHomePageState();
}

class _ExampleHomePageState extends State<ExampleHomePage> {
  int _selectedIndex = 0;

  final List<Widget> _pages = [
    const BasicExamplePage(),
    const ConfigurationExamplePage(),
    const ThemeExamplePage(),
    const InteractiveExamplePage(),
    const CotisationTestPage(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Tontine Calendar Examples'),
      ),
      body: _pages[_selectedIndex],
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed,
        currentIndex: _selectedIndex,
        onTap: (index) => setState(() => _selectedIndex = index),
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.calendar_today),
            label: 'Basic',
          ),
          BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'Config'),
          BottomNavigationBarItem(icon: Icon(Icons.palette), label: 'Themes'),
          BottomNavigationBarItem(
            icon: Icon(Icons.touch_app),
            label: 'Interactive',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.account_balance_wallet),
            label: 'Cotisation',
          ),
        ],
      ),
    );
  }
}

class BasicExamplePage extends StatelessWidget {
  const BasicExamplePage({super.key});

  @override
  Widget build(BuildContext context) {
    // Create some sample validated days
    final validatedDays = [
      const TontineDay(day: 1, month: 1, isValidated: true, amount: 1000),
      const TontineDay(day: 2, month: 1, isValidated: true, amount: 1000),
      const TontineDay(day: 3, month: 1, isValidated: true, amount: 1000),
      const TontineDay(day: 1, month: 2, isValidated: true, amount: 1000),
      const TontineDay(day: 2, month: 2, isValidated: true, amount: 1000),
    ];

    final config = TontineCalendarConfig(
      monthCount: 6,
      daysPerMonth: 31,
      defaultDayAmount: 1000.0,
      validatedDays: validatedDays,
    );

    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            'Basic Tontine Calendar',
            style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          const Text(
            'A 6-month calendar with 31 days each. Some days are already validated.',
          ),
          const SizedBox(height: 16),
          Expanded(
            child: TontineCalendar(
              config: config,
              onDaySelected: (data) {
                _showSelectionDialog(context, data);
              },
              onValidatedDayTapped: (day) {
                _showDayDetailsDialog(context, day);
              },
            ),
          ),
        ],
      ),
    );
  }

  void _showSelectionDialog(BuildContext context, TontineCalendarData data) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Day Selected'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Selected: Day ${data.selectedDay.day}, ${data.selectedMonth.name}',
            ),
            Text('Total unvalidated days: ${data.totalUnvalidatedDays}'),
            Text('Total amount: ${data.totalAmount.toStringAsFixed(0)} FCFA'),
            const SizedBox(height: 8),
            const Text('Unvalidated days by month:'),
            ...data.unvalidatedDaysByMonth.entries.map(
              (entry) => Text('Month ${entry.key}: ${entry.value.length} days'),
            ),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }

  void _showDayDetailsDialog(BuildContext context, TontineDay day) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Day Details'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('Day: ${day.day}'),
            Text('Month: ${day.month}'),
            Text('Validated: ${day.isValidated ? 'Yes' : 'No'}'),
            if (day.amount != null)
              Text('Amount: ${day.amount!.toStringAsFixed(0)} FCFA'),
            if (day.validatedDate != null) Text('Date: ${day.validatedDate}'),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }
}

class ConfigurationExamplePage extends StatefulWidget {
  const ConfigurationExamplePage({super.key});

  @override
  State<ConfigurationExamplePage> createState() =>
      _ConfigurationExamplePageState();
}

class _ConfigurationExamplePageState extends State<ConfigurationExamplePage> {
  int _monthCount = 12;
  int _daysPerMonth = 31;
  bool _showModeSelection = true;
  bool _enableNavigation = true;
  bool _showCompletionStatus = true;
  bool _showTotalAmount = true;
  bool _restrictiveNavigation = true;
  bool _highlightSelectedDays = true;
  List<String> _monthNames = [
    'Janvier',
    'Février',
    'Mars',
    'Avril',
    'Mai',
    'Juin',
    'Juillet',
    'Août',
    'Septembre',
    'Octobre',
    'Novembre',
    'Décembre'
  ];
  String _selectedLanguage = 'Français';

  @override
  Widget build(BuildContext context) {
    final config = TontineCalendarConfig(
      monthCount: _monthCount,
      daysPerMonth: _daysPerMonth,
      monthNames: _monthNames.take(_monthCount).toList(),
      enableNavigation: _enableNavigation,
      showCompletionStatus: _showCompletionStatus,
      showTotalAmount: _showTotalAmount,
      restrictiveNavigation: _restrictiveNavigation,
      highlightSelectedDays: _highlightSelectedDays,
      defaultDayAmount: 500.0,
    );

    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              'Configuration Options',
              style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 16),
            Expanded(
              child: TontineCalendar(
                key: ValueKey(
                    '${_monthCount}_${_daysPerMonth}_${_selectedLanguage}'), // Clé unique pour forcer la reconstruction
                config: config,
                showModeSelection: _showModeSelection,
                onDaySelected: (data) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(
                      content:
                          Text('Selected ${data.totalUnvalidatedDays} days'),
                    ),
                  );
                },
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _showConfigurationBottomSheet,
        child: const Icon(Icons.settings),
        tooltip: 'Configuration',
      ),
    );
  }

  void _showConfigurationBottomSheet() {
    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
      ),
      builder: (BuildContext context) {
        return StatefulBuilder(
          builder: (BuildContext context, StateSetter setModalState) {
            return Container(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  // Handle bar
                  Center(
                    child: Container(
                      width: 40,
                      height: 4,
                      decoration: BoxDecoration(
                        color: Colors.grey[300],
                        borderRadius: BorderRadius.circular(2),
                      ),
                    ),
                  ),
                  const SizedBox(height: 16),
                  const Text(
                    'Configuration du Calendrier',
                    style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 16),
                  _buildConfigurationControlsForBottomSheet(setModalState),
                  const SizedBox(height: 16),
                ],
              ),
            );
          },
        );
      },
    );
  }

  Widget _buildConfigurationControlsForBottomSheet(StateSetter setModalState) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Row(
              children: [
                const Text('Months: ', style: TextStyle(fontSize: 12)),
                Expanded(
                  child: Slider(
                    value: _monthCount.toDouble(),
                    min: 2,
                    max: 12,
                    divisions: 10,
                    label: _monthCount.toString(),
                    onChanged: (value) {
                      setState(() => _monthCount = value.toInt());
                      setModalState(() => _monthCount = value.toInt());
                    },
                  ),
                ),
                Text(_monthCount.toString(),
                    style: const TextStyle(
                        fontSize: 12, fontWeight: FontWeight.bold)),
              ],
            ),
            Row(
              children: [
                const Text('Days/month: ', style: TextStyle(fontSize: 12)),
                Expanded(
                  child: Slider(
                    value: _daysPerMonth.toDouble(),
                    min: 28,
                    max: 31,
                    divisions: 3,
                    label: _daysPerMonth.toString(),
                    onChanged: (value) {
                      setState(() => _daysPerMonth = value.toInt());
                      setModalState(() => _daysPerMonth = value.toInt());
                    },
                  ),
                ),
                Text(_daysPerMonth.toString(),
                    style: const TextStyle(
                        fontSize: 12, fontWeight: FontWeight.bold)),
              ],
            ),
            Row(
              children: [
                Expanded(
                  child: SwitchListTile(
                    title: const Text('Mode Selection',
                        style: TextStyle(fontSize: 12)),
                    value: _showModeSelection,
                    onChanged: (value) {
                      setState(() => _showModeSelection = value);
                      setModalState(() => _showModeSelection = value);
                    },
                    dense: true,
                  ),
                ),
                Expanded(
                  child: SwitchListTile(
                    title: const Text('Navigation',
                        style: TextStyle(fontSize: 12)),
                    value: _enableNavigation,
                    onChanged: (value) {
                      setState(() => _enableNavigation = value);
                      setModalState(() => _enableNavigation = value);
                    },
                    dense: true,
                  ),
                ),
              ],
            ),
            Row(
              children: [
                Expanded(
                  child: SwitchListTile(
                    title: const Text('Completion Status',
                        style: TextStyle(fontSize: 12)),
                    value: _showCompletionStatus,
                    onChanged: (value) {
                      setState(() => _showCompletionStatus = value);
                      setModalState(() => _showCompletionStatus = value);
                    },
                    dense: true,
                  ),
                ),
                Expanded(
                  child: SwitchListTile(
                    title: const Text('Total Amount',
                        style: TextStyle(fontSize: 12)),
                    value: _showTotalAmount,
                    onChanged: (value) {
                      setState(() => _showTotalAmount = value);
                      setModalState(() => _showTotalAmount = value);
                    },
                    dense: true,
                  ),
                ),
              ],
            ),
            Row(
              children: [
                Expanded(
                  child: SwitchListTile(
                    title: const Text('Restrictive Nav',
                        style: TextStyle(fontSize: 12)),
                    value: _restrictiveNavigation,
                    onChanged: (value) {
                      setState(() => _restrictiveNavigation = value);
                      setModalState(() => _restrictiveNavigation = value);
                    },
                    dense: true,
                  ),
                ),
                Expanded(
                  child: SwitchListTile(
                    title: const Text('Highlight Days',
                        style: TextStyle(fontSize: 12)),
                    value: _highlightSelectedDays,
                    onChanged: (value) {
                      setState(() => _highlightSelectedDays = value);
                      setModalState(() => _highlightSelectedDays = value);
                    },
                    dense: true,
                  ),
                ),
              ],
            ),
            const Divider(height: 8),
            Row(
              children: [
                const Text('Langue: ',
                    style:
                        TextStyle(fontWeight: FontWeight.bold, fontSize: 12)),
                Expanded(
                  child: DropdownButton<String>(
                    value: _selectedLanguage,
                    isExpanded: true,
                    isDense: true,
                    items: const [
                      DropdownMenuItem(
                          value: 'Français',
                          child:
                              Text('Français', style: TextStyle(fontSize: 12))),
                      DropdownMenuItem(
                          value: 'English',
                          child:
                              Text('English', style: TextStyle(fontSize: 12))),
                      DropdownMenuItem(
                          value: 'Español',
                          child:
                              Text('Español', style: TextStyle(fontSize: 12))),
                    ],
                    onChanged: (value) {
                      if (value != null) {
                        setState(() {
                          _selectedLanguage = value;
                          _updateMonthNames(value);
                        });
                        setModalState(() {
                          _selectedLanguage = value;
                          _updateMonthNames(value);
                        });
                      }
                    },
                  ),
                ),
              ],
            ),
            Wrap(
              spacing: 1,
              runSpacing: 1,
              children: _monthNames
                  .take(_monthCount)
                  .map((month) => Chip(
                        label: Text(month, style: const TextStyle(fontSize: 9)),
                        backgroundColor: Colors.blue[100],
                        materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
                        visualDensity: VisualDensity.compact,
                      ))
                  .toList(),
            ),
          ],
        ),
      ),
    );
  }

  void _updateMonthNames(String language) {
    switch (language) {
      case 'Français':
        _monthNames = [
          'Janvier',
          'Février',
          'Mars',
          'Avril',
          'Mai',
          'Juin',
          'Juillet',
          'Août',
          'Septembre',
          'Octobre',
          'Novembre',
          'Décembre'
        ];
        break;
      case 'English':
        _monthNames = [
          'January',
          'February',
          'March',
          'April',
          'May',
          'June',
          'July',
          'August',
          'September',
          'October',
          'November',
          'December'
        ];
        break;
      case 'Español':
        _monthNames = [
          'Enero',
          'Febrero',
          'Marzo',
          'Abril',
          'Mayo',
          'Junio',
          'Julio',
          'Agosto',
          'Septiembre',
          'Octubre',
          'Noviembre',
          'Diciembre'
        ];
        break;
    }
  }
}

class ThemeExamplePage extends StatefulWidget {
  const ThemeExamplePage({super.key});

  @override
  State<ThemeExamplePage> createState() => _ThemeExamplePageState();
}

class _ThemeExamplePageState extends State<ThemeExamplePage> {
  String _selectedTheme = 'Default';

  @override
  Widget build(BuildContext context) {
    final config = TontineCalendarConfig.sixMonths();
    final style = TontineCalendarTheme.getThemeByName(
      _selectedTheme,
      context: context,
    );

    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            'Theme Examples',
            style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 16),
          DropdownButton<String>(
            value: _selectedTheme,
            isExpanded: true,
            items: TontineCalendarTheme.availableThemes
                .map(
                  (theme) => DropdownMenuItem(value: theme, child: Text(theme)),
                )
                .toList(),
            onChanged: (value) => setState(() => _selectedTheme = value!),
          ),
          const SizedBox(height: 16),
          Expanded(
            child: TontineCalendar(
              config: config,
              style: style,
              onDaySelected: (data) {
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(
                    content: Text(
                      'Theme: $_selectedTheme - Selected ${data.totalUnvalidatedDays} days',
                    ),
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

class InteractiveExamplePage extends StatefulWidget {
  const InteractiveExamplePage({super.key});

  @override
  State<InteractiveExamplePage> createState() => _InteractiveExamplePageState();
}

class _InteractiveExamplePageState extends State<InteractiveExamplePage> {
  List<TontineDay> _validatedDays = [];
  TontineCalendarData? _lastSelection;

  @override
  Widget build(BuildContext context) {
    final config = TontineCalendarConfig(
      monthCount: 6,
      daysPerMonth: 31,
      validatedDays: _validatedDays,
      defaultDayAmount: 1000.0,
    );

    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text(
            'Interactive Example',
            style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
          ),
          const Text(
            'Tap days to simulate validation. View selection details below.',
          ),
          const SizedBox(height: 16),
          Expanded(
            flex: 2,
            child: TontineCalendar(
              config: config,
              onDaySelected: (data) {
                setState(() {
                  _lastSelection = data;
                  // Simulate validating the selected days
                  for (final entry in data.unvalidatedDaysByMonth.entries) {
                    for (final day in entry.value) {
                      _validatedDays.add(
                        day.copyWith(
                          isValidated: true,
                          validatedDate: DateTime.now(),
                        ),
                      );
                    }
                  }
                });
              },
            ),
          ),
          const SizedBox(height: 16),
          Expanded(flex: 1, child: _buildSelectionDetails()),
        ],
      ),
    );
  }

  Widget _buildSelectionDetails() {
    if (_lastSelection == null) {
      return const Card(
        child: Center(child: Text('Select a day to see details')),
      );
    }

    final data = _lastSelection!;
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Last Selection',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 8),
            Text(
              'Selected: Day ${data.selectedDay.day}, ${data.selectedMonth.name}',
            ),
            Text('Total unvalidated days: ${data.totalUnvalidatedDays}'),
            Text('Total amount: ${data.totalAmount.toStringAsFixed(0)} FCFA'),
            Text('Validated days: ${_validatedDays.length}'),
            const SizedBox(height: 8),
            const Text('Breakdown by month:'),
            Expanded(
              child: ListView(
                children: data.unvalidatedDaysByMonth.entries.map((entry) {
                  return ListTile(
                    title: Text('Month ${entry.key}'),
                    subtitle: Text('${entry.value.length} days'),
                    trailing: Text('${entry.value.length * 1000} FCFA'),
                  );
                }).toList(),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class CotisationTestPage extends StatefulWidget {
  const CotisationTestPage({super.key});

  @override
  State<CotisationTestPage> createState() => _CotisationTestPageState();
}

class _CotisationTestPageState extends State<CotisationTestPage> {
  List<TontineDay> _validatedDays = [];
  TontineCalendarData? _lastSelection;
  double _dailyAmount = 1000.0;
  bool _restrictiveMode = true;
  bool _highlightDays = true;

  @override
  Widget build(BuildContext context) {
    // Créer une nouvelle configuration à chaque build pour forcer la mise à jour
    final config = TontineCalendarConfig(
      monthCount: 12,
      daysPerMonth: 31,
      validatedDays: List.from(_validatedDays), // Créer une nouvelle liste
      defaultDayAmount: _dailyAmount,
      restrictiveNavigation: _restrictiveMode,
      highlightSelectedDays: _highlightDays,
    );

    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              'Test de Cotisation 12×31',
              style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
            ),
            const Text('Calendrier complet avec 12 mois de 31 jours chacun.'),
            const SizedBox(height: 16),
            Expanded(
              child: TontineCalendar(
                key: ValueKey(_validatedDays
                    .length), // Clé unique pour forcer la reconstruction
                config: config,
                onDaySelected: (data) {
                  setState(() {
                    _lastSelection = data;
                  });
                  _showPaymentConfirmationDialog(data);
                },
                onValidatedDayTapped: (day) {
                  // Afficher les détails du jour validé sans demander de validation
                  _showValidatedDayDetails(day);
                },
              ),
            ),
            const SizedBox(height: 16),
            _buildSummary(),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _showControlsBottomSheet,
        child: const Icon(Icons.settings),
        tooltip: 'Contrôles et Actions',
      ),
    );
  }

  void _showControlsBottomSheet() {
    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
      ),
      builder: (BuildContext context) {
        return StatefulBuilder(
          builder: (BuildContext context, StateSetter setModalState) {
            return Container(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  // Handle bar
                  Center(
                    child: Container(
                      width: 40,
                      height: 4,
                      decoration: BoxDecoration(
                        color: Colors.grey[300],
                        borderRadius: BorderRadius.circular(2),
                      ),
                    ),
                  ),
                  const SizedBox(height: 16),
                  const Text(
                    'Contrôles et Actions',
                    style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 16),
                  _buildControlsForBottomSheet(setModalState),
                  const SizedBox(height: 16),
                  _buildQuickActionsForBottomSheet(),
                  const SizedBox(height: 16),
                ],
              ),
            );
          },
        );
      },
    );
  }

  Widget _buildControlsForBottomSheet(StateSetter setModalState) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            Row(
              children: [
                const Text('Montant par jour: '),
                Expanded(
                  child: Slider(
                    value: _dailyAmount,
                    min: 500,
                    max: 5000,
                    divisions: 18,
                    label: '${_dailyAmount.toInt()} FCFA',
                    onChanged: (value) {
                      setState(() => _dailyAmount = value);
                      setModalState(() => _dailyAmount = value);
                    },
                  ),
                ),
              ],
            ),
            Row(
              children: [
                Expanded(
                  child: SwitchListTile(
                    title: const Text('Mode Restrictif'),
                    subtitle: const Text('Complétion requise'),
                    value: _restrictiveMode,
                    onChanged: (value) {
                      setState(() => _restrictiveMode = value);
                      setModalState(() => _restrictiveMode = value);
                    },
                  ),
                ),
                Expanded(
                  child: SwitchListTile(
                    title: const Text('Highlighting'),
                    subtitle: const Text('Bordures sélection'),
                    value: _highlightDays,
                    onChanged: (value) {
                      setState(() => _highlightDays = value);
                      setModalState(() => _highlightDays = value);
                    },
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildQuickActionsForBottomSheet() {
    final totalDays = 12 * 31;
    final validatedCount = _validatedDays.length;
    final remainingDays = totalDays - validatedCount;

    if (remainingDays == 0) {
      return const SizedBox.shrink(); // Masquer si tout est validé
    }

    return Card(
      color: Colors.blue[50],
      child: Padding(
        padding: const EdgeInsets.all(12.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              'Actions Rapides de Cotisation',
              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
            ),
            const SizedBox(height: 8),
            Wrap(
              spacing: 8,
              runSpacing: 4,
              children: [
                _buildQuickActionButton(
                  'Mois Actuel',
                  Icons.calendar_today,
                  () => _validateCurrentMonth(),
                ),
                _buildQuickActionButton(
                  '3 Mois',
                  Icons.calendar_view_month,
                  () => _validateMultipleMonths(3),
                ),
                _buildQuickActionButton(
                  'Tout Valider',
                  Icons.event_available,
                  () => _validateAllCalendar(),
                ),
                _buildQuickActionButton(
                  'Réinitialiser',
                  Icons.refresh,
                  () {
                    setState(() => _validatedDays.clear());
                    Navigator.pop(context); // Fermer le bottom sheet
                  },
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildQuickActions() {
    final totalDays = 12 * 31;
    final validatedCount = _validatedDays.length;
    final remainingDays = totalDays - validatedCount;

    if (remainingDays == 0) {
      return const SizedBox.shrink(); // Masquer si tout est validé
    }

    return Card(
      color: Colors.blue[50],
      child: Padding(
        padding: const EdgeInsets.all(12.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              'Actions Rapides de Cotisation',
              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
            ),
            const SizedBox(height: 8),
            Wrap(
              spacing: 8,
              runSpacing: 4,
              children: [
                _buildQuickActionButton(
                  'Mois Actuel',
                  Icons.calendar_today,
                  () => _validateCurrentMonth(),
                ),
                _buildQuickActionButton(
                  '3 Mois',
                  Icons.calendar_view_month,
                  () => _validateMultipleMonths(3),
                ),
                _buildQuickActionButton(
                  '6 Mois',
                  Icons.date_range,
                  () => _validateMultipleMonths(6),
                ),
                _buildQuickActionButton(
                  'Tout (12×31)',
                  Icons.event_available,
                  () => _validateAllCalendar(),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildQuickActionButton(
      String label, IconData icon, VoidCallback onPressed) {
    return ElevatedButton.icon(
      onPressed: onPressed,
      icon: Icon(icon, size: 16),
      label: Text(label, style: const TextStyle(fontSize: 12)),
      style: ElevatedButton.styleFrom(
        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
        minimumSize: const Size(0, 32),
      ),
    );
  }

  Widget _buildSummary() {
    final totalDays = 12 * 31;
    final validatedCount = _validatedDays.length;
    final remainingDays = totalDays - validatedCount;
    final totalPaid = validatedCount * _dailyAmount;
    final remainingAmount = remainingDays * _dailyAmount;
    final completionPercentage = validatedCount / totalDays;

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(12.0),
        child: SingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: [
              Text(
                'Résumé de Cotisation',
                style: Theme.of(context).textTheme.titleMedium,
              ),
              const SizedBox(height: 6),
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          'Validés: $validatedCount/$totalDays',
                          style: const TextStyle(fontSize: 12),
                        ),
                        Text(
                          'Progression: ${(completionPercentage * 100).toStringAsFixed(1)}%',
                          style: const TextStyle(fontSize: 12),
                        ),
                      ],
                    ),
                  ),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.end,
                      children: [
                        Text(
                          'Restants: $remainingDays',
                          style: const TextStyle(fontSize: 12),
                        ),
                        Text(
                          'Payé: ${totalPaid.toStringAsFixed(0)} FCFA',
                          style: const TextStyle(fontSize: 12),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 6),
              LinearProgressIndicator(
                value: completionPercentage,
                backgroundColor: Colors.grey[300],
                valueColor: AlwaysStoppedAnimation<Color>(
                  completionPercentage == 1.0 ? Colors.green : Colors.blue,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  void _showPaymentConfirmationDialog(TontineCalendarData data) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Confirmer la Cotisation'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
                'Jour sélectionné: ${data.selectedDay.day} ${data.selectedMonth.name}'),
            Text('Jours à payer: ${data.totalUnvalidatedDays}'),
            Text('Montant total: ${data.totalAmount.toStringAsFixed(0)} FCFA'),
            const SizedBox(height: 8),
            const Text('Détail par mois:'),
            ...data.unvalidatedDaysByMonth.entries.map((entry) => Text(
                '• Mois ${entry.key}: ${entry.value.length} jours (${(entry.value.length * _dailyAmount).toStringAsFixed(0)} FCFA)')),
            const SizedBox(height: 16),
            const Text(
              'Voulez-vous effectuer cette cotisation ?',
              style: TextStyle(fontWeight: FontWeight.bold),
            ),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('Non'),
          ),
          ElevatedButton(
            onPressed: () {
              Navigator.of(context).pop();
              _processPayment(data);
            },
            child: const Text('Oui'),
          ),
        ],
      ),
    );
  }

  void _processPayment(TontineCalendarData data) {
    setState(() {
      // Valider les jours sélectionnés
      for (final entry in data.unvalidatedDaysByMonth.entries) {
        for (final day in entry.value) {
          _validatedDays.add(day.copyWith(
            isValidated: true,
            validatedDate: DateTime.now(),
            amount: _dailyAmount,
          ));
        }
      }
    });

    // Afficher le résultat
    _showPaymentResultDialog(data);

    // Vérifier si le mois actuel est complété et naviguer automatiquement
    _checkAndNavigateToNextMonth(data.selectedMonth.month);
  }

  void _checkAndNavigateToNextMonth(int currentMonth) {
    // Vérifier si le mois actuel est complété
    final currentMonthDays =
        _validatedDays.where((day) => day.month == currentMonth).length;
    final isMonthCompleted = currentMonthDays >= 31;

    if (isMonthCompleted && currentMonth < 12) {
      // Attendre un peu avant de naviguer pour que l'utilisateur voie le changement
      Future.delayed(const Duration(milliseconds: 1500), () {
        // Ici on pourrait déclencher la navigation vers le mois suivant
        // Pour l'instant, on affiche juste un message
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text(
                  'Mois $currentMonth complété ! Passage au mois ${currentMonth + 1}'),
              backgroundColor: Colors.green,
            ),
          );
        }
      });
    }
  }

  void _showPaymentResultDialog(TontineCalendarData data) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Cotisation Effectuée ✅'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Icon(
              Icons.check_circle,
              color: Colors.green,
              size: 48,
            ),
            const SizedBox(height: 16),
            Text('${data.totalUnvalidatedDays} jours validés avec succès !'),
            Text('Montant payé: ${data.totalAmount.toStringAsFixed(0)} FCFA'),
            const SizedBox(height: 8),
            Text('Total validé: ${_validatedDays.length}/372 jours'),
            LinearProgressIndicator(
              value: _validatedDays.length / 372,
              backgroundColor: Colors.grey[300],
              valueColor: const AlwaysStoppedAnimation<Color>(Colors.green),
            ),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }

  void _showValidatedDayDetails(TontineDay day) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Jour Déjà Validé ✅'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Icon(
              Icons.check_circle,
              color: Colors.green,
              size: 48,
            ),
            const SizedBox(height: 16),
            Text('Jour: ${day.day}'),
            Text('Mois: ${day.month}'),
            Text(
                'Montant: ${day.amount?.toStringAsFixed(0) ?? _dailyAmount.toStringAsFixed(0)} FCFA'),
            if (day.validatedDate != null)
              Text(
                  'Validé le: ${day.validatedDate!.day}/${day.validatedDate!.month}/${day.validatedDate!.year}'),
            const SizedBox(height: 8),
            const Text(
              'Ce jour a déjà été payé.',
              style: TextStyle(
                fontStyle: FontStyle.italic,
                color: Colors.grey,
              ),
            ),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }

  // Méthodes pour les actions rapides
  void _validateCurrentMonth() {
    final currentMonth = _getCurrentMonth();
    _showBulkValidationDialog(
      'Valider le Mois Actuel',
      'Voulez-vous valider tous les jours restants du mois $currentMonth ?',
      () => _performBulkValidation([currentMonth]),
    );
  }

  void _validateMultipleMonths(int monthCount) {
    final currentMonth = _getCurrentMonth();
    final monthsToValidate = <int>[];

    for (int i = currentMonth;
        i <= 12 && monthsToValidate.length < monthCount;
        i++) {
      if (_hasUnvalidatedDaysInMonth(i)) {
        monthsToValidate.add(i);
      }
    }

    if (monthsToValidate.isEmpty) {
      _showInfoDialog(
          'Aucun mois à valider', 'Tous les mois sont déjà complétés.');
      return;
    }

    final monthNames = monthsToValidate.map((m) => _getMonthName(m)).join(', ');
    _showBulkValidationDialog(
      'Valider $monthCount Mois',
      'Voulez-vous valider tous les jours restants des mois :\n$monthNames ?',
      () => _performBulkValidation(monthsToValidate),
    );
  }

  void _validateAllCalendar() {
    final monthsToValidate = <int>[];
    for (int i = 1; i <= 12; i++) {
      if (_hasUnvalidatedDaysInMonth(i)) {
        monthsToValidate.add(i);
      }
    }

    if (monthsToValidate.isEmpty) {
      _showInfoDialog(
          'Calendrier Complet', 'Tous les jours sont déjà validés !');
      return;
    }

    final totalDays = monthsToValidate.length * 31;
    final totalAmount = totalDays * _dailyAmount;

    _showBulkValidationDialog(
      'Valider Tout le Calendrier',
      'Voulez-vous valider TOUT le calendrier restant ?\n\n'
          '• Mois restants: ${monthsToValidate.length}\n'
          '• Jours à valider: $totalDays\n'
          '• Montant total: ${totalAmount.toStringAsFixed(0)} FCFA',
      () => _performBulkValidation(monthsToValidate),
    );
  }

  // Méthodes utilitaires
  int _getCurrentMonth() {
    // Trouve le premier mois avec des jours non validés
    for (int month = 1; month <= 12; month++) {
      if (_hasUnvalidatedDaysInMonth(month)) {
        return month;
      }
    }
    return 1; // Par défaut
  }

  bool _hasUnvalidatedDaysInMonth(int month) {
    final validatedInMonth =
        _validatedDays.where((day) => day.month == month).length;
    return validatedInMonth < 31;
  }

  String _getMonthName(int month) {
    const monthNames = [
      'Janvier',
      'Février',
      'Mars',
      'Avril',
      'Mai',
      'Juin',
      'Juillet',
      'Août',
      'Septembre',
      'Octobre',
      'Novembre',
      'Décembre'
    ];
    return monthNames[month - 1];
  }

  void _performBulkValidation(List<int> months) {
    final daysToValidate = <TontineDay>[];

    for (final month in months) {
      for (int day = 1; day <= 31; day++) {
        final isAlreadyValidated = _validatedDays.any(
          (validatedDay) =>
              validatedDay.month == month && validatedDay.day == day,
        );

        if (!isAlreadyValidated) {
          daysToValidate.add(TontineDay(
            day: day,
            month: month,
            isValidated: true,
            amount: _dailyAmount,
            validatedDate: DateTime.now(),
          ));
        }
      }
    }

    setState(() {
      _validatedDays.addAll(daysToValidate);
    });

    _showBulkValidationResult(daysToValidate.length, months);
  }

  // Méthodes de dialog
  void _showBulkValidationDialog(
      String title, String content, VoidCallback onConfirm) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(title),
        content: Text(content),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('Annuler'),
          ),
          ElevatedButton(
            onPressed: () {
              Navigator.of(context).pop(); // Fermer le dialogue
              onConfirm();
              // Fermer le bottom sheet après confirmation
              Navigator.of(context).pop();
            },
            child: const Text('Confirmer'),
          ),
        ],
      ),
    );
  }

  void _showInfoDialog(String title, String content) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(title),
        content: Text(content),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.of(context).pop(); // Fermer le dialogue
              // Fermer le bottom sheet
              Navigator.of(context).pop();
            },
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }

  void _showBulkValidationResult(int daysValidated, List<int> months) {
    final monthNames = months.map((m) => _getMonthName(m)).join(', ');
    final totalAmount = daysValidated * _dailyAmount;

    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Cotisation Groupée Effectuée ✅'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Icon(
              Icons.check_circle,
              color: Colors.green,
              size: 48,
            ),
            const SizedBox(height: 16),
            Text('Jours validés: $daysValidated'),
            Text('Mois concernés: $monthNames'),
            Text('Montant total: ${totalAmount.toStringAsFixed(0)} FCFA'),
            const SizedBox(height: 8),
            Text('Progression: ${_validatedDays.length}/372 jours'),
            LinearProgressIndicator(
              value: _validatedDays.length / 372,
              backgroundColor: Colors.grey[300],
              valueColor: const AlwaysStoppedAnimation<Color>(Colors.green),
            ),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }
}
0
likes
150
points
17
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

A customizable Flutter calendar widget designed for tontine applications with paginated months, configurable days, and contribution tracking.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, intl

More

Packages that depend on tontine_calendar