Refactor UI components to use contentTheme colors

- Updated various screens to replace hardcoded color values with contentTheme.buttonColor for consistency.
- Changed icons in NotesView, UserDocumentsPage, EmployeeDetailPage, EmployeesScreen, ExpenseDetailScreen, and others to use updated icon styles.
- Refactored OfflineScreen and TenantSelectionScreen to utilize new wave background widget.
- Introduced ThemeEditorWidget in UserProfileBar for dynamic theme adjustments.
- Enhanced logout confirmation dialog styling to align with new theme colors.
This commit is contained in:
Vaibhav Surve 2025-10-28 17:38:19 +05:30
parent 97b45ebd91
commit 04da062f4f
22 changed files with 1039 additions and 851 deletions

View File

@ -17,16 +17,20 @@ enum ContentThemeColor {
light, light,
dark, dark,
pink, pink,
green,
red, red,
brandRed; brand,
button;
Color get color { Color get color {
return (AdminTheme.theme.contentTheme.getMappedIntoThemeColor[this]?['color']) ?? Colors.black; return (AdminTheme.theme.contentTheme.getMappedIntoThemeColor[this]
?['color']) ??
Colors.black;
} }
Color get onColor { Color get onColor {
return (AdminTheme.theme.contentTheme.getMappedIntoThemeColor[this]?['onColor']) ?? Colors.white; return (AdminTheme.theme.contentTheme.getMappedIntoThemeColor[this]
?['onColor']) ??
Colors.white;
} }
} }
@ -43,8 +47,6 @@ class LeftBarTheme {
this.activeItemBackground = const Color(0x15663399), this.activeItemBackground = const Color(0x15663399),
}); });
//-------------------------------------- Left Bar Theme ----------------------------------------//
static final LeftBarTheme lightLeftBarTheme = LeftBarTheme(); static final LeftBarTheme lightLeftBarTheme = LeftBarTheme();
static final LeftBarTheme darkLeftBarTheme = LeftBarTheme( static final LeftBarTheme darkLeftBarTheme = LeftBarTheme(
@ -73,11 +75,11 @@ class TopBarTheme {
this.onBackground = const Color(0xff313a46), this.onBackground = const Color(0xff313a46),
}); });
//-------------------------------------- Left Bar Theme ----------------------------------------//
static final TopBarTheme lightTopBarTheme = TopBarTheme(); 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 { class RightBarTheme {
@ -91,8 +93,6 @@ class RightBarTheme {
this.onDisabled = const Color(0xff313a46), this.onDisabled = const Color(0xff313a46),
}); });
//-------------------------------------- Left Bar Theme ----------------------------------------//
static final RightBarTheme lightRightBarTheme = RightBarTheme( static final RightBarTheme lightRightBarTheme = RightBarTheme(
disabled: const Color(0xffffffff), disabled: const Color(0xffffffff),
onDisabled: const Color(0xffdee2e6), onDisabled: const Color(0xffdee2e6),
@ -117,20 +117,24 @@ class ContentTheme {
final Color info, onInfo; final Color info, onInfo;
final Color light, onLight; final Color light, onLight;
final Color dark, onDark; final Color dark, onDark;
final Color purple, onPurple;
final Color pink, onPink; final Color pink, onPink;
final Color red, onRed; final Color red, onRed;
final Color brandRed, onBrandRed;
final Color brandColor, onBrandColor;
final Color buttonColor, onButtonColor;
final Color cardBackground, cardShadow, cardBorder, cardText, cardTextMuted; final Color cardBackground, cardShadow, cardBorder, cardText, cardTextMuted;
final Color title; final Color title;
final Color disabled, onDisabled; final Color disabled, onDisabled;
Map<ContentThemeColor, Map<String, Color>> get getMappedIntoThemeColor { Map<ContentThemeColor, Map<String, Color>> get getMappedIntoThemeColor {
var c = AdminTheme.theme.contentTheme; var c = this;
return { return {
ContentThemeColor.primary: {'color': c.primary, 'onColor': c.onPrimary}, ContentThemeColor.primary: {'color': c.primary, 'onColor': c.onPrimary},
ContentThemeColor.secondary: {'color': c.secondary, 'onColor': c.onSecondary}, ContentThemeColor.secondary: {
'color': c.secondary,
'onColor': c.onSecondary
},
ContentThemeColor.success: {'color': c.success, 'onColor': c.onSuccess}, ContentThemeColor.success: {'color': c.success, 'onColor': c.onSuccess},
ContentThemeColor.info: {'color': c.info, 'onColor': c.onInfo}, ContentThemeColor.info: {'color': c.info, 'onColor': c.onInfo},
ContentThemeColor.warning: {'color': c.warning, 'onColor': c.onWarning}, ContentThemeColor.warning: {'color': c.warning, 'onColor': c.onWarning},
@ -139,37 +143,44 @@ class ContentTheme {
ContentThemeColor.dark: {'color': c.dark, 'onColor': c.onDark}, ContentThemeColor.dark: {'color': c.dark, 'onColor': c.onDark},
ContentThemeColor.pink: {'color': c.pink, 'onColor': c.onPink}, ContentThemeColor.pink: {'color': c.pink, 'onColor': c.onPink},
ContentThemeColor.red: {'color': c.red, 'onColor': c.onRed}, ContentThemeColor.red: {'color': c.red, 'onColor': c.onRed},
ContentThemeColor.brandRed: {'color': c.brandRed, 'onColor': c.onBrandRed}, ContentThemeColor.brand: {
'color': c.brandColor,
'onColor': c.onBrandColor
},
ContentThemeColor.button: {
'color': c.buttonColor,
'onColor': c.onButtonColor
},
}; };
} }
ContentTheme({ ContentTheme({
this.background = const Color(0xfffafbfe), this.background = const Color(0xfffafbfe),
this.onBackground = const Color(0xffF1F1F2), this.onBackground = const Color(0xff313a46),
this.primary = const Color(0xff663399), this.primary = const Color(0xFF49BF3C),
this.onPrimary = const Color(0xffffffff), this.onPrimary = Colors.white,
this.secondary = const Color(0xff6c757d), this.secondary = const Color(0xff6c757d),
this.onSecondary = const Color(0xffffffff), this.onSecondary = Colors.white,
this.success = const Color(0xff00be82), this.success = const Color(0xff00be82),
this.onSuccess = const Color(0xffffffff), this.onSuccess = Colors.white,
this.danger = const Color(0xffdc3545), this.danger = const Color(0xffdc3545),
this.onDanger = const Color(0xffffffff), this.onDanger = Colors.white,
this.warning = const Color(0xffffc107), this.warning = const Color(0xffffc107),
this.onWarning = const Color(0xff313a46), this.onWarning = const Color(0xff313a46),
this.info = const Color(0xff0dcaf0), this.info = const Color(0xff0dcaf0),
this.onInfo = const Color(0xffffffff), this.onInfo = Colors.white,
this.light = const Color(0xffeef2f7), this.light = const Color(0xffeef2f7),
this.onLight = const Color(0xff313a46), this.onLight = const Color(0xff313a46),
this.dark = const Color(0xff313a46), this.dark = const Color(0xff313a46),
this.onDark = const Color(0xffffffff), this.onDark = Colors.white,
this.purple = const Color(0xff800080),
this.onPurple = const Color(0xffFF0000),
this.pink = const Color(0xffFF1087), this.pink = const Color(0xffFF1087),
this.onPink = const Color(0xffffffff), this.onPink = Colors.white,
this.red = const Color(0xffFF0000), this.red = const Color(0xffFF0000),
this.onRed = const Color(0xffffffff), this.onRed = Colors.white,
this.brandRed = const Color.fromARGB(255, 255, 0, 0), this.brandColor = const Color(0xffff0000),
this.onBrandRed = const Color(0xffffffff), this.onBrandColor = Colors.white,
this.buttonColor = const Color(0xFF2196F3),
this.onButtonColor = Colors.white,
this.cardBackground = const Color(0xffffffff), this.cardBackground = const Color(0xffffffff),
this.cardShadow = const Color(0xffffffff), this.cardShadow = const Color(0xffffffff),
this.cardBorder = const Color(0xffffffff), this.cardBorder = const Color(0xffffffff),
@ -180,10 +191,21 @@ class ContentTheme {
this.onDisabled = const Color(0xffffffff), this.onDisabled = const Color(0xffffffff),
}); });
//-------------------------------------- Left Bar Theme ----------------------------------------// // copyWith for dynamic updates
ContentTheme copyWith({
Color? primary,
Color? brandColor,
Color? buttonColor,
}) {
return ContentTheme(
primary: primary ?? this.primary,
brandColor: brandColor ?? this.brandColor,
buttonColor: buttonColor ?? this.buttonColor,
);
}
static final ContentTheme lightContentTheme = ContentTheme( static final ContentTheme lightContentTheme = ContentTheme(
primary: Color(0xff663399), primary: Colors.indigo,
background: const Color(0xfffafbfe), background: const Color(0xfffafbfe),
onBackground: const Color(0xff313a46), onBackground: const Color(0xff313a46),
cardBorder: const Color(0xffe8ecf1), cardBorder: const Color(0xffe8ecf1),
@ -192,12 +214,12 @@ class ContentTheme {
cardText: const Color(0xff6c757d), cardText: const Color(0xff6c757d),
title: const Color(0xff6c757d), title: const Color(0xff6c757d),
cardTextMuted: const Color(0xff98a6ad), cardTextMuted: const Color(0xff98a6ad),
brandRed: const Color.fromARGB(255, 255, 0, 0), brandColor: const Color(0xffff0000),
onBrandRed: const Color(0xffffffff), onBrandColor: Colors.white,
); );
static final ContentTheme darkContentTheme = ContentTheme( static final ContentTheme darkContentTheme = ContentTheme(
primary: Color(0xff32BFAE), primary: Colors.indigo,
background: const Color(0xff343a40), background: const Color(0xff343a40),
onBackground: const Color(0xffF1F1F2), onBackground: const Color(0xffF1F1F2),
disabled: const Color(0xff444d57), disabled: const Color(0xff444d57),
@ -208,8 +230,8 @@ class ContentTheme {
cardText: const Color(0xffaab8c5), cardText: const Color(0xffaab8c5),
title: const Color(0xffaab8c5), title: const Color(0xffaab8c5),
cardTextMuted: const Color(0xff8391a2), cardTextMuted: const Color(0xff8391a2),
brandRed: const Color.fromARGB(255, 255, 0, 0), brandColor: const Color(0xffff0000),
onBrandRed: const Color(0xffffffff), onBrandColor: Colors.white,
); );
} }
@ -226,8 +248,6 @@ class AdminTheme {
required this.contentTheme, required this.contentTheme,
}); });
//-------------------------------------- Left Bar Theme ----------------------------------------//
static AdminTheme theme = AdminTheme( static AdminTheme theme = AdminTheme(
leftBarTheme: LeftBarTheme.lightLeftBarTheme, leftBarTheme: LeftBarTheme.lightLeftBarTheme,
topBarTheme: TopBarTheme.lightTopBarTheme, topBarTheme: TopBarTheme.lightTopBarTheme,
@ -236,9 +256,35 @@ class AdminTheme {
static void setTheme() { static void setTheme() {
theme = AdminTheme( theme = AdminTheme(
leftBarTheme: ThemeCustomizer.instance.theme == ThemeMode.dark ? LeftBarTheme.darkLeftBarTheme : LeftBarTheme.lightLeftBarTheme, leftBarTheme: ThemeCustomizer.instance.theme == ThemeMode.dark
topBarTheme: ThemeCustomizer.instance.theme == ThemeMode.dark ? TopBarTheme.darkTopBarTheme : TopBarTheme.lightTopBarTheme, ? LeftBarTheme.darkLeftBarTheme
rightBarTheme: ThemeCustomizer.instance.theme == ThemeMode.dark ? RightBarTheme.darkRightBarTheme : RightBarTheme.lightRightBarTheme, : LeftBarTheme.lightLeftBarTheme,
contentTheme: ThemeCustomizer.instance.theme == ThemeMode.dark ? ContentTheme.darkContentTheme : ContentTheme.lightContentTheme); 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);
}
// Dynamic theme color update
static void updateThemeColors({
Color? primary,
Color? brandColor,
Color? buttonColor,
}) {
theme = AdminTheme(
leftBarTheme: theme.leftBarTheme,
topBarTheme: theme.topBarTheme,
rightBarTheme: theme.rightBarTheme,
contentTheme: theme.contentTheme.copyWith(
primary: primary,
brandColor: brandColor,
buttonColor: buttonColor,
),
);
} }
} }

View File

@ -0,0 +1,216 @@
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';
class ThemeOption {
final String label;
final Color primary;
final Color button;
final Color brand;
ThemeOption(this.label, this.primary, this.button, this.brand);
}
// Define your themes
final List<ThemeOption> themeOptions = [
ThemeOption("Theme 1", Colors.red, Colors.red, Colors.red),
ThemeOption("Theme 2", const Color(0xFF49BF3C), const Color(0xFF49BF3C), const Color(0xFF49BF3C)),
ThemeOption("Theme 3", const Color(0xFF3F51B5), const Color(0xFF3F51B5), const Color(0xFF3F51B5)),
ThemeOption("Theme 4", const Color(0xFF40B7FF), const Color(0xFF40B7FF), const Color(0xFF40B7FF)),
];
// Reactive controller
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;
// Update AdminTheme
AdminTheme.updateThemeColors(
primary: themeOptions[index].primary,
buttonColor: themeOptions[index].button,
brandColor: themeOptions[index].brand,
);
await Future.delayed(const Duration(milliseconds: 600));
showApplied.value = false;
}
}
// Theme Editor Widget
class ThemeEditorWidget extends StatelessWidget {
final VoidCallback onClose;
ThemeEditorWidget({Key? key, required this.onClose}) : super(key: key);
final ThemeController themeController = Get.put(ThemeController());
@override
Widget build(BuildContext context) {
themeController.init();
return Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.bodyLarge("Theme Customization", fontWeight: 600),
IconButton(
icon: const Icon(Icons.close),
onPressed: onClose,
tooltip: "Back",
iconSize: 20,
),
],
),
const SizedBox(height: 12),
// Theme Cards
Center(
child: Obx(() => Wrap(
spacing: 12,
runSpacing: 12,
alignment: WrapAlignment.center,
children: List.generate(themeOptions.length, (i) {
final theme = themeOptions[i];
final isSelected = themeController.selectedIndex.value == i;
return SizedBox(
width: 80,
child: GestureDetector(
onTap: () => themeController.applyTheme(i),
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? theme.brand : Colors.transparent,
width: 2,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 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(theme.brand, 0.15),
),
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Hello, User!",
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.w600,
color: theme.primary,
fontSize: 12,
),
),
const SizedBox(height: 4),
SizedBox(
height: 18,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: theme.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(
theme.label,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 12,
color: Colors.grey[700],
),
),
],
),
),
),
);
}),
)),
),
const SizedBox(height: 12),
// Applied Indicator
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),
),
],
),
);
}
}

