Merge pull request 'Feature_Theme_Chnage_Main' (#76) from Feature_Theme_Chnage_Main into main

Reviewed-on: #76
This commit is contained in:
vaibhav.surve 2025-10-30 05:26:05 +00:00
commit 16a2e1e53a
27 changed files with 1170 additions and 1136 deletions

View File

@ -2,34 +2,9 @@ import 'package:flutter/material.dart';
import 'package:marco/helpers/theme/theme_customizer.dart'; import 'package:marco/helpers/theme/theme_customizer.dart';
enum LeftBarThemeType { light, dark } enum LeftBarThemeType { light, dark }
enum ContentThemeType { light, dark } enum ContentThemeType { light, dark }
enum RightBarThemeType { light, dark } enum RightBarThemeType { light, dark }
enum ContentThemeColor {
primary,
secondary,
success,
info,
warning,
danger,
light,
dark,
pink,
green,
red,
brandRed;
Color get color {
return (AdminTheme.theme.contentTheme.getMappedIntoThemeColor[this]?['color']) ?? Colors.black;
}
Color get onColor {
return (AdminTheme.theme.contentTheme.getMappedIntoThemeColor[this]?['onColor']) ?? Colors.white;
}
}
class LeftBarTheme { class LeftBarTheme {
final Color background, onBackground; final Color background, onBackground;
final Color labelColor; final Color labelColor;
@ -43,8 +18,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(
@ -52,7 +25,8 @@ class LeftBarTheme {
onBackground: const Color(0xffdcdcdc), onBackground: const Color(0xffdcdcdc),
labelColor: const Color(0xff32BFAE), labelColor: const Color(0xff32BFAE),
activeItemBackground: const Color(0x1532BFAE), activeItemBackground: const Color(0x1532BFAE),
activeItemColor: const Color(0xff32BFAE)); activeItemColor: const Color(0xff32BFAE),
);
static LeftBarTheme getThemeFromType(LeftBarThemeType leftBarThemeType) { static LeftBarTheme getThemeFromType(LeftBarThemeType leftBarThemeType) {
switch (leftBarThemeType) { switch (leftBarThemeType) {
@ -73,11 +47,12 @@ 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,19 +66,41 @@ 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),
activeSwitchBorderColor: const Color(0xff727cf5), activeSwitchBorderColor: const Color(0xff727cf5),
inactiveSwitchBorderColor: const Color(0xffdee2e6)); inactiveSwitchBorderColor: const Color(0xffdee2e6),
);
static final RightBarTheme darkRightBarTheme = RightBarTheme( static final RightBarTheme darkRightBarTheme = RightBarTheme(
disabled: const Color(0xff444d57), disabled: const Color(0xff444d57),
activeSwitchBorderColor: const Color(0xff727cf5), activeSwitchBorderColor: const Color(0xff727cf5),
inactiveSwitchBorderColor: const Color(0xffdee2e6), inactiveSwitchBorderColor: const Color(0xffdee2e6),
onDisabled: const Color(0xff515a65)); onDisabled: const Color(0xff515a65),
);
}
enum ContentThemeColor {
primary,
secondary,
success,
info,
warning,
danger,
light,
dark,
pink,
green,
red;
Color get color {
return (AdminTheme.theme.contentTheme.getMappedIntoThemeColor[this]?['color']) ?? Colors.black;
}
Color get onColor {
return (AdminTheme.theme.contentTheme.getMappedIntoThemeColor[this]?['onColor']) ?? Colors.white;
}
} }
class ContentTheme { class ContentTheme {
@ -120,29 +117,11 @@ class ContentTheme {
final Color purple, onPurple; 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 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 {
var c = AdminTheme.theme.contentTheme;
return {
ContentThemeColor.primary: {'color': c.primary, 'onColor': c.onPrimary},
ContentThemeColor.secondary: {'color': c.secondary, 'onColor': c.onSecondary},
ContentThemeColor.success: {'color': c.success, 'onColor': c.onSuccess},
ContentThemeColor.info: {'color': c.info, 'onColor': c.onInfo},
ContentThemeColor.warning: {'color': c.warning, 'onColor': c.onWarning},
ContentThemeColor.danger: {'color': c.danger, 'onColor': c.onDanger},
ContentThemeColor.light: {'color': c.light, 'onColor': c.onLight},
ContentThemeColor.dark: {'color': c.dark, 'onColor': c.onDark},
ContentThemeColor.pink: {'color': c.pink, 'onColor': c.onPink},
ContentThemeColor.red: {'color': c.red, 'onColor': c.onRed},
ContentThemeColor.brandRed: {'color': c.brandRed, 'onColor': c.onBrandRed},
};
}
ContentTheme({ ContentTheme({
this.background = const Color(0xfffafbfe), this.background = const Color(0xfffafbfe),
this.onBackground = const Color(0xffF1F1F2), this.onBackground = const Color(0xffF1F1F2),
@ -163,13 +142,11 @@ class ContentTheme {
this.dark = const Color(0xff313a46), this.dark = const Color(0xff313a46),
this.onDark = const Color(0xffffffff), this.onDark = const Color(0xffffffff),
this.purple = const Color(0xff800080), this.purple = const Color(0xff800080),
this.onPurple = const Color(0xffFF0000), this.onPurple = const Color(0xffffffff),
this.pink = const Color(0xffFF1087), this.pink = const Color(0xffff1087),
this.onPink = const Color(0xffffffff), this.onPink = const Color(0xffffffff),
this.red = const Color(0xffFF0000), this.red = const Color(0xffff0000),
this.onRed = const Color(0xffffffff), this.onRed = const Color(0xffffffff),
this.brandRed = const Color.fromARGB(255, 255, 0, 0),
this.onBrandRed = const Color(0xffffffff),
this.cardBackground = const Color(0xffffffff), this.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,44 +157,103 @@ class ContentTheme {
this.onDisabled = const Color(0xffffffff), this.onDisabled = const Color(0xffffffff),
}); });
//-------------------------------------- Left Bar Theme ----------------------------------------// Map<ContentThemeColor, Map<String, Color>> get getMappedIntoThemeColor {
return {
ContentThemeColor.primary: {'color': primary, 'onColor': onPrimary},
ContentThemeColor.secondary: {'color': secondary, 'onColor': onSecondary},
ContentThemeColor.success: {'color': success, 'onColor': onSuccess},
ContentThemeColor.info: {'color': info, 'onColor': onInfo},
ContentThemeColor.warning: {'color': warning, 'onColor': onWarning},
ContentThemeColor.danger: {'color': danger, 'onColor': onDanger},
ContentThemeColor.light: {'color': light, 'onColor': onLight},
ContentThemeColor.dark: {'color': dark, 'onColor': onDark},
ContentThemeColor.pink: {'color': pink, 'onColor': onPink},
ContentThemeColor.red: {'color': red, 'onColor': onRed},
};
}
static final ContentTheme lightContentTheme = ContentTheme( ContentTheme copyWith({
primary: Color(0xff663399), Color? primary,
background: const Color(0xfffafbfe), Color? onPrimary,
onBackground: const Color(0xff313a46), Color? secondary,
cardBorder: const Color(0xffe8ecf1), Color? onSecondary,
cardBackground: const Color(0xffffffff), Color? background,
cardShadow: const Color(0xff9aa1ab), Color? onBackground,
cardText: const Color(0xff6c757d), }) {
title: const Color(0xff6c757d), return ContentTheme(
cardTextMuted: const Color(0xff98a6ad), primary: primary ?? this.primary,
brandRed: const Color.fromARGB(255, 255, 0, 0), onPrimary: onPrimary ?? this.onPrimary,
onBrandRed: const Color(0xffffffff), secondary: secondary ?? this.secondary,
onSecondary: onSecondary ?? this.onSecondary,
background: background ?? this.background,
onBackground: onBackground ?? this.onBackground,
success: success,
onSuccess: onSuccess,
danger: danger,
onDanger: onDanger,
warning: warning,
onWarning: onWarning,
info: info,
onInfo: onInfo,
light: light,
onLight: onLight,
dark: dark,
onDark: onDark,
purple: purple,
onPurple: onPurple,
pink: pink,
onPink: onPink,
red: red,
onRed: onRed,
cardBackground: cardBackground,
cardShadow: cardShadow,
cardBorder: cardBorder,
cardText: cardText,
cardTextMuted: cardTextMuted,
title: title,
disabled: disabled,
onDisabled: onDisabled,
); );
}
static final ContentTheme darkContentTheme = ContentTheme( static ContentTheme withColorTheme(
primary: Color(0xff32BFAE), ColorThemeType colorTheme, {
ThemeMode mode = ThemeMode.light,
}) {
final baseTheme = mode == ThemeMode.light
? ContentTheme()
: ContentTheme(
primary: const Color(0xff32BFAE),
background: const Color(0xff343a40), background: const Color(0xff343a40),
onBackground: const Color(0xffF1F1F2), onBackground: const Color(0xffF1F1F2),
disabled: const Color(0xff444d57),
onDisabled: const Color(0xff515a65),
cardBorder: const Color(0xff464f5b), cardBorder: const Color(0xff464f5b),
cardBackground: const Color(0xff37404a), cardBackground: const Color(0xff37404a),
cardShadow: const Color(0xff01030E), cardShadow: const Color(0xff01030E),
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),
onBrandRed: const Color(0xffffffff),
); );
switch (colorTheme) {
case ColorThemeType.purple:
return baseTheme.copyWith(primary: const Color(0xff663399), onPrimary: Colors.white);
case ColorThemeType.red:
return baseTheme.copyWith(primary: const Color(0xffff0000), onPrimary: Colors.white);
case ColorThemeType.green:
return baseTheme.copyWith(primary: const Color(0xff49BF3C), onPrimary: Colors.white);
case ColorThemeType.blue:
return baseTheme.copyWith(primary: const Color(0xff007bff), onPrimary: Colors.white);
}
}
} }
enum ColorThemeType { purple, red, green, blue }
class AdminTheme { class AdminTheme {
final ContentTheme contentTheme;
final LeftBarTheme leftBarTheme; final LeftBarTheme leftBarTheme;
final RightBarTheme rightBarTheme; final RightBarTheme rightBarTheme;
final TopBarTheme topBarTheme; final TopBarTheme topBarTheme;
final ContentTheme contentTheme;
AdminTheme({ AdminTheme({
required this.leftBarTheme, required this.leftBarTheme,
@ -226,19 +262,22 @@ 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,
rightBarTheme: RightBarTheme.lightRightBarTheme, rightBarTheme: RightBarTheme.lightRightBarTheme,
contentTheme: ContentTheme.lightContentTheme); contentTheme: ContentTheme.withColorTheme(ColorThemeType.purple, mode: ThemeMode.light),
);
static void setTheme() { static void setTheme() {
final themeMode = ThemeCustomizer.instance.theme;
final colorTheme = ThemeCustomizer.instance.colorTheme;
theme = AdminTheme( theme = AdminTheme(
leftBarTheme: ThemeCustomizer.instance.theme == ThemeMode.dark ? LeftBarTheme.darkLeftBarTheme : LeftBarTheme.lightLeftBarTheme, leftBarTheme: themeMode == ThemeMode.dark ? LeftBarTheme.darkLeftBarTheme : LeftBarTheme.lightLeftBarTheme,
topBarTheme: ThemeCustomizer.instance.theme == ThemeMode.dark ? TopBarTheme.darkTopBarTheme : TopBarTheme.lightTopBarTheme, topBarTheme: themeMode == ThemeMode.dark ? TopBarTheme.darkTopBarTheme : TopBarTheme.lightTopBarTheme,
rightBarTheme: ThemeCustomizer.instance.theme == ThemeMode.dark ? RightBarTheme.darkRightBarTheme : RightBarTheme.lightRightBarTheme, rightBarTheme: themeMode == ThemeMode.dark ? RightBarTheme.darkRightBarTheme : RightBarTheme.lightRightBarTheme,
contentTheme: ThemeCustomizer.instance.theme == ThemeMode.dark ? ContentTheme.darkContentTheme : ContentTheme.lightContentTheme); contentTheme: ContentTheme.withColorTheme(colorTheme, mode: themeMode),
);
} }
} }

View File

@ -24,7 +24,7 @@ class ThemeCustomizer {
ThemeMode leftBarTheme = ThemeMode.light; ThemeMode leftBarTheme = ThemeMode.light;
ThemeMode rightBarTheme = ThemeMode.light; ThemeMode rightBarTheme = ThemeMode.light;
ThemeMode topBarTheme = ThemeMode.light; ThemeMode topBarTheme = ThemeMode.light;
ColorThemeType colorTheme = ColorThemeType.red;
bool rightBarOpen = false; bool rightBarOpen = false;
bool leftBarCondensed = false; bool leftBarCondensed = false;
@ -73,6 +73,11 @@ class ThemeCustomizer {
} }
} }
/// Public method to trigger theme updates externally
static void applyThemeChange() {
_notify();
}
static void notify() { static void notify() {
for (var value in _notifier) { for (var value in _notifier) {
value(oldInstance, instance); value(oldInstance, instance);

View File

@ -0,0 +1,271 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/wave_background.dart';
import 'package:marco/helpers/theme/admin_theme.dart';
import 'package:marco/helpers/theme/theme_customizer.dart';
class ThemeOption {
final String label;
final Color primary;
final Color button;
final Color brand;
final ColorThemeType colorThemeType;
ThemeOption(
this.label, this.primary, this.button, this.brand, this.colorThemeType);
}
final List<ThemeOption> themeOptions = [
ThemeOption(
"Theme 1", Colors.red, Colors.red, Colors.red, ColorThemeType.red),
ThemeOption(
"Theme 2",
const Color(0xFF49BF3C),
const Color(0xFF49BF3C),
const Color(0xFF49BF3C),
ColorThemeType.green,
),
ThemeOption(
"Theme 3",
const Color(0xFF3F51B5),
const Color(0xFF3F51B5),
const Color(0xFF3F51B5),
ColorThemeType.blue,
),
ThemeOption(
"Theme 4",
const Color(0xFF663399),
const Color(0xFF663399),
const Color(0xFF663399),
ColorThemeType.purple,
),
];
class ThemeController extends GetxController {
RxInt selectedIndex = 0.obs;
RxBool showApplied = false.obs;
void init() {
final currentPrimary = AdminTheme.theme.contentTheme.primary;
int index = themeOptions
.indexWhere((opt) => opt.primary.value == currentPrimary.value);
selectedIndex.value = index == -1 ? 0 : index;
}
void applyTheme(int index) async {
selectedIndex.value = index;
showApplied.value = true;
ThemeCustomizer.instance.colorTheme = themeOptions[index].colorThemeType;
ThemeCustomizer.applyThemeChange();
await Future.delayed(const Duration(milliseconds: 600));
showApplied.value = false;
}
}
class ThemeEditorWidget extends StatefulWidget {
final VoidCallback onClose;
const ThemeEditorWidget({super.key, required this.onClose});
@override
_ThemeEditorWidgetState createState() => _ThemeEditorWidgetState();
}
class _ThemeEditorWidgetState extends State<ThemeEditorWidget> {
final ThemeController themeController = Get.put(ThemeController());
@override
void initState() {
super.initState();
themeController.init();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header row with title and close button
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.bodyLarge("Theme Customization", fontWeight: 600),
IconButton(
icon: const Icon(Icons.close),
onPressed: widget.onClose,
tooltip: "Back",
iconSize: 20,
),
],
),
const SizedBox(height: 12),
// Theme cards wrapped in reactive Obx widget
Center(
child: Obx(
() => Wrap(
spacing: 12,
runSpacing: 12,
alignment: WrapAlignment.center,
children: List.generate(themeOptions.length, (i) {
return ThemeCard(
themeOption: themeOptions[i],
isSelected: themeController.selectedIndex.value == i,
onTap: () => themeController.applyTheme(i),
);
}),
),
),
),
const SizedBox(height: 12),
// Applied indicator reactive widget
Obx(
() => themeController.showApplied.value
? Padding(
padding: const EdgeInsets.only(top: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.check_circle,
color:
themeOptions[themeController.selectedIndex.value]
.brand,
size: 20,
),
const SizedBox(width: 6),
Text(
"Theme Applied!",
style: TextStyle(
color: themeOptions[
themeController.selectedIndex.value]
.brand,
fontWeight: FontWeight.w700,
),
),
],
),
)
: const SizedBox(),
),
const SizedBox(height: 16),
const Text(
"Preview and select a theme. You can change this anytime.",
style: TextStyle(fontSize: 13, color: Colors.black54),
),
],
),
);
}
}
class ThemeCard extends StatelessWidget {
final ThemeOption themeOption;
final bool isSelected;
final VoidCallback onTap;
const ThemeCard({
Key? key,
required this.themeOption,
required this.isSelected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 80,
child: Material(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
elevation: isSelected ? 4 : 1,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? themeOption.brand : Colors.transparent,
width: 2,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 80,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Stack(
fit: StackFit.expand,
children: [
CustomPaint(
painter: RedWavePainter(themeOption.brand, 0.15)),
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Hello, User!",
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.w600,
color: themeOption.primary,
fontSize: 12,
),
),
const SizedBox(height: 4),
SizedBox(
height: 18,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: themeOption.button,
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
elevation: 1,
textStyle: const TextStyle(fontSize: 10),
),
onPressed: () {},
child: const Text("Welcome"),
),
),
],
),
),
],
),
),
),
const SizedBox(height: 6),
Text(
themeOption.label,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 12,
color: Colors.grey[700],
),
),
],
),
),
),
),
);
}
}

