diff --git a/lib/helpers/theme/admin_theme.dart b/lib/helpers/theme/admin_theme.dart index 29ab18d..55d5680 100644 --- a/lib/helpers/theme/admin_theme.dart +++ b/lib/helpers/theme/admin_theme.dart @@ -2,34 +2,9 @@ import 'package:flutter/material.dart'; import 'package:marco/helpers/theme/theme_customizer.dart'; enum LeftBarThemeType { light, dark } - enum ContentThemeType { light, dark } - enum RightBarThemeType { light, dark } -enum ContentThemeColor { - primary, - secondary, - success, - info, - warning, - danger, - light, - dark, - pink, - green, - red, - brandRed; - - Color get color { - return (AdminTheme.theme.contentTheme.getMappedIntoThemeColor[this]?['color']) ?? Colors.black; - } - - Color get onColor { - return (AdminTheme.theme.contentTheme.getMappedIntoThemeColor[this]?['onColor']) ?? Colors.white; - } -} - class LeftBarTheme { final Color background, onBackground; final Color labelColor; @@ -43,16 +18,15 @@ class LeftBarTheme { this.activeItemBackground = const Color(0x15663399), }); - //-------------------------------------- Left Bar Theme ----------------------------------------// - static final LeftBarTheme lightLeftBarTheme = LeftBarTheme(); static final LeftBarTheme darkLeftBarTheme = LeftBarTheme( - background: const Color(0xff282c32), - onBackground: const Color(0xffdcdcdc), - labelColor: const Color(0xff32BFAE), - activeItemBackground: const Color(0x1532BFAE), - activeItemColor: const Color(0xff32BFAE)); + background: const Color(0xff282c32), + onBackground: const Color(0xffdcdcdc), + labelColor: const Color(0xff32BFAE), + activeItemBackground: const Color(0x1532BFAE), + activeItemColor: const Color(0xff32BFAE), + ); static LeftBarTheme getThemeFromType(LeftBarThemeType leftBarThemeType) { switch (leftBarThemeType) { @@ -73,11 +47,12 @@ class TopBarTheme { this.onBackground = const Color(0xff313a46), }); - //-------------------------------------- Left Bar Theme ----------------------------------------// - static final TopBarTheme lightTopBarTheme = TopBarTheme(); - static final TopBarTheme darkTopBarTheme = TopBarTheme(background: const Color(0xff2c3036), onBackground: const Color(0xffdcdcdc)); + static final TopBarTheme darkTopBarTheme = TopBarTheme( + background: const Color(0xff2c3036), + onBackground: const Color(0xffdcdcdc), + ); } class RightBarTheme { @@ -91,19 +66,41 @@ class RightBarTheme { this.onDisabled = const Color(0xff313a46), }); - //-------------------------------------- Left Bar Theme ----------------------------------------// - static final RightBarTheme lightRightBarTheme = RightBarTheme( - disabled: const Color(0xffffffff), - onDisabled: const Color(0xffdee2e6), - activeSwitchBorderColor: const Color(0xff727cf5), - inactiveSwitchBorderColor: const Color(0xffdee2e6)); + disabled: const Color(0xffffffff), + onDisabled: const Color(0xffdee2e6), + activeSwitchBorderColor: const Color(0xff727cf5), + inactiveSwitchBorderColor: const Color(0xffdee2e6), + ); static final RightBarTheme darkRightBarTheme = RightBarTheme( - disabled: const Color(0xff444d57), - activeSwitchBorderColor: const Color(0xff727cf5), - inactiveSwitchBorderColor: const Color(0xffdee2e6), - onDisabled: const Color(0xff515a65)); + disabled: const Color(0xff444d57), + activeSwitchBorderColor: const Color(0xff727cf5), + inactiveSwitchBorderColor: const Color(0xffdee2e6), + onDisabled: const Color(0xff515a65), + ); +} + +enum ContentThemeColor { + primary, + secondary, + success, + info, + warning, + danger, + light, + dark, + pink, + green, + red; + + Color get color { + return (AdminTheme.theme.contentTheme.getMappedIntoThemeColor[this]?['color']) ?? Colors.black; + } + + Color get onColor { + return (AdminTheme.theme.contentTheme.getMappedIntoThemeColor[this]?['onColor']) ?? Colors.white; + } } class ContentTheme { @@ -120,29 +117,11 @@ class ContentTheme { final Color purple, onPurple; final Color pink, onPink; final Color red, onRed; - final Color brandRed, onBrandRed; final Color cardBackground, cardShadow, cardBorder, cardText, cardTextMuted; final Color title; final Color disabled, onDisabled; - Map> get getMappedIntoThemeColor { - var c = AdminTheme.theme.contentTheme; - return { - ContentThemeColor.primary: {'color': c.primary, 'onColor': c.onPrimary}, - ContentThemeColor.secondary: {'color': c.secondary, 'onColor': c.onSecondary}, - ContentThemeColor.success: {'color': c.success, 'onColor': c.onSuccess}, - ContentThemeColor.info: {'color': c.info, 'onColor': c.onInfo}, - ContentThemeColor.warning: {'color': c.warning, 'onColor': c.onWarning}, - ContentThemeColor.danger: {'color': c.danger, 'onColor': c.onDanger}, - ContentThemeColor.light: {'color': c.light, 'onColor': c.onLight}, - ContentThemeColor.dark: {'color': c.dark, 'onColor': c.onDark}, - ContentThemeColor.pink: {'color': c.pink, 'onColor': c.onPink}, - ContentThemeColor.red: {'color': c.red, 'onColor': c.onRed}, - ContentThemeColor.brandRed: {'color': c.brandRed, 'onColor': c.onBrandRed}, - }; - } - ContentTheme({ this.background = const Color(0xfffafbfe), this.onBackground = const Color(0xffF1F1F2), @@ -163,13 +142,11 @@ class ContentTheme { this.dark = const Color(0xff313a46), this.onDark = const Color(0xffffffff), this.purple = const Color(0xff800080), - this.onPurple = const Color(0xffFF0000), - this.pink = const Color(0xffFF1087), + this.onPurple = const Color(0xffffffff), + this.pink = const Color(0xffff1087), this.onPink = const Color(0xffffffff), - this.red = const Color(0xffFF0000), + this.red = const Color(0xffff0000), this.onRed = const Color(0xffffffff), - this.brandRed = const Color.fromARGB(255, 255, 0, 0), - this.onBrandRed = const Color(0xffffffff), this.cardBackground = const Color(0xffffffff), this.cardShadow = const Color(0xffffffff), this.cardBorder = const Color(0xffffffff), @@ -180,44 +157,103 @@ class ContentTheme { this.onDisabled = const Color(0xffffffff), }); - //-------------------------------------- Left Bar Theme ----------------------------------------// + Map> get getMappedIntoThemeColor { + return { + ContentThemeColor.primary: {'color': primary, 'onColor': onPrimary}, + ContentThemeColor.secondary: {'color': secondary, 'onColor': onSecondary}, + ContentThemeColor.success: {'color': success, 'onColor': onSuccess}, + ContentThemeColor.info: {'color': info, 'onColor': onInfo}, + ContentThemeColor.warning: {'color': warning, 'onColor': onWarning}, + ContentThemeColor.danger: {'color': danger, 'onColor': onDanger}, + ContentThemeColor.light: {'color': light, 'onColor': onLight}, + ContentThemeColor.dark: {'color': dark, 'onColor': onDark}, + ContentThemeColor.pink: {'color': pink, 'onColor': onPink}, + ContentThemeColor.red: {'color': red, 'onColor': onRed}, + }; + } - static final ContentTheme lightContentTheme = ContentTheme( - primary: Color(0xff663399), - background: const Color(0xfffafbfe), - onBackground: const Color(0xff313a46), - cardBorder: const Color(0xffe8ecf1), - cardBackground: const Color(0xffffffff), - cardShadow: const Color(0xff9aa1ab), - cardText: const Color(0xff6c757d), - title: const Color(0xff6c757d), - cardTextMuted: const Color(0xff98a6ad), - brandRed: const Color.fromARGB(255, 255, 0, 0), - onBrandRed: const Color(0xffffffff), - ); + ContentTheme copyWith({ + Color? primary, + Color? onPrimary, + Color? secondary, + Color? onSecondary, + Color? background, + Color? onBackground, + }) { + return ContentTheme( + primary: primary ?? this.primary, + onPrimary: onPrimary ?? this.onPrimary, + secondary: secondary ?? this.secondary, + onSecondary: onSecondary ?? this.onSecondary, + background: background ?? this.background, + onBackground: onBackground ?? this.onBackground, + success: success, + onSuccess: onSuccess, + danger: danger, + onDanger: onDanger, + warning: warning, + onWarning: onWarning, + info: info, + onInfo: onInfo, + light: light, + onLight: onLight, + dark: dark, + onDark: onDark, + purple: purple, + onPurple: onPurple, + pink: pink, + onPink: onPink, + red: red, + onRed: onRed, + cardBackground: cardBackground, + cardShadow: cardShadow, + cardBorder: cardBorder, + cardText: cardText, + cardTextMuted: cardTextMuted, + title: title, + disabled: disabled, + onDisabled: onDisabled, + ); + } - static final ContentTheme darkContentTheme = ContentTheme( - primary: Color(0xff32BFAE), - background: const Color(0xff343a40), - onBackground: const Color(0xffF1F1F2), - disabled: const Color(0xff444d57), - onDisabled: const Color(0xff515a65), - cardBorder: const Color(0xff464f5b), - cardBackground: const Color(0xff37404a), - cardShadow: const Color(0xff01030E), - cardText: const Color(0xffaab8c5), - title: const Color(0xffaab8c5), - cardTextMuted: const Color(0xff8391a2), - brandRed: const Color.fromARGB(255, 255, 0, 0), - onBrandRed: const Color(0xffffffff), - ); + static ContentTheme withColorTheme( + ColorThemeType colorTheme, { + ThemeMode mode = ThemeMode.light, + }) { + final baseTheme = mode == ThemeMode.light + ? ContentTheme() + : ContentTheme( + primary: const Color(0xff32BFAE), + background: const Color(0xff343a40), + onBackground: const Color(0xffF1F1F2), + cardBorder: const Color(0xff464f5b), + cardBackground: const Color(0xff37404a), + cardShadow: const Color(0xff01030E), + cardText: const Color(0xffaab8c5), + title: const Color(0xffaab8c5), + cardTextMuted: const Color(0xff8391a2), + ); + + switch (colorTheme) { + case ColorThemeType.purple: + return baseTheme.copyWith(primary: const Color(0xff663399), onPrimary: Colors.white); + case ColorThemeType.red: + return baseTheme.copyWith(primary: const Color(0xffff0000), onPrimary: Colors.white); + case ColorThemeType.green: + return baseTheme.copyWith(primary: const Color(0xff49BF3C), onPrimary: Colors.white); + case ColorThemeType.blue: + return baseTheme.copyWith(primary: const Color(0xff007bff), onPrimary: Colors.white); + } + } } +enum ColorThemeType { purple, red, green, blue } + class AdminTheme { + final ContentTheme contentTheme; final LeftBarTheme leftBarTheme; final RightBarTheme rightBarTheme; final TopBarTheme topBarTheme; - final ContentTheme contentTheme; AdminTheme({ required this.leftBarTheme, @@ -226,19 +262,22 @@ class AdminTheme { required this.contentTheme, }); - //-------------------------------------- Left Bar Theme ----------------------------------------// - static AdminTheme theme = AdminTheme( - leftBarTheme: LeftBarTheme.lightLeftBarTheme, - topBarTheme: TopBarTheme.lightTopBarTheme, - rightBarTheme: RightBarTheme.lightRightBarTheme, - contentTheme: ContentTheme.lightContentTheme); + leftBarTheme: LeftBarTheme.lightLeftBarTheme, + topBarTheme: TopBarTheme.lightTopBarTheme, + rightBarTheme: RightBarTheme.lightRightBarTheme, + contentTheme: ContentTheme.withColorTheme(ColorThemeType.purple, mode: ThemeMode.light), + ); static void setTheme() { + final themeMode = ThemeCustomizer.instance.theme; + final colorTheme = ThemeCustomizer.instance.colorTheme; + theme = AdminTheme( - leftBarTheme: ThemeCustomizer.instance.theme == ThemeMode.dark ? LeftBarTheme.darkLeftBarTheme : LeftBarTheme.lightLeftBarTheme, - topBarTheme: ThemeCustomizer.instance.theme == ThemeMode.dark ? TopBarTheme.darkTopBarTheme : TopBarTheme.lightTopBarTheme, - rightBarTheme: ThemeCustomizer.instance.theme == ThemeMode.dark ? RightBarTheme.darkRightBarTheme : RightBarTheme.lightRightBarTheme, - contentTheme: ThemeCustomizer.instance.theme == ThemeMode.dark ? ContentTheme.darkContentTheme : ContentTheme.lightContentTheme); + leftBarTheme: themeMode == ThemeMode.dark ? LeftBarTheme.darkLeftBarTheme : LeftBarTheme.lightLeftBarTheme, + topBarTheme: themeMode == ThemeMode.dark ? TopBarTheme.darkTopBarTheme : TopBarTheme.lightTopBarTheme, + rightBarTheme: themeMode == ThemeMode.dark ? RightBarTheme.darkRightBarTheme : RightBarTheme.lightRightBarTheme, + contentTheme: ContentTheme.withColorTheme(colorTheme, mode: themeMode), + ); } } diff --git a/lib/helpers/theme/theme_customizer.dart b/lib/helpers/theme/theme_customizer.dart index 4abb4e7..cf6f16a 100644 --- a/lib/helpers/theme/theme_customizer.dart +++ b/lib/helpers/theme/theme_customizer.dart @@ -24,7 +24,7 @@ class ThemeCustomizer { ThemeMode leftBarTheme = ThemeMode.light; ThemeMode rightBarTheme = ThemeMode.light; ThemeMode topBarTheme = ThemeMode.light; - + ColorThemeType colorTheme = ColorThemeType.red; bool rightBarOpen = false; bool leftBarCondensed = false; @@ -73,6 +73,11 @@ class ThemeCustomizer { } } + /// Public method to trigger theme updates externally + static void applyThemeChange() { + _notify(); + } + static void notify() { for (var value in _notifier) { value(oldInstance, instance); diff --git a/lib/helpers/theme/theme_editor_widget.dart b/lib/helpers/theme/theme_editor_widget.dart new file mode 100644 index 0000000..8e6b2b3 --- /dev/null +++ b/lib/helpers/theme/theme_editor_widget.dart @@ -0,0 +1,271 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/wave_background.dart'; +import 'package:marco/helpers/theme/admin_theme.dart'; +import 'package:marco/helpers/theme/theme_customizer.dart'; + +class ThemeOption { + final String label; + final Color primary; + final Color button; + final Color brand; + final ColorThemeType colorThemeType; + + ThemeOption( + this.label, this.primary, this.button, this.brand, this.colorThemeType); +} + +final List themeOptions = [ + ThemeOption( + "Theme 1", Colors.red, Colors.red, Colors.red, ColorThemeType.red), + ThemeOption( + "Theme 2", + const Color(0xFF49BF3C), + const Color(0xFF49BF3C), + const Color(0xFF49BF3C), + ColorThemeType.green, + ), + ThemeOption( + "Theme 3", + const Color(0xFF3F51B5), + const Color(0xFF3F51B5), + const Color(0xFF3F51B5), + ColorThemeType.blue, + ), + ThemeOption( + "Theme 4", + const Color(0xFF663399), + const Color(0xFF663399), + const Color(0xFF663399), + ColorThemeType.purple, + ), +]; + +class ThemeController extends GetxController { + RxInt selectedIndex = 0.obs; + RxBool showApplied = false.obs; + + void init() { + final currentPrimary = AdminTheme.theme.contentTheme.primary; + int index = themeOptions + .indexWhere((opt) => opt.primary.value == currentPrimary.value); + selectedIndex.value = index == -1 ? 0 : index; + } + + void applyTheme(int index) async { + selectedIndex.value = index; + showApplied.value = true; + + ThemeCustomizer.instance.colorTheme = themeOptions[index].colorThemeType; + + ThemeCustomizer.applyThemeChange(); + + await Future.delayed(const Duration(milliseconds: 600)); + showApplied.value = false; + } +} + +class ThemeEditorWidget extends StatefulWidget { + final VoidCallback onClose; + + const ThemeEditorWidget({super.key, required this.onClose}); + + @override + _ThemeEditorWidgetState createState() => _ThemeEditorWidgetState(); +} + +class _ThemeEditorWidgetState extends State { + final ThemeController themeController = Get.put(ThemeController()); + + @override + void initState() { + super.initState(); + themeController.init(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header row with title and close button + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MyText.bodyLarge("Theme Customization", fontWeight: 600), + IconButton( + icon: const Icon(Icons.close), + onPressed: widget.onClose, + tooltip: "Back", + iconSize: 20, + ), + ], + ), + const SizedBox(height: 12), + + // Theme cards wrapped in reactive Obx widget + Center( + child: Obx( + () => Wrap( + spacing: 12, + runSpacing: 12, + alignment: WrapAlignment.center, + children: List.generate(themeOptions.length, (i) { + return ThemeCard( + themeOption: themeOptions[i], + isSelected: themeController.selectedIndex.value == i, + onTap: () => themeController.applyTheme(i), + ); + }), + ), + ), + ), + + const SizedBox(height: 12), + + // Applied indicator reactive widget + Obx( + () => themeController.showApplied.value + ? Padding( + padding: const EdgeInsets.only(top: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.check_circle, + color: + themeOptions[themeController.selectedIndex.value] + .brand, + size: 20, + ), + const SizedBox(width: 6), + Text( + "Theme Applied!", + style: TextStyle( + color: themeOptions[ + themeController.selectedIndex.value] + .brand, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ) + : const SizedBox(), + ), + + const SizedBox(height: 16), + const Text( + "Preview and select a theme. You can change this anytime.", + style: TextStyle(fontSize: 13, color: Colors.black54), + ), + ], + ), + ); + } +} + +class ThemeCard extends StatelessWidget { + final ThemeOption themeOption; + final bool isSelected; + final VoidCallback onTap; + + const ThemeCard({ + Key? key, + required this.themeOption, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 80, + child: Material( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + elevation: isSelected ? 4 : 1, + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected ? themeOption.brand : Colors.transparent, + width: 2, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 80, + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Stack( + fit: StackFit.expand, + children: [ + CustomPaint( + painter: RedWavePainter(themeOption.brand, 0.15)), + Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Hello, User!", + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.w600, + color: themeOption.primary, + fontSize: 12, + ), + ), + const SizedBox(height: 4), + SizedBox( + height: 18, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: themeOption.button, + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + elevation: 1, + textStyle: const TextStyle(fontSize: 10), + ), + onPressed: () {}, + child: const Text("Welcome"), + ), + ), + ], + ), + ), + ], + ), + ), + ), + const SizedBox(height: 6), + Text( + themeOption.label, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 12, + color: Colors.grey[700], + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/helpers/utils/attendance_actions.dart b/lib/helpers/utils/attendance_actions.dart index cc67376..0a7a315 100644 --- a/lib/helpers/utils/attendance_actions.dart +++ b/lib/helpers/utils/attendance_actions.dart @@ -95,7 +95,7 @@ class AttendanceButtonHelper { } } - static Color getButtonColor({ + static Color getprimary({ required bool isYesterday, required bool isTodayApproved, required int activity, diff --git a/lib/helpers/utils/base_bottom_sheet.dart b/lib/helpers/utils/base_bottom_sheet.dart index 359f82c..c2094e1 100644 --- a/lib/helpers/utils/base_bottom_sheet.dart +++ b/lib/helpers/utils/base_bottom_sheet.dart @@ -1,15 +1,17 @@ import 'package:flutter/material.dart'; import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; -class BaseBottomSheet extends StatelessWidget { +class BaseBottomSheet extends StatefulWidget { final String title; + final String? subtitle; final Widget child; final VoidCallback onCancel; final VoidCallback onSubmit; final bool isSubmitting; final String submitText; - final Color submitColor; + final Color? submitColor; final IconData submitIcon; final bool showButtons; final Widget? bottomContent; @@ -20,18 +22,26 @@ class BaseBottomSheet extends StatelessWidget { required this.child, required this.onCancel, required this.onSubmit, + this.subtitle, this.isSubmitting = false, this.submitText = 'Submit', - this.submitColor = Colors.indigo, + this.submitColor, this.submitIcon = Icons.check_circle_outline, this.showButtons = true, this.bottomContent, }); + @override + State createState() => _BaseBottomSheetState(); +} + +class _BaseBottomSheetState extends State with UIMixin { @override Widget build(BuildContext context) { final theme = Theme.of(context); final mediaQuery = MediaQuery.of(context); + final effectiveSubmitColor = + widget.submitColor ?? contentTheme.primary; return SingleChildScrollView( padding: mediaQuery.viewInsets, @@ -50,33 +60,50 @@ class BaseBottomSheet extends StatelessWidget { ], ), child: SafeArea( - // 👈 prevents overlap with nav bar top: false, child: Padding( padding: const EdgeInsets.fromLTRB(20, 16, 20, 32), child: Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ MySpacing.height(5), - Container( - width: 40, - height: 5, - decoration: BoxDecoration( - color: Colors.grey.shade300, - borderRadius: BorderRadius.circular(10), + Center( + child: Container( + width: 40, + height: 5, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(10), + ), ), ), MySpacing.height(12), - MyText.titleLarge(title, fontWeight: 700), + Center( + child: MyText.titleLarge( + widget.title, + fontWeight: 700, + textAlign: TextAlign.center, + ), + ), + if (widget.subtitle != null && + widget.subtitle!.isNotEmpty) ...[ + MySpacing.height(4), + MyText.bodySmall( + widget.subtitle!, + fontWeight: 600, + color: Colors.grey[700], + ), + ], MySpacing.height(12), - child, + widget.child, MySpacing.height(12), - if (showButtons) ...[ + if (widget.showButtons) ...[ Row( children: [ Expanded( child: ElevatedButton.icon( - onPressed: onCancel, + onPressed: widget.onCancel, icon: const Icon(Icons.close, color: Colors.white), label: MyText.bodyMedium( "Cancel", @@ -88,34 +115,40 @@ class BaseBottomSheet extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), - padding: const EdgeInsets.symmetric(vertical: 8), + padding: + const EdgeInsets.symmetric(vertical: 8), ), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton.icon( - onPressed: isSubmitting ? null : onSubmit, - icon: Icon(submitIcon, color: Colors.white), + onPressed: + widget.isSubmitting ? null : widget.onSubmit, + icon: + Icon(widget.submitIcon, color: Colors.white), label: MyText.bodyMedium( - isSubmitting ? "Submitting..." : submitText, + widget.isSubmitting + ? "Submitting..." + : widget.submitText, color: Colors.white, fontWeight: 600, ), style: ElevatedButton.styleFrom( - backgroundColor: submitColor, + backgroundColor: effectiveSubmitColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), - padding: const EdgeInsets.symmetric(vertical: 8), + padding: + const EdgeInsets.symmetric(vertical: 8), ), ), ), ], ), - if (bottomContent != null) ...[ + if (widget.bottomContent != null) ...[ MySpacing.height(12), - bottomContent!, + widget.bottomContent!, ], ], ], diff --git a/lib/helpers/widgets/wave_background.dart b/lib/helpers/widgets/wave_background.dart new file mode 100644 index 0000000..fcb2584 --- /dev/null +++ b/lib/helpers/widgets/wave_background.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; + +class RedWaveBackground extends StatelessWidget { + final Color brandRed; + final double heightFactor; + + const RedWaveBackground({ + super.key, + required this.brandRed, + this.heightFactor = 0.2, + }); + + @override + Widget build(BuildContext context) { + return CustomPaint( + painter: RedWavePainter(brandRed, heightFactor), + size: Size.infinite, + ); + } +} + +class RedWavePainter extends CustomPainter { + final Color brandRed; + final double heightFactor; + + RedWavePainter(this.brandRed, this.heightFactor); + + @override + void paint(Canvas canvas, Size size) { + final paint1 = Paint() + ..shader = LinearGradient( + colors: [brandRed, const Color.fromARGB(255, 97, 22, 22)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ).createShader(Rect.fromLTWH(0, 0, size.width, size.height)); + + final path1 = Path() + ..moveTo(0, size.height * heightFactor) + ..quadraticBezierTo( + size.width * 0.25, size.height * 0.05, + size.width * 0.5, size.height * 0.15, + ) + ..quadraticBezierTo( + size.width * 0.75, size.height * 0.25, + size.width, size.height * 0.1, + ) + ..lineTo(size.width, 0) + ..lineTo(0, 0) + ..close(); + + canvas.drawPath(path1, paint1); + + final paint2 = Paint()..color = brandRed.withOpacity(0.15); + final path2 = Path() + ..moveTo(0, size.height * (heightFactor + 0.05)) + ..quadraticBezierTo( + size.width * 0.4, size.height * 0.1, + size.width, size.height * 0.2, + ) + ..lineTo(size.width, 0) + ..lineTo(0, 0) + ..close(); + + canvas.drawPath(path2, paint2); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} diff --git a/lib/model/attendance/attendence_action_button.dart b/lib/model/attendance/attendence_action_button.dart index 84f698f..f292ea5 100644 --- a/lib/model/attendance/attendence_action_button.dart +++ b/lib/model/attendance/attendence_action_button.dart @@ -235,7 +235,7 @@ class _AttendanceActionButtonState extends State { isTodayApproved: isTodayApproved, ); - final buttonColor = AttendanceButtonHelper.getButtonColor( + final primary = AttendanceButtonHelper.getprimary( isYesterday: isYesterday, isTodayApproved: isTodayApproved, activity: emp.activity, @@ -245,7 +245,7 @@ class _AttendanceActionButtonState extends State { isUploading: isUploading, isButtonDisabled: isButtonDisabled, buttonText: buttonText, - buttonColor: buttonColor, + primary: primary, onPressed: isButtonDisabled ? null : _handleButtonPressed, ); }); @@ -256,7 +256,7 @@ class AttendanceActionButtonUI extends StatelessWidget { final bool isUploading; final bool isButtonDisabled; final String buttonText; - final Color buttonColor; + final Color primary; final VoidCallback? onPressed; const AttendanceActionButtonUI({ @@ -264,7 +264,7 @@ class AttendanceActionButtonUI extends StatelessWidget { required this.isUploading, required this.isButtonDisabled, required this.buttonText, - required this.buttonColor, + required this.primary, required this.onPressed, }); @@ -275,7 +275,7 @@ class AttendanceActionButtonUI extends StatelessWidget { child: ElevatedButton( onPressed: onPressed, style: ElevatedButton.styleFrom( - backgroundColor: buttonColor, + backgroundColor: primary, padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12), textStyle: const TextStyle(fontSize: 12), ), diff --git a/lib/model/attendance/info_card.dart b/lib/model/attendance/info_card.dart index b1cf4ac..c2df83f 100644 --- a/lib/model/attendance/info_card.dart +++ b/lib/model/attendance/info_card.dart @@ -15,7 +15,7 @@ class ReusableListCard extends StatelessWidget { final String Function(T item)? getOutTimeText; final bool Function(T item)? isUploading; final String Function(T item)? getButtonText; - final Color Function(String buttonText)? getButtonColor; + final Color Function(String buttonText)? getprimary; const ReusableListCard({ Key? key, @@ -30,7 +30,7 @@ class ReusableListCard extends StatelessWidget { this.getOutTimeText, this.isUploading, this.getButtonText, - this.getButtonColor, + this.getprimary, }) : super(key: key); @override @@ -47,8 +47,8 @@ class ReusableListCard extends StatelessWidget { final item = items[index]; final buttonText = getButtonText?.call(item) ?? 'Action'; final uploading = isUploading?.call(item) ?? false; - final buttonColor = - getButtonColor?.call(buttonText) ?? Theme.of(context).primaryColor; + final primary = + getprimary?.call(buttonText) ?? Theme.of(context).primaryColor; return Column( children: [ @@ -121,7 +121,7 @@ class ReusableListCard extends StatelessWidget { ? null : () => onActionPressed(item), style: ElevatedButton.styleFrom( - backgroundColor: buttonColor, + backgroundColor: primary, padding: const EdgeInsets.symmetric(horizontal: 12), textStyle: const TextStyle(fontSize: 12), ), diff --git a/lib/model/document/user_document_filter_bottom_sheet.dart b/lib/model/document/user_document_filter_bottom_sheet.dart index fa0546a..616ec46 100644 --- a/lib/model/document/user_document_filter_bottom_sheet.dart +++ b/lib/model/document/user_document_filter_bottom_sheet.dart @@ -8,8 +8,9 @@ import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text_style.dart'; import 'package:marco/model/document/document_filter_model.dart'; import 'dart:convert'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; -class UserDocumentFilterBottomSheet extends StatelessWidget { +class UserDocumentFilterBottomSheet extends StatelessWidget with UIMixin { final String entityId; final String entityTypeId; final DocumentController docController = Get.find(); @@ -100,7 +101,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget { vertical: 10), decoration: BoxDecoration( color: docController.isUploadedAt.value - ? Colors.indigo.shade400 + ? contentTheme.primary : Colors.transparent, borderRadius: const BorderRadius.horizontal( @@ -131,7 +132,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget { vertical: 10), decoration: BoxDecoration( color: !docController.isUploadedAt.value - ? Colors.indigo.shade400 + ? contentTheme.primary : Colors.transparent, borderRadius: const BorderRadius.horizontal( @@ -264,7 +265,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget { onChanged: (val) => docController.isVerified.value = val, activeColor: - Colors.indigo, + contentTheme.primary, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), diff --git a/lib/view/auth/email_login_form.dart b/lib/view/auth/email_login_form.dart index c2ef521..e716c02 100644 --- a/lib/view/auth/email_login_form.dart +++ b/lib/view/auth/email_login_form.dart @@ -94,7 +94,7 @@ class _EmailLoginFormState extends State with UIMixin { MaterialStateProperty.resolveWith( (states) => states.contains(WidgetState.selected) - ? contentTheme.brandRed + ? contentTheme.primary : Colors.white, ), checkColor: contentTheme.onPrimary, @@ -132,7 +132,7 @@ class _EmailLoginFormState extends State with UIMixin { elevation: 2, padding: MySpacing.xy(80, 16), borderRadiusAll: 10, - backgroundColor: contentTheme.brandRed, + backgroundColor: contentTheme.primary, child: MyText.labelLarge( isLoading ? 'Logging in...' : 'Login', fontWeight: 700, diff --git a/lib/view/auth/forgot_password_screen.dart b/lib/view/auth/forgot_password_screen.dart index 9875b89..d9e8150 100644 --- a/lib/view/auth/forgot_password_screen.dart +++ b/lib/view/auth/forgot_password_screen.dart @@ -8,6 +8,7 @@ import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/my_button.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/images.dart'; +import 'package:marco/helpers/widgets/wave_background.dart'; class ForgotPasswordScreen extends StatefulWidget { const ForgotPasswordScreen({super.key}); @@ -59,7 +60,7 @@ class _ForgotPasswordScreenState extends State return Scaffold( body: Stack( children: [ - _RedWaveBackground(brandRed: contentTheme.brandRed), + RedWaveBackground(brandRed: contentTheme.primary), SafeArea( child: Center( child: Column( @@ -230,8 +231,8 @@ class _ForgotPasswordScreenState extends State padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 16), borderRadiusAll: 10, backgroundColor: _isLoading - ? contentTheme.brandRed.withOpacity(0.6) - : contentTheme.brandRed, + ? contentTheme.primary.withOpacity(0.6) + : contentTheme.primary, child: _isLoading ? const SizedBox( height: 20, @@ -253,68 +254,13 @@ class _ForgotPasswordScreenState extends State Widget _buildBackButton() { return TextButton.icon( onPressed: () async => await LocalStorage.logout(), - icon: const Icon(Icons.arrow_back, size: 18, color: Colors.redAccent), + icon: Icon(Icons.arrow_back, size: 18, color: contentTheme.primary,), label: MyText.bodyMedium( 'Back to Login', - color: contentTheme.brandRed, + color: contentTheme.primary, fontWeight: 600, fontSize: 14, ), ); } } - -class _RedWaveBackground extends StatelessWidget { - final Color brandRed; - const _RedWaveBackground({required this.brandRed}); - - @override - Widget build(BuildContext context) { - return CustomPaint( - painter: _WavePainter(brandRed), - size: Size.infinite, - ); - } -} - -class _WavePainter extends CustomPainter { - final Color brandRed; - - _WavePainter(this.brandRed); - - @override - void paint(Canvas canvas, Size size) { - final paint1 = Paint() - ..shader = LinearGradient( - colors: [brandRed, const Color.fromARGB(255, 97, 22, 22)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ).createShader(Rect.fromLTWH(0, 0, size.width, size.height)); - - final path1 = Path() - ..moveTo(0, size.height * 0.2) - ..quadraticBezierTo(size.width * 0.25, size.height * 0.05, - size.width * 0.5, size.height * 0.15) - ..quadraticBezierTo( - size.width * 0.75, size.height * 0.25, size.width, size.height * 0.1) - ..lineTo(size.width, 0) - ..lineTo(0, 0) - ..close(); - - canvas.drawPath(path1, paint1); - - final paint2 = Paint()..color = Colors.redAccent.withOpacity(0.15); - final path2 = Path() - ..moveTo(0, size.height * 0.25) - ..quadraticBezierTo( - size.width * 0.4, size.height * 0.1, size.width, size.height * 0.2) - ..lineTo(size.width, 0) - ..lineTo(0, 0) - ..close(); - - canvas.drawPath(path2, paint2); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) => false; -} diff --git a/lib/view/auth/login_option_screen.dart b/lib/view/auth/login_option_screen.dart index c3364ed..2f08def 100644 --- a/lib/view/auth/login_option_screen.dart +++ b/lib/view/auth/login_option_screen.dart @@ -7,6 +7,7 @@ import 'package:marco/view/auth/otp_login_form.dart'; import 'package:marco/helpers/services/api_endpoints.dart'; import 'package:marco/view/auth/request_demo_bottom_sheet.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/helpers/widgets/wave_background.dart'; enum LoginOption { email, otp } @@ -83,7 +84,7 @@ class _WelcomeScreenState extends State ), const SizedBox(height: 20), option == LoginOption.email - ? EmailLoginForm() + ? EmailLoginForm() : const OTPLoginScreen(), ], ), @@ -101,13 +102,14 @@ class _WelcomeScreenState extends State return Scaffold( body: Stack( children: [ - _RedWaveBackground(brandRed: contentTheme.brandRed), + RedWaveBackground(brandRed: contentTheme.primary), SafeArea( child: Center( child: SingleChildScrollView( padding: const EdgeInsets.all(24), child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: isNarrow ? double.infinity : 420), + constraints: BoxConstraints( + maxWidth: isNarrow ? double.infinity : 420), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -166,7 +168,10 @@ class _WelcomeScreenState extends State decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle, - boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10, offset: Offset(0, 4))], + boxShadow: [ + BoxShadow( + color: Colors.black12, blurRadius: 10, offset: Offset(0, 4)) + ], ), child: Image.asset(Images.logoDark), ), @@ -230,9 +235,10 @@ class _WelcomeScreenState extends State ), ), style: ElevatedButton.styleFrom( - backgroundColor: contentTheme.brandRed, + backgroundColor: contentTheme.primary, foregroundColor: Colors.white, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), elevation: 4, shadowColor: Colors.black26, ), @@ -247,55 +253,3 @@ class _WelcomeScreenState extends State ); } } - -// Red wave background painter -class _RedWaveBackground extends StatelessWidget { - final Color brandRed; - const _RedWaveBackground({required this.brandRed}); - - @override - Widget build(BuildContext context) { - return CustomPaint( - painter: _WavePainter(brandRed), - size: Size.infinite, - ); - } -} - -class _WavePainter extends CustomPainter { - final Color brandRed; - _WavePainter(this.brandRed); - - @override - void paint(Canvas canvas, Size size) { - final paint1 = Paint() - ..shader = LinearGradient( - colors: [brandRed, const Color.fromARGB(255, 97, 22, 22)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ).createShader(Rect.fromLTWH(0, 0, size.width, size.height)); - - final path1 = Path() - ..moveTo(0, size.height * 0.2) - ..quadraticBezierTo(size.width * 0.25, size.height * 0.05, size.width * 0.5, size.height * 0.15) - ..quadraticBezierTo(size.width * 0.75, size.height * 0.25, size.width, size.height * 0.1) - ..lineTo(size.width, 0) - ..lineTo(0, 0) - ..close(); - - canvas.drawPath(path1, paint1); - - final paint2 = Paint()..color = Colors.redAccent.withOpacity(0.15); - final path2 = Path() - ..moveTo(0, size.height * 0.25) - ..quadraticBezierTo(size.width * 0.4, size.height * 0.1, size.width, size.height * 0.2) - ..lineTo(size.width, 0) - ..lineTo(0, 0) - ..close(); - - canvas.drawPath(path2, paint2); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) => false; -} diff --git a/lib/view/auth/mpin_auth_screen.dart b/lib/view/auth/mpin_auth_screen.dart index 32343f7..fdef51d 100644 --- a/lib/view/auth/mpin_auth_screen.dart +++ b/lib/view/auth/mpin_auth_screen.dart @@ -8,6 +8,8 @@ import 'package:marco/images.dart'; import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/api_endpoints.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; +import 'package:marco/helpers/widgets/wave_background.dart'; + class MPINAuthScreen extends StatefulWidget { const MPINAuthScreen({super.key}); @@ -51,7 +53,7 @@ class _MPINAuthScreenState extends State return Scaffold( body: Stack( children: [ - _RedWaveBackground(brandRed: contentTheme.brandRed), + RedWaveBackground(brandRed: contentTheme.primary), SafeArea( child: Center( child: Column( @@ -281,8 +283,8 @@ class _MPINAuthScreenState extends State padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), borderRadiusAll: 10, backgroundColor: controller.isLoading.value - ? contentTheme.brandRed.withOpacity(0.6) - : contentTheme.brandRed, + ? contentTheme.primary.withOpacity(0.6) + : contentTheme.primary, child: controller.isLoading.value ? const SizedBox( height: 20, @@ -315,11 +317,11 @@ class _MPINAuthScreenState extends State if (isNewUser || isChangeMpin) TextButton.icon( onPressed: () => Get.toNamed('/dashboard'), - icon: const Icon(Icons.arrow_back, - size: 18, color: Colors.redAccent), + icon: Icon(Icons.arrow_back, + size: 18, color: contentTheme.primary), label: MyText.bodyMedium( 'Back to Home Page', - color: contentTheme.brandRed, + color: contentTheme.primary, fontWeight: 600, fontSize: 14, ), @@ -331,7 +333,7 @@ class _MPINAuthScreenState extends State size: 18, color: Colors.redAccent), label: MyText.bodyMedium( 'Go back to Login Screen', - color: contentTheme.brandRed, + color: contentTheme.primary, fontWeight: 600, fontSize: 14, ), @@ -341,57 +343,3 @@ class _MPINAuthScreenState extends State }); } } - -class _RedWaveBackground extends StatelessWidget { - final Color brandRed; - const _RedWaveBackground({required this.brandRed}); - - @override - Widget build(BuildContext context) { - return CustomPaint( - painter: _WavePainter(brandRed), - size: Size.infinite, - ); - } -} - -class _WavePainter extends CustomPainter { - final Color brandRed; - const _WavePainter(this.brandRed); - - @override - void paint(Canvas canvas, Size size) { - final paint1 = Paint() - ..shader = LinearGradient( - colors: [brandRed, const Color.fromARGB(255, 97, 22, 22)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ).createShader(Rect.fromLTWH(0, 0, size.width, size.height)); - - final path1 = Path() - ..moveTo(0, size.height * 0.2) - ..quadraticBezierTo(size.width * 0.25, size.height * 0.05, - size.width * 0.5, size.height * 0.15) - ..quadraticBezierTo( - size.width * 0.75, size.height * 0.25, size.width, size.height * 0.1) - ..lineTo(size.width, 0) - ..lineTo(0, 0) - ..close(); - - canvas.drawPath(path1, paint1); - - final paint2 = Paint()..color = Colors.redAccent.withOpacity(0.15); - final path2 = Path() - ..moveTo(0, size.height * 0.25) - ..quadraticBezierTo( - size.width * 0.4, size.height * 0.1, size.width, size.height * 0.2) - ..lineTo(size.width, 0) - ..lineTo(0, 0) - ..close(); - - canvas.drawPath(path2, paint2); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) => false; -} diff --git a/lib/view/auth/otp_login_form.dart b/lib/view/auth/otp_login_form.dart index 1276e80..e8b75b0 100644 --- a/lib/view/auth/otp_login_form.dart +++ b/lib/view/auth/otp_login_form.dart @@ -81,7 +81,7 @@ class _OTPLoginScreenState extends State with UIMixin { elevation: 2, padding: MySpacing.xy(24, 16), borderRadiusAll: 10, - backgroundColor: isDisabled ? Colors.grey : contentTheme.brandRed, + backgroundColor: isDisabled ? Colors.grey : contentTheme.primary, child: controller.isSending.value ? SizedBox( width: 20, @@ -170,7 +170,7 @@ class _OTPLoginScreenState extends State with UIMixin { ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: contentTheme.brandRed, width: 2), + borderSide: BorderSide(color: contentTheme.primary, width: 2), ), ), ), @@ -220,7 +220,7 @@ class _OTPLoginScreenState extends State with UIMixin { elevation: 2, padding: MySpacing.xy(24, 16), borderRadiusAll: 10, - backgroundColor: contentTheme.brandRed, + backgroundColor: contentTheme.primary, child: MyText.labelMedium( 'Verify OTP', fontWeight: 600, diff --git a/lib/view/auth/request_demo_bottom_sheet.dart b/lib/view/auth/request_demo_bottom_sheet.dart index 6977089..d7f839d 100644 --- a/lib/view/auth/request_demo_bottom_sheet.dart +++ b/lib/view/auth/request_demo_bottom_sheet.dart @@ -4,6 +4,7 @@ import 'package:marco/helpers/services/auth_service.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/my_text.dart'; +import 'package:marco/helpers/widgets/my_spacing.dart'; class OrganizationFormBottomSheet { static void show(BuildContext context) { @@ -81,170 +82,203 @@ class _OrganizationFormState extends State<_OrganizationForm> with UIMixin { @override Widget build(BuildContext context) { - return Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), - ), - padding: const EdgeInsets.fromLTRB(20, 20, 20, 40), - child: SingleChildScrollView( - controller: widget.scrollController, - child: Form( - key: validator.formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Center( - child: Container( - width: 40, - height: 5, - margin: const EdgeInsets.only(bottom: 20), - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(10), - ), - ), - ), - Center( - child: Column( - children: [ - MyText.titleLarge( - 'Adventure starts here 🚀', - fontWeight: 600, - color: Colors.black87, - ), - const SizedBox(height: 4), - MyText.bodySmall( - "Make your app management easy and fun!", - color: Colors.grey, - ), - ], - ), - ), - const SizedBox(height: 20), - _sectionHeader('Organization Info'), - _buildTextField('organizationName', 'Organization Name'), - _buildTextField('email', 'Email', - keyboardType: TextInputType.emailAddress), - _buildTextField('about', 'About Organization'), - _sectionHeader('Contact Details'), - _buildTextField('contactPerson', 'Contact Person'), - _buildTextField('contactNumber', 'Contact Number', - keyboardType: TextInputType.phone), - _buildTextField('address', 'Current Address'), - _sectionHeader('Additional Details'), - _buildPopupMenuField( - 'Organization Size', - _sizes, - _selectedSize, - (val) => setState(() => _selectedSize = val), - 'Please select organization size', - ), - _buildPopupMenuField( - 'Industry', - _industries.map((e) => e['name'] as String).toList(), - _selectedIndustryId != null - ? _industries.firstWhere( - (e) => e['id'] == _selectedIndustryId)['name'] - : null, - (val) { - setState(() { - final selectedIndustry = _industries.firstWhere( - (element) => element['name'] == val, - orElse: () => {}, - ); - _selectedIndustryId = selectedIndustry['id']; - }); - }, - 'Please select industry', - ), - const SizedBox(height: 12), - Row( - children: [ - Checkbox( - value: _agreed, - onChanged: (val) => setState(() => _agreed = val ?? false), - fillColor: MaterialStateProperty.resolveWith((states) => - states.contains(MaterialState.selected) - ? contentTheme.brandRed - : Colors.white), - checkColor: Colors.white, - side: const BorderSide(color: Colors.red, width: 2), - ), - Row( - children: [ - MyText( - 'I agree to the ', - color: Colors.black87, - ), - MyText( - 'privacy policy & terms', - color: contentTheme.brandRed, - fontWeight: 600, - ), - ], - ) - ], - ), - const SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - OutlinedButton.icon( - onPressed: () => Navigator.pop(context), - icon: const Icon(Icons.arrow_back, color: Colors.red), - label: MyText.bodyMedium("Back", color: Colors.red), - style: OutlinedButton.styleFrom( - side: const BorderSide(color: Colors.red), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.symmetric( - horizontal: 20, vertical: 14), - ), - ), - ElevatedButton.icon( - onPressed: _loading ? null : _submitForm, - icon: _loading - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2, - ), - ) - : const Icon(Icons.check_circle_outline, - color: Colors.white), - label: _loading - ? const SizedBox.shrink() - : MyText.bodyMedium("Submit", color: Colors.white), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.indigo, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.symmetric( - horizontal: 28, vertical: 14), - ), - ), - ], - ), - const SizedBox(height: 8), - Center( - child: TextButton.icon( - onPressed: () => Navigator.pop(context), - icon: - const Icon(Icons.arrow_back, size: 18, color: Colors.red), - label: MyText.bodySmall( - 'Back to log in', - fontWeight: 600, - color: contentTheme.brandRed, - ), - ), + return SingleChildScrollView( + padding: MediaQuery.of(context).viewInsets, + child: Padding( + padding: const EdgeInsets.only(top: 60), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + boxShadow: const [ + BoxShadow( + color: Colors.black12, + blurRadius: 12, + offset: Offset(0, -2), ), ], ), + child: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 32), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 40, + height: 5, + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(10), + ), + ), + ), + MySpacing.height(12), + Center( + child: MyText.titleLarge( + 'Adventure starts here 🚀', + fontWeight: 700, + textAlign: TextAlign.center, + ), + ), + MySpacing.height(4), + Center( + child: MyText.bodySmall( + "Make your app management easy and fun!", + color: Colors.grey[700], + ), + ), + MySpacing.height(12), + Form( + key: validator.formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _sectionHeader('Organization Info'), + _buildTextField('organizationName', 'Organization Name'), + _buildTextField('email', 'Email', + keyboardType: TextInputType.emailAddress), + _buildTextField('about', 'About Organization'), + _sectionHeader('Contact Details'), + _buildTextField('contactPerson', 'Contact Person'), + _buildTextField('contactNumber', 'Contact Number', + keyboardType: TextInputType.phone), + _buildTextField('address', 'Current Address'), + _sectionHeader('Additional Details'), + _buildPopupMenuField( + 'Organization Size', + _sizes, + _selectedSize, + (val) => setState(() => _selectedSize = val), + 'Please select organization size', + ), + _buildPopupMenuField( + 'Industry', + _industries.map((e) => e['name'] as String).toList(), + _selectedIndustryId != null + ? _industries.firstWhere( + (e) => e['id'] == _selectedIndustryId)['name'] + : null, + (val) { + setState(() { + final selectedIndustry = _industries.firstWhere( + (element) => element['name'] == val, + orElse: () => {}, + ); + _selectedIndustryId = selectedIndustry['id']; + }); + }, + 'Please select industry', + ), + const SizedBox(height: 12), + Row( + children: [ + Checkbox( + value: _agreed, + onChanged: (val) => + setState(() => _agreed = val ?? false), + fillColor: MaterialStateProperty.resolveWith( + (states) => states.contains(MaterialState.selected) + ? contentTheme.primary + : Colors.white), + checkColor: Colors.white, + side: BorderSide(color: contentTheme.primary, width: 2), + ), + Flexible( + child: Wrap( + children: [ + MyText( + 'I agree to the ', + color: Colors.black87, + ), + MyText( + 'privacy policy & terms', + color: contentTheme.primary, + fontWeight: 600, + ), + ], + ), + ), + ], + ), + ], + ), + ), + MySpacing.height(12), + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.close, color: Colors.white), + label: MyText.bodyMedium( + "Cancel", + color: Colors.white, + fontWeight: 600, + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: _loading ? null : _submitForm, + icon: _loading + ? SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : const Icon(Icons.check_circle_outline, + color: Colors.white), + label: MyText.bodyMedium( + _loading ? "Submitting..." : "Submit", + color: Colors.white, + fontWeight: 600, + ), + style: ElevatedButton.styleFrom( + backgroundColor: contentTheme.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + ], + ), + MySpacing.height(12), + Center( + child: TextButton.icon( + onPressed: () => Navigator.pop(context), + icon: Icon( + Icons.arrow_back, + size: 18, + color: contentTheme.primary, + ), + label: MyText.bodySmall( + 'Back to log in', + fontWeight: 600, + color: contentTheme.primary, + ), + ), + ), + ], + ), + ), + ), ), ), ); @@ -300,7 +334,7 @@ class _OrganizationFormState extends State<_OrganizationForm> with UIMixin { ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: contentTheme.brandRed, width: 1.5), + borderSide: BorderSide(color: contentTheme.primary, width: 1.5), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), @@ -377,7 +411,7 @@ class _OrganizationFormState extends State<_OrganizationForm> with UIMixin { focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: - BorderSide(color: contentTheme.brandRed, width: 1.5), + BorderSide(color: contentTheme.primary, width: 1.5), ), errorText: fieldState.errorText, ), @@ -386,8 +420,7 @@ class _OrganizationFormState extends State<_OrganizationForm> with UIMixin { children: [ MyText.bodyMedium( selectedValue ?? 'Select $label', - color: - selectedValue == null ? Colors.grey : Colors.black, + color: selectedValue == null ? Colors.grey : Colors.black, ), const Icon(Icons.arrow_drop_down, color: Colors.grey), ], diff --git a/lib/view/directory/directory_view.dart b/lib/view/directory/directory_view.dart index 7817c3a..caf5433 100644 --- a/lib/view/directory/directory_view.dart +++ b/lib/view/directory/directory_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; - +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/controller/directory/directory_controller.dart'; import 'package:marco/controller/directory/create_bucket_controller.dart'; import 'package:marco/helpers/utils/launcher_utils.dart'; @@ -24,7 +24,7 @@ class DirectoryView extends StatefulWidget { State createState() => _DirectoryViewState(); } -class _DirectoryViewState extends State { +class _DirectoryViewState extends State with UIMixin { final DirectoryController controller = Get.find(); final TextEditingController searchController = TextEditingController(); final PermissionController permissionController = @@ -127,7 +127,7 @@ class _DirectoryViewState extends State { child: ElevatedButton( onPressed: () => Navigator.pop(context, true), style: ElevatedButton.styleFrom( - backgroundColor: Colors.indigo, + backgroundColor: contentTheme.primary, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), @@ -173,7 +173,7 @@ class _DirectoryViewState extends State { backgroundColor: Colors.grey[100], floatingActionButton: FloatingActionButton.extended( heroTag: 'createContact', - backgroundColor: Colors.red, + backgroundColor: contentTheme.primary, onPressed: _handleCreateContact, icon: const Icon(Icons.person_add_alt_1, color: Colors.white), label: const Text("Add Contact", style: TextStyle(color: Colors.white)), @@ -319,13 +319,13 @@ class _DirectoryViewState extends State { PopupMenuItem( value: 2, child: Row( - children: const [ + children: [ Icon(Icons.add_box_outlined, size: 20, color: Colors.black87), SizedBox(width: 10), Expanded(child: Text("Create Bucket")), Icon(Icons.chevron_right, - size: 20, color: Colors.red), + size: 20, color: contentTheme.primary), ], ), onTap: () { @@ -352,13 +352,13 @@ class _DirectoryViewState extends State { PopupMenuItem( value: 1, child: Row( - children: const [ + children: [ Icon(Icons.label_outline, size: 20, color: Colors.black87), SizedBox(width: 10), Expanded(child: Text("Manage Buckets")), Icon(Icons.chevron_right, - size: 20, color: Colors.red), + size: 20, color: contentTheme.primary), ], ), onTap: () { @@ -395,7 +395,7 @@ class _DirectoryViewState extends State { child: Text('Show Deleted Contacts')), Switch.adaptive( value: !controller.isActive.value, - activeColor: Colors.indigo, + activeColor: contentTheme.primary, onChanged: (val) { controller.isActive.value = !val; controller.fetchContacts(active: !val); diff --git a/lib/view/directory/notes_view.dart b/lib/view/directory/notes_view.dart index 00f579e..d374fdc 100644 --- a/lib/view/directory/notes_view.dart +++ b/lib/view/directory/notes_view.dart @@ -134,8 +134,8 @@ class _NotesViewState extends State with UIMixin { ), if (note.isActive) ...[ IconButton( - icon: Icon(isEditing ? Icons.close : Icons.edit_outlined, - color: Colors.indigo, size: 18), + icon: Icon(isEditing ? Icons.close : Icons.edit, + color: contentTheme.primary, size: 18), splashRadius: 18, onPressed: () { controller.editingNoteId.value = @@ -143,7 +143,7 @@ class _NotesViewState extends State with UIMixin { }, ), IconButton( - icon: const Icon(Icons.delete_outline, + icon: const Icon(Icons.delete, size: 18, color: Colors.red), splashRadius: 18, onPressed: () async { diff --git a/lib/view/document/user_document_screen.dart b/lib/view/document/user_document_screen.dart index e7228eb..941d730 100644 --- a/lib/view/document/user_document_screen.dart +++ b/lib/view/document/user_document_screen.dart @@ -19,6 +19,8 @@ import 'package:marco/model/document/document_upload_bottom_sheet.dart'; import 'package:marco/model/document/documents_list_model.dart'; import 'package:marco/model/document/user_document_filter_bottom_sheet.dart'; import 'package:marco/view/document/document_details_page.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; + class UserDocumentsPage extends StatefulWidget { final String? entityId; @@ -34,7 +36,7 @@ class UserDocumentsPage extends StatefulWidget { State createState() => _UserDocumentsPageState(); } -class _UserDocumentsPageState extends State { +class _UserDocumentsPageState extends State with UIMixin { final DocumentController docController = Get.put(DocumentController()); final PermissionController permissionController = Get.put(PermissionController()); final DocumentDetailsController controller = Get.put(DocumentDetailsController()); @@ -395,7 +397,7 @@ class _UserDocumentsPageState extends State { const Expanded(child: Text('Show Deleted Documents')), Switch.adaptive( value: docController.showInactive.value, - activeColor: Colors.indigo, + activeColor: contentTheme.primary, onChanged: (val) { docController.showInactive.value = val; docController.fetchDocuments( @@ -614,7 +616,7 @@ class _UserDocumentsPageState extends State { color: Colors.white, fontWeight: 600, ), - backgroundColor: Colors.red, + backgroundColor: contentTheme.primary, ) : SizedBox.shrink(); }), diff --git a/lib/view/employees/employee_detail_screen.dart b/lib/view/employees/employee_detail_screen.dart index 6d0c418..3c66f6c 100644 --- a/lib/view/employees/employee_detail_screen.dart +++ b/lib/view/employees/employee_detail_screen.dart @@ -12,6 +12,8 @@ import 'package:marco/controller/permission_controller.dart'; import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; import 'package:marco/model/employees/add_employee_bottom_sheet.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; + class EmployeeDetailPage extends StatefulWidget { final String employeeId; @@ -27,7 +29,7 @@ class EmployeeDetailPage extends StatefulWidget { State createState() => _EmployeeDetailPageState(); } -class _EmployeeDetailPageState extends State { +class _EmployeeDetailPageState extends State with UIMixin { final EmployeesScreenController controller = Get.put(EmployeesScreenController()); final PermissionController permissionController = @@ -252,7 +254,7 @@ class _EmployeeDetailPageState extends State { ), IconButton( icon: - const Icon(Icons.edit, size: 24, color: Colors.red), + Icon(Icons.edit, size: 24, color: contentTheme.primary), onPressed: () async { final result = await showModalBottomSheet>( @@ -313,7 +315,7 @@ class _EmployeeDetailPageState extends State { ), ); }, - backgroundColor: Colors.red, + backgroundColor: contentTheme.primary, icon: const Icon(Icons.assignment), label: const Text( 'Assign to Project', diff --git a/lib/view/employees/employees_screen.dart b/lib/view/employees/employees_screen.dart index d96d53f..12bb2ea 100644 --- a/lib/view/employees/employees_screen.dart +++ b/lib/view/employees/employees_screen.dart @@ -267,7 +267,7 @@ class _EmployeesScreenState extends State with UIMixin { child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( - color: Colors.red, + color: contentTheme.primary, borderRadius: BorderRadius.circular(28), boxShadow: const [ BoxShadow( @@ -426,11 +426,11 @@ class _EmployeesScreenState extends State with UIMixin { value: _employeeController.isAllEmployeeSelected.value, onChanged: (_) => Navigator.pop(context, 'all_employees'), checkColor: Colors.white, - activeColor: Colors.blueAccent, + activeColor: contentTheme.primary, side: const BorderSide(color: Colors.black, width: 1.5), fillColor: MaterialStateProperty.resolveWith( (states) => states.contains(MaterialState.selected) - ? Colors.blueAccent + ? contentTheme.primary : Colors.white), ), const Text('All Employees'), diff --git a/lib/view/expense/expense_detail_screen.dart b/lib/view/expense/expense_detail_screen.dart index 1975d18..8906244 100644 --- a/lib/view/expense/expense_detail_screen.dart +++ b/lib/view/expense/expense_detail_screen.dart @@ -21,6 +21,7 @@ import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/model/employees/employee_info.dart'; import 'package:timeline_tile/timeline_tile.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; class ExpenseDetailScreen extends StatefulWidget { final String expenseId; @@ -30,7 +31,7 @@ class ExpenseDetailScreen extends StatefulWidget { State createState() => _ExpenseDetailScreenState(); } -class _ExpenseDetailScreenState extends State { +class _ExpenseDetailScreenState extends State with UIMixin { final controller = Get.put(ExpenseDetailController()); final projectController = Get.find(); final permissionController = Get.put(PermissionController()); @@ -195,7 +196,7 @@ final permissionController = Get.put(PermissionController()); await showAddExpenseBottomSheet(isEdit: true); await controller.fetchExpenseDetails(); }, - backgroundColor: Colors.red, + backgroundColor: contentTheme.primary, icon: const Icon(Icons.edit), label: MyText.bodyMedium( "Edit Expense", fontWeight: 600, color: Colors.white), @@ -256,10 +257,10 @@ final permissionController = Get.put(PermissionController()); Widget _statusButton(BuildContext context, ExpenseDetailController controller, ExpenseDetailModel expense, dynamic next) { - Color buttonColor = Colors.red; + Color primary = Colors.red; if (next.color.isNotEmpty) { try { - buttonColor = Color(int.parse(next.color.replaceFirst('#', '0xff'))); + primary = Color(int.parse(next.color.replaceFirst('#', '0xff'))); } catch (_) {} } DateTime onlyDate(DateTime date) { @@ -270,7 +271,7 @@ final permissionController = Get.put(PermissionController()); style: ElevatedButton.styleFrom( minimumSize: const Size(100, 40), padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), - backgroundColor: buttonColor, + backgroundColor: primary, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), ), onPressed: () async { diff --git a/lib/view/expense/expense_filter_bottom_sheet.dart b/lib/view/expense/expense_filter_bottom_sheet.dart index 31b0306..1ddce15 100644 --- a/lib/view/expense/expense_filter_bottom_sheet.dart +++ b/lib/view/expense/expense_filter_bottom_sheet.dart @@ -1,3 +1,5 @@ +// ignore_for_file: must_be_immutable + import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/controller/expense/expense_screen_controller.dart'; @@ -8,8 +10,9 @@ import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text_style.dart'; import 'package:marco/model/employees/employee_model.dart'; import 'package:marco/model/expense/employee_selector_for_filter_bottom_sheet.dart'; +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; -class ExpenseFilterBottomSheet extends StatelessWidget { +class ExpenseFilterBottomSheet extends StatefulWidget { final ExpenseController expenseController; final ScrollController scrollController; @@ -19,12 +22,17 @@ class ExpenseFilterBottomSheet extends StatelessWidget { required this.scrollController, }); + @override + State createState() => + _ExpenseFilterBottomSheetState(); +} + +class _ExpenseFilterBottomSheetState extends State + with UIMixin { // FIX: create search adapter - Future> searchEmployeesForBottomSheet( - String query) async { - await expenseController - .searchEmployees(query); // async method, returns void - return expenseController.employeeSearchResults.toList(); + Future> searchEmployeesForBottomSheet(String query) async { + await widget.expenseController.searchEmployees(query); + return widget.expenseController.employeeSearchResults.toList(); } @override @@ -34,21 +42,21 @@ class ExpenseFilterBottomSheet extends StatelessWidget { title: 'Filter Expenses', onCancel: () => Get.back(), onSubmit: () { - expenseController.fetchExpenses(); + widget.expenseController.fetchExpenses(); Get.back(); }, submitText: 'Submit', - submitColor: Colors.indigo, + submitColor: contentTheme.primary, submitIcon: Icons.check_circle_outline, child: SingleChildScrollView( - controller: scrollController, + controller: widget.scrollController, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Align( alignment: Alignment.centerRight, child: TextButton( - onPressed: () => expenseController.clearFilters(), + onPressed: () => widget.expenseController.clearFilters(), child: MyText( "Reset Filter", style: MyTextStyle.labelMedium( @@ -91,11 +99,12 @@ class ExpenseFilterBottomSheet extends StatelessWidget { "Project", _popupSelector( context, - currentValue: expenseController.selectedProject.value.isEmpty + currentValue: widget.expenseController.selectedProject.value.isEmpty ? 'Select Project' - : expenseController.selectedProject.value, - items: expenseController.globalProjects, - onSelected: (value) => expenseController.selectedProject.value = value, + : widget.expenseController.selectedProject.value, + items: widget.expenseController.globalProjects, + onSelected: (value) => + widget.expenseController.selectedProject.value = value, ), ); } @@ -105,18 +114,19 @@ class ExpenseFilterBottomSheet extends StatelessWidget { "Expense Status", _popupSelector( context, - currentValue: expenseController.selectedStatus.value.isEmpty + currentValue: widget.expenseController.selectedStatus.value.isEmpty ? 'Select Expense Status' - : expenseController.expenseStatuses - .firstWhereOrNull( - (e) => e.id == expenseController.selectedStatus.value) + : widget.expenseController.expenseStatuses + .firstWhereOrNull((e) => + e.id == widget.expenseController.selectedStatus.value) ?.name ?? 'Select Expense Status', - items: expenseController.expenseStatuses.map((e) => e.name).toList(), + items: + widget.expenseController.expenseStatuses.map((e) => e.name).toList(), onSelected: (name) { - final status = expenseController.expenseStatuses + final status = widget.expenseController.expenseStatuses .firstWhere((e) => e.name == name); - expenseController.selectedStatus.value = status.id; + widget.expenseController.selectedStatus.value = status.id; }, ), ); @@ -130,14 +140,13 @@ class ExpenseFilterBottomSheet extends StatelessWidget { children: [ Obx(() { return SizedBox( - width: double.infinity, // Make it full width + width: double.infinity, child: SegmentedButton( - segments: expenseController.dateTypes + segments: widget.expenseController.dateTypes .map( (type) => ButtonSegment( value: type, label: Center( - // Center label text child: MyText( type, style: MyTextStyle.bodySmall( @@ -150,10 +159,10 @@ class ExpenseFilterBottomSheet extends StatelessWidget { ), ) .toList(), - selected: {expenseController.selectedDateType.value}, + selected: {widget.expenseController.selectedDateType.value}, onSelectionChanged: (newSelection) { if (newSelection.isNotEmpty) { - expenseController.selectedDateType.value = + widget.expenseController.selectedDateType.value = newSelection.first; } }, @@ -195,28 +204,30 @@ class ExpenseFilterBottomSheet extends StatelessWidget { children: [ Expanded( child: _dateButton( - label: expenseController.startDate.value == null + label: widget.expenseController.startDate.value == null ? 'Start Date' : DateTimeUtils.formatDate( - expenseController.startDate.value!, 'dd MMM yyyy'), + widget.expenseController.startDate.value!, + 'dd MMM yyyy'), onTap: () => _selectDate( context, - expenseController.startDate, - lastDate: expenseController.endDate.value, + widget.expenseController.startDate, + lastDate: widget.expenseController.endDate.value, ), ), ), MySpacing.width(12), Expanded( child: _dateButton( - label: expenseController.endDate.value == null + label: widget.expenseController.endDate.value == null ? 'End Date' : DateTimeUtils.formatDate( - expenseController.endDate.value!, 'dd MMM yyyy'), + widget.expenseController.endDate.value!, + 'dd MMM yyyy'), onTap: () => _selectDate( context, - expenseController.endDate, - firstDate: expenseController.startDate.value, + widget.expenseController.endDate, + firstDate: widget.expenseController.startDate.value, ), ), ), @@ -232,8 +243,8 @@ class ExpenseFilterBottomSheet extends StatelessWidget { "Paid By", _employeeSelector( context: context, - selectedEmployees: expenseController.selectedPaidByEmployees, - searchEmployees: searchEmployeesForBottomSheet, // FIXED + selectedEmployees: widget.expenseController.selectedPaidByEmployees, + searchEmployees: searchEmployeesForBottomSheet, title: 'Search Paid By', ), ); @@ -244,19 +255,15 @@ class ExpenseFilterBottomSheet extends StatelessWidget { "Created By", _employeeSelector( context: context, - selectedEmployees: expenseController.selectedCreatedByEmployees, - searchEmployees: searchEmployeesForBottomSheet, // FIXED + selectedEmployees: widget.expenseController.selectedCreatedByEmployees, + searchEmployees: searchEmployeesForBottomSheet, title: 'Search Created By', ), ); } - Future _selectDate( - BuildContext context, - Rx dateNotifier, { - DateTime? firstDate, - DateTime? lastDate, - }) async { + Future _selectDate(BuildContext context, Rx dateNotifier, + {DateTime? firstDate, DateTime? lastDate}) async { final DateTime? picked = await showDatePicker( context: context, initialDate: dateNotifier.value ?? DateTime.now(), @@ -268,12 +275,10 @@ class ExpenseFilterBottomSheet extends StatelessWidget { } } - Widget _popupSelector( - BuildContext context, { - required String currentValue, - required List items, - required ValueChanged onSelected, - }) { + Widget _popupSelector(BuildContext context, + {required String currentValue, + required List items, + required ValueChanged onSelected}) { return PopupMenuButton( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), onSelected: onSelected, @@ -374,10 +379,12 @@ class ExpenseFilterBottomSheet extends StatelessWidget { return Wrap( spacing: 8, children: selectedEmployees - .map((emp) => Chip( - label: MyText(emp.name), - onDeleted: () => selectedEmployees.remove(emp), - )) + .map( + (emp) => Chip( + label: MyText(emp.name), + onDeleted: () => selectedEmployees.remove(emp), + ), + ) .toList(), ); }), @@ -408,5 +415,4 @@ class ExpenseFilterBottomSheet extends StatelessWidget { ], ); } - } diff --git a/lib/view/expense/expense_screen.dart b/lib/view/expense/expense_screen.dart index ed356f2..e32b08a 100644 --- a/lib/view/expense/expense_screen.dart +++ b/lib/view/expense/expense_screen.dart @@ -11,7 +11,7 @@ import 'package:marco/view/expense/expense_filter_bottom_sheet.dart'; import 'package:marco/helpers/widgets/expense/expense_main_components.dart'; import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; - +import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; class ExpenseMainScreen extends StatefulWidget { const ExpenseMainScreen({super.key}); @@ -21,12 +21,12 @@ class ExpenseMainScreen extends StatefulWidget { } class _ExpenseMainScreenState extends State - with SingleTickerProviderStateMixin { + with SingleTickerProviderStateMixin, UIMixin { late TabController _tabController; final searchController = TextEditingController(); final expenseController = Get.put(ExpenseController()); final projectController = Get.find(); -final permissionController = Get.put(PermissionController()); + final permissionController = Get.put(PermissionController()); @override void initState() { @@ -81,83 +81,85 @@ final permissionController = Get.put(PermissionController()); .toList(); } -@override -Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.white, - appBar: ExpenseAppBar(projectController: projectController), - body: Column( - children: [ - // ---------------- TabBar ---------------- - Container( - color: Colors.white, - child: TabBar( - controller: _tabController, - labelColor: Colors.black, - unselectedLabelColor: Colors.grey, - indicatorColor: Colors.red, - tabs: const [ - Tab(text: "Current Month"), - Tab(text: "History"), - ], - ), - ), - - // ---------------- Gray background for rest ---------------- - Expanded( - child: Container( - color: Colors.grey[100], - child: Column( - children: [ - // ---------------- Search ---------------- - Padding( - padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0), - child: SearchAndFilter( - controller: searchController, - onChanged: (_) => setState(() {}), - onFilterTap: _openFilterBottomSheet, - expenseController: expenseController, - ), - ), - - // ---------------- TabBarView ---------------- - Expanded( - child: TabBarView( - controller: _tabController, - children: [ - _buildExpenseList(isHistory: false), - _buildExpenseList(isHistory: true), - ], - ), - ), + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: ExpenseAppBar(projectController: projectController), + body: Column( + children: [ + // ---------------- TabBar ---------------- + Container( + color: Colors.white, + child: TabBar( + controller: _tabController, + labelColor: Colors.black, + unselectedLabelColor: Colors.grey, + indicatorColor: Colors.red, + tabs: const [ + Tab(text: "Current Month"), + Tab(text: "History"), ], ), ), - ), - ], - ), - // ✅ FAB reacts only to upload permission - floatingActionButton: Obx(() { - // Show loader or hide FAB while permissions are loading - if (permissionController.permissions.isEmpty) { - return const SizedBox.shrink(); - } + // ---------------- Gray background for rest ---------------- + Expanded( + child: Container( + color: Colors.grey[100], + child: Column( + children: [ + // ---------------- Search ---------------- + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 0, vertical: 0), + child: SearchAndFilter( + controller: searchController, + onChanged: (_) => setState(() {}), + onFilterTap: _openFilterBottomSheet, + expenseController: expenseController, + ), + ), - final canUpload = - permissionController.hasPermission(Permissions.expenseUpload); + // ---------------- TabBarView ---------------- + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildExpenseList(isHistory: false), + _buildExpenseList(isHistory: true), + ], + ), + ), + ], + ), + ), + ), + ], + ), + floatingActionButton: Obx(() { + // Show loader or hide FAB while permissions are loading + if (permissionController.permissions.isEmpty) { + return const SizedBox.shrink(); + } - return canUpload - ? FloatingActionButton( - backgroundColor: Colors.red, - onPressed: showAddExpenseBottomSheet, - child: const Icon(Icons.add, color: Colors.white), - ) - : const SizedBox.shrink(); - }), - ); -} + final canUpload = + permissionController.hasPermission(Permissions.expenseUpload); + return canUpload + ? FloatingActionButton.extended( + backgroundColor: contentTheme.primary, + onPressed: showAddExpenseBottomSheet, + icon: const Icon(Icons.add, color: Colors.white), + label: const Text( + "Add Expense", + style: TextStyle(color: Colors.white, fontSize: 16), + ), + ) + : const SizedBox.shrink(); + }), + ); + } Widget _buildExpenseList({required bool isHistory}) { return Obx(() { @@ -207,4 +209,3 @@ Widget build(BuildContext context) { }); } } - diff --git a/lib/view/layouts/offline_screen.dart b/lib/view/layouts/offline_screen.dart index c5d4a77..15fc1d8 100644 --- a/lib/view/layouts/offline_screen.dart +++ b/lib/view/layouts/offline_screen.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/images.dart'; +import 'package:marco/helpers/widgets/wave_background.dart'; + class OfflineScreen extends StatefulWidget { const OfflineScreen({super.key}); @@ -39,12 +41,11 @@ class _OfflineScreenState extends State return Scaffold( body: Stack( children: [ - _RedWaveBackground(brandRed: contentTheme.brandRed), + RedWaveBackground(brandRed: contentTheme.primary), SafeArea( child: Center( child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0), + padding: const EdgeInsets.symmetric(horizontal: 12.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -69,14 +70,14 @@ class _OfflineScreenState extends State ), ), // Increased spacing here - const SizedBox(height: 120), + const SizedBox(height: 120), const Icon(Icons.wifi_off, size: 100, color: Colors.redAccent), const SizedBox(height: 20), const Text( - "No Internet Connection", + "No Internet Connection", style: TextStyle( - fontSize: 26, + fontSize: 26, fontWeight: FontWeight.bold, color: Colors.black87, ), @@ -88,7 +89,6 @@ class _OfflineScreenState extends State textAlign: TextAlign.center, style: TextStyle(fontSize: 16, color: Colors.grey), ), - ], ), ), @@ -99,58 +99,3 @@ class _OfflineScreenState extends State ); } } - -class _RedWaveBackground extends StatelessWidget { - final Color brandRed; - const _RedWaveBackground({required this.brandRed}); - - @override - Widget build(BuildContext context) { - return CustomPaint( - painter: _WavePainter(brandRed), - size: Size.infinite, - ); - } -} - -class _WavePainter extends CustomPainter { - final Color brandRed; - - _WavePainter(this.brandRed); - - @override - void paint(Canvas canvas, Size size) { - final paint1 = Paint() - ..shader = LinearGradient( - colors: [brandRed, const Color.fromARGB(255, 97, 22, 22)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ).createShader(Rect.fromLTWH(0, 0, size.width, size.height)); - - final path1 = Path() - ..moveTo(0, size.height * 0.2) - ..quadraticBezierTo(size.width * 0.25, size.height * 0.05, - size.width * 0.5, size.height * 0.15) - ..quadraticBezierTo( - size.width * 0.75, size.height * 0.25, size.width, size.height * 0.1) - ..lineTo(size.width, 0) - ..lineTo(0, 0) - ..close(); - - canvas.drawPath(path1, paint1); - - final paint2 = Paint()..color = Colors.redAccent.withOpacity(0.15); - final path2 = Path() - ..moveTo(0, size.height * 0.25) - ..quadraticBezierTo( - size.width * 0.4, size.height * 0.1, size.width, size.height * 0.2) - ..lineTo(size.width, 0) - ..lineTo(0, 0) - ..close(); - - canvas.drawPath(path2, paint2); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) => false; -} \ No newline at end of file diff --git a/lib/view/layouts/right_bar.dart b/lib/view/layouts/right_bar.dart index 9e93b4a..b1d0160 100644 --- a/lib/view/layouts/right_bar.dart +++ b/lib/view/layouts/right_bar.dart @@ -5,29 +5,12 @@ import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/widgets/custom_switch.dart'; import 'package:flutter/material.dart'; -// typedef void OnLeftBarColorSchemeChange(LeftBarThemeType leftBarThemeType); -// typedef void OnTopBarColorSchemeChange(TopBarThemeType topBarThemeType); -// typedef void OnRightBarColorSchemeChange(RightBarThemeType topBarThemeType); -// typedef void OnContentSchemeChange(ContentThemeType contentThemeType); - class RightBar extends StatefulWidget { - // final RightBarThemeType rightBarThemeType; - // final LeftBarThemeType leftBarThemeType; - // final TopBarThemeType topBarThemeType; - // final ContentThemeType contentThemeType; - // final OnLeftBarColorSchemeChange onLeftBarColorSchemeChange; - // final OnTopBarColorSchemeChange onTopBarColorSchemeChange; - // final OnRightBarColorSchemeChange onRightBarColorSchemeChange; - // final OnContentSchemeChange onContentSchemeChange; + const RightBar({ - super.key, // this.leftBarThemeType, - // this.topBarThemeType, - // this.contentThemeType, - // this.onLeftBarColorSchemeChange, - // this.onTopBarColorSchemeChange, - // this.onContentSchemeChange, - // this.onRightBarColorSchemeChange + super.key, + }); @override @@ -116,194 +99,12 @@ class _RightBarState extends State ) ], ), - // Spacing.height(8), - // Row( - // children: [ - // CustomSwitch.small( - // value: widget.contentThemeType == ContentThemeType.dark, - // activeBorderColor: rightBarTheme.activeSwitchBorderColor, - // inactiveBorderColor: rightBarTheme.inactiveSwitchBorderColor, - // inactiveTrackColor: rightBarTheme.disabled, - // activeTrackColor: rightBarTheme.primary, - // inactiveThumbColor: rightBarTheme.onDisabled, - // activeThumbColor: rightBarTheme.onPrimary, - // onChanged: (value) { - // if (value && widget.onContentSchemeChange != null) { - // widget.onContentSchemeChange(ContentThemeType.dark); - // } - // }, - // ), - // Spacing.width(12), - // Text( - // "Dark", - // style: AppTheme.getTextStyle(themeData.textTheme.bodyText2, - // color: rightBarTheme.onBackground), - // ) - // ], - // ), - // Spacing.height(36), - // Text("Left Bar", - // style: AppTheme.getTextStyle(themeData.textTheme.bodyText1, - // color: rightBarTheme.onBackground, fontWeight: 600)), + Divider(), - // Spacing.height(8), - // Row( - // children: [ - // CustomSwitch.small( - // value: widget.leftBarThemeType == LeftBarThemeType.light, - // activeBorderColor: rightBarTheme.activeSwitchBorderColor, - // inactiveBorderColor: rightBarTheme.inactiveSwitchBorderColor, - // inactiveTrackColor: rightBarTheme.disabled, - // activeTrackColor: rightBarTheme.primary, - // inactiveThumbColor: rightBarTheme.onDisabled, - // activeThumbColor: rightBarTheme.onPrimary, - // onChanged: (value) { - // if (value && widget.onLeftBarColorSchemeChange != null) { - // widget.onLeftBarColorSchemeChange(LeftBarThemeType.light); - // } - // }, - // ), - // Spacing.width(12), - // Text( - // "Light", - // style: AppTheme.getTextStyle(themeData.textTheme.bodyText2, - // color: rightBarTheme.onBackground), - // ) - // ], - // ), - // Spacing.height(8), - // Row( - // children: [ - // CustomSwitch.small( - // value: widget.leftBarThemeType == LeftBarThemeType.dark, - // activeBorderColor: rightBarTheme.activeSwitchBorderColor, - // inactiveBorderColor: rightBarTheme.inactiveSwitchBorderColor, - // inactiveTrackColor: rightBarTheme.disabled, - // activeTrackColor: rightBarTheme.primary, - // inactiveThumbColor: rightBarTheme.onDisabled, - // activeThumbColor: rightBarTheme.onPrimary, - // onChanged: (value) { - // if (value && widget.onLeftBarColorSchemeChange != null) { - // widget.onLeftBarColorSchemeChange(LeftBarThemeType.dark); - // } - // }, - // ), - // Spacing.width(12), - // Text( - // "Dark", - // style: AppTheme.getTextStyle(themeData.textTheme.bodyText2, - // color: rightBarTheme.onBackground), - // ) - // ], - // ), - // Spacing.height(36), + Text("Top Bar"), Divider(), - // Spacing.height(8), - // Row( - // children: [ - // CustomSwitch.small( - // value: widget.topBarThemeType == TopBarThemeType.light, - // inactiveTrackColor: rightBarTheme.disabled, - // activeBorderColor: rightBarTheme.activeSwitchBorderColor, - // inactiveBorderColor: rightBarTheme.inactiveSwitchBorderColor, - // activeTrackColor: rightBarTheme.primary, - // inactiveThumbColor: rightBarTheme.onDisabled, - // activeThumbColor: rightBarTheme.onPrimary, - // onChanged: (value) { - // if (value && widget.onTopBarColorSchemeChange != null) { - // widget.onTopBarColorSchemeChange(TopBarThemeType.light); - // } - // }, - // ), - // Spacing.width(12), - // Text( - // "Light", - // style: AppTheme.getTextStyle(themeData.textTheme.bodyText2, - // color: rightBarTheme.onBackground), - // ) - // ], - // ), - // Spacing.height(8), - // Row( - // children: [ - // CustomSwitch.small( - // value: widget.topBarThemeType == TopBarThemeType.dark, - // inactiveTrackColor: rightBarTheme.disabled, - // activeBorderColor: rightBarTheme.activeSwitchBorderColor, - // inactiveBorderColor: rightBarTheme.inactiveSwitchBorderColor, - // activeTrackColor: rightBarTheme.primary, - // inactiveThumbColor: rightBarTheme.onDisabled, - // activeThumbColor: rightBarTheme.onPrimary, - // onChanged: (value) { - // if (value && widget.onTopBarColorSchemeChange != null) { - // widget.onTopBarColorSchemeChange(TopBarThemeType.dark); - // } - // }, - // ), - // Spacing.width(12), - // Text( - // "Dark", - // style: AppTheme.getTextStyle(themeData.textTheme.bodyText2, - // color: rightBarTheme.onBackground), - // ) - // ], - // ), - // Spacing.height(36), - // Text("Right Bar", - // style: AppTheme.getTextStyle(themeData.textTheme.bodyText1, - // color: rightBarTheme.onBackground, fontWeight: 600)), - // Divider(), - // Spacing.height(8), - // Row( - // children: [ - // CustomSwitch.small( - // value: widget.rightBarThemeType == RightBarThemeType.light, - // inactiveTrackColor: rightBarTheme.disabled, - // activeBorderColor: rightBarTheme.activeSwitchBorderColor, - // inactiveBorderColor: rightBarTheme.inactiveSwitchBorderColor, - // activeTrackColor: rightBarTheme.primary, - // inactiveThumbColor: rightBarTheme.onDisabled, - // activeThumbColor: rightBarTheme.onPrimary, - // onChanged: (value) { - // if (value && widget.onRightBarColorSchemeChange != null) { - // widget.onRightBarColorSchemeChange(RightBarThemeType.light); - // } - // }, - // ), - // Spacing.width(12), - // Text( - // "Light", - // style: AppTheme.getTextStyle(themeData.textTheme.bodyText2, - // color: rightBarTheme.onBackground), - // ) - // ], - // ), - // Spacing.height(8), - // Row( - // children: [ - // CustomSwitch.small( - // value: widget.rightBarThemeType == RightBarThemeType.dark, - // inactiveTrackColor: rightBarTheme.disabled, - // activeBorderColor: rightBarTheme.activeSwitchBorderColor, - // inactiveBorderColor: rightBarTheme.inactiveSwitchBorderColor, - // activeTrackColor: rightBarTheme.primary, - // inactiveThumbColor: rightBarTheme.onDisabled, - // activeThumbColor: rightBarTheme.onPrimary, - // onChanged: (value) { - // if (value && widget.onRightBarColorSchemeChange != null) { - // widget.onRightBarColorSchemeChange(RightBarThemeType.dark); - // } - // }, - // ), - // Spacing.width(12), - // Text( - // "Dark", - // style: AppTheme.getTextStyle(themeData.textTheme.bodyText2, - // color: rightBarTheme.onBackground), - // ) - // ], - // ), + ], ), )) diff --git a/lib/view/layouts/user_profile_right_bar.dart b/lib/view/layouts/user_profile_right_bar.dart index 9fb8caf..2ef33ba 100644 --- a/lib/view/layouts/user_profile_right_bar.dart +++ b/lib/view/layouts/user_profile_right_bar.dart @@ -13,7 +13,7 @@ import 'package:marco/view/employees/employee_profile_screen.dart'; import 'package:marco/helpers/services/tenant_service.dart'; import 'package:marco/view/tenant/tenant_selection_screen.dart'; import 'package:marco/controller/tenant/tenant_switch_controller.dart'; - +import 'package:marco/helpers/theme/theme_editor_widget.dart'; class UserProfileBar extends StatefulWidget { @@ -29,6 +29,7 @@ class _UserProfileBarState extends State late EmployeeInfo employeeInfo; bool _isLoading = true; bool hasMpin = true; + bool _isThemeEditorVisible = false; @override void initState() { @@ -58,8 +59,8 @@ class _UserProfileBarState extends State decoration: BoxDecoration( gradient: LinearGradient( colors: [ - Colors.white.withValues(alpha: 0.95), - Colors.white.withValues(alpha: 0.85), + Colors.white.withOpacity(0.95), + Colors.white.withOpacity(0.85), ], begin: Alignment.topLeft, end: Alignment.bottomRight, @@ -67,150 +68,158 @@ class _UserProfileBarState extends State borderRadius: BorderRadius.circular(22), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.06), + color: Colors.black.withOpacity(0.06), blurRadius: 18, offset: const Offset(0, 8), ) ], border: Border.all( - color: Colors.grey.withValues(alpha: 0.25), + color: Colors.grey.withOpacity(0.25), width: 1, ), ), child: SafeArea( - bottom: true, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _isLoading - ? const _LoadingSection() - : _userProfileSection(isCondensed), - - // --- SWITCH TENANT ROW BELOW AVATAR --- - if (!_isLoading && !isCondensed) _switchTenantRow(), - - MySpacing.height(12), - Divider( - indent: 18, - endIndent: 18, - thickness: 0.7, - color: Colors.grey.withValues(alpha: 0.25), - ), - MySpacing.height(12), - _supportAndSettingsMenu(isCondensed), - const Spacer(), - Divider( - indent: 18, - endIndent: 18, - thickness: 0.35, - color: Colors.grey.withValues(alpha: 0.18), - ), - _logoutButton(isCondensed), - ], - ), - ), + bottom: true, + child: Stack( + children: [ + Offstage( + offstage: _isThemeEditorVisible, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _isLoading + ? const _LoadingSection() + : _userProfileSection(isCondensed), + if (!_isLoading && !isCondensed) _switchTenantRow(), + MySpacing.height(12), + Divider( + indent: 18, + endIndent: 18, + thickness: 0.7, + color: Colors.grey.withOpacity(0.25), + ), + MySpacing.height(12), + _supportAndSettingsMenu(isCondensed), + const Spacer(), + Divider( + indent: 18, + endIndent: 18, + thickness: 0.35, + color: Colors.grey.withOpacity(0.18), + ), + _logoutButton(isCondensed), + ], + ), + ), + Offstage( + offstage: !_isThemeEditorVisible, + child: ThemeEditorWidget( + onClose: () { + setState(() => _isThemeEditorVisible = false); + }, + ), + ), + ], + )), ), ), ), ); } - /// Row widget to switch tenant with popup menu (button only) - /// Row widget to switch tenant with popup menu (button only) -Widget _switchTenantRow() { - // Use the dedicated switch controller - final TenantSwitchController tenantSwitchController = - Get.put(TenantSwitchController()); + // ==================== CONTINUE EXISTING CODE ===================== + Widget _switchTenantRow() { + final TenantSwitchController tenantSwitchController = + Get.put(TenantSwitchController()); - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - child: Obx(() { - if (tenantSwitchController.isLoading.value) { - return _loadingTenantContainer(); - } + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: Obx(() { + if (tenantSwitchController.isLoading.value) { + return _loadingTenantContainer(); + } - final tenants = tenantSwitchController.tenants; - if (tenants.isEmpty) return _noTenantContainer(); + final tenants = tenantSwitchController.tenants; + if (tenants.isEmpty) return _noTenantContainer(); - final selectedTenant = TenantService.currentTenant; + final selectedTenant = TenantService.currentTenant; - // Sort tenants: selected tenant first - final sortedTenants = List.of(tenants); - if (selectedTenant != null) { - sortedTenants.sort((a, b) { - if (a.id == selectedTenant.id) return -1; - if (b.id == selectedTenant.id) return 1; - return 0; - }); - } + final sortedTenants = List.of(tenants); + if (selectedTenant != null) { + sortedTenants.sort((a, b) { + if (a.id == selectedTenant.id) return -1; + if (b.id == selectedTenant.id) return 1; + return 0; + }); + } - return PopupMenuButton( - onSelected: (tenantId) => - tenantSwitchController.switchTenant(tenantId), - itemBuilder: (_) => sortedTenants.map((tenant) { - return PopupMenuItem( - value: tenant.id, - child: Row( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Container( - width: 20, - height: 20, - color: Colors.grey.shade200, - child: TenantLogo(logoImage: tenant.logoImage), + return PopupMenuButton( + onSelected: (tenantId) => + tenantSwitchController.switchTenant(tenantId), + itemBuilder: (_) => sortedTenants.map((tenant) { + return PopupMenuItem( + value: tenant.id, + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Container( + width: 20, + height: 20, + color: Colors.grey.shade200, + child: TenantLogo(logoImage: tenant.logoImage), + ), ), - ), - const SizedBox(width: 10), + const SizedBox(width: 10), + Expanded( + child: Text( + tenant.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: tenant.id == selectedTenant?.id + ? FontWeight.bold + : FontWeight.w600, + color: tenant.id == selectedTenant?.id + ? contentTheme.primary + : Colors.black87, + ), + ), + ), + if (tenant.id == selectedTenant?.id) + Icon(Icons.check_circle, + color: contentTheme.primary, size: 18), + ], + ), + ); + }).toList(), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Icon(Icons.swap_horiz, color: contentTheme.primary), Expanded( - child: Text( - tenant.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontWeight: tenant.id == selectedTenant?.id - ? FontWeight.bold - : FontWeight.w600, - color: tenant.id == selectedTenant?.id - ? Colors.blueAccent - : Colors.black87, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Text( + "Switch Organization", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: contentTheme.primary, + fontWeight: FontWeight.bold), ), ), ), - if (tenant.id == selectedTenant?.id) - const Icon(Icons.check_circle, - color: Colors.blueAccent, size: 18), + Icon(Icons.arrow_drop_down, color: contentTheme.primary), ], ), - ); - }).toList(), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Icon(Icons.swap_horiz, color: Colors.blue.shade600), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: Text( - "Switch Organization", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Colors.blue, fontWeight: FontWeight.bold), - ), - ), - ), - Icon(Icons.arrow_drop_down, color: Colors.blue.shade600), - ], ), - ), - ); - }), - ); -} - + ); + }), + ); + } Widget _loadingTenantContainer() => Container( padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), @@ -254,7 +263,7 @@ Widget _switchTenantRow() { shape: BoxShape.circle, boxShadow: [ BoxShadow( - color: Theme.of(context).primaryColor.withValues(alpha: 0.15), + color: Theme.of(context).primaryColor.withOpacity(0.15), blurRadius: 10, spreadRadius: 1, ), @@ -310,6 +319,11 @@ Widget _switchTenantRow() { _menuItemRow( icon: LucideIcons.settings, label: 'Settings', + onTap: () { + setState(() { + _isThemeEditorVisible = true; + }); + }, ), SizedBox(height: spacingHeight), _menuItemRow( @@ -320,8 +334,6 @@ Widget _switchTenantRow() { _menuItemRow( icon: LucideIcons.lock, label: hasMpin ? 'Change MPIN' : 'Set MPIN', - iconColor: Colors.redAccent, - textColor: Colors.redAccent, onTap: _onMpinTap, ), ], @@ -395,9 +407,9 @@ Widget _switchTenantRow() { fontWeight: 700, ), style: ElevatedButton.styleFrom( - backgroundColor: Colors.red.shade600, + backgroundColor: contentTheme.primary, foregroundColor: Colors.white, - shadowColor: Colors.red.shade200, + shadowColor: contentTheme.primary, padding: EdgeInsets.symmetric( vertical: condensed ? 14 : 18, horizontal: condensed ? 14 : 22, @@ -419,54 +431,71 @@ Widget _switchTenantRow() { } Widget _buildLogoutDialog(BuildContext context) { + final theme = Theme.of(context); + final primaryColor = contentTheme.primary; + return Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), elevation: 10, - backgroundColor: Colors.white, + backgroundColor: theme.cardColor, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 34), child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(LucideIcons.log_out, size: 56, color: Colors.red.shade700), - const SizedBox(height: 18), - const Text( + // Top icon + Icon(LucideIcons.log_out, size: 56, color: primaryColor), + MySpacing.height(18), + // Title + MyText.titleLarge( "Logout Confirmation", - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.w700, - color: Colors.black87), - ), - const SizedBox(height: 14), - const Text( - "Are you sure you want to logout?\nYou will need to login again to continue.", + fontWeight: 700, textAlign: TextAlign.center, - style: TextStyle(fontSize: 16, color: Colors.black54), ), - const SizedBox(height: 30), + MySpacing.height(14), + // Subtitle + MyText.bodyMedium( + "Are you sure you want to logout?\nYou will need to login again to continue.", + color: Colors.grey[700], + textAlign: TextAlign.center, + ), + MySpacing.height(30), + // Buttons Row( children: [ - Expanded( - child: TextButton( - onPressed: () => Navigator.pop(context, false), - style: TextButton.styleFrom( - foregroundColor: Colors.grey.shade700, - padding: const EdgeInsets.symmetric(vertical: 12)), - child: const Text("Cancel"), - ), - ), - const SizedBox(width: 18), Expanded( child: ElevatedButton( - onPressed: () => Navigator.pop(context, true), + onPressed: () => Navigator.pop(context, false), style: ElevatedButton.styleFrom( - backgroundColor: Colors.red.shade700, + backgroundColor: Colors.grey, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14)), ), - child: const Text("Logout"), + child: MyText.bodyMedium( + "Cancel", + color: Colors.white, + fontWeight: 600, + ), + ), + ), + MySpacing.width(18), + Expanded( + child: ElevatedButton( + onPressed: () => Navigator.pop(context, true), + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14)), + ), + child: MyText.bodyMedium( + "Logout", + color: Colors.white, + fontWeight: 600, + ), ), ), ], diff --git a/lib/view/tenant/tenant_selection_screen.dart b/lib/view/tenant/tenant_selection_screen.dart index e7b0755..7fe1875 100644 --- a/lib/view/tenant/tenant_selection_screen.dart +++ b/lib/view/tenant/tenant_selection_screen.dart @@ -8,6 +8,7 @@ import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/images.dart'; import 'package:marco/controller/tenant/tenant_selection_controller.dart'; import 'package:marco/view/splash_screen.dart'; +import 'package:marco/helpers/widgets/wave_background.dart'; class TenantSelectionScreen extends StatefulWidget { const TenantSelectionScreen({super.key}); @@ -63,7 +64,7 @@ class _TenantSelectionScreenState extends State return Scaffold( body: Stack( children: [ - _RedWaveBackground(brandRed: contentTheme.brandRed), + RedWaveBackground(brandRed: contentTheme.primary), SafeArea( child: Center( child: Column( @@ -188,12 +189,12 @@ class _BetaBadge extends StatelessWidget { } /// Tenant Card List -class TenantCardList extends StatelessWidget { +class TenantCardList extends StatelessWidget with UIMixin { final TenantSelectionController controller; final bool isLoading; final Function(String tenantId) onTenantSelected; - const TenantCardList({ + TenantCardList({ required this.controller, required this.isLoading, required this.onTenantSelected, @@ -220,24 +221,23 @@ class TenantCardList extends StatelessWidget { ), const SizedBox(height: 16), ], - - if (hasTenants) ...controller.tenants.map( - (tenant) => _TenantCard( - tenant: tenant, - onTap: () => onTenantSelected(tenant.id), + if (hasTenants) + ...controller.tenants.map( + (tenant) => _TenantCard( + tenant: tenant, + onTap: () => onTenantSelected(tenant.id), + ), ), - ), - const SizedBox(height: 16), - TextButton.icon( onPressed: () async { await LocalStorage.logout(); }, - icon: const Icon(Icons.arrow_back, size: 20, color: Colors.redAccent), + icon: + Icon(Icons.arrow_back, size: 20, color: contentTheme.primary,), label: MyText( 'Back to Login', - color: Colors.red, + color: contentTheme.primary, fontWeight: 600, fontSize: 14, ), @@ -249,10 +249,10 @@ class TenantCardList extends StatelessWidget { } /// Single Tenant Card -class _TenantCard extends StatelessWidget { +class _TenantCard extends StatelessWidget with UIMixin { final dynamic tenant; final VoidCallback onTap; - const _TenantCard({required this.tenant, required this.onTap}); + _TenantCard({required this.tenant, required this.onTap}); @override Widget build(BuildContext context) { @@ -297,7 +297,7 @@ class _TenantCard extends StatelessWidget { ], ), ), - const Icon(Icons.arrow_forward_ios, size: 24, color: Colors.red), + Icon(Icons.arrow_forward_ios, size: 24, color: contentTheme.primary,), ], ), ), @@ -335,55 +335,3 @@ class TenantLogo extends StatelessWidget { } } } - -/// Red Wave Background -class _RedWaveBackground extends StatelessWidget { - final Color brandRed; - const _RedWaveBackground({required this.brandRed}); - - @override - Widget build(BuildContext context) { - return CustomPaint( - painter: _WavePainter(brandRed), - size: Size.infinite, - ); - } -} - -class _WavePainter extends CustomPainter { - final Color brandRed; - _WavePainter(this.brandRed); - - @override - void paint(Canvas canvas, Size size) { - final paint1 = Paint() - ..shader = LinearGradient( - colors: [brandRed, const Color.fromARGB(255, 97, 22, 22)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ).createShader(Rect.fromLTWH(0, 0, size.width, size.height)); - - final path1 = Path() - ..moveTo(0, size.height * 0.2) - ..quadraticBezierTo( - size.width * 0.25, size.height * 0.05, size.width * 0.5, size.height * 0.15) - ..quadraticBezierTo( - size.width * 0.75, size.height * 0.25, size.width, size.height * 0.1) - ..lineTo(size.width, 0) - ..lineTo(0, 0) - ..close(); - canvas.drawPath(path1, paint1); - - final paint2 = Paint()..color = Colors.redAccent.withOpacity(0.15); - final path2 = Path() - ..moveTo(0, size.height * 0.25) - ..quadraticBezierTo(size.width * 0.4, size.height * 0.1, size.width, size.height * 0.2) - ..lineTo(size.width, 0) - ..lineTo(0, 0) - ..close(); - canvas.drawPath(path2, paint2); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) => false; -}