diff --git a/app/devtools_options.yaml b/app/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/app/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/app/lib/pages/add_menu_page.dart b/app/lib/pages/add_menu_page.dart index ce9a8c6..382bfa5 100644 --- a/app/lib/pages/add_menu_page.dart +++ b/app/lib/pages/add_menu_page.dart @@ -25,8 +25,16 @@ class _AddMedicineMenuState extends State { final _stockController = TextEditingController(); TimeOfDay _selectedTime = TimeOfDay.now(); - int _cycle = 1; + String _cycle = '1/day'; int _priority = 1; + int _selectedType = 0; + DateTime _selectedDate = DateTime.now(); + + final List _medicineTypes = const [ + 'Tablet', + 'Capsule', + 'Liquid', + ]; @override void initState() { @@ -70,6 +78,30 @@ class _AddMedicineMenuState extends State { } } + Future _selectDate(BuildContext context) async { + final DateTime? picked = await showDatePicker( + context: context, + initialDate: _selectedDate, + firstDate: DateTime.now(), + lastDate: DateTime.now().add(const Duration(days: 365)), + ); + if (picked != null && picked != _selectedDate) { + setState(() { + _selectedDate = picked; + }); + } + } + + String _dateLabel() { + final now = DateTime.now(); + if (_selectedDate.year == now.year && + _selectedDate.month == now.month && + _selectedDate.day == now.day) { + return 'Today'; + } + return '${_selectedDate.day}/${_selectedDate.month}/${_selectedDate.year}'; + } + void _saveMedicine() async { if (_formKey.currentState!.validate()) { final String timeString = @@ -147,155 +179,414 @@ class _AddMedicineMenuState extends State { return Scaffold( backgroundColor: colorScheme.surface, appBar: AppBar( - toolbarHeight: 130, - leading: Align( - alignment: Alignment.topCenter, - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: BackButton(onPressed: () => Navigator.pop(context)), - ), - ), - titleSpacing: -37, - title: Padding( - padding: const EdgeInsets.only(top: 70.0), - child: Text( - widget.medicineToEdit == null ? "Add Reminder" : "Edit Reminder", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 35, - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - centerTitle: false, + leading: BackButton(onPressed: () => Navigator.pop(context)), automaticallyImplyLeading: false, ), - body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Form( - key: _formKey, - child: ListView( - padding: const EdgeInsets.only(top: 20), - children: [ - TextFormField( - controller: _nameController, - decoration: _buildInputDecoration("Medicine Name"), - validator: (value) => value!.isEmpty ? 'Required' : null, - ), - const SizedBox(height: 16), - - TextFormField( - controller: _dosageController, - keyboardType: TextInputType.number, - decoration: _buildInputDecoration( - "Dosage", - suffixText: "pills/spoons", - ), - validator: (value) => value!.isEmpty ? 'Required' : null, - ), - const SizedBox(height: 16), - - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: InkWell( - onTap: () => _selectTime(context), - child: InputDecorator( - decoration: _buildInputDecoration("Time"), + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Zone 1: Page Title + Text( + widget.medicineToEdit == null + ? 'Add Medicine' + : 'Edit Medicine', + style: Theme.of(context) + .textTheme + .displaySmall + ?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + const SizedBox(height: 20), + + // Zone 2: Header Banner Card + Card( + margin: EdgeInsets.zero, + color: colorScheme.primaryContainer, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(28), + ), + elevation: 0, + child: Padding( + padding: const EdgeInsets.all(24.0), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + + Expanded( + child: ValueListenableBuilder( + valueListenable: _nameController, + builder: (context, value, child) { + final nameText = value.text.isEmpty ? 'Medicine Name' : value.text; + final alphaValue = value.text.isEmpty ? 0.5 : 1.0; + return Text( + nameText, + style: Theme.of(context) + .textTheme + .headlineMedium + ?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.onPrimaryContainer + .withValues(alpha: alphaValue), + ), + ); + }, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 24), + + // Medicine Type Chips + Row( + children: List.generate( + _medicineTypes.length, + (index) { + final isSelected = _selectedType == index; + return Expanded( + child: Padding( + padding: EdgeInsets.only( + right: index < _medicineTypes.length - 1 ? 12.0 : 0.0, + ), + child: ChoiceChip( + label: SizedBox( + width: double.infinity, + child: Center( + child: Text(_medicineTypes[index]), + ), + ), + selected: isSelected, + onSelected: (selected) { + if (selected) { + setState(() => _selectedType = index); + } + }, + selectedColor: colorScheme.secondaryContainer, + labelStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: isSelected + ? colorScheme.onSecondaryContainer + : colorScheme.onSurfaceVariant, + ), + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + showCheckmark: false, + ), + ), + ); + }, + ), + ), + const SizedBox(height: 24), + + // Date/Time Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Date side + GestureDetector( + onTap: () => _selectDate(context), + child: Row( + children: [ + Icon( + Icons.calendar_month, + color: colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 8), + Text( + _dateLabel(), + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.bold), + ), + ], + ), + ), + // Time side + Row( + children: [ + GestureDetector( + onTap: () => _selectTime(context), + child: Card( + color: colorScheme.secondaryContainer, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + child: Text( + _selectedTime.hourOfPeriod + .toString() + .padLeft(2, '0'), + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme + .onSecondaryContainer, + ), + ), + ), + ), + ), Text( - _selectedTime.format(context), - style: TextStyle(color: colorScheme.onSurface), + ':', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.bold), ), - Icon( - Icons.access_time, - size: 20, - color: colorScheme.onSurfaceVariant, + GestureDetector( + onTap: () => _selectTime(context), + child: Card( + color: colorScheme.secondaryContainer, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + child: Text( + _selectedTime.minute + .toString() + .padLeft(2, '0'), + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme + .onSecondaryContainer, + ), + ), + ), + ), + ), + const SizedBox(width: 4), + Card( + color: colorScheme.secondaryContainer, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + child: Text( + _selectedTime.period == DayPeriod.am + ? 'AM' + : 'PM', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + fontWeight: FontWeight.bold, + color: + colorScheme.onSecondaryContainer, + ), + ), + ), ), ], ), - ), + ], + ), + const SizedBox(height: 24), + + // Zone 3: Form Fields + TextFormField( + controller: _nameController, + decoration: _buildInputDecoration("Name"), + validator: (value) => value!.isEmpty ? 'Required' : null, ), - ), - const SizedBox(width: 12), - Expanded( - child: DropdownButtonFormField( - initialValue: _cycle, - decoration: _buildInputDecoration("Cycle").copyWith( - contentPadding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 16, + const SizedBox(height: 16), + + TextFormField( + controller: _dosageController, + keyboardType: TextInputType.number, + decoration: _buildInputDecoration("Dosage"), + validator: (value) => value!.isEmpty ? 'Required' : null, + ), + const SizedBox(height: 16), + + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: DropdownMenu( + initialSelection: _cycle, + label: const Text("Cycle"), + expandedInsets: EdgeInsets.zero, + menuHeight: 200, + menuStyle: MenuStyle( + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: colorScheme.surfaceContainer, + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(20), + borderSide: const BorderSide(width: 3.0), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(20), + borderSide: BorderSide(width: 3.0, color: colorScheme.outlineVariant), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(20), + borderSide: BorderSide(width: 3.0, color: colorScheme.primary), + ), + ), + dropdownMenuEntries: const [ + DropdownMenuEntry(value: '6h', label: '6 hours'), + DropdownMenuEntry(value: '12h', label: '12 hours'), + DropdownMenuEntry(value: '1/day', label: '1/day'), + DropdownMenuEntry(value: '2/day', label: '2/day'), + DropdownMenuEntry(value: '3/day', label: '3/day'), + DropdownMenuEntry(value: 'weekly', label: 'Weekly'), + DropdownMenuEntry(value: 'monthly', label: 'Monthly'), + ], + onSelected: (val) { + if (val != null) setState(() => _cycle = val); + }, + ), ), - ), - items: List.generate(5, (index) => index + 1).map(( - int value, - ) { - return DropdownMenuItem( - value: value, - child: Text("$value /day"), - ); - }).toList(), - onChanged: (val) => setState(() => _cycle = val!), + const SizedBox(width: 12), + Expanded( + child: DropdownMenu( + initialSelection: _priority, + label: const Text("Priority"), + expandedInsets: EdgeInsets.zero, + menuHeight: 200, + menuStyle: MenuStyle( + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: colorScheme.surfaceContainer, + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(20), + borderSide: const BorderSide(width: 3.0), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(20), + borderSide: BorderSide(width: 3.0, color: colorScheme.outlineVariant), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(20), + borderSide: BorderSide(width: 3.0, color: colorScheme.primary), + ), + ), + dropdownMenuEntries: const [ + DropdownMenuEntry(value: 0, label: 'Low'), + DropdownMenuEntry(value: 1, label: 'Medium'), + DropdownMenuEntry(value: 2, label: 'High'), + // No leading icons needed since this is a simple priority, but could add them if requested. + ], + onSelected: (val) { + if (val != null) setState(() => _priority = val); + }, + ), + ), + ], ), - ), - ], - ), - const SizedBox(height: 16), + const SizedBox(height: 16), - TextFormField( - controller: _conditionController, - decoration: _buildInputDecoration("Condition"), - ), - const SizedBox(height: 16), + TextFormField( + controller: _conditionController, + decoration: _buildInputDecoration("Condition"), + ), + const SizedBox(height: 16), - TextFormField( - controller: _doctorController, - decoration: _buildInputDecoration("Prescribed By"), - ), - const SizedBox(height: 16), + TextFormField( + controller: _doctorController, + decoration: _buildInputDecoration("Prescribed By"), + ), + const SizedBox(height: 16), - TextFormField( - controller: _stockController, - keyboardType: TextInputType.number, - decoration: _buildInputDecoration("Current Stock"), - ), - const SizedBox(height: 16), - - DropdownButtonFormField( - initialValue: _priority, - decoration: _buildInputDecoration("Priority").copyWith( - contentPadding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 16, - ), + TextFormField( + controller: _stockController, + keyboardType: TextInputType.number, + decoration: _buildInputDecoration("Current Stock"), + ), + const SizedBox(height: 24), + ], ), - items: const [ - DropdownMenuItem(value: 2, child: Text("High")), - DropdownMenuItem(value: 1, child: Text("Medium")), - DropdownMenuItem(value: 0, child: Text("Low")), - ], - onChanged: (val) => setState(() => _priority = val!), ), - const SizedBox(height: 30), + ), + ), - FilledButton( - onPressed: _saveMedicine, + // Zone 4: Sticky Footer + Padding( + padding: const EdgeInsets.fromLTRB(24, 12, 24, 24), + child: SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: () { + if (_nameController.text.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Please enter a medicine name'), + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ); + return; + } + _saveMedicine(); + }, style: FilledButton.styleFrom( + backgroundColor: colorScheme.primary, + foregroundColor: colorScheme.onPrimary, padding: const EdgeInsets.symmetric(vertical: 16), ), child: Text( widget.medicineToEdit == null - ? "Save Reminder" - : "Update Reminder", + ? 'Save Reminder' + : 'Update Reminder', + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: colorScheme.onPrimary, + ), ), ), - const SizedBox(height: 40), - ], + ), ), - ), + ], ), ); } diff --git a/app/lib/pages/edit_profile_page.dart b/app/lib/pages/edit_profile_page.dart index c564738..276b734 100644 --- a/app/lib/pages/edit_profile_page.dart +++ b/app/lib/pages/edit_profile_page.dart @@ -188,14 +188,24 @@ class _EditProfilePageState extends State { Row( children: [ Expanded( - child: DropdownButtonFormField( - initialValue: _bloodTypeController.text.isEmpty + child: DropdownMenu( + initialSelection: _bloodTypeController.text.isEmpty ? null : _bloodTypeController.text, - decoration: InputDecoration( - labelText: 'Blood', + label: const Text('Blood'), + leadingIcon: const Icon(Icons.water_drop, size: 20), + expandedInsets: EdgeInsets.zero, + menuHeight: 250, + textStyle: const TextStyle(fontSize: 14), + menuStyle: MenuStyle( + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( labelStyle: const TextStyle(fontSize: 14), - prefixIcon: const Icon(Icons.water_drop, size: 20), contentPadding: const EdgeInsets.symmetric( horizontal: 8, vertical: 16, @@ -219,36 +229,36 @@ class _EditProfilePageState extends State { ), ), ), - isExpanded: true, - style: TextStyle(fontSize: 14, color: cs.onSurface), - items: - ['A+', 'A-', 'B+', 'B-', 'AB+', 'AB-', 'O+', 'O-'] - .map( - (t) => DropdownMenuItem( - value: t, - child: Text( - t, - overflow: TextOverflow.ellipsis, - ), - ), - ) - .toList(), - onChanged: (val) => - setState(() => _bloodTypeController.text = val!), - validator: (value) => - value == null || value.isEmpty ? 'Required' : null, + dropdownMenuEntries: ['A+', 'A-', 'B+', 'B-', 'AB+', 'AB-', 'O+', 'O-'] + .map((t) => DropdownMenuEntry(value: t, label: t)) + .toList(), + onSelected: (val) { + if (val != null) { + setState(() => _bloodTypeController.text = val); + } + }, ), ), const SizedBox(width: 12), Expanded( - child: DropdownButtonFormField( - initialValue: _sexController.text.isEmpty + child: DropdownMenu( + initialSelection: _sexController.text.isEmpty ? null : _sexController.text, - decoration: InputDecoration( - labelText: 'Sex', + label: const Text('Sex'), + leadingIcon: const Icon(Icons.wc, size: 20), + expandedInsets: EdgeInsets.zero, + menuHeight: 200, + textStyle: const TextStyle(fontSize: 14), + menuStyle: MenuStyle( + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( labelStyle: const TextStyle(fontSize: 14), - prefixIcon: const Icon(Icons.wc, size: 20), contentPadding: const EdgeInsets.symmetric( horizontal: 8, vertical: 16, @@ -272,20 +282,14 @@ class _EditProfilePageState extends State { ), ), ), - isExpanded: true, - style: TextStyle(fontSize: 14, color: cs.onSurface), - items: ['Male', 'Female', 'Other', 'N/A'] - .map( - (s) => DropdownMenuItem( - value: s, - child: Text(s, overflow: TextOverflow.ellipsis), - ), - ) + dropdownMenuEntries: ['Male', 'Female', 'Other', 'N/A'] + .map((s) => DropdownMenuEntry(value: s, label: s)) .toList(), - onChanged: (val) => - setState(() => _sexController.text = val!), - validator: (value) => - value == null || value.isEmpty ? 'Required' : null, + onSelected: (val) { + if (val != null) { + setState(() => _sexController.text = val); + } + }, ), ), ], diff --git a/app/lib/pages/onboarding_page.dart b/app/lib/pages/onboarding_page.dart index 49dd546..a07a02e 100644 --- a/app/lib/pages/onboarding_page.dart +++ b/app/lib/pages/onboarding_page.dart @@ -686,7 +686,15 @@ class _OnboardingScreenState extends State { label: Text(label, style: const TextStyle(fontSize: 14)), leadingIcon: Icon(icon, size: 20), expandedInsets: EdgeInsets.zero, + menuHeight: 250, textStyle: const TextStyle(fontSize: 14), + menuStyle: MenuStyle( + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + ), inputDecorationTheme: InputDecorationTheme( filled: true, fillColor: cs.surfaceContainer, diff --git a/app/lib/pages/profile_details_page.dart b/app/lib/pages/profile_details_page.dart index c6b058c..64b2d2e 100644 --- a/app/lib/pages/profile_details_page.dart +++ b/app/lib/pages/profile_details_page.dart @@ -81,6 +81,7 @@ class _ProfileDetailsPageState extends State { child: Column( children: [ Card( + margin: EdgeInsets.zero, elevation: 0, color: Theme.of(context).colorScheme.surfaceContainer, shape: RoundedRectangleBorder( @@ -146,6 +147,7 @@ class _ProfileDetailsPageState extends State { ), const SizedBox(height: 16), GridView.count( + padding: EdgeInsets.zero, crossAxisCount: 2, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), @@ -204,6 +206,7 @@ class _ProfileDetailsPageState extends State { required String value, }) { return Card( + margin: EdgeInsets.zero, elevation: 0, color: Theme.of(context).colorScheme.surfaceContainer, shape: RoundedRectangleBorder( diff --git a/app/lib/pages/profile_page.dart b/app/lib/pages/profile_page.dart index 305cc7a..f4d4877 100644 --- a/app/lib/pages/profile_page.dart +++ b/app/lib/pages/profile_page.dart @@ -12,7 +12,7 @@ class ProfilePage extends StatelessWidget { Widget build(BuildContext context) { return Center( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 8.0), + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 8.0), child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ @@ -91,7 +91,7 @@ class ProfilePage extends StatelessWidget { required VoidCallback onTap, }) { return SizedBox( - width: 350, + width: double.infinity, child: OutlinedButton( onPressed: onTap, style: OutlinedButton.styleFrom(