View File

@ -95,7 +95,7 @@ class AttendanceButtonHelper {
} }
} }
static Color getButtonColor({ static Color getprimary({
required bool isYesterday, required bool isYesterday,
required bool isTodayApproved, required bool isTodayApproved,
required int activity, required int activity,

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,15 +60,16 @@ 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(
child: Container(
width: 40, width: 40,
height: 5, height: 5,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -66,17 +77,33 @@ class BaseBottomSheet extends StatelessWidget {
borderRadius: BorderRadius.circular(10), 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

@ -235,7 +235,7 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
isTodayApproved: isTodayApproved, isTodayApproved: isTodayApproved,
); );
final buttonColor = AttendanceButtonHelper.getButtonColor( final primary = AttendanceButtonHelper.getprimary(
isYesterday: isYesterday, isYesterday: isYesterday,
isTodayApproved: isTodayApproved, isTodayApproved: isTodayApproved,
activity: emp.activity, activity: emp.activity,
@ -245,7 +245,7 @@ class _AttendanceActionButtonState extends State<AttendanceActionButton> {
isUploading: isUploading, isUploading: isUploading,
isButtonDisabled: isButtonDisabled, isButtonDisabled: isButtonDisabled,
buttonText: buttonText, buttonText: buttonText,
buttonColor: buttonColor, primary: primary,
onPressed: isButtonDisabled ? null : _handleButtonPressed, onPressed: isButtonDisabled ? null : _handleButtonPressed,
); );
}); });
@ -256,7 +256,7 @@ class AttendanceActionButtonUI extends StatelessWidget {
final bool isUploading; final bool isUploading;
final bool isButtonDisabled; final bool isButtonDisabled;
final String buttonText; final String buttonText;
final Color buttonColor; final Color primary;
final VoidCallback? onPressed; final VoidCallback? onPressed;
const AttendanceActionButtonUI({ const AttendanceActionButtonUI({
@ -264,7 +264,7 @@ class AttendanceActionButtonUI extends StatelessWidget {
required this.isUploading, required this.isUploading,
required this.isButtonDisabled, required this.isButtonDisabled,
required this.buttonText, required this.buttonText,
required this.buttonColor, required this.primary,
required this.onPressed, required this.onPressed,
}); });
@ -275,7 +275,7 @@ class AttendanceActionButtonUI extends StatelessWidget {
child: ElevatedButton( child: ElevatedButton(
onPressed: onPressed, onPressed: onPressed,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: buttonColor, backgroundColor: primary,
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12), padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
textStyle: const TextStyle(fontSize: 12), textStyle: const TextStyle(fontSize: 12),
), ),

View File

@ -15,7 +15,7 @@ class ReusableListCard<T> extends StatelessWidget {
final String Function(T item)? getOutTimeText; final String Function(T item)? getOutTimeText;
final bool Function(T item)? isUploading; final bool Function(T item)? isUploading;
final String Function(T item)? getButtonText; final String Function(T item)? getButtonText;
final Color Function(String buttonText)? getButtonColor; final Color Function(String buttonText)? getprimary;
const ReusableListCard({ const ReusableListCard({
Key? key, Key? key,
@ -30,7 +30,7 @@ class ReusableListCard<T> extends StatelessWidget {
this.getOutTimeText, this.getOutTimeText,
this.isUploading, this.isUploading,
this.getButtonText, this.getButtonText,
this.getButtonColor, this.getprimary,
}) : super(key: key); }) : super(key: key);
@override @override
@ -47,8 +47,8 @@ class ReusableListCard<T> extends StatelessWidget {
final item = items[index]; final item = items[index];
final buttonText = getButtonText?.call(item) ?? 'Action'; final buttonText = getButtonText?.call(item) ?? 'Action';
final uploading = isUploading?.call(item) ?? false; final uploading = isUploading?.call(item) ?? false;
final buttonColor = final primary =
getButtonColor?.call(buttonText) ?? Theme.of(context).primaryColor; getprimary?.call(buttonText) ?? Theme.of(context).primaryColor;
return Column( return Column(
children: [ children: [
@ -121,7 +121,7 @@ class ReusableListCard<T> extends StatelessWidget {
? null ? null
: () => onActionPressed(item), : () => onActionPressed(item),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: buttonColor, backgroundColor: primary,
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
textStyle: const TextStyle(fontSize: 12), textStyle: const TextStyle(fontSize: 12),
), ),

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.primary
: 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.primary
: 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.primary,
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.primary
: 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.primary,
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.primary),
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.primary.withOpacity(0.6)
: contentTheme.brandRed, : contentTheme.primary,
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.primary,),
label: MyText.bodyMedium( label: MyText.bodyMedium(
'Back to Login', 'Back to Login',
color: contentTheme.brandRed, color: contentTheme.primary,
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 }
@ -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.primary),
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.primary,
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.primary),
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.primary.withOpacity(0.6)
: contentTheme.brandRed, : contentTheme.primary,
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.primary),
label: MyText.bodyMedium( label: MyText.bodyMedium(
'Back to Home Page', 'Back to Home Page',
color: contentTheme.brandRed, color: contentTheme.primary,
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.primary,
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.primary,
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.primary, 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.primary,
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,47 +82,61 @@ 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,
child: Padding(
padding: const EdgeInsets.only(top: 60),
child: Container(
decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)), borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
boxShadow: const [
BoxShadow(
color: Colors.black12,
blurRadius: 12,
offset: Offset(0, -2),
), ),
padding: const EdgeInsets.fromLTRB(20, 20, 20, 40), ],
child: SingleChildScrollView( ),
controller: widget.scrollController, child: SafeArea(
child: Form( top: false,
key: validator.formKey, child: Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 32),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Center( Center(
child: Container( child: Container(
width: 40, width: 40,
height: 5, height: 5,
margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[300], color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
), ),
), ),
MySpacing.height(12),
Center( Center(
child: Column( child: MyText.titleLarge(
children: [
MyText.titleLarge(
'Adventure starts here 🚀', 'Adventure starts here 🚀',
fontWeight: 600, fontWeight: 700,
color: Colors.black87, textAlign: TextAlign.center,
), ),
const SizedBox(height: 4), ),
MyText.bodySmall( MySpacing.height(4),
Center(
child: MyText.bodySmall(
"Make your app management easy and fun!", "Make your app management easy and fun!",
color: Colors.grey, color: Colors.grey[700],
),
],
), ),
), ),
const SizedBox(height: 20), MySpacing.height(12),
Form(
key: validator.formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionHeader('Organization Info'), _sectionHeader('Organization Info'),
_buildTextField('organizationName', 'Organization Name'), _buildTextField('organizationName', 'Organization Name'),
_buildTextField('email', 'Email', _buildTextField('email', 'Email',
@ -163,15 +178,17 @@ class _OrganizationFormState extends State<_OrganizationForm> with UIMixin {
children: [ children: [
Checkbox( Checkbox(
value: _agreed, value: _agreed,
onChanged: (val) => setState(() => _agreed = val ?? false), onChanged: (val) =>
fillColor: MaterialStateProperty.resolveWith((states) => setState(() => _agreed = val ?? false),
states.contains(MaterialState.selected) fillColor: MaterialStateProperty.resolveWith(
? contentTheme.brandRed (states) => states.contains(MaterialState.selected)
? contentTheme.primary
: Colors.white), : Colors.white),
checkColor: Colors.white, checkColor: Colors.white,
side: const BorderSide(color: Colors.red, width: 2), side: BorderSide(color: contentTheme.primary, width: 2),
), ),
Row( Flexible(
child: Wrap(
children: [ children: [
MyText( MyText(
'I agree to the ', 'I agree to the ',
@ -179,34 +196,44 @@ class _OrganizationFormState extends State<_OrganizationForm> with UIMixin {
), ),
MyText( MyText(
'privacy policy & terms', 'privacy policy & terms',
color: contentTheme.brandRed, color: contentTheme.primary,
fontWeight: 600, fontWeight: 600,
), ),
], ],
) ),
),
], ],
), ),
const SizedBox(height: 20), ],
),
),
MySpacing.height(12),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
OutlinedButton.icon( Expanded(
child: ElevatedButton.icon(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.arrow_back, color: Colors.red), icon: const Icon(Icons.close, color: Colors.white),
label: MyText.bodyMedium("Back", color: Colors.red), label: MyText.bodyMedium(
style: OutlinedButton.styleFrom( "Cancel",
side: const BorderSide(color: Colors.red), color: Colors.white,
fontWeight: 600,
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(vertical: 12),
horizontal: 20, vertical: 14),
), ),
), ),
ElevatedButton.icon( ),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: _loading ? null : _submitForm, onPressed: _loading ? null : _submitForm,
icon: _loading icon: _loading
? const SizedBox( ? SizedBox(
width: 18, width: 18,
height: 18, height: 18,
child: CircularProgressIndicator( child: CircularProgressIndicator(
@ -216,30 +243,35 @@ class _OrganizationFormState extends State<_OrganizationForm> with UIMixin {
) )
: const Icon(Icons.check_circle_outline, : const Icon(Icons.check_circle_outline,
color: Colors.white), color: Colors.white),
label: _loading label: MyText.bodyMedium(
? const SizedBox.shrink() _loading ? "Submitting..." : "Submit",
: MyText.bodyMedium("Submit", color: Colors.white), color: Colors.white,
fontWeight: 600,
),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo, backgroundColor: contentTheme.primary,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(vertical: 12),
horizontal: 28, vertical: 14), ),
), ),
), ),
], ],
), ),
const SizedBox(height: 8), MySpacing.height(12),
Center( Center(
child: TextButton.icon( child: TextButton.icon(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
icon: icon: Icon(
const Icon(Icons.arrow_back, size: 18, color: Colors.red), Icons.arrow_back,
size: 18,
color: contentTheme.primary,
),
label: MyText.bodySmall( label: MyText.bodySmall(
'Back to log in', 'Back to log in',
fontWeight: 600, fontWeight: 600,
color: contentTheme.brandRed, color: contentTheme.primary,
), ),
), ),
), ),
@ -247,6 +279,8 @@ class _OrganizationFormState extends State<_OrganizationForm> with UIMixin {
), ),
), ),
), ),
),
),
); );
} }
@ -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.primary, 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.primary, 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.primary,
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.primary,
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.primary),
], ],
), ),
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.primary),
], ],
), ),
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.primary,
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.primary, 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 {

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.primary,
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.primary,
) )
: 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.primary),
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.primary,
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.primary,
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.primary,
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.primary
: 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.primary,
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),
@ -256,10 +257,10 @@ final permissionController = Get.put(PermissionController());
Widget _statusButton(BuildContext context, ExpenseDetailController controller, Widget _statusButton(BuildContext context, ExpenseDetailController controller,
ExpenseDetailModel expense, dynamic next) { ExpenseDetailModel expense, dynamic next) {
Color buttonColor = Colors.red; Color primary = Colors.red;
if (next.color.isNotEmpty) { if (next.color.isNotEmpty) {
try { try {
buttonColor = Color(int.parse(next.color.replaceFirst('#', '0xff'))); primary = Color(int.parse(next.color.replaceFirst('#', '0xff')));
} catch (_) {} } catch (_) {}
} }
DateTime onlyDate(DateTime date) { DateTime onlyDate(DateTime date) {
@ -270,7 +271,7 @@ final permissionController = Get.put(PermissionController());
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
minimumSize: const Size(100, 40), minimumSize: const Size(100, 40),
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
backgroundColor: buttonColor, backgroundColor: primary,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
), ),
onPressed: () async { onPressed: () async {

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.primary,
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(
(emp) => Chip(
label: MyText(emp.name), label: MyText(emp.name),
onDeleted: () => selectedEmployees.remove(emp), 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,8 +81,8 @@ 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),
@ -111,7 +111,8 @@ Widget build(BuildContext context) {
children: [ children: [
// ---------------- Search ---------------- // ---------------- Search ----------------
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0), padding:
const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
child: SearchAndFilter( child: SearchAndFilter(
controller: searchController, controller: searchController,
onChanged: (_) => setState(() {}), onChanged: (_) => setState(() {}),
@ -136,8 +137,6 @@ Widget build(BuildContext context) {
), ),
], ],
), ),
// FAB reacts only to upload permission
floatingActionButton: Obx(() { floatingActionButton: Obx(() {
// Show loader or hide FAB while permissions are loading // Show loader or hide FAB while permissions are loading
if (permissionController.permissions.isEmpty) { if (permissionController.permissions.isEmpty) {
@ -148,16 +147,19 @@ Widget build(BuildContext context) {
permissionController.hasPermission(Permissions.expenseUpload); permissionController.hasPermission(Permissions.expenseUpload);
return canUpload return canUpload
? FloatingActionButton( ? FloatingActionButton.extended(
backgroundColor: Colors.red, backgroundColor: contentTheme.primary,
onPressed: showAddExpenseBottomSheet, onPressed: showAddExpenseBottomSheet,
child: const Icon(Icons.add, color: Colors.white), icon: const Icon(Icons.add, color: Colors.white),
label: const Text(
"Add Expense",
style: TextStyle(color: Colors.white, fontSize: 16),
),
) )
: const SizedBox.shrink(); : 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.primary),
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

@ -5,29 +5,12 @@ import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/widgets/custom_switch.dart'; import 'package:marco/widgets/custom_switch.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
// typedef void OnLeftBarColorSchemeChange(LeftBarThemeType leftBarThemeType);
// typedef void OnTopBarColorSchemeChange(TopBarThemeType topBarThemeType);
// typedef void OnRightBarColorSchemeChange(RightBarThemeType topBarThemeType);
// typedef void OnContentSchemeChange(ContentThemeType contentThemeType);
class RightBar extends StatefulWidget { class RightBar extends StatefulWidget {
// final RightBarThemeType rightBarThemeType;
// final LeftBarThemeType leftBarThemeType;
// final TopBarThemeType topBarThemeType;
// final ContentThemeType contentThemeType;
// final OnLeftBarColorSchemeChange onLeftBarColorSchemeChange;
// final OnTopBarColorSchemeChange onTopBarColorSchemeChange;
// final OnRightBarColorSchemeChange onRightBarColorSchemeChange;
// final OnContentSchemeChange onContentSchemeChange;
const RightBar({ const RightBar({
super.key, // this.leftBarThemeType, super.key,
// this.topBarThemeType,
// this.contentThemeType,
// this.onLeftBarColorSchemeChange,
// this.onTopBarColorSchemeChange,
// this.onContentSchemeChange,
// this.onRightBarColorSchemeChange
}); });
@override @override
@ -116,194 +99,12 @@ class _RightBarState extends State<RightBar>
) )
], ],
), ),
// Spacing.height(8),
// Row(
// children: [
// CustomSwitch.small(
// value: widget.contentThemeType == ContentThemeType.dark,
// activeBorderColor: rightBarTheme.activeSwitchBorderColor,
// inactiveBorderColor: rightBarTheme.inactiveSwitchBorderColor,
// inactiveTrackColor: rightBarTheme.disabled,
// activeTrackColor: rightBarTheme.primary,
// inactiveThumbColor: rightBarTheme.onDisabled,
// activeThumbColor: rightBarTheme.onPrimary,
// onChanged: (value) {
// if (value && widget.onContentSchemeChange != null) {
// widget.onContentSchemeChange(ContentThemeType.dark);
// }
// },
// ),
// Spacing.width(12),
// Text(
// "Dark",
// style: AppTheme.getTextStyle(themeData.textTheme.bodyText2,
// color: rightBarTheme.onBackground),
// )
// ],
// ),
// Spacing.height(36),
// Text("Left Bar",
// style: AppTheme.getTextStyle(themeData.textTheme.bodyText1,
// color: rightBarTheme.onBackground, fontWeight: 600)),
Divider(), Divider(),
// Spacing.height(8),
// Row(
// children: [
// CustomSwitch.small(
// value: widget.leftBarThemeType == LeftBarThemeType.light,
// activeBorderColor: rightBarTheme.activeSwitchBorderColor,
// inactiveBorderColor: rightBarTheme.inactiveSwitchBorderColor,
// inactiveTrackColor: rightBarTheme.disabled,
// activeTrackColor: rightBarTheme.primary,
// inactiveThumbColor: rightBarTheme.onDisabled,
// activeThumbColor: rightBarTheme.onPrimary,
// onChanged: (value) {
// if (value && widget.onLeftBarColorSchemeChange != null) {
// widget.onLeftBarColorSchemeChange(LeftBarThemeType.light);
// }
// },
// ),
// Spacing.width(12),
// Text(
// "Light",
// style: AppTheme.getTextStyle(themeData.textTheme.bodyText2,
// color: rightBarTheme.onBackground),
// )
// ],
// ),
// Spacing.height(8),
// Row(
// children: [
// CustomSwitch.small(
// value: widget.leftBarThemeType == LeftBarThemeType.dark,
// activeBorderColor: rightBarTheme.activeSwitchBorderColor,
// inactiveBorderColor: rightBarTheme.inactiveSwitchBorderColor,
// inactiveTrackColor: rightBarTheme.disabled,
// activeTrackColor: rightBarTheme.primary,
// inactiveThumbColor: rightBarTheme.onDisabled,
// activeThumbColor: rightBarTheme.onPrimary,
// onChanged: (value) {
// if (value && widget.onLeftBarColorSchemeChange != null) {
// widget.onLeftBarColorSchemeChange(LeftBarThemeType.dark);
// }
// },
// ),
// Spacing.width(12),
// Text(
// "Dark",
// style: AppTheme.getTextStyle(themeData.textTheme.bodyText2,
// color: rightBarTheme.onBackground),
// )
// ],
// ),
// Spacing.height(36),
Text("Top Bar"), Text("Top Bar"),
Divider(), Divider(),
// Spacing.height(8),
// Row(
// children: [
// CustomSwitch.small(
// value: widget.topBarThemeType == TopBarThemeType.light,
// inactiveTrackColor: rightBarTheme.disabled,
// activeBorderColor: rightBarTheme.activeSwitchBorderColor,
// inactiveBorderColor: rightBarTheme.inactiveSwitchBorderColor,
// activeTrackColor: rightBarTheme.primary,
// inactiveThumbColor: rightBarTheme.onDisabled,
// activeThumbColor: rightBarTheme.onPrimary,
// onChanged: (value) {
// if (value && widget.onTopBarColorSchemeChange != null) {
// widget.onTopBarColorSchemeChange(TopBarThemeType.light);
// }
// },
// ),
// Spacing.width(12),
// Text(
// "Light",
// style: AppTheme.getTextStyle(themeData.textTheme.bodyText2,
// color: rightBarTheme.onBackground),
// )
// ],
// ),
// Spacing.height(8),
// Row(
// children: [
// CustomSwitch.small(
// value: widget.topBarThemeType == TopBarThemeType.dark,
// inactiveTrackColor: rightBarTheme.disabled,
// activeBorderColor: rightBarTheme.activeSwitchBorderColor,
// inactiveBorderColor: rightBarTheme.inactiveSwitchBorderColor,
// activeTrackColor: rightBarTheme.primary,
// inactiveThumbColor: rightBarTheme.onDisabled,
// activeThumbColor: rightBarTheme.onPrimary,
// onChanged: (value) {
// if (value && widget.onTopBarColorSchemeChange != null) {
// widget.onTopBarColorSchemeChange(TopBarThemeType.dark);
// }
// },
// ),
// Spacing.width(12),
// Text(
// "Dark",
// style: AppTheme.getTextStyle(themeData.textTheme.bodyText2,
// color: rightBarTheme.onBackground),
// )
// ],
// ),
// Spacing.height(36),
// Text("Right Bar",
// style: AppTheme.getTextStyle(themeData.textTheme.bodyText1,
// color: rightBarTheme.onBackground, fontWeight: 600)),
// Divider(),
// Spacing.height(8),
// Row(
// children: [
// CustomSwitch.small(
// value: widget.rightBarThemeType == RightBarThemeType.light,
// inactiveTrackColor: rightBarTheme.disabled,
// activeBorderColor: rightBarTheme.activeSwitchBorderColor,
// inactiveBorderColor: rightBarTheme.inactiveSwitchBorderColor,
// activeTrackColor: rightBarTheme.primary,
// inactiveThumbColor: rightBarTheme.onDisabled,
// activeThumbColor: rightBarTheme.onPrimary,
// onChanged: (value) {
// if (value && widget.onRightBarColorSchemeChange != null) {
// widget.onRightBarColorSchemeChange(RightBarThemeType.light);
// }
// },
// ),
// Spacing.width(12),
// Text(
// "Light",
// style: AppTheme.getTextStyle(themeData.textTheme.bodyText2,
// color: rightBarTheme.onBackground),
// )
// ],
// ),
// Spacing.height(8),
// Row(
// children: [
// CustomSwitch.small(
// value: widget.rightBarThemeType == RightBarThemeType.dark,
// inactiveTrackColor: rightBarTheme.disabled,
// activeBorderColor: rightBarTheme.activeSwitchBorderColor,
// inactiveBorderColor: rightBarTheme.inactiveSwitchBorderColor,
// activeTrackColor: rightBarTheme.primary,
// inactiveThumbColor: rightBarTheme.onDisabled,
// activeThumbColor: rightBarTheme.onPrimary,
// onChanged: (value) {
// if (value && widget.onRightBarColorSchemeChange != null) {
// widget.onRightBarColorSchemeChange(RightBarThemeType.dark);
// }
// },
// ),
// Spacing.width(12),
// Text(
// "Dark",
// style: AppTheme.getTextStyle(themeData.textTheme.bodyText2,
// color: rightBarTheme.onBackground),
// )
// ],
// ),
], ],
), ),
)) ))

