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

View File

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

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 isTodayApproved,
required int activity,

View File

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

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

View File

@ -15,7 +15,7 @@ class ReusableListCard<T> extends StatelessWidget {
final String Function(T item)? getOutTimeText;
final bool Function(T item)? isUploading;
final String Function(T item)? getButtonText;
final Color Function(String buttonText)? getButtonColor;
final Color Function(String buttonText)? getprimary;
const ReusableListCard({
Key? key,
@ -30,7 +30,7 @@ class ReusableListCard<T> extends StatelessWidget {
this.getOutTimeText,
this.isUploading,
this.getButtonText,
this.getButtonColor,
this.getprimary,
}) : super(key: key);
@override
@ -47,8 +47,8 @@ class ReusableListCard<T> extends StatelessWidget {
final item = items[index];
final buttonText = getButtonText?.call(item) ?? 'Action';
final uploading = isUploading?.call(item) ?? false;
final buttonColor =
getButtonColor?.call(buttonText) ?? Theme.of(context).primaryColor;
final primary =
getprimary?.call(buttonText) ?? Theme.of(context).primaryColor;
return Column(
children: [
@ -121,7 +121,7 @@ class ReusableListCard<T> extends StatelessWidget {
? null
: () => onActionPressed(item),
style: ElevatedButton.styleFrom(
backgroundColor: buttonColor,
backgroundColor: primary,
padding: const EdgeInsets.symmetric(horizontal: 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/model/document/document_filter_model.dart';
import 'dart:convert';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
class UserDocumentFilterBottomSheet extends StatelessWidget {
class UserDocumentFilterBottomSheet extends StatelessWidget with UIMixin {
final String entityId;
final String entityTypeId;
final DocumentController docController = Get.find<DocumentController>();
@ -100,7 +101,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget {
vertical: 10),
decoration: BoxDecoration(
color: docController.isUploadedAt.value
? Colors.indigo.shade400
? contentTheme.primary
: Colors.transparent,
borderRadius:
const BorderRadius.horizontal(
@ -131,7 +132,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget {
vertical: 10),
decoration: BoxDecoration(
color: !docController.isUploadedAt.value
? Colors.indigo.shade400
? contentTheme.primary
: Colors.transparent,
borderRadius:
const BorderRadius.horizontal(
@ -264,7 +265,7 @@ class UserDocumentFilterBottomSheet extends StatelessWidget {
onChanged: (val) =>
docController.isVerified.value = val,
activeColor:
Colors.indigo,
contentTheme.primary,
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
),

View File

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

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_text.dart';
import 'package:marco/images.dart';
import 'package:marco/helpers/widgets/wave_background.dart';
class ForgotPasswordScreen extends StatefulWidget {
const ForgotPasswordScreen({super.key});
@ -59,7 +60,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen>
return Scaffold(
body: Stack(
children: [
_RedWaveBackground(brandRed: contentTheme.brandRed),
RedWaveBackground(brandRed: contentTheme.primary),
SafeArea(
child: Center(
child: Column(
@ -230,8 +231,8 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen>
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 16),
borderRadiusAll: 10,
backgroundColor: _isLoading
? contentTheme.brandRed.withOpacity(0.6)
: contentTheme.brandRed,
? contentTheme.primary.withOpacity(0.6)
: contentTheme.primary,
child: _isLoading
? const SizedBox(
height: 20,
@ -253,68 +254,13 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen>
Widget _buildBackButton() {
return TextButton.icon(
onPressed: () async => await LocalStorage.logout(),
icon: const Icon(Icons.arrow_back, size: 18, color: Colors.redAccent),
icon: Icon(Icons.arrow_back, size: 18, color: contentTheme.primary,),
label: MyText.bodyMedium(
'Back to Login',
color: contentTheme.brandRed,
color: contentTheme.primary,
fontWeight: 600,
fontSize: 14,
),
);
}
}
class _RedWaveBackground extends StatelessWidget {
final Color brandRed;
const _RedWaveBackground({required this.brandRed});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _WavePainter(brandRed),
size: Size.infinite,
);
}
}
class _WavePainter extends CustomPainter {
final Color brandRed;
_WavePainter(this.brandRed);
@override
void paint(Canvas canvas, Size size) {
final paint1 = Paint()
..shader = LinearGradient(
colors: [brandRed, const Color.fromARGB(255, 97, 22, 22)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
final path1 = Path()
..moveTo(0, size.height * 0.2)
..quadraticBezierTo(size.width * 0.25, size.height * 0.05,
size.width * 0.5, size.height * 0.15)
..quadraticBezierTo(
size.width * 0.75, size.height * 0.25, size.width, size.height * 0.1)
..lineTo(size.width, 0)
..lineTo(0, 0)
..close();
canvas.drawPath(path1, paint1);
final paint2 = Paint()..color = Colors.redAccent.withOpacity(0.15);
final path2 = Path()
..moveTo(0, size.height * 0.25)
..quadraticBezierTo(
size.width * 0.4, size.height * 0.1, size.width, size.height * 0.2)
..lineTo(size.width, 0)
..lineTo(0, 0)
..close();
canvas.drawPath(path2, paint2);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}

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/view/auth/request_demo_bottom_sheet.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/wave_background.dart';
enum LoginOption { email, otp }
@ -83,7 +84,7 @@ class _WelcomeScreenState extends State<WelcomeScreen>
),
const SizedBox(height: 20),
option == LoginOption.email
? EmailLoginForm()
? EmailLoginForm()
: const OTPLoginScreen(),
],
),
@ -101,13 +102,14 @@ class _WelcomeScreenState extends State<WelcomeScreen>
return Scaffold(
body: Stack(
children: [
_RedWaveBackground(brandRed: contentTheme.brandRed),
RedWaveBackground(brandRed: contentTheme.primary),
SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: isNarrow ? double.infinity : 420),
constraints: BoxConstraints(
maxWidth: isNarrow ? double.infinity : 420),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@ -166,7 +168,10 @@ class _WelcomeScreenState extends State<WelcomeScreen>
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10, offset: Offset(0, 4))],
boxShadow: [
BoxShadow(
color: Colors.black12, blurRadius: 10, offset: Offset(0, 4))
],
),
child: Image.asset(Images.logoDark),
),
@ -230,9 +235,10 @@ class _WelcomeScreenState extends State<WelcomeScreen>
),
),
style: ElevatedButton.styleFrom(
backgroundColor: contentTheme.brandRed,
backgroundColor: contentTheme.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
elevation: 4,
shadowColor: Colors.black26,
),
@ -247,55 +253,3 @@ class _WelcomeScreenState extends State<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/api_endpoints.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/wave_background.dart';
class MPINAuthScreen extends StatefulWidget {
const MPINAuthScreen({super.key});
@ -51,7 +53,7 @@ class _MPINAuthScreenState extends State<MPINAuthScreen>
return Scaffold(
body: Stack(
children: [
_RedWaveBackground(brandRed: contentTheme.brandRed),
RedWaveBackground(brandRed: contentTheme.primary),
SafeArea(
child: Center(
child: Column(
@ -281,8 +283,8 @@ class _MPINAuthScreenState extends State<MPINAuthScreen>
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
borderRadiusAll: 10,
backgroundColor: controller.isLoading.value
? contentTheme.brandRed.withOpacity(0.6)
: contentTheme.brandRed,
? contentTheme.primary.withOpacity(0.6)
: contentTheme.primary,
child: controller.isLoading.value
? const SizedBox(
height: 20,
@ -315,11 +317,11 @@ class _MPINAuthScreenState extends State<MPINAuthScreen>
if (isNewUser || isChangeMpin)
TextButton.icon(
onPressed: () => Get.toNamed('/dashboard'),
icon: const Icon(Icons.arrow_back,
size: 18, color: Colors.redAccent),
icon: Icon(Icons.arrow_back,
size: 18, color: contentTheme.primary),
label: MyText.bodyMedium(
'Back to Home Page',
color: contentTheme.brandRed,
color: contentTheme.primary,
fontWeight: 600,
fontSize: 14,
),
@ -331,7 +333,7 @@ class _MPINAuthScreenState extends State<MPINAuthScreen>
size: 18, color: Colors.redAccent),
label: MyText.bodyMedium(
'Go back to Login Screen',
color: contentTheme.brandRed,
color: contentTheme.primary,
fontWeight: 600,
fontSize: 14,
),
@ -341,57 +343,3 @@ class _MPINAuthScreenState extends State<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,
padding: MySpacing.xy(24, 16),
borderRadiusAll: 10,
backgroundColor: isDisabled ? Colors.grey : contentTheme.brandRed,
backgroundColor: isDisabled ? Colors.grey : contentTheme.primary,
child: controller.isSending.value
? SizedBox(
width: 20,
@ -170,7 +170,7 @@ class _OTPLoginScreenState extends State<OTPLoginScreen> with UIMixin {
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: contentTheme.brandRed, width: 2),
borderSide: BorderSide(color: contentTheme.primary, width: 2),
),
),
),
@ -220,7 +220,7 @@ class _OTPLoginScreenState extends State<OTPLoginScreen> with UIMixin {
elevation: 2,
padding: MySpacing.xy(24, 16),
borderRadiusAll: 10,
backgroundColor: contentTheme.brandRed,
backgroundColor: contentTheme.primary,
child: MyText.labelMedium(
'Verify OTP',
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/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
class OrganizationFormBottomSheet {
static void show(BuildContext context) {
@ -81,170 +82,203 @@ class _OrganizationFormState extends State<_OrganizationForm> with UIMixin {
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
padding: const EdgeInsets.fromLTRB(20, 20, 20, 40),
child: SingleChildScrollView(
controller: widget.scrollController,
child: Form(
key: validator.formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 40,
height: 5,
margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(10),
),
),
),
Center(
child: Column(
children: [
MyText.titleLarge(
'Adventure starts here 🚀',
fontWeight: 600,
color: Colors.black87,
),
const SizedBox(height: 4),
MyText.bodySmall(
"Make your app management easy and fun!",
color: Colors.grey,
),
],
),
),
const SizedBox(height: 20),
_sectionHeader('Organization Info'),
_buildTextField('organizationName', 'Organization Name'),
_buildTextField('email', 'Email',
keyboardType: TextInputType.emailAddress),
_buildTextField('about', 'About Organization'),
_sectionHeader('Contact Details'),
_buildTextField('contactPerson', 'Contact Person'),
_buildTextField('contactNumber', 'Contact Number',
keyboardType: TextInputType.phone),
_buildTextField('address', 'Current Address'),
_sectionHeader('Additional Details'),
_buildPopupMenuField(
'Organization Size',
_sizes,
_selectedSize,
(val) => setState(() => _selectedSize = val),
'Please select organization size',
),
_buildPopupMenuField(
'Industry',
_industries.map((e) => e['name'] as String).toList(),
_selectedIndustryId != null
? _industries.firstWhere(
(e) => e['id'] == _selectedIndustryId)['name']
: null,
(val) {
setState(() {
final selectedIndustry = _industries.firstWhere(
(element) => element['name'] == val,
orElse: () => {},
);
_selectedIndustryId = selectedIndustry['id'];
});
},
'Please select industry',
),
const SizedBox(height: 12),
Row(
children: [
Checkbox(
value: _agreed,
onChanged: (val) => setState(() => _agreed = val ?? false),
fillColor: MaterialStateProperty.resolveWith((states) =>
states.contains(MaterialState.selected)
? contentTheme.brandRed
: Colors.white),
checkColor: Colors.white,
side: const BorderSide(color: Colors.red, width: 2),
),
Row(
children: [
MyText(
'I agree to the ',
color: Colors.black87,
),
MyText(
'privacy policy & terms',
color: contentTheme.brandRed,
fontWeight: 600,
),
],
)
],
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
OutlinedButton.icon(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.arrow_back, color: Colors.red),
label: MyText.bodyMedium("Back", color: Colors.red),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.red),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 14),
),
),
ElevatedButton.icon(
onPressed: _loading ? null : _submitForm,
icon: _loading
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Icon(Icons.check_circle_outline,
color: Colors.white),
label: _loading
? const SizedBox.shrink()
: MyText.bodyMedium("Submit", color: Colors.white),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(
horizontal: 28, vertical: 14),
),
),
],
),
const SizedBox(height: 8),
Center(
child: TextButton.icon(
onPressed: () => Navigator.pop(context),
icon:
const Icon(Icons.arrow_back, size: 18, color: Colors.red),
label: MyText.bodySmall(
'Back to log in',
fontWeight: 600,
color: contentTheme.brandRed,
),
),
return SingleChildScrollView(
padding: MediaQuery.of(context).viewInsets,
child: Padding(
padding: const EdgeInsets.only(top: 60),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
boxShadow: const [
BoxShadow(
color: Colors.black12,
blurRadius: 12,
offset: Offset(0, -2),
),
],
),
child: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 40,
height: 5,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(10),
),
),
),
MySpacing.height(12),
Center(
child: MyText.titleLarge(
'Adventure starts here 🚀',
fontWeight: 700,
textAlign: TextAlign.center,
),
),
MySpacing.height(4),
Center(
child: MyText.bodySmall(
"Make your app management easy and fun!",
color: Colors.grey[700],
),
),
MySpacing.height(12),
Form(
key: validator.formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionHeader('Organization Info'),
_buildTextField('organizationName', 'Organization Name'),
_buildTextField('email', 'Email',
keyboardType: TextInputType.emailAddress),
_buildTextField('about', 'About Organization'),
_sectionHeader('Contact Details'),
_buildTextField('contactPerson', 'Contact Person'),
_buildTextField('contactNumber', 'Contact Number',
keyboardType: TextInputType.phone),
_buildTextField('address', 'Current Address'),
_sectionHeader('Additional Details'),
_buildPopupMenuField(
'Organization Size',
_sizes,
_selectedSize,
(val) => setState(() => _selectedSize = val),
'Please select organization size',
),
_buildPopupMenuField(
'Industry',
_industries.map((e) => e['name'] as String).toList(),
_selectedIndustryId != null
? _industries.firstWhere(
(e) => e['id'] == _selectedIndustryId)['name']
: null,
(val) {
setState(() {
final selectedIndustry = _industries.firstWhere(
(element) => element['name'] == val,
orElse: () => {},
);
_selectedIndustryId = selectedIndustry['id'];
});
},
'Please select industry',
),
const SizedBox(height: 12),
Row(
children: [
Checkbox(
value: _agreed,
onChanged: (val) =>
setState(() => _agreed = val ?? false),
fillColor: MaterialStateProperty.resolveWith(
(states) => states.contains(MaterialState.selected)
? contentTheme.primary
: Colors.white),
checkColor: Colors.white,
side: BorderSide(color: contentTheme.primary, width: 2),
),
Flexible(
child: Wrap(
children: [
MyText(
'I agree to the ',
color: Colors.black87,
),
MyText(
'privacy policy & terms',
color: contentTheme.primary,
fontWeight: 600,
),
],
),
),
],
),
],
),
),
MySpacing.height(12),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close, color: Colors.white),
label: MyText.bodyMedium(
"Cancel",
color: Colors.white,
fontWeight: 600,
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: _loading ? null : _submitForm,
icon: _loading
? SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Icon(Icons.check_circle_outline,
color: Colors.white),
label: MyText.bodyMedium(
_loading ? "Submitting..." : "Submit",
color: Colors.white,
fontWeight: 600,
),
style: ElevatedButton.styleFrom(
backgroundColor: contentTheme.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
],
),
MySpacing.height(12),
Center(
child: TextButton.icon(
onPressed: () => Navigator.pop(context),
icon: Icon(
Icons.arrow_back,
size: 18,
color: contentTheme.primary,
),
label: MyText.bodySmall(
'Back to log in',
fontWeight: 600,
color: contentTheme.primary,
),
),
),
],
),
),
),
),
),
);
@ -300,7 +334,7 @@ class _OrganizationFormState extends State<_OrganizationForm> with UIMixin {
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: contentTheme.brandRed, width: 1.5),
borderSide: BorderSide(color: contentTheme.primary, width: 1.5),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
@ -377,7 +411,7 @@ class _OrganizationFormState extends State<_OrganizationForm> with UIMixin {
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide:
BorderSide(color: contentTheme.brandRed, width: 1.5),
BorderSide(color: contentTheme.primary, width: 1.5),
),
errorText: fieldState.errorText,
),
@ -386,8 +420,7 @@ class _OrganizationFormState extends State<_OrganizationForm> with UIMixin {
children: [
MyText.bodyMedium(
selectedValue ?? 'Select $label',
color:
selectedValue == null ? Colors.grey : Colors.black,
color: selectedValue == null ? Colors.grey : Colors.black,
),
const Icon(Icons.arrow_drop_down, color: Colors.grey),
],

View File

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

View File

@ -134,8 +134,8 @@ class _NotesViewState extends State<NotesView> with UIMixin {
),
if (note.isActive) ...[
IconButton(
icon: Icon(isEditing ? Icons.close : Icons.edit_outlined,
color: Colors.indigo, size: 18),
icon: Icon(isEditing ? Icons.close : Icons.edit,
color: contentTheme.primary, size: 18),
splashRadius: 18,
onPressed: () {
controller.editingNoteId.value =
@ -143,7 +143,7 @@ class _NotesViewState extends State<NotesView> with UIMixin {
},
),
IconButton(
icon: const Icon(Icons.delete_outline,
icon: const Icon(Icons.delete,
size: 18, color: Colors.red),
splashRadius: 18,
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/user_document_filter_bottom_sheet.dart';
import 'package:marco/view/document/document_details_page.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
class UserDocumentsPage extends StatefulWidget {
final String? entityId;
@ -34,7 +36,7 @@ class UserDocumentsPage extends StatefulWidget {
State<UserDocumentsPage> createState() => _UserDocumentsPageState();
}
class _UserDocumentsPageState extends State<UserDocumentsPage> {
class _UserDocumentsPageState extends State<UserDocumentsPage> with UIMixin {
final DocumentController docController = Get.put(DocumentController());
final PermissionController permissionController = Get.put(PermissionController());
final DocumentDetailsController controller = Get.put(DocumentDetailsController());
@ -395,7 +397,7 @@ class _UserDocumentsPageState extends State<UserDocumentsPage> {
const Expanded(child: Text('Show Deleted Documents')),
Switch.adaptive(
value: docController.showInactive.value,
activeColor: Colors.indigo,
activeColor: contentTheme.primary,
onChanged: (val) {
docController.showInactive.value = val;
docController.fetchDocuments(
@ -614,7 +616,7 @@ class _UserDocumentsPageState extends State<UserDocumentsPage> {
color: Colors.white,
fontWeight: 600,
),
backgroundColor: Colors.red,
backgroundColor: contentTheme.primary,
)
: 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/widgets/my_refresh_indicator.dart';
import 'package:marco/model/employees/add_employee_bottom_sheet.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
class EmployeeDetailPage extends StatefulWidget {
final String employeeId;
@ -27,7 +29,7 @@ class EmployeeDetailPage extends StatefulWidget {
State<EmployeeDetailPage> createState() => _EmployeeDetailPageState();
}
class _EmployeeDetailPageState extends State<EmployeeDetailPage> {
class _EmployeeDetailPageState extends State<EmployeeDetailPage> with UIMixin {
final EmployeesScreenController controller =
Get.put(EmployeesScreenController());
final PermissionController permissionController =
@ -252,7 +254,7 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> {
),
IconButton(
icon:
const Icon(Icons.edit, size: 24, color: Colors.red),
Icon(Icons.edit, size: 24, color: contentTheme.primary),
onPressed: () async {
final result =
await showModalBottomSheet<Map<String, dynamic>>(
@ -313,7 +315,7 @@ class _EmployeeDetailPageState extends State<EmployeeDetailPage> {
),
);
},
backgroundColor: Colors.red,
backgroundColor: contentTheme.primary,
icon: const Icon(Icons.assignment),
label: const Text(
'Assign to Project',

View File

@ -267,7 +267,7 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.red,
color: contentTheme.primary,
borderRadius: BorderRadius.circular(28),
boxShadow: const [
BoxShadow(
@ -426,11 +426,11 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
value: _employeeController.isAllEmployeeSelected.value,
onChanged: (_) => Navigator.pop(context, 'all_employees'),
checkColor: Colors.white,
activeColor: Colors.blueAccent,
activeColor: contentTheme.primary,
side: const BorderSide(color: Colors.black, width: 1.5),
fillColor: MaterialStateProperty.resolveWith<Color>(
(states) => states.contains(MaterialState.selected)
? Colors.blueAccent
? contentTheme.primary
: Colors.white),
),
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/model/employees/employee_info.dart';
import 'package:timeline_tile/timeline_tile.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
class ExpenseDetailScreen extends StatefulWidget {
final String expenseId;
@ -30,7 +31,7 @@ class ExpenseDetailScreen extends StatefulWidget {
State<ExpenseDetailScreen> createState() => _ExpenseDetailScreenState();
}
class _ExpenseDetailScreenState extends State<ExpenseDetailScreen> {
class _ExpenseDetailScreenState extends State<ExpenseDetailScreen> with UIMixin {
final controller = Get.put(ExpenseDetailController());
final projectController = Get.find<ProjectController>();
final permissionController = Get.put(PermissionController());
@ -195,7 +196,7 @@ final permissionController = Get.put(PermissionController());
await showAddExpenseBottomSheet(isEdit: true);
await controller.fetchExpenseDetails();
},
backgroundColor: Colors.red,
backgroundColor: contentTheme.primary,
icon: const Icon(Icons.edit),
label: MyText.bodyMedium(
"Edit Expense", fontWeight: 600, color: Colors.white),
@ -256,10 +257,10 @@ final permissionController = Get.put(PermissionController());
Widget _statusButton(BuildContext context, ExpenseDetailController controller,
ExpenseDetailModel expense, dynamic next) {
Color buttonColor = Colors.red;
Color primary = Colors.red;
if (next.color.isNotEmpty) {
try {
buttonColor = Color(int.parse(next.color.replaceFirst('#', '0xff')));
primary = Color(int.parse(next.color.replaceFirst('#', '0xff')));
} catch (_) {}
}
DateTime onlyDate(DateTime date) {
@ -270,7 +271,7 @@ final permissionController = Get.put(PermissionController());
style: ElevatedButton.styleFrom(
minimumSize: const Size(100, 40),
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
backgroundColor: buttonColor,
backgroundColor: primary,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
),
onPressed: () async {

View File

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

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

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/images.dart';
import 'package:marco/helpers/widgets/wave_background.dart';
class OfflineScreen extends StatefulWidget {
const OfflineScreen({super.key});
@ -39,12 +41,11 @@ class _OfflineScreenState extends State<OfflineScreen>
return Scaffold(
body: Stack(
children: [
_RedWaveBackground(brandRed: contentTheme.brandRed),
RedWaveBackground(brandRed: contentTheme.primary),
SafeArea(
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0),
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@ -69,14 +70,14 @@ class _OfflineScreenState extends State<OfflineScreen>
),
),
// Increased spacing here
const SizedBox(height: 120),
const SizedBox(height: 120),
const Icon(Icons.wifi_off,
size: 100, color: Colors.redAccent),
const SizedBox(height: 20),
const Text(
"No Internet Connection",
"No Internet Connection",
style: TextStyle(
fontSize: 26,
fontSize: 26,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
@ -88,7 +89,6 @@ class _OfflineScreenState extends State<OfflineScreen>
textAlign: TextAlign.center,
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:flutter/material.dart';
// typedef void OnLeftBarColorSchemeChange(LeftBarThemeType leftBarThemeType);
// typedef void OnTopBarColorSchemeChange(TopBarThemeType topBarThemeType);
// typedef void OnRightBarColorSchemeChange(RightBarThemeType topBarThemeType);
// typedef void OnContentSchemeChange(ContentThemeType contentThemeType);
class RightBar extends StatefulWidget {
// final RightBarThemeType rightBarThemeType;
// final LeftBarThemeType leftBarThemeType;
// final TopBarThemeType topBarThemeType;
// final ContentThemeType contentThemeType;
// final OnLeftBarColorSchemeChange onLeftBarColorSchemeChange;
// final OnTopBarColorSchemeChange onTopBarColorSchemeChange;
// final OnRightBarColorSchemeChange onRightBarColorSchemeChange;
// final OnContentSchemeChange onContentSchemeChange;
const RightBar({
super.key, // this.leftBarThemeType,
// this.topBarThemeType,
// this.contentThemeType,
// this.onLeftBarColorSchemeChange,
// this.onTopBarColorSchemeChange,
// this.onContentSchemeChange,
// this.onRightBarColorSchemeChange
super.key,
});
@override
@ -116,194 +99,12 @@ class _RightBarState extends State<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(),
// Spacing.height(8),
// Row(
// children: [
// CustomSwitch.small(
// value: widget.leftBarThemeType == LeftBarThemeType.light,
// activeBorderColor: rightBarTheme.activeSwitchBorderColor,
// inactiveBorderColor: rightBarTheme.inactiveSwitchBorderColor,
// inactiveTrackColor: rightBarTheme.disabled,
// activeTrackColor: rightBarTheme.primary,
// inactiveThumbColor: rightBarTheme.onDisabled,
// activeThumbColor: rightBarTheme.onPrimary,
// onChanged: (value) {
// if (value && widget.onLeftBarColorSchemeChange != null) {
// widget.onLeftBarColorSchemeChange(LeftBarThemeType.light);
// }
// },
// ),
// Spacing.width(12),
// Text(
// "Light",
// style: AppTheme.getTextStyle(themeData.textTheme.bodyText2,
// color: rightBarTheme.onBackground),
// )
// ],
// ),
// Spacing.height(8),
// Row(
// children: [
// CustomSwitch.small(
// value: widget.leftBarThemeType == LeftBarThemeType.dark,
// activeBorderColor: rightBarTheme.activeSwitchBorderColor,
// inactiveBorderColor: rightBarTheme.inactiveSwitchBorderColor,
// inactiveTrackColor: rightBarTheme.disabled,
// activeTrackColor: rightBarTheme.primary,
// inactiveThumbColor: rightBarTheme.onDisabled,
// activeThumbColor: rightBarTheme.onPrimary,
// onChanged: (value) {
// if (value && widget.onLeftBarColorSchemeChange != null) {
// widget.onLeftBarColorSchemeChange(LeftBarThemeType.dark);
// }
// },
// ),
// Spacing.width(12),
// Text(
// "Dark",
// style: AppTheme.getTextStyle(themeData.textTheme.bodyText2,
// color: rightBarTheme.onBackground),
// )
// ],
// ),
// Spacing.height(36),
Text("Top Bar"),
Divider(),
// Spacing.height(8),
// Row(
// children: [
// CustomSwitch.small(
// value: widget.topBarThemeType == TopBarThemeType.light,
// inactiveTrackColor: rightBarTheme.disabled,
// activeBorderColor: rightBarTheme.activeSwitchBorderColor,
// inactiveBorderColor: rightBarTheme.inactiveSwitchBorderColor,
// activeTrackColor: rightBarTheme.primary,
// inactiveThumbColor: rightBarTheme.onDisabled,
// activeThumbColor: rightBarTheme.onPrimary,
// onChanged: (value) {
// if (value && widget.onTopBarColorSchemeChange != null) {
// widget.onTopBarColorSchemeChange(TopBarThemeType.light);
// }
// },
// ),
// Spacing.width(12),
// Text(
// "Light",
// style: AppTheme.getTextStyle(themeData.textTheme.bodyText2,
// color: rightBarTheme.onBackground),
// )
// ],
// ),
// Spacing.height(8),
// Row(
// children: [
// CustomSwitch.small(
// value: widget.topBarThemeType == TopBarThemeType.dark,
// inactiveTrackColor: rightBarTheme.disabled,
// activeBorderColor: rightBarTheme.activeSwitchBorderColor,
// inactiveBorderColor: rightBarTheme.inactiveSwitchBorderColor,
// activeTrackColor: rightBarTheme.primary,
// inactiveThumbColor: rightBarTheme.onDisabled,
// activeThumbColor: rightBarTheme.onPrimary,
// onChanged: (value) {
// if (value && widget.onTopBarColorSchemeChange != null) {
// widget.onTopBarColorSchemeChange(TopBarThemeType.dark);
// }
// },
// ),
// Spacing.width(12),
// Text(
// "Dark",
// style: AppTheme.getTextStyle(themeData.textTheme.bodyText2,
// color: rightBarTheme.onBackground),
// )
// ],
// ),
// Spacing.height(36),
// Text("Right Bar",
// style: AppTheme.getTextStyle(themeData.textTheme.bodyText1,
// color: rightBarTheme.onBackground, fontWeight: 600)),
// Divider(),
// Spacing.height(8),
// Row(
// children: [
// CustomSwitch.small(
// value: widget.rightBarThemeType == RightBarThemeType.light,
// inactiveTrackColor: rightBarTheme.disabled,
// activeBorderColor: rightBarTheme.activeSwitchBorderColor,
// inactiveBorderColor: rightBarTheme.inactiveSwitchBorderColor,
// activeTrackColor: rightBarTheme.primary,
// inactiveThumbColor: rightBarTheme.onDisabled,
// activeThumbColor: rightBarTheme.onPrimary,
// onChanged: (value) {
// if (value && widget.onRightBarColorSchemeChange != null) {
// widget.onRightBarColorSchemeChange(RightBarThemeType.light);
// }
// },
// ),
// Spacing.width(12),
// Text(
// "Light",
// style: AppTheme.getTextStyle(themeData.textTheme.bodyText2,
// color: rightBarTheme.onBackground),
// )
// ],
// ),
// Spacing.height(8),
// Row(
// children: [
// CustomSwitch.small(
// value: widget.rightBarThemeType == RightBarThemeType.dark,
// inactiveTrackColor: rightBarTheme.disabled,
// activeBorderColor: rightBarTheme.activeSwitchBorderColor,
// inactiveBorderColor: rightBarTheme.inactiveSwitchBorderColor,
// activeTrackColor: rightBarTheme.primary,
// inactiveThumbColor: rightBarTheme.onDisabled,
// activeThumbColor: rightBarTheme.onPrimary,
// onChanged: (value) {
// if (value && widget.onRightBarColorSchemeChange != null) {
// widget.onRightBarColorSchemeChange(RightBarThemeType.dark);
// }
// },
// ),
// Spacing.width(12),
// Text(
// "Dark",
// style: AppTheme.getTextStyle(themeData.textTheme.bodyText2,
// color: rightBarTheme.onBackground),
// )
// ],
// ),
],
),
))

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

View File

@ -8,6 +8,7 @@ import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/images.dart';
import 'package:marco/controller/tenant/tenant_selection_controller.dart';
import 'package:marco/view/splash_screen.dart';
import 'package:marco/helpers/widgets/wave_background.dart';
class TenantSelectionScreen extends StatefulWidget {
const TenantSelectionScreen({super.key});
@ -63,7 +64,7 @@ class _TenantSelectionScreenState extends State<TenantSelectionScreen>
return Scaffold(
body: Stack(
children: [
_RedWaveBackground(brandRed: contentTheme.brandRed),
RedWaveBackground(brandRed: contentTheme.primary),
SafeArea(
child: Center(
child: Column(
@ -188,12 +189,12 @@ class _BetaBadge extends StatelessWidget {
}
/// Tenant Card List
class TenantCardList extends StatelessWidget {
class TenantCardList extends StatelessWidget with UIMixin {
final TenantSelectionController controller;
final bool isLoading;
final Function(String tenantId) onTenantSelected;
const TenantCardList({
TenantCardList({
required this.controller,
required this.isLoading,
required this.onTenantSelected,
@ -220,24 +221,23 @@ class TenantCardList extends StatelessWidget {
),
const SizedBox(height: 16),
],
if (hasTenants) ...controller.tenants.map(
(tenant) => _TenantCard(
tenant: tenant,
onTap: () => onTenantSelected(tenant.id),
if (hasTenants)
...controller.tenants.map(
(tenant) => _TenantCard(
tenant: tenant,
onTap: () => onTenantSelected(tenant.id),
),
),
),
const SizedBox(height: 16),
TextButton.icon(
onPressed: () async {
await LocalStorage.logout();
},
icon: const Icon(Icons.arrow_back, size: 20, color: Colors.redAccent),
icon:
Icon(Icons.arrow_back, size: 20, color: contentTheme.primary,),
label: MyText(
'Back to Login',
color: Colors.red,
color: contentTheme.primary,
fontWeight: 600,
fontSize: 14,
),
@ -249,10 +249,10 @@ class TenantCardList extends StatelessWidget {
}
/// Single Tenant Card
class _TenantCard extends StatelessWidget {
class _TenantCard extends StatelessWidget with UIMixin {
final dynamic tenant;
final VoidCallback onTap;
const _TenantCard({required this.tenant, required this.onTap});
_TenantCard({required this.tenant, required this.onTap});
@override
Widget build(BuildContext context) {
@ -297,7 +297,7 @@ class _TenantCard extends StatelessWidget {
],
),
),
const Icon(Icons.arrow_forward_ios, size: 24, color: Colors.red),
Icon(Icons.arrow_forward_ios, size: 24, color: contentTheme.primary,),
],
),
),
@ -335,55 +335,3 @@ class TenantLogo extends StatelessWidget {
}
}
}
/// Red Wave Background
class _RedWaveBackground extends StatelessWidget {
final Color brandRed;
const _RedWaveBackground({required this.brandRed});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _WavePainter(brandRed),
size: Size.infinite,
);
}
}
class _WavePainter extends CustomPainter {
final Color brandRed;
_WavePainter(this.brandRed);
@override
void paint(Canvas canvas, Size size) {
final paint1 = Paint()
..shader = LinearGradient(
colors: [brandRed, const Color.fromARGB(255, 97, 22, 22)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
final path1 = Path()
..moveTo(0, size.height * 0.2)
..quadraticBezierTo(
size.width * 0.25, size.height * 0.05, size.width * 0.5, size.height * 0.15)
..quadraticBezierTo(
size.width * 0.75, size.height * 0.25, size.width, size.height * 0.1)
..lineTo(size.width, 0)
..lineTo(0, 0)
..close();
canvas.drawPath(path1, paint1);
final paint2 = Paint()..color = Colors.redAccent.withOpacity(0.15);
final path2 = Path()
..moveTo(0, size.height * 0.25)
..quadraticBezierTo(size.width * 0.4, size.height * 0.1, size.width, size.height * 0.2)
..lineTo(size.width, 0)
..lineTo(0, 0)
..close();
canvas.drawPath(path2, paint2);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}