added splash screen
This commit is contained in:
parent
041b62ca2f
commit
d5a8d08e63
@ -14,6 +14,7 @@ class LoginController extends MyController {
|
|||||||
final RxBool isLoading = false.obs;
|
final RxBool isLoading = false.obs;
|
||||||
final RxBool showPassword = false.obs;
|
final RxBool showPassword = false.obs;
|
||||||
final RxBool isChecked = false.obs;
|
final RxBool isChecked = false.obs;
|
||||||
|
final RxBool showSplash = false.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@ -40,18 +41,14 @@ class LoginController extends MyController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onChangeCheckBox(bool? value) {
|
void onChangeCheckBox(bool? value) => isChecked.value = value ?? false;
|
||||||
isChecked.value = value ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void onChangeShowPassword() {
|
void onChangeShowPassword() => showPassword.toggle();
|
||||||
showPassword.toggle();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> onLogin() async {
|
Future<void> onLogin() async {
|
||||||
if (!basicValidator.validateForm()) return;
|
if (!basicValidator.validateForm()) return;
|
||||||
|
|
||||||
isLoading.value = true;
|
showSplash.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final loginData = basicValidator.getData();
|
final loginData = basicValidator.getData();
|
||||||
@ -60,39 +57,30 @@ class LoginController extends MyController {
|
|||||||
final errors = await AuthService.loginUser(loginData);
|
final errors = await AuthService.loginUser(loginData);
|
||||||
|
|
||||||
if (errors != null) {
|
if (errors != null) {
|
||||||
logSafe(
|
|
||||||
"Login failed for user: ${loginData['username']} with errors: $errors",
|
|
||||||
level: LogLevel.warning);
|
|
||||||
|
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Login Failed",
|
title: "Login Failed",
|
||||||
message: "Username or password is incorrect",
|
message: "Username or password is incorrect",
|
||||||
type: SnackbarType.error,
|
type: SnackbarType.error,
|
||||||
);
|
);
|
||||||
|
|
||||||
basicValidator.addErrors(errors);
|
basicValidator.addErrors(errors);
|
||||||
basicValidator.validateForm();
|
basicValidator.validateForm();
|
||||||
basicValidator.clearErrors();
|
basicValidator.clearErrors();
|
||||||
} else {
|
} else {
|
||||||
await _handleRememberMe();
|
await _handleRememberMe();
|
||||||
// ✅ Enable remote logging after successful login
|
|
||||||
enableRemoteLogging();
|
enableRemoteLogging();
|
||||||
logSafe("✅ Remote logging enabled after login.");
|
|
||||||
|
|
||||||
logSafe("Login successful for user: ${loginData['username']}");
|
logSafe("Login successful for user: ${loginData['username']}");
|
||||||
|
Get.offNamed('/select-tenant');
|
||||||
Get.toNamed('/select-tenant');
|
|
||||||
}
|
}
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
logSafe("Exception during login",
|
|
||||||
level: LogLevel.error, error: e, stackTrace: stacktrace);
|
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Login Error",
|
title: "Login Error",
|
||||||
message: "An unexpected error occurred",
|
message: "An unexpected error occurred",
|
||||||
type: SnackbarType.error,
|
type: SnackbarType.error,
|
||||||
);
|
);
|
||||||
|
logSafe("Exception during login",
|
||||||
|
level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
showSplash.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,11 +112,7 @@ class LoginController extends MyController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void goToForgotPassword() {
|
void goToForgotPassword() => Get.toNamed('/auth/forgot_password');
|
||||||
Get.toNamed('/auth/forgot_password');
|
|
||||||
}
|
|
||||||
|
|
||||||
void gotoRegister() {
|
void gotoRegister() => Get.offAndToNamed('/auth/register_account');
|
||||||
Get.offAndToNamed('/auth/register_account');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -9,19 +9,28 @@ import 'package:marco/controller/permission_controller.dart';
|
|||||||
class TenantSelectionController extends GetxController {
|
class TenantSelectionController extends GetxController {
|
||||||
final TenantService _tenantService = TenantService();
|
final TenantService _tenantService = TenantService();
|
||||||
|
|
||||||
|
// Tenant list
|
||||||
final tenants = <Tenant>[].obs;
|
final tenants = <Tenant>[].obs;
|
||||||
|
|
||||||
|
// Loading state
|
||||||
final isLoading = false.obs;
|
final isLoading = false.obs;
|
||||||
|
|
||||||
|
// Selected tenant ID
|
||||||
final selectedTenantId = RxnString();
|
final selectedTenantId = RxnString();
|
||||||
|
|
||||||
|
// Flag to indicate auto-selection (for splash screen)
|
||||||
|
final isAutoSelecting = false.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
loadTenants();
|
loadTenants();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load tenants and perform smart auto-selection
|
/// Load tenants and handle auto-selection
|
||||||
Future<void> loadTenants() async {
|
Future<void> loadTenants() async {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
isAutoSelecting.value = true; // show splash during auto-selection
|
||||||
try {
|
try {
|
||||||
final data = await _tenantService.getTenants();
|
final data = await _tenantService.getTenants();
|
||||||
if (data == null || data.isEmpty) {
|
if (data == null || data.isEmpty) {
|
||||||
@ -34,21 +43,23 @@ class TenantSelectionController extends GetxController {
|
|||||||
|
|
||||||
final recentTenantId = LocalStorage.getRecentTenantId();
|
final recentTenantId = LocalStorage.getRecentTenantId();
|
||||||
|
|
||||||
|
// Auto-select if only one tenant
|
||||||
if (tenants.length == 1) {
|
if (tenants.length == 1) {
|
||||||
await _selectTenant(tenants.first.id);
|
await _selectTenant(tenants.first.id);
|
||||||
} else if (recentTenantId != null) {
|
}
|
||||||
final recentTenant = tenants
|
// Auto-select recent tenant if available
|
||||||
.firstWhereOrNull((t) => t.id == recentTenantId);
|
else if (recentTenantId != null) {
|
||||||
|
final recentTenant =
|
||||||
|
tenants.firstWhereOrNull((t) => t.id == recentTenantId);
|
||||||
if (recentTenant != null) {
|
if (recentTenant != null) {
|
||||||
await _selectTenant(recentTenant.id);
|
await _selectTenant(recentTenant.id);
|
||||||
} else {
|
} else {
|
||||||
selectedTenantId.value = null;
|
_clearSelection();
|
||||||
TenantService.currentTenant = null;
|
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
selectedTenantId.value = null;
|
// No auto-selection
|
||||||
TenantService.currentTenant = null;
|
else {
|
||||||
|
_clearSelection();
|
||||||
}
|
}
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
logSafe("❌ Exception in loadTenants",
|
logSafe("❌ Exception in loadTenants",
|
||||||
@ -60,22 +71,24 @@ class TenantSelectionController extends GetxController {
|
|||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
isAutoSelecting.value = false; // hide splash
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manually select tenant (user triggered)
|
/// User manually selects a tenant
|
||||||
Future<void> onTenantSelected(String tenantId) async {
|
Future<void> onTenantSelected(String tenantId) async {
|
||||||
|
isAutoSelecting.value = true;
|
||||||
await _selectTenant(tenantId);
|
await _selectTenant(tenantId);
|
||||||
|
isAutoSelecting.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Internal tenant selection logic
|
/// Internal tenant selection logic
|
||||||
Future<void> _selectTenant(String tenantId) async {
|
Future<void> _selectTenant(String tenantId) async {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
final success = await _tenantService.selectTenant(tenantId);
|
final success = await _tenantService.selectTenant(tenantId);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
logSafe("❌ Tenant selection failed for: $tenantId",
|
|
||||||
level: LogLevel.warning);
|
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: "Unable to select organization. Please try again.",
|
message: "Unable to select organization. Please try again.",
|
||||||
@ -84,39 +97,27 @@ class TenantSelectionController extends GetxController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update tenant & persist
|
||||||
final selectedTenant = tenants.firstWhere((t) => t.id == tenantId);
|
final selectedTenant = tenants.firstWhere((t) => t.id == tenantId);
|
||||||
TenantService.setSelectedTenant(selectedTenant);
|
TenantService.setSelectedTenant(selectedTenant);
|
||||||
selectedTenantId.value = tenantId;
|
selectedTenantId.value = tenantId;
|
||||||
|
|
||||||
// Persist recent tenant
|
|
||||||
await LocalStorage.setRecentTenantId(tenantId);
|
await LocalStorage.setRecentTenantId(tenantId);
|
||||||
|
|
||||||
logSafe("✅ Tenant selection successful: $tenantId");
|
// Load permissions if token exists
|
||||||
|
|
||||||
// 🔹 Load permissions after tenant selection (null-safe)
|
|
||||||
final token = LocalStorage.getJwtToken();
|
final token = LocalStorage.getJwtToken();
|
||||||
if (token != null && token.isNotEmpty) {
|
if (token != null && token.isNotEmpty) {
|
||||||
if (!Get.isRegistered<PermissionController>()) {
|
if (!Get.isRegistered<PermissionController>()) {
|
||||||
Get.put(PermissionController());
|
Get.put(PermissionController());
|
||||||
logSafe("✅ PermissionController injected after tenant selection.");
|
|
||||||
}
|
}
|
||||||
await Get.find<PermissionController>().loadData(token);
|
await Get.find<PermissionController>().loadData(token);
|
||||||
} else {
|
|
||||||
logSafe("⚠️ JWT token is null. Cannot load permissions.",
|
|
||||||
level: LogLevel.warning);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to dashboard
|
// Navigate **before changing isAutoSelecting**
|
||||||
Get.offAllNamed('/dashboard');
|
await Get.offAllNamed('/dashboard');
|
||||||
|
|
||||||
showAppSnackbar(
|
// Then hide splash
|
||||||
title: "Success",
|
isAutoSelecting.value = false;
|
||||||
message: "Organization selected successfully.",
|
} catch (e) {
|
||||||
type: SnackbarType.success,
|
|
||||||
);
|
|
||||||
} catch (e, st) {
|
|
||||||
logSafe("❌ Exception in _selectTenant",
|
|
||||||
level: LogLevel.error, error: e, stackTrace: st);
|
|
||||||
showAppSnackbar(
|
showAppSnackbar(
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: "An unexpected error occurred while selecting organization.",
|
message: "An unexpected error occurred while selecting organization.",
|
||||||
@ -126,4 +127,10 @@ class TenantSelectionController extends GetxController {
|
|||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clear tenant selection
|
||||||
|
void _clearSelection() {
|
||||||
|
selectedTenantId.value = null;
|
||||||
|
TenantService.currentTenant = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,20 +123,24 @@ class _EmailLoginFormState extends State<EmailLoginForm> with UIMixin {
|
|||||||
),
|
),
|
||||||
MySpacing.height(28),
|
MySpacing.height(28),
|
||||||
Center(
|
Center(
|
||||||
child: MyButton.rounded(
|
child: Obx(() {
|
||||||
onPressed: controller.onLogin,
|
final isLoading = controller.isLoading.value;
|
||||||
|
return MyButton.rounded(
|
||||||
|
onPressed: isLoading
|
||||||
|
? null
|
||||||
|
: controller.onLogin,
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
padding: MySpacing.xy(80, 16),
|
padding: MySpacing.xy(80, 16),
|
||||||
borderRadiusAll: 10,
|
borderRadiusAll: 10,
|
||||||
backgroundColor: contentTheme.brandRed,
|
backgroundColor: contentTheme.brandRed,
|
||||||
child: MyText.labelLarge(
|
child: MyText.labelLarge(
|
||||||
'Login',
|
isLoading ? 'Logging in...' : 'Login',
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: contentTheme.onPrimary,
|
color: contentTheme.onPrimary,
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -37,7 +37,7 @@ class _LoginScreenState extends State<LoginScreen> with UIMixin {
|
|||||||
builder: (_) {
|
builder: (_) {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
if (controller.isLoading.value) {
|
if (controller.isLoading.value) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: LinearProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
return Form(
|
return Form(
|
||||||
|
120
lib/view/splash_screen.dart
Normal file
120
lib/view/splash_screen.dart
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:marco/images.dart';
|
||||||
|
|
||||||
|
class SplashScreen extends StatefulWidget {
|
||||||
|
final String? message;
|
||||||
|
final double? logoSize;
|
||||||
|
final Color? backgroundColor;
|
||||||
|
|
||||||
|
const SplashScreen({
|
||||||
|
super.key,
|
||||||
|
this.message,
|
||||||
|
this.logoSize = 120,
|
||||||
|
this.backgroundColor = Colors.white,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SplashScreen> createState() => _SplashScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SplashScreenState extends State<SplashScreen>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<double> _animation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_controller = AnimationController(
|
||||||
|
duration: const Duration(seconds: 1),
|
||||||
|
vsync: this,
|
||||||
|
)..repeat(reverse: true);
|
||||||
|
|
||||||
|
_animation = Tween<double>(begin: 0.0, end: 8.0).animate(
|
||||||
|
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAnimatedDots() {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: List.generate(3, (index) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _animation,
|
||||||
|
builder: (context, child) {
|
||||||
|
double opacity;
|
||||||
|
if (index == 0) {
|
||||||
|
opacity = (0.3 + _animation.value / 8).clamp(0.0, 1.0);
|
||||||
|
} else if (index == 1) {
|
||||||
|
opacity = (0.3 + (_animation.value / 8)).clamp(0.0, 1.0);
|
||||||
|
} else {
|
||||||
|
opacity = (0.3 + (1 - _animation.value / 8)).clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blueAccent.withOpacity(opacity),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: widget.backgroundColor,
|
||||||
|
body: SafeArea(
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// Logo with slight bounce animation
|
||||||
|
ScaleTransition(
|
||||||
|
scale: Tween(begin: 0.8, end: 1.0).animate(
|
||||||
|
CurvedAnimation(
|
||||||
|
parent: _controller,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
width: widget.logoSize,
|
||||||
|
height: widget.logoSize,
|
||||||
|
child: Image.asset(Images.logoDark),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
// Text message
|
||||||
|
if (widget.message != null)
|
||||||
|
Text(
|
||||||
|
widget.message!,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
// Animated loading dots
|
||||||
|
_buildAnimatedDots(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@ import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
|||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
import 'package:marco/images.dart';
|
import 'package:marco/images.dart';
|
||||||
import 'package:marco/controller/tenant/tenant_selection_controller.dart';
|
import 'package:marco/controller/tenant/tenant_selection_controller.dart';
|
||||||
|
import 'package:marco/view/splash_screen.dart';
|
||||||
|
|
||||||
class TenantSelectionScreen extends StatefulWidget {
|
class TenantSelectionScreen extends StatefulWidget {
|
||||||
const TenantSelectionScreen({super.key});
|
const TenantSelectionScreen({super.key});
|
||||||
@ -20,25 +21,23 @@ class _TenantSelectionScreenState extends State<TenantSelectionScreen>
|
|||||||
late final AnimationController _logoAnimController;
|
late final AnimationController _logoAnimController;
|
||||||
late final Animation<double> _logoAnimation;
|
late final Animation<double> _logoAnimation;
|
||||||
final bool _isBetaEnvironment = ApiEndpoints.baseUrl.contains("stage");
|
final bool _isBetaEnvironment = ApiEndpoints.baseUrl.contains("stage");
|
||||||
bool _isLoading = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_controller =
|
_controller = Get.put(TenantSelectionController());
|
||||||
Get.put(TenantSelectionController());
|
|
||||||
_logoAnimController = AnimationController(
|
_logoAnimController = AnimationController(
|
||||||
vsync: this,
|
vsync: this,
|
||||||
duration: const Duration(milliseconds: 800),
|
duration: const Duration(milliseconds: 800),
|
||||||
);
|
);
|
||||||
|
|
||||||
_logoAnimation = CurvedAnimation(
|
_logoAnimation = CurvedAnimation(
|
||||||
parent: _logoAnimController,
|
parent: _logoAnimController,
|
||||||
curve: Curves.easeOutBack,
|
curve: Curves.easeOutBack,
|
||||||
);
|
);
|
||||||
_logoAnimController.forward();
|
|
||||||
|
|
||||||
// 🔥 Tell controller this is tenant selection screen
|
_logoAnimController.forward();
|
||||||
_controller.loadTenants();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -49,13 +48,17 @@ class _TenantSelectionScreenState extends State<TenantSelectionScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onTenantSelected(String tenantId) async {
|
Future<void> _onTenantSelected(String tenantId) async {
|
||||||
setState(() => _isLoading = true);
|
|
||||||
await _controller.onTenantSelected(tenantId);
|
await _controller.onTenantSelected(tenantId);
|
||||||
setState(() => _isLoading = false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
return Obx(() {
|
||||||
|
// Splash screen for auto-selection
|
||||||
|
if (_controller.isAutoSelecting.value) {
|
||||||
|
return const SplashScreen();
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
@ -82,10 +85,9 @@ class _TenantSelectionScreenState extends State<TenantSelectionScreen>
|
|||||||
const _BetaBadge(),
|
const _BetaBadge(),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 36),
|
const SizedBox(height: 36),
|
||||||
// Tenant list directly reacts to controller
|
|
||||||
TenantCardList(
|
TenantCardList(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
isLoading: _isLoading,
|
isLoading: _controller.isLoading.value,
|
||||||
onTenantSelected: _onTenantSelected,
|
onTenantSelected: _onTenantSelected,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -101,9 +103,11 @@ class _TenantSelectionScreenState extends State<TenantSelectionScreen>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Animated Logo Widget
|
||||||
class _AnimatedLogo extends StatelessWidget {
|
class _AnimatedLogo extends StatelessWidget {
|
||||||
final Animation<double> animation;
|
final Animation<double> animation;
|
||||||
const _AnimatedLogo({required this.animation});
|
const _AnimatedLogo({required this.animation});
|
||||||
@ -133,6 +137,7 @@ class _AnimatedLogo extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Welcome Texts
|
||||||
class _WelcomeTexts extends StatelessWidget {
|
class _WelcomeTexts extends StatelessWidget {
|
||||||
const _WelcomeTexts();
|
const _WelcomeTexts();
|
||||||
|
|
||||||
@ -149,7 +154,7 @@ class _WelcomeTexts extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
MyText(
|
MyText(
|
||||||
"Please select which dashboard you want to explore!.",
|
"Please select which dashboard you want to explore!",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Colors.black54,
|
color: Colors.black54,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
@ -159,6 +164,7 @@ class _WelcomeTexts extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Beta Badge
|
||||||
class _BetaBadge extends StatelessWidget {
|
class _BetaBadge extends StatelessWidget {
|
||||||
const _BetaBadge();
|
const _BetaBadge();
|
||||||
|
|
||||||
@ -180,6 +186,7 @@ class _BetaBadge extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tenant Card List
|
||||||
class TenantCardList extends StatelessWidget {
|
class TenantCardList extends StatelessWidget {
|
||||||
final TenantSelectionController controller;
|
final TenantSelectionController controller;
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
@ -195,10 +202,9 @@ class TenantCardList extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
if (controller.isLoading.value || isLoading) {
|
if (controller.isLoading.value || isLoading) {
|
||||||
return const Center(
|
return const Center(child: CircularProgressIndicator(strokeWidth: 2));
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (controller.tenants.isEmpty) {
|
if (controller.tenants.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
child: MyText(
|
child: MyText(
|
||||||
@ -209,10 +215,8 @@ class TenantCardList extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Show tenant even if only 1 tenant
|
|
||||||
return SingleChildScrollView(
|
return Column(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 24),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
...controller.tenants.map(
|
...controller.tenants.map(
|
||||||
@ -224,8 +228,8 @@ class TenantCardList extends StatelessWidget {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () => Get.back(),
|
onPressed: () => Get.back(),
|
||||||
icon: const Icon(Icons.arrow_back,
|
icon:
|
||||||
size: 20, color: Colors.redAccent),
|
const Icon(Icons.arrow_back, size: 20, color: Colors.redAccent),
|
||||||
label: MyText(
|
label: MyText(
|
||||||
'Back to Login',
|
'Back to Login',
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
@ -234,12 +238,12 @@ class TenantCardList extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Single Tenant Card
|
||||||
class _TenantCard extends StatelessWidget {
|
class _TenantCard extends StatelessWidget {
|
||||||
final dynamic tenant;
|
final dynamic tenant;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
@ -252,9 +256,7 @@ class _TenantCard extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(5),
|
||||||
child: Card(
|
child: Card(
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
||||||
borderRadius: BorderRadius.circular(5),
|
|
||||||
),
|
|
||||||
margin: const EdgeInsets.only(bottom: 20),
|
margin: const EdgeInsets.only(bottom: 20),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
@ -290,11 +292,7 @@ class _TenantCard extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Icon(
|
const Icon(Icons.arrow_forward_ios, size: 24, color: Colors.red),
|
||||||
Icons.arrow_forward_ios,
|
|
||||||
size: 24,
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -303,6 +301,7 @@ class _TenantCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tenant Logo (supports base64 and URL)
|
||||||
class TenantLogo extends StatelessWidget {
|
class TenantLogo extends StatelessWidget {
|
||||||
final String? logoImage;
|
final String? logoImage;
|
||||||
const TenantLogo({required this.logoImage});
|
const TenantLogo({required this.logoImage});
|
||||||
@ -310,9 +309,7 @@ class TenantLogo extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (logoImage == null || logoImage!.isEmpty) {
|
if (logoImage == null || logoImage!.isEmpty) {
|
||||||
return Center(
|
return Center(child: Icon(Icons.business, color: Colors.grey.shade600));
|
||||||
child: Icon(Icons.business, color: Colors.grey.shade600),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (logoImage!.startsWith("data:image")) {
|
if (logoImage!.startsWith("data:image")) {
|
||||||
try {
|
try {
|
||||||
@ -320,9 +317,7 @@ class TenantLogo extends StatelessWidget {
|
|||||||
final bytes = base64Decode(base64Str);
|
final bytes = base64Decode(base64Str);
|
||||||
return Image.memory(bytes, fit: BoxFit.cover);
|
return Image.memory(bytes, fit: BoxFit.cover);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return Center(
|
return Center(child: Icon(Icons.business, color: Colors.grey.shade600));
|
||||||
child: Icon(Icons.business, color: Colors.grey.shade600),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Image.network(
|
return Image.network(
|
||||||
@ -336,6 +331,7 @@ class TenantLogo extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Red Wave Background
|
||||||
class _RedWaveBackground extends StatelessWidget {
|
class _RedWaveBackground extends StatelessWidget {
|
||||||
final Color brandRed;
|
final Color brandRed;
|
||||||
const _RedWaveBackground({required this.brandRed});
|
const _RedWaveBackground({required this.brandRed});
|
||||||
@ -351,7 +347,6 @@ class _RedWaveBackground extends StatelessWidget {
|
|||||||
|
|
||||||
class _WavePainter extends CustomPainter {
|
class _WavePainter extends CustomPainter {
|
||||||
final Color brandRed;
|
final Color brandRed;
|
||||||
|
|
||||||
_WavePainter(this.brandRed);
|
_WavePainter(this.brandRed);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
Loading…
x
Reference in New Issue
Block a user