View File

@ -1,15 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.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 title;
final String? subtitle;
final Widget child; final Widget child;
final VoidCallback onCancel; final VoidCallback onCancel;
final VoidCallback onSubmit; final VoidCallback onSubmit;
final bool isSubmitting; final bool isSubmitting;
final String submitText; final String submitText;
final Color submitColor; final Color? submitColor;
final IconData submitIcon; final IconData submitIcon;
final bool showButtons; final bool showButtons;
final Widget? bottomContent; final Widget? bottomContent;
@ -20,18 +22,26 @@ class BaseBottomSheet extends StatelessWidget {
required this.child, required this.child,
required this.onCancel, required this.onCancel,
required this.onSubmit, required this.onSubmit,
this.subtitle,
this.isSubmitting = false, this.isSubmitting = false,
this.submitText = 'Submit', this.submitText = 'Submit',
this.submitColor = Colors.indigo, this.submitColor,
this.submitIcon = Icons.check_circle_outline, this.submitIcon = Icons.check_circle_outline,
this.showButtons = true, this.showButtons = true,
this.bottomContent, this.bottomContent,
}); });
@override
State<BaseBottomSheet> createState() => _BaseBottomSheetState();
}
class _BaseBottomSheetState extends State<BaseBottomSheet> with UIMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final effectiveSubmitColor =
widget.submitColor ?? contentTheme.primary;
return SingleChildScrollView( return SingleChildScrollView(
padding: mediaQuery.viewInsets, padding: mediaQuery.viewInsets,
@ -50,33 +60,50 @@ class BaseBottomSheet extends StatelessWidget {
], ],
), ),
child: SafeArea( child: SafeArea(
// 👈 prevents overlap with nav bar
top: false, top: false,
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 32), padding: const EdgeInsets.fromLTRB(20, 16, 20, 32),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
MySpacing.height(5), MySpacing.height(5),
Container( Center(
width: 40, child: Container(
height: 5, width: 40,
decoration: BoxDecoration( height: 5,
color: Colors.grey.shade300, decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10), color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(10),
),
), ),
), ),
MySpacing.height(12), 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), MySpacing.height(12),
child, widget.child,
MySpacing.height(12), MySpacing.height(12),
if (showButtons) ...[ if (widget.showButtons) ...[
Row( Row(
children: [ children: [
Expanded( Expanded(
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: onCancel, onPressed: widget.onCancel,
icon: const Icon(Icons.close, color: Colors.white), icon: const Icon(Icons.close, color: Colors.white),
label: MyText.bodyMedium( label: MyText.bodyMedium(
"Cancel", "Cancel",
@ -88,34 +115,40 @@ class BaseBottomSheet extends StatelessWidget {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
padding: const EdgeInsets.symmetric(vertical: 8), padding:
const EdgeInsets.symmetric(vertical: 8),
), ),
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: isSubmitting ? null : onSubmit, onPressed:
icon: Icon(submitIcon, color: Colors.white), widget.isSubmitting ? null : widget.onSubmit,
icon:
Icon(widget.submitIcon, color: Colors.white),
label: MyText.bodyMedium( label: MyText.bodyMedium(
isSubmitting ? "Submitting..." : submitText, widget.isSubmitting
? "Submitting..."
: widget.submitText,
color: Colors.white, color: Colors.white,
fontWeight: 600, fontWeight: 600,
), ),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: submitColor, backgroundColor: effectiveSubmitColor,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), 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), MySpacing.height(12),
bottomContent!, widget.bottomContent!,
], ],
], ],
], ],

View File

@ -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;
}

