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