diff --git a/backend/index.js b/backend/index.js index 370a3a3..21de5dd 100644 --- a/backend/index.js +++ b/backend/index.js @@ -88,6 +88,15 @@ app.listen(PORT, '0.0.0.0', () => { console.error('Failed to start auto-archiving schedule:', err.message); } + // Run teacher document requirement due date reminders check on startup and hourly + try { + const { checkAndRunTeacherDueDateReminders } = require('./src/services/teacherReminderService'); + checkAndRunTeacherDueDateReminders(); + setInterval(() => checkAndRunTeacherDueDateReminders(), 60 * 60 * 1000); + } catch (err) { + console.error('Failed to start teacher reminder service:', err.message); + } + // Initialize Auto Cloudflare Tunnel activation when internet is connected try { const { startAutoTunnel } = require('./src/services/tunnelService'); diff --git a/backend/src/controllers/settingsController.js b/backend/src/controllers/settingsController.js index ae53145..057abef 100644 --- a/backend/src/controllers/settingsController.js +++ b/backend/src/controllers/settingsController.js @@ -1,12 +1,15 @@ const db = require('../config/db'); const autoArchiveService = require('../services/autoArchiveService'); +const { checkAndRunTeacherDueDateReminders } = require('../services/teacherReminderService'); exports.getSettings = (req, res) => { try { const rows = db.prepare('SELECT key, value FROM system_settings').all(); const settings = { auto_update_enrollment_from_sf: 'false', - enrollment_grace_period_days: '30' + enrollment_grace_period_days: '30', + remind_teachers_due_date_enabled: 'false', + remind_teachers_due_date_days: '3' }; for (const row of rows) { settings[row.key] = row.value; @@ -56,6 +59,10 @@ exports.updateSettings = (req, res) => { autoArchiveService.scheduleAutoArchiveTimer(); } + if (updates.remind_teachers_due_date_enabled !== undefined || updates.remind_teachers_due_date_days !== undefined) { + checkAndRunTeacherDueDateReminders(); + } + const rows = db.prepare('SELECT key, value FROM system_settings').all(); const updatedSettings = {}; for (const row of rows) { diff --git a/backend/src/models/schema.js b/backend/src/models/schema.js index abef1f4..bcd1e5d 100644 --- a/backend/src/models/schema.js +++ b/backend/src/models/schema.js @@ -935,6 +935,14 @@ const initSchema = () => { INSERT OR IGNORE INTO system_settings (key, value) VALUES ('auto_update_enrollment_from_sf', 'false') `).run(); + db.prepare(` + INSERT OR IGNORE INTO system_settings (key, value) + VALUES ('remind_teachers_due_date_enabled', 'false') + `).run(); + db.prepare(` + INSERT OR IGNORE INTO system_settings (key, value) + VALUES ('remind_teachers_due_date_days', '3') + `).run(); // Seed Admin if not exists const adminUser = db.prepare("SELECT * FROM users WHERE username = 'admin' OR role = 'admin'").get(); diff --git a/backend/src/services/teacherReminderService.js b/backend/src/services/teacherReminderService.js new file mode 100644 index 0000000..fcf0dfe --- /dev/null +++ b/backend/src/services/teacherReminderService.js @@ -0,0 +1,246 @@ +const db = require('../config/db'); +const { sendTeacherAttentionReminder } = require('./emailService'); + +const logActivity = (userId, action, entityType, entityId, description) => { + try { + let uid = userId; + const userCheck = uid ? db.prepare("SELECT id FROM users WHERE id = ?").get(uid) : null; + if (!userCheck) { + const anyUser = db.prepare("SELECT id FROM users WHERE role = 'admin' LIMIT 1").get(); + uid = anyUser ? anyUser.id : null; + } + db.prepare(` + INSERT INTO activity_log (user_id, action, entity_type, entity_id, description) + VALUES (?, ?, ?, ?, ?) + `).run(uid, action, entityType, entityId, description); + } catch (err) { + console.error('[teacherReminderService] logActivity error:', err.message); + } +}; + +/** + * Checks document requirements with due dates and sends automated reminder emails + * to advisory teachers whose enrolled students are missing those requirements. + */ +const checkAndRunTeacherDueDateReminders = async (forced = false) => { + try { + const enabledRow = db.prepare("SELECT value FROM system_settings WHERE key = 'remind_teachers_due_date_enabled'").get(); + const isEnabled = enabledRow?.value === 'true'; + + if (!isEnabled && !forced) { + return { executed: false, reason: 'Teacher due date reminders are disabled in system settings.' }; + } + + const daysRow = db.prepare("SELECT value FROM system_settings WHERE key = 'remind_teachers_due_date_days'").get(); + const reminderDays = parseInt(daysRow?.value || '3', 10) || 3; + + // Today's date string YYYY-MM-DD + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + const todayStr = `${year}-${month}-${day}`; + + // Check if already executed today + if (!forced) { + const lastRunRow = db.prepare("SELECT value FROM system_settings WHERE key = 'last_teacher_reminder_date'").get(); + if (lastRunRow?.value === todayStr) { + return { executed: false, reason: 'Teacher due date reminders already ran today.' }; + } + } + + // Get enabled document requirements that have a due date set + const requirements = db.prepare(` + SELECT id, name, category, due_date, is_mandatory + FROM document_requirements + WHERE is_enabled = 1 AND due_date IS NOT NULL AND TRIM(due_date) != '' + `).all(); + + if (requirements.length === 0) { + if (!forced) { + db.prepare(` + INSERT INTO system_settings (key, value, updated_at) + VALUES ('last_teacher_reminder_date', ?, CURRENT_TIMESTAMP) + ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP + `).run(todayStr); + } + return { executed: false, reason: 'No document requirements with due dates found.' }; + } + + // Filter requirements that are due within reminderDays (0 <= diffDays <= reminderDays) + const todayDateOnly = new Date(`${todayStr}T00:00:00`); + const dueSoonRequirements = requirements.filter(req => { + const reqDateStr = req.due_date.split('T')[0].split(' ')[0]; + const reqDate = new Date(`${reqDateStr}T00:00:00`); + const diffTime = reqDate.getTime() - todayDateOnly.getTime(); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + return diffDays >= 0 && diffDays <= reminderDays; + }); + + if (dueSoonRequirements.length === 0) { + if (!forced) { + db.prepare(` + INSERT INTO system_settings (key, value, updated_at) + VALUES ('last_teacher_reminder_date', ?, CURRENT_TIMESTAMP) + ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP + `).run(todayStr); + } + return { executed: false, reason: `No document requirements due within the next ${reminderDays} day(s).` }; + } + + // Get active teachers with emails + const teachers = db.prepare(` + SELECT id, username, first_name, middle_name, last_name, email + FROM users + WHERE role = 'teacher' AND is_active = 1 AND email IS NOT NULL AND TRIM(email) != '' + `).all(); + + if (teachers.length === 0) { + return { executed: false, reason: 'No active teachers with valid emails found.' }; + } + + let sentCount = 0; + let skippedCount = 0; + + for (const teacher of teachers) { + const teacherName = `${teacher.first_name} ${teacher.last_name}`.trim(); + + // Get sections assigned to this teacher + const sections = db.prepare(` + SELECT s.id, s.name, s.grade_level + FROM teacher_sections ts + JOIN sections s ON ts.section_id = s.id + WHERE ts.teacher_id = ? + ORDER BY s.grade_level ASC, s.name ASC + `).all(teacher.id); + + if (sections.length === 0) { + skippedCount++; + continue; + } + + const sectionsWithAttentionStudents = []; + + for (const section of sections) { + // Find enrolled students in this section + const students = db.prepare(` + SELECT s.id, s.lrn, s.first_name, s.middle_name, s.last_name, s.extension + FROM enrollments e + JOIN students s ON e.student_id = s.id + WHERE e.section_id = ? AND s.status = 'Enrolled' + ORDER BY s.last_name ASC, s.first_name ASC + `).all(section.id); + + const sectionAttentionStudents = []; + + for (const student of students) { + const studentFullName = [student.last_name, student.first_name, student.middle_name, student.extension] + .filter(Boolean) + .join(' '); + + // Check missing requirements among the due soon requirements + const studentLevelCategory = section.grade_level <= 10 ? 'JHS' : 'SHS'; + const matchingReqs = dueSoonRequirements.filter(r => r.category === studentLevelCategory); + + if (matchingReqs.length === 0) continue; + + const missingForStudent = []; + + for (const req of matchingReqs) { + const hasCompleted = db.prepare(` + SELECT id FROM documents + WHERE student_id = ? AND requirement_id = ? AND status = 'Completed' AND deleted_at IS NULL + LIMIT 1 + `).get(student.id, req.id); + + if (!hasCompleted) { + const reqDateStr = req.due_date.split('T')[0].split(' ')[0]; + missingForStudent.push(`${req.name} (Due: ${reqDateStr})`); + } + } + + if (missingForStudent.length > 0) { + sectionAttentionStudents.push({ + id: student.id, + lrn: student.lrn, + name: studentFullName, + missingDocs: missingForStudent + }); + } + } + + if (sectionAttentionStudents.length > 0) { + sectionsWithAttentionStudents.push({ + sectionId: section.id, + sectionName: section.name, + gradeLevel: section.grade_level, + students: sectionAttentionStudents + }); + } + } + + if (sectionsWithAttentionStudents.length > 0) { + try { + // Validate teacher email before sending reminder (MEV / QEV) + let validEmail = true; + try { + const { validateEmailMEV } = require('../controllers/authController'); + const mevResult = await validateEmailMEV(teacher.email); + if (!mevResult.valid) { + validEmail = false; + console.warn(`[TeacherReminderService] Skipping ${teacher.email}: ${mevResult.reason}`); + } + } catch (_) {} + + if (!validEmail) { + skippedCount++; + continue; + } + + await sendTeacherAttentionReminder({ + to: teacher.email, + teacherName, + sectionsWithStudents: sectionsWithAttentionStudents + }); + sentCount++; + } catch (err) { + console.error(`[TeacherReminderService] Failed to send reminder email to ${teacher.email}:`, err.message); + } + } else { + skippedCount++; + } + } + + // Mark today as executed + db.prepare(` + INSERT INTO system_settings (key, value, updated_at) + VALUES ('last_teacher_reminder_date', ?, CURRENT_TIMESTAMP) + ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP + `).run(todayStr); + + if (sentCount > 0) { + logActivity( + null, + 'NOTIFY', + 'system_settings', + null, + `Automated due date reminder emails sent to ${sentCount} teacher(s)` + ); + console.log(`[TeacherReminderService] Successfully sent due date reminder emails to ${sentCount} teacher(s).`); + } + + return { + executed: true, + sentCount, + skippedCount, + dueRequirementsCount: dueSoonRequirements.length + }; + } catch (error) { + console.error('[TeacherReminderService] checkAndRunTeacherDueDateReminders error:', error); + return { executed: false, error: error.message }; + } +}; + +module.exports = { + checkAndRunTeacherDueDateReminders +}; diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 6229bbb..04508c5 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -847,13 +847,30 @@ Files backup/restore - mirror mode or zip --> # SETTINGS SCREEN - make the fade at top and bottom fade show when it has item under it if not dont show --> + + +# CSV BULK IMPORT ENHANCEMENTS +- bulk student import (add bulk student modal / screen): + - add on-screen CSV format example & template guide (headers: LRN, First Name, Middle Name, Last Name, Extension, Sex, Birth Date, 4Ps) + - add live type / paste CSV input area with "Parse & Preview" to submit directly without needing a separate file + - validation table / live preview showing valid rows and detected errors before confirming bulk import +- academic & sections setup (teachers & academic setup screen): + - add bulk CSV import for academic structure: + - format: Academic Year, Grade Level, Section Name (e.g., `2024-2025, 10, Diamond`) + - add on-screen CSV format example / reference & sample rows + - add live type / paste CSV input area with instant parse, validation preview, and bulk submit + --- + # DONT DO -- CSV bulk add student, academic and section [acad year, grade level, sections] - - print list history, print email to the student make it optional, add date of print, add status (done, not done, in progress, picked up), add message to email can be manual message typed by registrar/staff, - teacher no assigned section theme color diff --git a/frontend/lib/ui/screens/settings/settings_screen.dart b/frontend/lib/ui/screens/settings/settings_screen.dart index 8886970..b8fcc5d 100644 --- a/frontend/lib/ui/screens/settings/settings_screen.dart +++ b/frontend/lib/ui/screens/settings/settings_screen.dart @@ -22,6 +22,7 @@ import '../../../domain/entities/setup_models.dart'; import '../../providers/system_settings_provider.dart'; import '../../providers/theme_provider.dart'; import '../../providers/preferences_provider.dart'; +import '../../providers/users_provider.dart'; import '../../../core/services/haptic_service.dart'; import 'security_screen.dart'; class TitleCaseTextInputFormatter extends TextInputFormatter { @@ -120,12 +121,14 @@ class _SettingsScreenState extends ConsumerState { bool _isProfileExpanded = false; bool _isAcademicYearExpanded = false; bool _isAutoEnrollExpanded = false; + bool _isTeacherRemindersExpanded = false; bool _isAppearanceExpanded = false; void _collapseAllSections() { _isProfileExpanded = false; _isAcademicYearExpanded = false; _isAutoEnrollExpanded = false; + _isTeacherRemindersExpanded = false; _isAppearanceExpanded = false; WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) _onScroll(); @@ -517,6 +520,63 @@ class _SettingsScreenState extends ConsumerState { } } + bool _isSendingManualReminder = false; + + Future _handleManualRemindTeachers() async { + final confirmed = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + title: const Row( + children: [ + Icon(Icons.mark_email_read_outlined, color: AppColors.primaryGreen, size: 24), + SizedBox(width: 8), + Expanded(child: Text('Send Teacher Reminders')), + ], + ), + content: const Text( + 'Send reminder emails to all active advisory teachers with the list of students in their assigned sections who currently have missing document requirements?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('CANCEL'), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryGreen, + foregroundColor: Colors.white, + ), + onPressed: () => Navigator.pop(ctx, true), + child: const Text('SEND REMINDERS'), + ), + ], + ), + ); + + if (confirmed != true || !mounted) return; + + setState(() => _isSendingManualReminder = true); + try { + final res = await ref.read(usersProvider.notifier).remindTeachers(); + if (!mounted) return; + showSuccessDialog( + context, + title: 'Reminders Sent', + message: res['message']?.toString() ?? 'Teacher reminders sent successfully.', + ); + } catch (e) { + if (!mounted) return; + showErrorDialog( + context, + 'Reminder Failed', + e.toString().replaceAll('Exception: ', ''), + ); + } finally { + if (mounted) setState(() => _isSendingManualReminder = false); + } + } + @override Widget build(BuildContext context) { ref.listen(activeTabProvider, (previous, next) { @@ -1496,6 +1556,196 @@ class _SettingsScreenState extends ConsumerState { }, ), const SizedBox(height: AppSizes.p24), + + // ── Remind Teachers (Admin Only) ────────────────────── + Consumer( + builder: (context, ref, _) { + final sysSettingsAsync = ref.watch(systemSettingsProvider); + final settingsMap = sysSettingsAsync.asData?.value ?? {}; + final isRemindEnabled = + (settingsMap['remind_teachers_due_date_enabled'] ?? 'false') == 'true'; + final reminderDays = + int.tryParse(settingsMap['remind_teachers_due_date_days'] ?? '3') ?? 3; + final isDark = Theme.of(context).brightness == Brightness.dark; + + return _buildCollapsibleCard( + title: 'Remind Teachers', + isExpanded: _isTeacherRemindersExpanded, + onToggle: () => _toggleSection( + () => _isTeacherRemindersExpanded = !_isTeacherRemindersExpanded, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.mark_email_read_outlined, + color: AppColors.primaryGreen, + size: 24, + ), + const SizedBox(width: AppSizes.p12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Automated Due Date Reminders', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const SizedBox(height: 2), + Text( + 'Notify teachers via email before document requirement due dates arrive', + style: TextStyle( + fontSize: 12, + color: isDark ? AppColors.darkTextSecondary : Colors.grey.shade600, + ), + ), + ], + ), + ), + Switch( + value: isRemindEnabled, + activeThumbColor: AppColors.primaryGreen, + onChanged: (val) { + ref + .read(systemSettingsProvider.notifier) + .updateSetting( + 'remind_teachers_due_date_enabled', + val ? 'true' : 'false', + ); + }, + ), + ], + ), + const SizedBox(height: AppSizes.p16), + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.primaryGreen.withValues(alpha: 0.06), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: AppColors.primaryGreen.withValues(alpha: 0.2), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon( + Icons.info_outline, + size: 16, + color: AppColors.primaryGreen, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'When enabled, advisory teachers will automatically receive an email listing the students in their assigned section(s) who currently have missing document requirements before the due date.', + style: TextStyle( + fontSize: 12, + color: isDark ? AppColors.darkTextPrimary : AppColors.primaryGreen, + height: 1.4, + ), + ), + ), + ], + ), + ), + const SizedBox(height: AppSizes.p16), + Text( + 'Reminder Schedule', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const SizedBox(height: 4), + Text( + 'Number of days before the requirement due date to send reminder emails:', + style: TextStyle( + fontSize: 12, + color: isDark ? AppColors.darkTextSecondary : Colors.grey.shade600, + ), + ), + const SizedBox(height: AppSizes.p12), + Wrap( + spacing: 8, + runSpacing: 8, + children: [1, 2, 3, 5, 7].map((days) { + final isSelected = reminderDays == days; + return ChoiceChip( + label: Text( + days == 3 ? '$days Days (Default)' : '$days Day${days > 1 ? 's' : ''}', + style: TextStyle( + fontSize: 12, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + color: isSelected + ? Colors.white + : (isDark ? AppColors.darkTextPrimary : AppColors.textPrimary), + ), + ), + selected: isSelected, + selectedColor: AppColors.primaryGreen, + backgroundColor: isDark ? AppColors.darkSurface2 : Colors.grey.shade100, + showCheckmark: false, + onSelected: (_) { + ref + .read(systemSettingsProvider.notifier) + .updateSetting( + 'remind_teachers_due_date_days', + days.toString(), + ); + }, + ); + }).toList(), + ), + const SizedBox(height: AppSizes.p16), + Divider(color: isDark ? AppColors.darkBorder : Colors.grey.shade200), + const SizedBox(height: AppSizes.p12), + Row( + children: [ + Expanded( + child: Text( + 'Need to remind teachers right away? You can trigger a manual reminder dispatch at any time.', + style: TextStyle( + fontSize: 12, + color: isDark ? AppColors.darkTextSecondary : Colors.grey.shade600, + ), + ), + ), + const SizedBox(width: 12), + ElevatedButton.icon( + onPressed: _isSendingManualReminder ? null : _handleManualRemindTeachers, + icon: _isSendingManualReminder + ? const SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + ) + : const Icon(Icons.send_rounded, size: 15), + label: Text(_isSendingManualReminder ? 'Sending...' : 'Send Reminders Now'), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryGreen, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + textStyle: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + ), + ], + ), + ], + ), + ); + }, + ), + const SizedBox(height: AppSizes.p24), ], // ── Appearance & Feedback Card (Collapsible) ─────────── diff --git a/frontend/lib/ui/screens/settings/teacher_management_screen.dart b/frontend/lib/ui/screens/settings/teacher_management_screen.dart index e169b74..506ce16 100644 --- a/frontend/lib/ui/screens/settings/teacher_management_screen.dart +++ b/frontend/lib/ui/screens/settings/teacher_management_screen.dart @@ -167,63 +167,6 @@ class _TeachersTab extends ConsumerStatefulWidget { } class _TeachersTabState extends ConsumerState<_TeachersTab> { - bool _isSending = false; - - Future _handleRemindAll() async { - final confirmed = await showDialog( - context: context, - builder: (ctx) => AlertDialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - title: const Row( - children: [ - Icon(Icons.mark_email_read_outlined, color: AppColors.primaryGreen, size: 24), - SizedBox(width: 8), - Expanded(child: Text('Remind All Teachers')), - ], - ), - content: const Text( - 'Send reminder emails to all active teachers with the list of students in their assigned sections who currently have missing mandatory documents?', - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx, false), - child: const Text('CANCEL'), - ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primaryGreen, - foregroundColor: Colors.white, - ), - onPressed: () => Navigator.pop(ctx, true), - child: const Text('SEND REMINDERS'), - ), - ], - ), - ); - - if (confirmed != true || !mounted) return; - - setState(() => _isSending = true); - try { - final res = await ref.read(usersProvider.notifier).remindTeachers(); - if (!mounted) return; - showSuccessDialog( - context, - title: 'Reminders Sent', - message: res['message']?.toString() ?? 'Teacher reminders sent successfully.', - ); - } catch (e) { - if (!mounted) return; - showErrorDialog( - context, - 'Reminder Failed', - e.toString().replaceAll('Exception: ', ''), - ); - } finally { - if (mounted) setState(() => _isSending = false); - } - } - @override Widget build(BuildContext context) { final usersAsync = ref.watch(usersProvider); @@ -274,25 +217,6 @@ class _TeachersTabState extends ConsumerState<_TeachersTab> { color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary, ), ), - const Spacer(), - ElevatedButton.icon( - onPressed: _isSending ? null : _handleRemindAll, - icon: _isSending - ? const SizedBox( - width: 14, - height: 14, - child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), - ) - : const Icon(Icons.mark_email_read_outlined, size: 16), - label: Text(_isSending ? 'Sending...' : 'Remind All Teachers'), - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primaryGreen, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - textStyle: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - ), - ), ], ), ), diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 7ee19d7..1665192 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.26+26 +version: 1.0.27+27 environment: sdk: ^3.11.5