View File

@ -8,8 +8,9 @@ import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_text_style.dart'; import 'package:marco/helpers/widgets/my_text_style.dart';
import 'package:marco/model/document/document_filter_model.dart'; import 'package:marco/model/document/document_filter_model.dart';
import 'dart:convert'; 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 entityId;
final String entityTypeId; final String entityTypeId;
final DocumentController docController = Get.find<DocumentController>(); final DocumentController docController = Get.find<DocumentController>();
@ -100,7 +101,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget {
vertical: 10), vertical: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: docController.isUploadedAt.value color: docController.isUploadedAt.value
? Colors.indigo.shade400 ? contentTheme.buttonColor
: Colors.transparent, : Colors.transparent,
borderRadius: borderRadius:
const BorderRadius.horizontal( const BorderRadius.horizontal(
@ -131,7 +132,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget {
vertical: 10), vertical: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: !docController.isUploadedAt.value color: !docController.isUploadedAt.value
? Colors.indigo.shade400 ? contentTheme.buttonColor
: Colors.transparent, : Colors.transparent,
borderRadius: borderRadius:
const BorderRadius.horizontal( const BorderRadius.horizontal(
@ -264,7 +265,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget {
onChanged: (val) => onChanged: (val) =>
docController.isVerified.value = val, docController.isVerified.value = val,
activeColor: activeColor:
Colors.indigo, contentTheme.buttonColor,
materialTapTargetSize: materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap, MaterialTapTargetSize.shrinkWrap,
), ),

View File

@ -94,7 +94,7 @@ class _EmailLoginFormState extends State<EmailLoginForm> with UIMixin {
MaterialStateProperty.resolveWith<Color>( MaterialStateProperty.resolveWith<Color>(
(states) => (states) =>
states.contains(WidgetState.selected) states.contains(WidgetState.selected)
? contentTheme.brandRed ? contentTheme.brandColor
: Colors.white, : Colors.white,
), ),
checkColor: contentTheme.onPrimary, checkColor: contentTheme.onPrimary,
@ -132,7 +132,7 @@ class _EmailLoginFormState extends State<EmailLoginForm> with UIMixin {
elevation: 2, elevation: 2,
padding: MySpacing.xy(80, 16), padding: MySpacing.xy(80, 16),
borderRadiusAll: 10, borderRadiusAll: 10,
backgroundColor: contentTheme.brandRed, backgroundColor: contentTheme.brandColor,
child: MyText.labelLarge( child: MyText.labelLarge(
isLoading ? 'Logging in...' : 'Login', isLoading ? 'Logging in...' : 'Login',
fontWeight: 700, fontWeight: 700,

View File

@ -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_button.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/images.dart'; import 'package:marco/images.dart';
import 'package:marco/helpers/widgets/wave_background.dart';
class ForgotPasswordScreen extends StatefulWidget { class ForgotPasswordScreen extends StatefulWidget {
const ForgotPasswordScreen({super.key}); const ForgotPasswordScreen({super.key});
@ -59,7 +60,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen>
return Scaffold( return Scaffold(
body: Stack( body: Stack(
children: [ children: [
_RedWaveBackground(brandRed: contentTheme.brandRed), RedWaveBackground(brandRed: contentTheme.brandColor),
SafeArea( SafeArea(
child: Center( child: Center(
child: Column( child: Column(
@ -230,8 +231,8 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen>
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 16), padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 16),
borderRadiusAll: 10, borderRadiusAll: 10,
backgroundColor: _isLoading backgroundColor: _isLoading
? contentTheme.brandRed.withOpacity(0.6) ? contentTheme.brandColor.withOpacity(0.6)
: contentTheme.brandRed, : contentTheme.brandColor,
child: _isLoading child: _isLoading
? const SizedBox( ? const SizedBox(
height: 20, height: 20,
@ -253,68 +254,13 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen>
Widget _buildBackButton() { Widget _buildBackButton() {
return TextButton.icon( return TextButton.icon(
onPressed: () async => await LocalStorage.logout(), 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.brandColor,),
label: MyText.bodyMedium( label: MyText.bodyMedium(
'Back to Login', 'Back to Login',
color: contentTheme.brandRed, color: contentTheme.brandColor,
fontWeight: 600, fontWeight: 600,
fontSize: 14, 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;
}

View File

@ -7,6 +7,7 @@ import 'package:marco/view/auth/otp_login_form.dart';
import 'package:marco/helpers/services/api_endpoints.dart'; import 'package:marco/helpers/services/api_endpoints.dart';
import 'package:marco/view/auth/request_demo_bottom_sheet.dart'; import 'package:marco/view/auth/request_demo_bottom_sheet.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/wave_background.dart';
enum LoginOption { email, otp } enum LoginOption { email, otp }
@ -83,7 +84,7 @@ class _WelcomeScreenState extends State<WelcomeScreen>
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
option == LoginOption.email option == LoginOption.email
? EmailLoginForm() ? EmailLoginForm()
: const OTPLoginScreen(), : const OTPLoginScreen(),
], ],
), ),
@ -101,13 +102,14 @@ class _WelcomeScreenState extends State<WelcomeScreen>
return Scaffold( return Scaffold(
body: Stack( body: Stack(
children: [ children: [
_RedWaveBackground(brandRed: contentTheme.brandRed), RedWaveBackground(brandRed: contentTheme.brandColor),
SafeArea( SafeArea(
child: Center( child: Center(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: isNarrow ? double.infinity : 420), constraints: BoxConstraints(
maxWidth: isNarrow ? double.infinity : 420),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
@ -166,7 +168,10 @@ class _WelcomeScreenState extends State<WelcomeScreen>
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: Colors.white, color: Colors.white,
shape: BoxShape.circle, 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), child: Image.asset(Images.logoDark),
), ),
@ -230,9 +235,10 @@ class _WelcomeScreenState extends State<WelcomeScreen>
), ),
), ),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: contentTheme.brandRed, backgroundColor: contentTheme.brandColor,
foregroundColor: Colors.white, foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
elevation: 4, elevation: 4,
shadowColor: Colors.black26, shadowColor: Colors.black26,
), ),
@ -247,55 +253,3 @@ class _WelcomeScreenState extends State<WelcomeScreen>
); );
} }
} }
// 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;
}

View File

@ -8,6 +8,8 @@ import 'package:marco/images.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/services/api_endpoints.dart'; import 'package:marco/helpers/services/api_endpoints.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/wave_background.dart';
class MPINAuthScreen extends StatefulWidget { class MPINAuthScreen extends StatefulWidget {
const MPINAuthScreen({super.key}); const MPINAuthScreen({super.key});
@ -51,7 +53,7 @@ class _MPINAuthScreenState extends State<MPINAuthScreen>
return Scaffold( return Scaffold(
body: Stack( body: Stack(
children: [ children: [
_RedWaveBackground(brandRed: contentTheme.brandRed), RedWaveBackground(brandRed: contentTheme.brandColor),
SafeArea( SafeArea(
child: Center( child: Center(
child: Column( child: Column(
@ -281,8 +283,8 @@ class _MPINAuthScreenState extends State<MPINAuthScreen>
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
borderRadiusAll: 10, borderRadiusAll: 10,
backgroundColor: controller.isLoading.value backgroundColor: controller.isLoading.value
? contentTheme.brandRed.withOpacity(0.6) ? contentTheme.brandColor.withOpacity(0.6)
: contentTheme.brandRed, : contentTheme.brandColor,
child: controller.isLoading.value child: controller.isLoading.value
? const SizedBox( ? const SizedBox(
height: 20, height: 20,
@ -315,11 +317,11 @@ class _MPINAuthScreenState extends State<MPINAuthScreen>
if (isNewUser || isChangeMpin) if (isNewUser || isChangeMpin)
TextButton.icon( TextButton.icon(
onPressed: () => Get.toNamed('/dashboard'), onPressed: () => Get.toNamed('/dashboard'),
icon: const Icon(Icons.arrow_back, icon: Icon(Icons.arrow_back,
size: 18, color: Colors.redAccent), size: 18, color: contentTheme.brandColor),
label: MyText.bodyMedium( label: MyText.bodyMedium(
'Back to Home Page', 'Back to Home Page',
color: contentTheme.brandRed, color: contentTheme.brandColor,
fontWeight: 600, fontWeight: 600,
fontSize: 14, fontSize: 14,
), ),
@ -331,7 +333,7 @@ class _MPINAuthScreenState extends State<MPINAuthScreen>
size: 18, color: Colors.redAccent), size: 18, color: Colors.redAccent),
label: MyText.bodyMedium( label: MyText.bodyMedium(
'Go back to Login Screen', 'Go back to Login Screen',
color: contentTheme.brandRed, color: contentTheme.brandColor,
fontWeight: 600, fontWeight: 600,
fontSize: 14, fontSize: 14,
), ),
@ -341,57 +343,3 @@ class _MPINAuthScreenState extends State<MPINAuthScreen>
}); });
} }
} }
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;
}

View File

@ -81,7 +81,7 @@ class _OTPLoginScreenState extends State<OTPLoginScreen> with UIMixin {
elevation: 2, elevation: 2,
padding: MySpacing.xy(24, 16), padding: MySpacing.xy(24, 16),
borderRadiusAll: 10, borderRadiusAll: 10,
backgroundColor: isDisabled ? Colors.grey : contentTheme.brandRed, backgroundColor: isDisabled ? Colors.grey : contentTheme.brandColor,
child: controller.isSending.value child: controller.isSending.value
? SizedBox( ? SizedBox(
width: 20, width: 20,
@ -170,7 +170,7 @@ class _OTPLoginScreenState extends State<OTPLoginScreen> with UIMixin {
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: contentTheme.brandRed, width: 2), borderSide: BorderSide(color: contentTheme.brandColor, width: 2),
), ),
), ),
), ),
@ -220,7 +220,7 @@ class _OTPLoginScreenState extends State<OTPLoginScreen> with UIMixin {
elevation: 2, elevation: 2,
padding: MySpacing.xy(24, 16), padding: MySpacing.xy(24, 16),
borderRadiusAll: 10, borderRadiusAll: 10,
backgroundColor: contentTheme.brandRed, backgroundColor: contentTheme.brandColor,
child: MyText.labelMedium( child: MyText.labelMedium(
'Verify OTP', 'Verify OTP',
fontWeight: 600, fontWeight: 600,

View File

@ -4,6 +4,7 @@ import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
class OrganizationFormBottomSheet { class OrganizationFormBottomSheet {
static void show(BuildContext context) { static void show(BuildContext context) {
@ -81,170 +82,203 @@ class _OrganizationFormState extends State<_OrganizationForm> with UIMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return SingleChildScrollView(
decoration: const BoxDecoration( padding: MediaQuery.of(context).viewInsets,
color: Colors.white, child: Padding(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)), padding: const EdgeInsets.only(top: 60),
), child: Container(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 40), decoration: BoxDecoration(
child: SingleChildScrollView( color: Colors.white,
controller: widget.scrollController, borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
child: Form( boxShadow: const [
key: validator.formKey, BoxShadow(
child: Column( color: Colors.black12,
crossAxisAlignment: CrossAxisAlignment.start, blurRadius: 12,
children: [ offset: Offset(0, -2),
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,
),
),
), ),
], ],
), ),
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.brandColor
: Colors.white),
checkColor: Colors.white,
side: BorderSide(color: contentTheme.brandColor, width: 2),
),
Flexible(
child: Wrap(
children: [
MyText(
'I agree to the ',
color: Colors.black87,
),
MyText(
'privacy policy & terms',
color: contentTheme.brandColor,
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.buttonColor,
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.brandColor,
),
label: MyText.bodySmall(
'Back to log in',
fontWeight: 600,
color: contentTheme.brandColor,
),
),
),
],
),
),
),
), ),
), ),
); );
@ -300,7 +334,7 @@ class _OrganizationFormState extends State<_OrganizationForm> with UIMixin {
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: contentTheme.brandRed, width: 1.5), borderSide: BorderSide(color: contentTheme.brandColor, width: 1.5),
), ),
errorBorder: OutlineInputBorder( errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@ -377,7 +411,7 @@ class _OrganizationFormState extends State<_OrganizationForm> with UIMixin {
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
borderSide: borderSide:
BorderSide(color: contentTheme.brandRed, width: 1.5), BorderSide(color: contentTheme.brandColor, width: 1.5),
), ),
errorText: fieldState.errorText, errorText: fieldState.errorText,
), ),
@ -386,8 +420,7 @@ class _OrganizationFormState extends State<_OrganizationForm> with UIMixin {
children: [ children: [
MyText.bodyMedium( MyText.bodyMedium(
selectedValue ?? 'Select $label', selectedValue ?? 'Select $label',
color: color: selectedValue == null ? Colors.grey : Colors.black,
selectedValue == null ? Colors.grey : Colors.black,
), ),
const Icon(Icons.arrow_drop_down, color: Colors.grey), const Icon(Icons.arrow_drop_down, color: Colors.grey),
], ],

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:marco/helpers/widgets/my_refresh_indicator.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/directory_controller.dart';
import 'package:marco/controller/directory/create_bucket_controller.dart'; import 'package:marco/controller/directory/create_bucket_controller.dart';
import 'package:marco/helpers/utils/launcher_utils.dart'; import 'package:marco/helpers/utils/launcher_utils.dart';
@ -24,7 +24,7 @@ class DirectoryView extends StatefulWidget {
State<DirectoryView> createState() => _DirectoryViewState(); State<DirectoryView> createState() => _DirectoryViewState();
} }
class _DirectoryViewState extends State<DirectoryView> { class _DirectoryViewState extends State<DirectoryView> with UIMixin {
final DirectoryController controller = Get.find(); final DirectoryController controller = Get.find();
final TextEditingController searchController = TextEditingController(); final TextEditingController searchController = TextEditingController();
final PermissionController permissionController = final PermissionController permissionController =
@ -127,7 +127,7 @@ class _DirectoryViewState extends State<DirectoryView> {
child: ElevatedButton( child: ElevatedButton(
onPressed: () => Navigator.pop(context, true), onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo, backgroundColor: contentTheme.buttonColor,
foregroundColor: Colors.white, foregroundColor: Colors.white,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
@ -173,7 +173,7 @@ class _DirectoryViewState extends State<DirectoryView> {
backgroundColor: Colors.grey[100], backgroundColor: Colors.grey[100],
floatingActionButton: FloatingActionButton.extended( floatingActionButton: FloatingActionButton.extended(
heroTag: 'createContact', heroTag: 'createContact',
backgroundColor: Colors.red, backgroundColor: contentTheme.buttonColor,
onPressed: _handleCreateContact, onPressed: _handleCreateContact,
icon: const Icon(Icons.person_add_alt_1, color: Colors.white), icon: const Icon(Icons.person_add_alt_1, color: Colors.white),
label: const Text("Add Contact", style: TextStyle(color: Colors.white)), label: const Text("Add Contact", style: TextStyle(color: Colors.white)),
@ -319,13 +319,13 @@ class _DirectoryViewState extends State<DirectoryView> {
PopupMenuItem<int>( PopupMenuItem<int>(
value: 2, value: 2,
child: Row( child: Row(
children: const [ children: [
Icon(Icons.add_box_outlined, Icon(Icons.add_box_outlined,
size: 20, color: Colors.black87), size: 20, color: Colors.black87),
SizedBox(width: 10), SizedBox(width: 10),
Expanded(child: Text("Create Bucket")), Expanded(child: Text("Create Bucket")),
Icon(Icons.chevron_right, Icon(Icons.chevron_right,
size: 20, color: Colors.red), size: 20, color: contentTheme.buttonColor),
], ],
), ),
onTap: () { onTap: () {
@ -352,13 +352,13 @@ class _DirectoryViewState extends State<DirectoryView> {
PopupMenuItem<int>( PopupMenuItem<int>(
value: 1, value: 1,
child: Row( child: Row(
children: const [ children: [
Icon(Icons.label_outline, Icon(Icons.label_outline,
size: 20, color: Colors.black87), size: 20, color: Colors.black87),
SizedBox(width: 10), SizedBox(width: 10),
Expanded(child: Text("Manage Buckets")), Expanded(child: Text("Manage Buckets")),
Icon(Icons.chevron_right, Icon(Icons.chevron_right,
size: 20, color: Colors.red), size: 20, color: contentTheme.buttonColor),
], ],
), ),
onTap: () { onTap: () {
@ -395,7 +395,7 @@ class _DirectoryViewState extends State<DirectoryView> {
child: Text('Show Deleted Contacts')), child: Text('Show Deleted Contacts')),
Switch.adaptive( Switch.adaptive(
value: !controller.isActive.value, value: !controller.isActive.value,
activeColor: Colors.indigo, activeColor: contentTheme.buttonColor,
onChanged: (val) { onChanged: (val) {
controller.isActive.value = !val; controller.isActive.value = !val;
controller.fetchContacts(active: !val); controller.fetchContacts(active: !val);

View File

@ -134,8 +134,8 @@ class _NotesViewState extends State<NotesView> with UIMixin {
), ),
if (note.isActive) ...[ if (note.isActive) ...[
IconButton( IconButton(
icon: Icon(isEditing ? Icons.close : Icons.edit_outlined, icon: Icon(isEditing ? Icons.close : Icons.edit,
color: Colors.indigo, size: 18), color: contentTheme.buttonColor, size: 18),
splashRadius: 18, splashRadius: 18,
onPressed: () { onPressed: () {
controller.editingNoteId.value = controller.editingNoteId.value =
@ -143,7 +143,7 @@ class _NotesViewState extends State<NotesView> with UIMixin {
}, },
), ),
IconButton( IconButton(
icon: const Icon(Icons.delete_outline, icon: const Icon(Icons.delete,
size: 18, color: Colors.red), size: 18, color: Colors.red),
splashRadius: 18, splashRadius: 18,
onPressed: () async { onPressed: () async {
@ -224,7 +224,7 @@ class _NotesViewState extends State<NotesView> with UIMixin {
fontWeight: 600, fontWeight: 600,
), ),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: contentTheme.primary, backgroundColor: contentTheme.buttonColor,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),

View File

@ -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/documents_list_model.dart';
import 'package:marco/model/document/user_document_filter_bottom_sheet.dart'; import 'package:marco/model/document/user_document_filter_bottom_sheet.dart';
import 'package:marco/view/document/document_details_page.dart'; import 'package:marco/view/document/document_details_page.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
class UserDocumentsPage extends StatefulWidget { class UserDocumentsPage extends StatefulWidget {
final String? entityId; final String? entityId;
@ -34,7 +36,7 @@ class UserDocumentsPage extends StatefulWidget {
State<UserDocumentsPage> createState() => _UserDocumentsPageState(); State<UserDocumentsPage> createState() => _UserDocumentsPageState();
} }
class _UserDocumentsPageState extends State<UserDocumentsPage> { class _UserDocumentsPageState extends State<UserDocumentsPage> with UIMixin {
final DocumentController docController = Get.put(DocumentController()); final DocumentController docController = Get.put(DocumentController());
final PermissionController permissionController = Get.put(PermissionController()); final PermissionController permissionController = Get.put(PermissionController());
final DocumentDetailsController controller = Get.put(DocumentDetailsController()); final DocumentDetailsController controller = Get.put(DocumentDetailsController());
@ -395,7 +397,7 @@ class _UserDocumentsPageState extends State<UserDocumentsPage> {
const Expanded(child: Text('Show Deleted Documents')), const Expanded(child: Text('Show Deleted Documents')),
Switch.adaptive( Switch.adaptive(
value: docController.showInactive.value, value: docController.showInactive.value,
activeColor: Colors.indigo, activeColor: contentTheme.buttonColor,
onChanged: (val) { onChanged: (val) {
docController.showInactive.value = val; docController.showInactive.value = val;
docController.fetchDocuments( docController.fetchDocuments(
@ -614,7 +616,7 @@ class _UserDocumentsPageState extends State<UserDocumentsPage> {
color: Colors.white, color: Colors.white,
fontWeight: 600, fontWeight: 600,
), ),
backgroundColor: Colors.red, backgroundColor: contentTheme.buttonColor,
) )
: SizedBox.shrink(); : SizedBox.shrink();
}), }),

View File

@ -12,6 +12,8 @@ import 'package:marco/controller/permission_controller.dart';
import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/utils/permission_constants.dart';
import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
import 'package:marco/model/employees/add_employee_bottom_sheet.dart'; import 'package:marco/model/employees/add_employee_bottom_sheet.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
class EmployeeDetailPage extends StatefulWidget { class EmployeeDetailPage extends StatefulWidget {
final String employeeId; final String employeeId;
@ -27,7 +29,7 @@ class EmployeeDetailPage extends StatefulWidget {
State<EmployeeDetailPage> createState() => _EmployeeDetailPageState(); State<EmployeeDetailPage> createState() => _EmployeeDetailPageState();
} }
class _EmployeeDetailPageState extends State<EmployeeDetailPage> { class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
final EmployeesScreenController controller = final EmployeesScreenController controller =
Get.put(EmployeesScreenController()); Get.put(EmployeesScreenController());
final PermissionController permissionController = final PermissionController permissionController =
@ -252,7 +254,7 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> {
), ),
IconButton( IconButton(
icon: icon:
const Icon(Icons.edit, size: 24, color: Colors.red), Icon(Icons.edit, size: 24, color: contentTheme.buttonColor),
onPressed: () async { onPressed: () async {
final result = final result =
await showModalBottomSheet<Map<String, dynamic>>( await showModalBottomSheet<Map<String, dynamic>>(
@ -313,7 +315,7 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> {
), ),
); );
}, },
backgroundColor: Colors.red, backgroundColor: contentTheme.buttonColor,
icon: const Icon(Icons.assignment), icon: const Icon(Icons.assignment),
label: const Text( label: const Text(
'Assign to Project', 'Assign to Project',

View File

@ -267,7 +267,7 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.red, color: contentTheme.buttonColor,
borderRadius: BorderRadius.circular(28), borderRadius: BorderRadius.circular(28),
boxShadow: const [ boxShadow: const [
BoxShadow( BoxShadow(
@ -426,11 +426,11 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
value: _employeeController.isAllEmployeeSelected.value, value: _employeeController.isAllEmployeeSelected.value,
onChanged: (_) => Navigator.pop(context, 'all_employees'), onChanged: (_) => Navigator.pop(context, 'all_employees'),
checkColor: Colors.white, checkColor: Colors.white,
activeColor: Colors.blueAccent, activeColor: contentTheme.buttonColor,
side: const BorderSide(color: Colors.black, width: 1.5), side: const BorderSide(color: Colors.black, width: 1.5),
fillColor: MaterialStateProperty.resolveWith<Color>( fillColor: MaterialStateProperty.resolveWith<Color>(
(states) => states.contains(MaterialState.selected) (states) => states.contains(MaterialState.selected)
? Colors.blueAccent ? contentTheme.buttonColor
: Colors.white), : Colors.white),
), ),
const Text('All Employees'), const Text('All Employees'),

View File

@ -21,6 +21,7 @@ import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/model/employees/employee_info.dart'; import 'package:marco/model/employees/employee_info.dart';
import 'package:timeline_tile/timeline_tile.dart'; import 'package:timeline_tile/timeline_tile.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
class ExpenseDetailScreen extends StatefulWidget { class ExpenseDetailScreen extends StatefulWidget {
final String expenseId; final String expenseId;
@ -30,7 +31,7 @@ class ExpenseDetailScreen extends StatefulWidget {
State<ExpenseDetailScreen> createState() => _ExpenseDetailScreenState(); State<ExpenseDetailScreen> createState() => _ExpenseDetailScreenState();
} }
class _ExpenseDetailScreenState extends State<ExpenseDetailScreen> { class _ExpenseDetailScreenState extends State<ExpenseDetailScreen> with UIMixin {
final controller = Get.put(ExpenseDetailController()); final controller = Get.put(ExpenseDetailController());
final projectController = Get.find<ProjectController>(); final projectController = Get.find<ProjectController>();
final permissionController = Get.put(PermissionController()); final permissionController = Get.put(PermissionController());
@ -195,7 +196,7 @@ final permissionController = Get.put(PermissionController());
await showAddExpenseBottomSheet(isEdit: true); await showAddExpenseBottomSheet(isEdit: true);
await controller.fetchExpenseDetails(); await controller.fetchExpenseDetails();
}, },
backgroundColor: Colors.red, backgroundColor: contentTheme.buttonColor,
icon: const Icon(Icons.edit), icon: const Icon(Icons.edit),
label: MyText.bodyMedium( label: MyText.bodyMedium(
"Edit Expense", fontWeight: 600, color: Colors.white), "Edit Expense", fontWeight: 600, color: Colors.white),

View File

@ -1,3 +1,5 @@
// ignore_for_file: must_be_immutable
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/controller/expense/expense_screen_controller.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/helpers/widgets/my_text_style.dart';
import 'package:marco/model/employees/employee_model.dart'; import 'package:marco/model/employees/employee_model.dart';
import 'package:marco/model/expense/employee_selector_for_filter_bottom_sheet.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 ExpenseController expenseController;
final ScrollController scrollController; final ScrollController scrollController;
@ -19,12 +22,17 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
required this.scrollController, required this.scrollController,
}); });
@override
State<ExpenseFilterBottomSheet> createState() =>
_ExpenseFilterBottomSheetState();
}
class _ExpenseFilterBottomSheetState extends State<ExpenseFilterBottomSheet>
with UIMixin {
// FIX: create search adapter // FIX: create search adapter
Future<List<EmployeeModel>> searchEmployeesForBottomSheet( Future<List<EmployeeModel>> searchEmployeesForBottomSheet(String query) async {
String query) async { await widget.expenseController.searchEmployees(query);
await expenseController return widget.expenseController.employeeSearchResults.toList();
.searchEmployees(query); // async method, returns void
return expenseController.employeeSearchResults.toList();
} }
@override @override
@ -34,21 +42,21 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
title: 'Filter Expenses', title: 'Filter Expenses',
onCancel: () => Get.back(), onCancel: () => Get.back(),
onSubmit: () { onSubmit: () {
expenseController.fetchExpenses(); widget.expenseController.fetchExpenses();
Get.back(); Get.back();
}, },
submitText: 'Submit', submitText: 'Submit',
submitColor: Colors.indigo, submitColor: contentTheme.buttonColor,
submitIcon: Icons.check_circle_outline, submitIcon: Icons.check_circle_outline,
child: SingleChildScrollView( child: SingleChildScrollView(
controller: scrollController, controller: widget.scrollController,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: TextButton( child: TextButton(
onPressed: () => expenseController.clearFilters(), onPressed: () => widget.expenseController.clearFilters(),
child: MyText( child: MyText(
"Reset Filter", "Reset Filter",
style: MyTextStyle.labelMedium( style: MyTextStyle.labelMedium(
@ -91,11 +99,12 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
"Project", "Project",
_popupSelector( _popupSelector(
context, context,
currentValue: expenseController.selectedProject.value.isEmpty currentValue: widget.expenseController.selectedProject.value.isEmpty
? 'Select Project' ? 'Select Project'
: expenseController.selectedProject.value, : widget.expenseController.selectedProject.value,
items: expenseController.globalProjects, items: widget.expenseController.globalProjects,
onSelected: (value) => expenseController.selectedProject.value = value, onSelected: (value) =>
widget.expenseController.selectedProject.value = value,
), ),
); );
} }
@ -105,18 +114,19 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
"Expense Status", "Expense Status",
_popupSelector( _popupSelector(
context, context,
currentValue: expenseController.selectedStatus.value.isEmpty currentValue: widget.expenseController.selectedStatus.value.isEmpty
? 'Select Expense Status' ? 'Select Expense Status'
: expenseController.expenseStatuses : widget.expenseController.expenseStatuses
.firstWhereOrNull( .firstWhereOrNull((e) =>
(e) => e.id == expenseController.selectedStatus.value) e.id == widget.expenseController.selectedStatus.value)
?.name ?? ?.name ??
'Select Expense Status', 'Select Expense Status',
items: expenseController.expenseStatuses.map((e) => e.name).toList(), items:
widget.expenseController.expenseStatuses.map((e) => e.name).toList(),
onSelected: (name) { onSelected: (name) {
final status = expenseController.expenseStatuses final status = widget.expenseController.expenseStatuses
.firstWhere((e) => e.name == name); .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: [ children: [
Obx(() { Obx(() {
return SizedBox( return SizedBox(
width: double.infinity, // Make it full width width: double.infinity,
child: SegmentedButton<String>( child: SegmentedButton<String>(
segments: expenseController.dateTypes segments: widget.expenseController.dateTypes
.map( .map(
(type) => ButtonSegment( (type) => ButtonSegment(
value: type, value: type,
label: Center( label: Center(
// Center label text
child: MyText( child: MyText(
type, type,
style: MyTextStyle.bodySmall( style: MyTextStyle.bodySmall(
@ -150,10 +159,10 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
), ),
) )
.toList(), .toList(),
selected: {expenseController.selectedDateType.value}, selected: {widget.expenseController.selectedDateType.value},
onSelectionChanged: (newSelection) { onSelectionChanged: (newSelection) {
if (newSelection.isNotEmpty) { if (newSelection.isNotEmpty) {
expenseController.selectedDateType.value = widget.expenseController.selectedDateType.value =
newSelection.first; newSelection.first;
} }
}, },
@ -195,28 +204,30 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
children: [ children: [
Expanded( Expanded(
child: _dateButton( child: _dateButton(
label: expenseController.startDate.value == null label: widget.expenseController.startDate.value == null
? 'Start Date' ? 'Start Date'
: DateTimeUtils.formatDate( : DateTimeUtils.formatDate(
expenseController.startDate.value!, 'dd MMM yyyy'), widget.expenseController.startDate.value!,
'dd MMM yyyy'),
onTap: () => _selectDate( onTap: () => _selectDate(
context, context,
expenseController.startDate, widget.expenseController.startDate,
lastDate: expenseController.endDate.value, lastDate: widget.expenseController.endDate.value,
), ),
), ),
), ),
MySpacing.width(12), MySpacing.width(12),
Expanded( Expanded(
child: _dateButton( child: _dateButton(
label: expenseController.endDate.value == null label: widget.expenseController.endDate.value == null
? 'End Date' ? 'End Date'
: DateTimeUtils.formatDate( : DateTimeUtils.formatDate(
expenseController.endDate.value!, 'dd MMM yyyy'), widget.expenseController.endDate.value!,
'dd MMM yyyy'),
onTap: () => _selectDate( onTap: () => _selectDate(
context, context,
expenseController.endDate, widget.expenseController.endDate,
firstDate: expenseController.startDate.value, firstDate: widget.expenseController.startDate.value,
), ),
), ),
), ),
@ -232,8 +243,8 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
"Paid By", "Paid By",
_employeeSelector( _employeeSelector(
context: context, context: context,
selectedEmployees: expenseController.selectedPaidByEmployees, selectedEmployees: widget.expenseController.selectedPaidByEmployees,
searchEmployees: searchEmployeesForBottomSheet, // FIXED searchEmployees: searchEmployeesForBottomSheet,
title: 'Search Paid By', title: 'Search Paid By',
), ),
); );
@ -244,19 +255,15 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
"Created By", "Created By",
_employeeSelector( _employeeSelector(
context: context, context: context,
selectedEmployees: expenseController.selectedCreatedByEmployees, selectedEmployees: widget.expenseController.selectedCreatedByEmployees,
searchEmployees: searchEmployeesForBottomSheet, // FIXED searchEmployees: searchEmployeesForBottomSheet,
title: 'Search Created By', title: 'Search Created By',
), ),
); );
} }
Future<void> _selectDate( Future<void> _selectDate(BuildContext context, Rx<DateTime?> dateNotifier,
BuildContext context, {DateTime? firstDate, DateTime? lastDate}) async {
Rx<DateTime?> dateNotifier, {
DateTime? firstDate,
DateTime? lastDate,
}) async {
final DateTime? picked = await showDatePicker( final DateTime? picked = await showDatePicker(
context: context, context: context,
initialDate: dateNotifier.value ?? DateTime.now(), initialDate: dateNotifier.value ?? DateTime.now(),
@ -268,12 +275,10 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
} }
} }
Widget _popupSelector( Widget _popupSelector(BuildContext context,
BuildContext context, { {required String currentValue,
required String currentValue, required List<String> items,
required List<String> items, required ValueChanged<String> onSelected}) {
required ValueChanged<String> onSelected,
}) {
return PopupMenuButton<String>( return PopupMenuButton<String>(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onSelected: onSelected, onSelected: onSelected,
@ -374,10 +379,12 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
return Wrap( return Wrap(
spacing: 8, spacing: 8,
children: selectedEmployees children: selectedEmployees
.map((emp) => Chip( .map(
label: MyText(emp.name), (emp) => Chip(
onDeleted: () => selectedEmployees.remove(emp), label: MyText(emp.name),
)) onDeleted: () => selectedEmployees.remove(emp),
),
)
.toList(), .toList(),
); );
}), }),
@ -408,5 +415,4 @@ class ExpenseFilterBottomSheet extends StatelessWidget {
], ],
); );
} }
} }

View File

@ -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/widgets/expense/expense_main_components.dart';
import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/utils/permission_constants.dart';
import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
class ExpenseMainScreen extends StatefulWidget { class ExpenseMainScreen extends StatefulWidget {
const ExpenseMainScreen({super.key}); const ExpenseMainScreen({super.key});
@ -21,12 +21,12 @@ class ExpenseMainScreen extends StatefulWidget {
} }
class _ExpenseMainScreenState extends State<ExpenseMainScreen> class _ExpenseMainScreenState extends State<ExpenseMainScreen>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin, UIMixin {
late TabController _tabController; late TabController _tabController;
final searchController = TextEditingController(); final searchController = TextEditingController();
final expenseController = Get.put(ExpenseController()); final expenseController = Get.put(ExpenseController());
final projectController = Get.find<ProjectController>(); final projectController = Get.find<ProjectController>();
final permissionController = Get.put(PermissionController()); final permissionController = Get.put(PermissionController());
@override @override
void initState() { void initState() {
@ -81,83 +81,85 @@ final permissionController = Get.put(PermissionController());
.toList(); .toList();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Colors.white, backgroundColor: Colors.white,
appBar: ExpenseAppBar(projectController: projectController), appBar: ExpenseAppBar(projectController: projectController),
body: Column( body: Column(
children: [ children: [
// ---------------- TabBar ---------------- // ---------------- TabBar ----------------
Container( Container(
color: Colors.white, color: Colors.white,
child: TabBar( child: TabBar(
controller: _tabController, controller: _tabController,
labelColor: Colors.black, labelColor: Colors.black,
unselectedLabelColor: Colors.grey, unselectedLabelColor: Colors.grey,
indicatorColor: Colors.red, indicatorColor: Colors.red,
tabs: const [ tabs: const [
Tab(text: "Current Month"), Tab(text: "Current Month"),
Tab(text: "History"), 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),
],
),
),
], ],
), ),
), ),
),
],
),
// FAB reacts only to upload permission // ---------------- Gray background for rest ----------------
floatingActionButton: Obx(() { Expanded(
// Show loader or hide FAB while permissions are loading child: Container(
if (permissionController.permissions.isEmpty) { color: Colors.grey[100],
return const SizedBox.shrink(); 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 = // ---------------- TabBarView ----------------
permissionController.hasPermission(Permissions.expenseUpload); 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 final canUpload =
? FloatingActionButton( permissionController.hasPermission(Permissions.expenseUpload);
backgroundColor: Colors.red,
onPressed: showAddExpenseBottomSheet,
child: const Icon(Icons.add, color: Colors.white),
)
: const SizedBox.shrink();
}),
);
}
return canUpload
? FloatingActionButton.extended(
backgroundColor: contentTheme.buttonColor,
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}) { Widget _buildExpenseList({required bool isHistory}) {
return Obx(() { return Obx(() {
@ -207,4 +209,3 @@ Widget build(BuildContext context) {
}); });
} }
} }

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/images.dart'; import 'package:marco/images.dart';
import 'package:marco/helpers/widgets/wave_background.dart';
class OfflineScreen extends StatefulWidget { class OfflineScreen extends StatefulWidget {
const OfflineScreen({super.key}); const OfflineScreen({super.key});
@ -39,12 +41,11 @@ class _OfflineScreenState extends State<OfflineScreen>
return Scaffold( return Scaffold(
body: Stack( body: Stack(
children: [ children: [
_RedWaveBackground(brandRed: contentTheme.brandRed), RedWaveBackground(brandRed: contentTheme.brandColor),
SafeArea( SafeArea(
child: Center( child: Center(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 12.0),
horizontal: 12.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@ -88,7 +89,6 @@ class _OfflineScreenState extends State<OfflineScreen>
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(fontSize: 16, color: Colors.grey), style: TextStyle(fontSize: 16, color: Colors.grey),
), ),
], ],
), ),
), ),
@ -99,58 +99,3 @@ class _OfflineScreenState extends State<OfflineScreen>
); );
} }
} }
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;
}

View File

@ -13,7 +13,15 @@ import 'package:marco/view/employees/employee_profile_screen.dart';
import 'package:marco/helpers/services/tenant_service.dart'; import 'package:marco/helpers/services/tenant_service.dart';
import 'package:marco/view/tenant/tenant_selection_screen.dart'; import 'package:marco/view/tenant/tenant_selection_screen.dart';
import 'package:marco/controller/tenant/tenant_switch_controller.dart'; import 'package:marco/controller/tenant/tenant_switch_controller.dart';
import 'package:marco/helpers/theme/theme_editor_widget.dart';
class ThemeOption {
final String label;
final Color primary;
final Color button;
final Color brand;
ThemeOption(this.label, this.primary, this.button, this.brand);
}
class UserProfileBar extends StatefulWidget { class UserProfileBar extends StatefulWidget {
@ -29,6 +37,7 @@ class _UserProfileBarState extends State<UserProfileBar>
late EmployeeInfo employeeInfo; late EmployeeInfo employeeInfo;
bool _isLoading = true; bool _isLoading = true;
bool hasMpin = true; bool hasMpin = true;
bool _isThemeEditorVisible = false;
@override @override
void initState() { void initState() {
@ -58,8 +67,8 @@ class _UserProfileBarState extends State<UserProfileBar>
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
colors: [ colors: [
Colors.white.withValues(alpha: 0.95), Colors.white.withOpacity(0.95),
Colors.white.withValues(alpha: 0.85), Colors.white.withOpacity(0.85),
], ],
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
@ -67,150 +76,158 @@ class _UserProfileBarState extends State<UserProfileBar>
borderRadius: BorderRadius.circular(22), borderRadius: BorderRadius.circular(22),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withValues(alpha: 0.06), color: Colors.black.withOpacity(0.06),
blurRadius: 18, blurRadius: 18,
offset: const Offset(0, 8), offset: const Offset(0, 8),
) )
], ],
border: Border.all( border: Border.all(
color: Colors.grey.withValues(alpha: 0.25), color: Colors.grey.withOpacity(0.25),
width: 1, width: 1,
), ),
), ),
child: SafeArea( child: SafeArea(
bottom: true, bottom: true,
child: Column( child: Stack(
crossAxisAlignment: CrossAxisAlignment.stretch, children: [
children: [ Offstage(
_isLoading offstage: _isThemeEditorVisible,
? const _LoadingSection() child: Column(
: _userProfileSection(isCondensed), crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// --- SWITCH TENANT ROW BELOW AVATAR --- _isLoading
if (!_isLoading && !isCondensed) _switchTenantRow(), ? const _LoadingSection()
: _userProfileSection(isCondensed),
MySpacing.height(12), if (!_isLoading && !isCondensed) _switchTenantRow(),
Divider( MySpacing.height(12),
indent: 18, Divider(
endIndent: 18, indent: 18,
thickness: 0.7, endIndent: 18,
color: Colors.grey.withValues(alpha: 0.25), thickness: 0.7,
), color: Colors.grey.withOpacity(0.25),
MySpacing.height(12), ),
_supportAndSettingsMenu(isCondensed), MySpacing.height(12),
const Spacer(), _supportAndSettingsMenu(isCondensed),
Divider( const Spacer(),
indent: 18, Divider(
endIndent: 18, indent: 18,
thickness: 0.35, endIndent: 18,
color: Colors.grey.withValues(alpha: 0.18), thickness: 0.35,
), color: Colors.grey.withOpacity(0.18),
_logoutButton(isCondensed), ),
], _logoutButton(isCondensed),
), ],
), ),
),
Offstage(
offstage: !_isThemeEditorVisible,
child: ThemeEditorWidget(
onClose: () {
setState(() => _isThemeEditorVisible = false);
},
),
),
],
)),
), ),
), ),
), ),
); );
} }
/// Row widget to switch tenant with popup menu (button only) // ==================== CONTINUE EXISTING CODE =====================
/// Row widget to switch tenant with popup menu (button only) Widget _switchTenantRow() {
Widget _switchTenantRow() { final TenantSwitchController tenantSwitchController =
// Use the dedicated switch controller Get.put(TenantSwitchController());
final TenantSwitchController tenantSwitchController =
Get.put(TenantSwitchController());
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: Obx(() { child: Obx(() {
if (tenantSwitchController.isLoading.value) { if (tenantSwitchController.isLoading.value) {
return _loadingTenantContainer(); return _loadingTenantContainer();
} }
final tenants = tenantSwitchController.tenants; final tenants = tenantSwitchController.tenants;
if (tenants.isEmpty) return _noTenantContainer(); if (tenants.isEmpty) return _noTenantContainer();
final selectedTenant = TenantService.currentTenant; final selectedTenant = TenantService.currentTenant;
// Sort tenants: selected tenant first final sortedTenants = List.of(tenants);
final sortedTenants = List.of(tenants); if (selectedTenant != null) {
if (selectedTenant != null) { sortedTenants.sort((a, b) {
sortedTenants.sort((a, b) { if (a.id == selectedTenant.id) return -1;
if (a.id == selectedTenant.id) return -1; if (b.id == selectedTenant.id) return 1;
if (b.id == selectedTenant.id) return 1; return 0;
return 0; });
}); }
}
return PopupMenuButton<String>( return PopupMenuButton<String>(
onSelected: (tenantId) => onSelected: (tenantId) =>
tenantSwitchController.switchTenant(tenantId), tenantSwitchController.switchTenant(tenantId),
itemBuilder: (_) => sortedTenants.map((tenant) { itemBuilder: (_) => sortedTenants.map((tenant) {
return PopupMenuItem( return PopupMenuItem(
value: tenant.id, value: tenant.id,
child: Row( child: Row(
children: [ children: [
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: Container( child: Container(
width: 20, width: 20,
height: 20, height: 20,
color: Colors.grey.shade200, color: Colors.grey.shade200,
child: TenantLogo(logoImage: tenant.logoImage), 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.buttonColor
: Colors.black87,
),
),
),
if (tenant.id == selectedTenant?.id)
Icon(Icons.check_circle,
color: contentTheme.buttonColor, 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.buttonColor),
Expanded( Expanded(
child: Text( child: Padding(
tenant.name, padding: const EdgeInsets.symmetric(horizontal: 6),
maxLines: 1, child: Text(
overflow: TextOverflow.ellipsis, "Switch Organization",
style: TextStyle( maxLines: 1,
fontWeight: tenant.id == selectedTenant?.id overflow: TextOverflow.ellipsis,
? FontWeight.bold style: TextStyle(
: FontWeight.w600, color: contentTheme.buttonColor,
color: tenant.id == selectedTenant?.id fontWeight: FontWeight.bold),
? Colors.blueAccent
: Colors.black87,
), ),
), ),
), ),
if (tenant.id == selectedTenant?.id) Icon(Icons.arrow_drop_down, color: contentTheme.buttonColor),
const Icon(Icons.check_circle,
color: Colors.blueAccent, size: 18),
], ],
), ),
);
}).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( Widget _loadingTenantContainer() => Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
@ -254,7 +271,7 @@ Widget _switchTenantRow() {
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Theme.of(context).primaryColor.withValues(alpha: 0.15), color: Theme.of(context).primaryColor.withOpacity(0.15),
blurRadius: 10, blurRadius: 10,
spreadRadius: 1, spreadRadius: 1,
), ),
@ -310,6 +327,11 @@ Widget _switchTenantRow() {
_menuItemRow( _menuItemRow(
icon: LucideIcons.settings, icon: LucideIcons.settings,
label: 'Settings', label: 'Settings',
onTap: () {
setState(() {
_isThemeEditorVisible = true;
});
},
), ),
SizedBox(height: spacingHeight), SizedBox(height: spacingHeight),
_menuItemRow( _menuItemRow(
@ -320,8 +342,6 @@ Widget _switchTenantRow() {
_menuItemRow( _menuItemRow(
icon: LucideIcons.lock, icon: LucideIcons.lock,
label: hasMpin ? 'Change MPIN' : 'Set MPIN', label: hasMpin ? 'Change MPIN' : 'Set MPIN',
iconColor: Colors.redAccent,
textColor: Colors.redAccent,
onTap: _onMpinTap, onTap: _onMpinTap,
), ),
], ],
@ -395,9 +415,9 @@ Widget _switchTenantRow() {
fontWeight: 700, fontWeight: 700,
), ),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade600, backgroundColor: contentTheme.buttonColor,
foregroundColor: Colors.white, foregroundColor: Colors.white,
shadowColor: Colors.red.shade200, shadowColor: contentTheme.buttonColor,
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
vertical: condensed ? 14 : 18, vertical: condensed ? 14 : 18,
horizontal: condensed ? 14 : 22, horizontal: condensed ? 14 : 22,
@ -419,54 +439,71 @@ Widget _switchTenantRow() {
} }
Widget _buildLogoutDialog(BuildContext context) { Widget _buildLogoutDialog(BuildContext context) {
final theme = Theme.of(context);
final primaryColor = contentTheme.buttonColor;
return Dialog( return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
elevation: 10, elevation: 10,
backgroundColor: Colors.white, backgroundColor: theme.cardColor,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 34), padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 34),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(LucideIcons.log_out, size: 56, color: Colors.red.shade700), // Top icon
const SizedBox(height: 18), Icon(LucideIcons.log_out, size: 56, color: primaryColor),
const Text( MySpacing.height(18),
// Title
MyText.titleLarge(
"Logout Confirmation", "Logout Confirmation",
style: TextStyle( fontWeight: 700,
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.",
textAlign: TextAlign.center, 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( Row(
children: [ 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( Expanded(
child: ElevatedButton( child: ElevatedButton(
onPressed: () => Navigator.pop(context, true), onPressed: () => Navigator.pop(context, false),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade700, backgroundColor: Colors.grey,
foregroundColor: Colors.white, foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14), padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14)), 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,
),
), ),
), ),
], ],

View File

@ -8,6 +8,7 @@ import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/images.dart'; import 'package:marco/images.dart';
import 'package:marco/controller/tenant/tenant_selection_controller.dart'; import 'package:marco/controller/tenant/tenant_selection_controller.dart';
import 'package:marco/view/splash_screen.dart'; import 'package:marco/view/splash_screen.dart';
import 'package:marco/helpers/widgets/wave_background.dart';
class TenantSelectionScreen extends StatefulWidget { class TenantSelectionScreen extends StatefulWidget {
const TenantSelectionScreen({super.key}); const TenantSelectionScreen({super.key});
@ -63,7 +64,7 @@ class _TenantSelectionScreenState extends State<TenantSelectionScreen>
return Scaffold( return Scaffold(
body: Stack( body: Stack(
children: [ children: [
_RedWaveBackground(brandRed: contentTheme.brandRed), RedWaveBackground(brandRed: contentTheme.brandColor),
SafeArea( SafeArea(
child: Center( child: Center(
child: Column( child: Column(
@ -188,12 +189,12 @@ class _BetaBadge extends StatelessWidget {
} }
/// Tenant Card List /// Tenant Card List
class TenantCardList extends StatelessWidget { class TenantCardList extends StatelessWidget with UIMixin {
final TenantSelectionController controller; final TenantSelectionController controller;
final bool isLoading; final bool isLoading;
final Function(String tenantId) onTenantSelected; final Function(String tenantId) onTenantSelected;
const TenantCardList({ TenantCardList({
required this.controller, required this.controller,
required this.isLoading, required this.isLoading,
required this.onTenantSelected, required this.onTenantSelected,
@ -220,24 +221,23 @@ class TenantCardList extends StatelessWidget {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
], ],
if (hasTenants)
if (hasTenants) ...controller.tenants.map( ...controller.tenants.map(
(tenant) => _TenantCard( (tenant) => _TenantCard(
tenant: tenant, tenant: tenant,
onTap: () => onTenantSelected(tenant.id), onTap: () => onTenantSelected(tenant.id),
),
), ),
),
const SizedBox(height: 16), const SizedBox(height: 16),
TextButton.icon( TextButton.icon(
onPressed: () async { onPressed: () async {
await LocalStorage.logout(); await LocalStorage.logout();
}, },
icon: const Icon(Icons.arrow_back, size: 20, color: Colors.redAccent), icon:
Icon(Icons.arrow_back, size: 20, color: contentTheme.brandColor,),
label: MyText( label: MyText(
'Back to Login', 'Back to Login',
color: Colors.red, color: contentTheme.brandColor,
fontWeight: 600, fontWeight: 600,
fontSize: 14, fontSize: 14,
), ),
@ -249,10 +249,10 @@ class TenantCardList extends StatelessWidget {
} }
/// Single Tenant Card /// Single Tenant Card
class _TenantCard extends StatelessWidget { class _TenantCard extends StatelessWidget with UIMixin {
final dynamic tenant; final dynamic tenant;
final VoidCallback onTap; final VoidCallback onTap;
const _TenantCard({required this.tenant, required this.onTap}); _TenantCard({required this.tenant, required this.onTap});
@override @override
Widget build(BuildContext context) { 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.brandColor,),
], ],
), ),
), ),
@ -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;
}