View File

@ -13,7 +13,7 @@ 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 UserProfileBar extends StatefulWidget { class UserProfileBar extends StatefulWidget {
@ -29,6 +29,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 +59,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,34 +68,35 @@ 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: Stack(
children: [
Offstage(
offstage: _isThemeEditorVisible,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
_isLoading _isLoading
? const _LoadingSection() ? const _LoadingSection()
: _userProfileSection(isCondensed), : _userProfileSection(isCondensed),
// --- SWITCH TENANT ROW BELOW AVATAR ---
if (!_isLoading && !isCondensed) _switchTenantRow(), if (!_isLoading && !isCondensed) _switchTenantRow(),
MySpacing.height(12), MySpacing.height(12),
Divider( Divider(
indent: 18, indent: 18,
endIndent: 18, endIndent: 18,
thickness: 0.7, thickness: 0.7,
color: Colors.grey.withValues(alpha: 0.25), color: Colors.grey.withOpacity(0.25),
), ),
MySpacing.height(12), MySpacing.height(12),
_supportAndSettingsMenu(isCondensed), _supportAndSettingsMenu(isCondensed),
@ -103,22 +105,30 @@ class _UserProfileBarState extends State<UserProfileBar>
indent: 18, indent: 18,
endIndent: 18, endIndent: 18,
thickness: 0.35, thickness: 0.35,
color: Colors.grey.withValues(alpha: 0.18), 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() {
// Use the dedicated switch controller
final TenantSwitchController tenantSwitchController = final TenantSwitchController tenantSwitchController =
Get.put(TenantSwitchController()); Get.put(TenantSwitchController());
@ -134,7 +144,6 @@ Widget _switchTenantRow() {
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) {
@ -172,14 +181,14 @@ Widget _switchTenantRow() {
? FontWeight.bold ? FontWeight.bold
: FontWeight.w600, : FontWeight.w600,
color: tenant.id == selectedTenant?.id color: tenant.id == selectedTenant?.id
? Colors.blueAccent ? contentTheme.primary
: Colors.black87, : Colors.black87,
), ),
), ),
), ),
if (tenant.id == selectedTenant?.id) if (tenant.id == selectedTenant?.id)
const Icon(Icons.check_circle, Icon(Icons.check_circle,
color: Colors.blueAccent, size: 18), color: contentTheme.primary, size: 18),
], ],
), ),
); );
@ -189,7 +198,7 @@ Widget _switchTenantRow() {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Icon(Icons.swap_horiz, color: Colors.blue.shade600), Icon(Icons.swap_horiz, color: contentTheme.primary),
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6), padding: const EdgeInsets.symmetric(horizontal: 6),
@ -198,19 +207,19 @@ Widget _switchTenantRow() {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
color: Colors.blue, fontWeight: FontWeight.bold), color: contentTheme.primary,
fontWeight: FontWeight.bold),
), ),
), ),
), ),
Icon(Icons.arrow_drop_down, color: Colors.blue.shade600), Icon(Icons.arrow_drop_down, color: contentTheme.primary),
], ],
), ),
), ),
); );
}), }),
); );
} }
Widget _loadingTenantContainer() => Container( Widget _loadingTenantContainer() => Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
@ -254,7 +263,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 +319,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 +334,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 +407,9 @@ Widget _switchTenantRow() {
fontWeight: 700, fontWeight: 700,
), ),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade600, backgroundColor: contentTheme.primary,
foregroundColor: Colors.white, foregroundColor: Colors.white,
shadowColor: Colors.red.shade200, shadowColor: contentTheme.primary,
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
vertical: condensed ? 14 : 18, vertical: condensed ? 14 : 18,
horizontal: condensed ? 14 : 22, horizontal: condensed ? 14 : 22,
@ -419,54 +431,71 @@ Widget _switchTenantRow() {
} }
Widget _buildLogoutDialog(BuildContext context) { Widget _buildLogoutDialog(BuildContext context) {
final theme = Theme.of(context);
final primaryColor = contentTheme.primary;
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.primary),
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.primary,),
label: MyText( label: MyText(
'Back to Login', 'Back to Login',
color: Colors.red, color: contentTheme.primary,
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.primary,),
], ],
), ),
), ),
@ -335,55 +335,3 @@ class TenantLogo extends StatelessWidget {
} }
} }
} }
/// Red Wave Background
class _RedWaveBackground extends StatelessWidget {
final Color brandRed;
const _RedWaveBackground({required this.brandRed});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _WavePainter(brandRed),
size: Size.infinite,
);
}
}
class _WavePainter extends CustomPainter {
final Color brandRed;
_WavePainter(this.brandRed);
@override
void paint(Canvas canvas, Size size) {
final paint1 = Paint()
..shader = LinearGradient(
colors: [brandRed, const Color.fromARGB(255, 97, 22, 22)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
final path1 = Path()
..moveTo(0, size.height * 0.2)
..quadraticBezierTo(
size.width * 0.25, size.height * 0.05, size.width * 0.5, size.height * 0.15)
..quadraticBezierTo(
size.width * 0.75, size.height * 0.25, size.width, size.height * 0.1)
..lineTo(size.width, 0)
..lineTo(0, 0)
..close();
canvas.drawPath(path1, paint1);
final paint2 = Paint()..color = Colors.redAccent.withOpacity(0.15);
final path2 = Path()
..moveTo(0, size.height * 0.25)
..quadraticBezierTo(size.width * 0.4, size.height * 0.1, size.width, size.height * 0.2)
..lineTo(size.width, 0)
..lineTo(0, 0)
..close();
canvas.drawPath(path2, paint2);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}