modern
This commit is contained in:
492
base_project/lib/BuilderField/shared/ui/entity_card.dart
Normal file
492
base_project/lib/BuilderField/shared/ui/entity_card.dart
Normal file
@@ -0,0 +1,492 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../../core/constants/ui_constants.dart';
|
||||
import '../../../core/providers/dynamic_theme_provider.dart';
|
||||
|
||||
/// Reusable card component for displaying entity data
|
||||
/// Uses dynamic theme and provides consistent styling across all entities
|
||||
class EntityCard extends StatelessWidget {
|
||||
final Map<String, dynamic> entity;
|
||||
final Function(Map<String, dynamic>) onEdit;
|
||||
final Function(Map<String, dynamic>) onDelete;
|
||||
final Function(Map<String, dynamic>)? onTap;
|
||||
final List<Map<String, dynamic>> displayFields;
|
||||
|
||||
const EntityCard({
|
||||
super.key,
|
||||
required this.entity,
|
||||
required this.onEdit,
|
||||
required this.onDelete,
|
||||
this.onTap,
|
||||
this.displayFields = const [],
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<DynamicThemeProvider>(
|
||||
builder: (context, dynamicThemeProvider, child) {
|
||||
final colorScheme = dynamicThemeProvider.getCurrentColorScheme(
|
||||
Theme.of(context).brightness == Brightness.dark,
|
||||
);
|
||||
final size = MediaQuery.of(context).size;
|
||||
final isMobile = size.width < 600;
|
||||
final isTablet = size.width >= 600 && size.width < 1024;
|
||||
final double maxCardHeight = isMobile
|
||||
? size.height * 0.34
|
||||
: isTablet
|
||||
? size.height * 0.38
|
||||
: size.height * 0.42;
|
||||
final double horizontalPadding = isMobile
|
||||
? UIConstants.spacing16
|
||||
: isTablet
|
||||
? UIConstants.spacing20
|
||||
: UIConstants.spacing24;
|
||||
final double verticalPadding = isMobile
|
||||
? UIConstants.spacing16
|
||||
: isTablet
|
||||
? UIConstants.spacing16
|
||||
: UIConstants.spacing20;
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: 160,
|
||||
maxHeight: maxCardHeight.clamp(160.0, size.height * 0.8),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
colorScheme.surface,
|
||||
colorScheme.surface.withOpacity(0.95),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius20),
|
||||
border: Border.all(
|
||||
color: colorScheme.primary.withOpacity(0.1),
|
||||
width: 1.5,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.primary.withOpacity(0.08),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
spreadRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap != null ? () => onTap!(entity) : null,
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius20),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: horizontalPadding,
|
||||
vertical: verticalPadding,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header with avatar and action buttons
|
||||
Row(
|
||||
children: [
|
||||
_buildAvatar(colorScheme,
|
||||
isMobile: isMobile, isTablet: isTablet),
|
||||
const Spacer(),
|
||||
_buildActionButtons(colorScheme),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing16),
|
||||
|
||||
// Dynamic field display (scrollable to avoid overflow)
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildFieldDisplays(colorScheme),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing12),
|
||||
// Status indicator pinned at bottom
|
||||
_buildStatusIndicator(colorScheme),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAvatar(ColorScheme colorScheme,
|
||||
{required bool isMobile, required bool isTablet}) {
|
||||
final double side = isMobile
|
||||
? 44
|
||||
: isTablet
|
||||
? 50
|
||||
: 56;
|
||||
final double icon = isMobile
|
||||
? 20
|
||||
: isTablet
|
||||
? 22
|
||||
: 24;
|
||||
return Container(
|
||||
width: side,
|
||||
height: side,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
colorScheme.primary,
|
||||
colorScheme.primary.withOpacity(0.8),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.primary.withOpacity(0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
Icons.person_rounded,
|
||||
color: colorScheme.onPrimary,
|
||||
size: icon,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons(ColorScheme colorScheme) {
|
||||
return Row(
|
||||
children: [
|
||||
// Edit Button
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primaryContainer.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius8),
|
||||
border: Border.all(
|
||||
color: colorScheme.primary.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () => onEdit(entity),
|
||||
icon: Icon(
|
||||
Icons.edit_rounded,
|
||||
color: colorScheme.primary,
|
||||
size: 18,
|
||||
),
|
||||
tooltip: 'Edit',
|
||||
padding: const EdgeInsets.all(8),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UIConstants.spacing8),
|
||||
// Delete Button
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.errorContainer.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius8),
|
||||
border: Border.all(
|
||||
color: colorScheme.error.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () => onDelete(entity),
|
||||
icon: Icon(
|
||||
Icons.delete_rounded,
|
||||
color: colorScheme.error,
|
||||
size: 18,
|
||||
),
|
||||
tooltip: 'Delete',
|
||||
padding: const EdgeInsets.all(8),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 32,
|
||||
minHeight: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildFieldDisplays(ColorScheme colorScheme) {
|
||||
// Dynamic field display based on entity data
|
||||
final displayFields = _getDisplayFields();
|
||||
|
||||
return displayFields.map((field) {
|
||||
final value = entity[field['key']]?.toString() ?? '';
|
||||
if (value.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: UIConstants.spacing8),
|
||||
child: _buildFieldDisplay(field, value, colorScheme),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Widget _buildFieldDisplay(
|
||||
Map<String, dynamic> field, String value, ColorScheme colorScheme) {
|
||||
switch (field['type']) {
|
||||
case 'phone':
|
||||
return _buildPhoneDisplay(value, colorScheme);
|
||||
case 'email':
|
||||
return _buildEmailDisplay(value, colorScheme);
|
||||
case 'number':
|
||||
return _buildNumberDisplay(field['label'], value, colorScheme);
|
||||
default:
|
||||
return _buildTextDisplay(field['label'], value, colorScheme);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildPhoneDisplay(String value, ColorScheme colorScheme) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UIConstants.spacing12,
|
||||
vertical: UIConstants.spacing8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primaryContainer.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius8),
|
||||
border: Border.all(
|
||||
color: colorScheme.primary.withOpacity(0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.phone_rounded,
|
||||
size: 16,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: UIConstants.spacing8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface.withOpacity(0.8),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmailDisplay(String value, ColorScheme colorScheme) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UIConstants.spacing12,
|
||||
vertical: UIConstants.spacing8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.secondaryContainer.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius8),
|
||||
border: Border.all(
|
||||
color: colorScheme.secondary.withOpacity(0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.email_rounded,
|
||||
size: 16,
|
||||
color: colorScheme.secondary,
|
||||
),
|
||||
const SizedBox(width: UIConstants.spacing8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface.withOpacity(0.8),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextDisplay(
|
||||
String label, String value, ColorScheme colorScheme) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (label.isNotEmpty) ...[
|
||||
const SizedBox(height: UIConstants.spacing4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface.withOpacity(0.7),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNumberDisplay(
|
||||
String label, String value, ColorScheme colorScheme) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UIConstants.spacing12,
|
||||
vertical: UIConstants.spacing8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.tertiaryContainer.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius8),
|
||||
border: Border.all(
|
||||
color: colorScheme.tertiary.withOpacity(0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.numbers_rounded,
|
||||
size: 16,
|
||||
color: colorScheme.tertiary,
|
||||
),
|
||||
const SizedBox(width: UIConstants.spacing8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface.withOpacity(0.8),
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusIndicator(ColorScheme colorScheme) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UIConstants.spacing8,
|
||||
vertical: UIConstants.spacing4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UIConstants.spacing6),
|
||||
Text(
|
||||
'Active',
|
||||
style: TextStyle(
|
||||
color: colorScheme.primary,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> _getDisplayFields() {
|
||||
// Use provided displayFields or default fields
|
||||
if (displayFields.isNotEmpty) {
|
||||
return displayFields;
|
||||
}
|
||||
|
||||
// Default fields - try to get meaningful fields from entity
|
||||
final defaultFields = <Map<String, dynamic>>[];
|
||||
|
||||
// Try to find primary fields
|
||||
final primaryFields = ['name', 'title', 'id'];
|
||||
for (final field in primaryFields) {
|
||||
if (entity.containsKey(field) && entity[field] != null) {
|
||||
defaultFields.add({'key': field, 'label': '', 'type': 'text'});
|
||||
break; // Only add first found primary field
|
||||
}
|
||||
}
|
||||
|
||||
// Add other non-empty fields (max 3 for card display)
|
||||
int addedCount = 0;
|
||||
for (final entry in entity.entries) {
|
||||
if (entry.value != null &&
|
||||
entry.value.toString().isNotEmpty &&
|
||||
!primaryFields.contains(entry.key) &&
|
||||
addedCount < 2) {
|
||||
defaultFields.add({
|
||||
'key': entry.key,
|
||||
'label': _formatFieldLabel(entry.key),
|
||||
'type': _getFieldType(entry.value)
|
||||
});
|
||||
addedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return defaultFields;
|
||||
}
|
||||
|
||||
String _formatFieldLabel(String key) {
|
||||
return key
|
||||
.split('_')
|
||||
.map((word) => word[0].toUpperCase() + word.substring(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
String _getFieldType(dynamic value) {
|
||||
if (value is num) return 'number';
|
||||
if (value.toString().contains('@')) return 'email';
|
||||
if (RegExp(r'^\+?[\d\s\-\(\)]+$').hasMatch(value.toString()))
|
||||
return 'phone';
|
||||
return 'text';
|
||||
}
|
||||
}
|
||||
584
base_project/lib/BuilderField/shared/ui/entity_details.dart
Normal file
584
base_project/lib/BuilderField/shared/ui/entity_details.dart
Normal file
@@ -0,0 +1,584 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../../core/providers/dynamic_theme_provider.dart';
|
||||
import '../../../core/constants/ui_constants.dart';
|
||||
import '../../../shared/widgets/app_bar/modern_app_bar.dart';
|
||||
import '../../../shared/widgets/buttons/modern_button.dart';
|
||||
|
||||
/// Generic details component for displaying entity information
|
||||
/// This component works with any entity type and provides consistent UI/UX
|
||||
class EntityDetails extends StatefulWidget {
|
||||
final Map<String, dynamic> entity;
|
||||
final Function(Map<String, dynamic>) onEdit;
|
||||
final Function(Map<String, dynamic>) onDelete;
|
||||
final String title;
|
||||
final List<Map<String, dynamic>> displayFields;
|
||||
final bool isLoading;
|
||||
|
||||
const EntityDetails({
|
||||
super.key,
|
||||
required this.entity,
|
||||
required this.onEdit,
|
||||
required this.onDelete,
|
||||
required this.title,
|
||||
this.displayFields = const [],
|
||||
this.isLoading = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EntityDetails> createState() => _EntityDetailsState();
|
||||
}
|
||||
|
||||
class _EntityDetailsState extends State<EntityDetails>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _fadeAnimation;
|
||||
late Animation<Offset> _slideAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeAnimations();
|
||||
}
|
||||
|
||||
void _initializeAnimations() {
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_fadeAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.0, 0.6, curve: Curves.easeOut),
|
||||
));
|
||||
|
||||
_slideAnimation = Tween<Offset>(
|
||||
begin: const Offset(0, 0.3),
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.2, 1.0, curve: Curves.easeOutCubic),
|
||||
));
|
||||
|
||||
_animationController.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<DynamicThemeProvider>(
|
||||
builder: (context, dynamicThemeProvider, child) {
|
||||
final colorScheme = dynamicThemeProvider.getCurrentColorScheme(
|
||||
Theme.of(context).brightness == Brightness.dark,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: colorScheme.surface,
|
||||
appBar: ModernAppBar(
|
||||
title: widget.title,
|
||||
showBackButton: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => widget.onEdit(widget.entity),
|
||||
icon: Icon(
|
||||
Icons.edit_rounded,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
tooltip: 'Edit',
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => _showDeleteDialog(colorScheme),
|
||||
icon: Icon(
|
||||
Icons.delete_rounded,
|
||||
color: colorScheme.error,
|
||||
),
|
||||
tooltip: 'Delete',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: widget.isLoading
|
||||
? _buildLoadingState(colorScheme)
|
||||
: _buildContent(colorScheme),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingState(ColorScheme colorScheme) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
color: colorScheme.primary,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing16),
|
||||
Text(
|
||||
'Loading details...',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface.withOpacity(0.7),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(ColorScheme colorScheme) {
|
||||
return SingleChildScrollView(
|
||||
padding: UIConstants.getResponsivePadding(
|
||||
context,
|
||||
mobile: UIConstants.screenPaddingMedium,
|
||||
tablet: UIConstants.screenPaddingLarge,
|
||||
desktop: UIConstants.screenPaddingLarge,
|
||||
),
|
||||
child: AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header Card
|
||||
_buildHeaderCard(colorScheme),
|
||||
|
||||
const SizedBox(height: UIConstants.spacing24),
|
||||
|
||||
// Details Section
|
||||
_buildDetailsSection(colorScheme),
|
||||
|
||||
const SizedBox(height: UIConstants.spacing32),
|
||||
|
||||
// Action Buttons
|
||||
_buildActionButtons(colorScheme),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderCard(ColorScheme colorScheme) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
colorScheme.primary,
|
||||
colorScheme.primary.withOpacity(0.8),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.primary.withOpacity(0.3),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
spreadRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: UIConstants.cardPaddingLarge,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Avatar and Title
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.onPrimary.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius16),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.person_rounded,
|
||||
color: colorScheme.onPrimary,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UIConstants.spacing16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_getPrimaryFieldValue(),
|
||||
style: TextStyle(
|
||||
color: colorScheme.onPrimary,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing4),
|
||||
Text(
|
||||
widget.title,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onPrimary.withOpacity(0.8),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: UIConstants.spacing16),
|
||||
|
||||
// Status Badge
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UIConstants.spacing12,
|
||||
vertical: UIConstants.spacing6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.onPrimary.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.onPrimary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UIConstants.spacing8),
|
||||
Text(
|
||||
'Active',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onPrimary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailsSection(ColorScheme colorScheme) {
|
||||
final displayFields = widget.displayFields.isNotEmpty
|
||||
? widget.displayFields
|
||||
: _getDefaultDisplayFields();
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
colorScheme.surface,
|
||||
colorScheme.surface.withOpacity(0.95),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius20),
|
||||
border: Border.all(
|
||||
color: colorScheme.primary.withOpacity(0.1),
|
||||
width: 1.5,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.primary.withOpacity(0.08),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
spreadRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: UIConstants.cardPaddingLarge,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Details',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing20),
|
||||
|
||||
// Field Details
|
||||
...displayFields
|
||||
.map((field) => _buildFieldDetail(field, colorScheme)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFieldDetail(
|
||||
Map<String, dynamic> field, ColorScheme colorScheme) {
|
||||
final value = widget.entity[field['key']]?.toString() ?? '';
|
||||
if (value.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: UIConstants.spacing16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
field['label'] ?? field['key'],
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface.withOpacity(0.7),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing4),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UIConstants.spacing16,
|
||||
vertical: UIConstants.spacing12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _getFieldBackgroundColor(field['type'], colorScheme),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius12),
|
||||
border: Border.all(
|
||||
color: _getFieldBorderColor(field['type'], colorScheme),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
_getFieldIcon(field['type']),
|
||||
size: 18,
|
||||
color: _getFieldIconColor(field['type'], colorScheme),
|
||||
),
|
||||
const SizedBox(width: UIConstants.spacing12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons(ColorScheme colorScheme) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ModernButton(
|
||||
text: 'Edit',
|
||||
type: ModernButtonType.secondary,
|
||||
size: ModernButtonSize.large,
|
||||
onPressed: () => widget.onEdit(widget.entity),
|
||||
icon: Icon(Icons.edit_rounded),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UIConstants.spacing16),
|
||||
Expanded(
|
||||
child: ModernButton(
|
||||
text: 'Delete',
|
||||
type: ModernButtonType.danger,
|
||||
size: ModernButtonSize.large,
|
||||
onPressed: () => _showDeleteDialog(colorScheme),
|
||||
icon: Icon(Icons.delete_rounded),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteDialog(ColorScheme colorScheme) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: colorScheme.surface,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius20),
|
||||
),
|
||||
title: Text(
|
||||
'Delete ${widget.title}',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
content: Text(
|
||||
'Are you sure you want to delete this ${widget.title.toLowerCase()}? This action cannot be undone.',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(
|
||||
'Cancel',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface.withOpacity(0.7),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
widget.onDelete(widget.entity);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: colorScheme.error,
|
||||
foregroundColor: colorScheme.onError,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius12),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Delete',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getPrimaryFieldValue() {
|
||||
// Try to get the primary field value (usually 'name' or first field)
|
||||
final primaryFields = ['name', 'title', 'id'];
|
||||
for (final field in primaryFields) {
|
||||
final value = widget.entity[field]?.toString();
|
||||
if (value != null && value.isNotEmpty) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// If no primary field found, return first non-empty field
|
||||
for (final entry in widget.entity.entries) {
|
||||
if (entry.value != null && entry.value.toString().isNotEmpty) {
|
||||
return entry.value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> _getDefaultDisplayFields() {
|
||||
return widget.entity.entries
|
||||
.where(
|
||||
(entry) => entry.value != null && entry.value.toString().isNotEmpty)
|
||||
.map((entry) => {
|
||||
'key': entry.key,
|
||||
'label': _formatFieldLabel(entry.key),
|
||||
'type': _getFieldType(entry.value),
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
String _formatFieldLabel(String key) {
|
||||
return key
|
||||
.split('_')
|
||||
.map((word) => word[0].toUpperCase() + word.substring(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
String _getFieldType(dynamic value) {
|
||||
if (value is num) return 'number';
|
||||
if (value.toString().contains('@')) return 'email';
|
||||
if (RegExp(r'^\+?[\d\s\-\(\)]+$').hasMatch(value.toString()))
|
||||
return 'phone';
|
||||
return 'text';
|
||||
}
|
||||
|
||||
Color _getFieldBackgroundColor(String type, ColorScheme colorScheme) {
|
||||
switch (type) {
|
||||
case 'phone':
|
||||
return colorScheme.primaryContainer.withOpacity(0.2);
|
||||
case 'email':
|
||||
return colorScheme.secondaryContainer.withOpacity(0.2);
|
||||
case 'number':
|
||||
return colorScheme.tertiaryContainer.withOpacity(0.2);
|
||||
default:
|
||||
return colorScheme.surfaceVariant.withOpacity(0.3);
|
||||
}
|
||||
}
|
||||
|
||||
Color _getFieldBorderColor(String type, ColorScheme colorScheme) {
|
||||
switch (type) {
|
||||
case 'phone':
|
||||
return colorScheme.primary.withOpacity(0.1);
|
||||
case 'email':
|
||||
return colorScheme.secondary.withOpacity(0.1);
|
||||
case 'number':
|
||||
return colorScheme.tertiary.withOpacity(0.1);
|
||||
default:
|
||||
return colorScheme.outline.withOpacity(0.2);
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getFieldIcon(String type) {
|
||||
switch (type) {
|
||||
case 'phone':
|
||||
return Icons.phone_rounded;
|
||||
case 'email':
|
||||
return Icons.email_rounded;
|
||||
case 'number':
|
||||
return Icons.numbers_rounded;
|
||||
default:
|
||||
return Icons.text_fields_rounded;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getFieldIconColor(String type, ColorScheme colorScheme) {
|
||||
switch (type) {
|
||||
case 'phone':
|
||||
return colorScheme.primary;
|
||||
case 'email':
|
||||
return colorScheme.secondary;
|
||||
case 'number':
|
||||
return colorScheme.tertiary;
|
||||
default:
|
||||
return colorScheme.onSurface.withOpacity(0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
146
base_project/lib/BuilderField/shared/ui/entity_form.dart
Normal file
146
base_project/lib/BuilderField/shared/ui/entity_form.dart
Normal file
@@ -0,0 +1,146 @@
|
||||
import 'package:base_project/core/providers/dynamic_theme_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../fields/base_field.dart';
|
||||
import '../../../shared/widgets/buttons/modern_button.dart';
|
||||
import '../../../core/constants/ui_constants.dart';
|
||||
|
||||
/// Reusable form component that dynamically renders fields based on field definitions
|
||||
/// This allows UI to be independent of field types and enables reusability
|
||||
class EntityForm extends StatefulWidget {
|
||||
final List<BaseField> fields;
|
||||
final Map<String, dynamic>? initialData;
|
||||
final Function(Map<String, dynamic>) onSubmit;
|
||||
final String submitButtonText;
|
||||
final bool isLoading;
|
||||
|
||||
const EntityForm({
|
||||
super.key,
|
||||
required this.fields,
|
||||
this.initialData,
|
||||
required this.onSubmit,
|
||||
this.submitButtonText = 'Submit',
|
||||
this.isLoading = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EntityForm> createState() => _EntityFormState();
|
||||
}
|
||||
|
||||
class _EntityFormState extends State<EntityForm> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final Map<String, TextEditingController> _controllers = {};
|
||||
final Map<String, BaseField> _fieldByKey = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeControllers();
|
||||
}
|
||||
|
||||
void _initializeControllers() {
|
||||
for (final field in widget.fields) {
|
||||
_controllers[field.fieldKey] = TextEditingController(
|
||||
text: widget.initialData?[field.fieldKey]?.toString() ?? '',
|
||||
);
|
||||
_fieldByKey[field.fieldKey] = field;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final controller in _controllers.values) {
|
||||
controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<DynamicThemeProvider>(
|
||||
builder: (context, dynamicThemeProvider, child) {
|
||||
final colorScheme = dynamicThemeProvider.getCurrentColorScheme(
|
||||
Theme.of(context).brightness == Brightness.dark,
|
||||
);
|
||||
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
// Dynamic field rendering
|
||||
...widget.fields.map((field) => Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(bottom: UIConstants.spacing16),
|
||||
child: field.buildField(
|
||||
controller: _controllers[field.fieldKey]!,
|
||||
colorScheme: colorScheme,
|
||||
onChanged: () => setState(() {}),
|
||||
),
|
||||
)),
|
||||
|
||||
const SizedBox(height: UIConstants.spacing24),
|
||||
|
||||
// Submit button
|
||||
ModernButton(
|
||||
text: widget.submitButtonText,
|
||||
type: ModernButtonType.primary,
|
||||
size: ModernButtonSize.large,
|
||||
isLoading: widget.isLoading,
|
||||
onPressed: widget.isLoading ? null : _handleSubmit,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _handleSubmit() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
// Dynamic cross-field match for any password-confirm group
|
||||
final Map<String, String> passwordByGroup = {};
|
||||
final Map<String, String> confirmByGroup = {};
|
||||
for (final entry in _controllers.entries) {
|
||||
final key = entry.key;
|
||||
final field = _fieldByKey[key];
|
||||
final props = field?.customProperties ?? const {};
|
||||
final isPassword = props['isPassword'] == true;
|
||||
if (!isPassword) continue;
|
||||
final String? groupId = props['groupId'];
|
||||
if (groupId == null) continue;
|
||||
final bool isConfirm = props['isConfirm'] == true;
|
||||
if (isConfirm) {
|
||||
confirmByGroup[groupId] = entry.value.text;
|
||||
} else {
|
||||
passwordByGroup[groupId] = entry.value.text;
|
||||
}
|
||||
}
|
||||
for (final gid in confirmByGroup.keys) {
|
||||
final confirm = confirmByGroup[gid] ?? '';
|
||||
final pass = passwordByGroup[gid] ?? '';
|
||||
if (confirm.isNotEmpty && confirm != pass) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Passwords do not match')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final formData = <String, dynamic>{};
|
||||
for (final entry in _controllers.entries) {
|
||||
final key = entry.key;
|
||||
final field = _fieldByKey[key];
|
||||
final value = entry.value.text.trim();
|
||||
|
||||
// Skip confirm entries for any password group
|
||||
final props = field?.customProperties ?? const {};
|
||||
final bool isPassword = props['isPassword'] == true;
|
||||
final bool isConfirm = props['isConfirm'] == true;
|
||||
if (isPassword && isConfirm) continue;
|
||||
|
||||
formData[key] = value;
|
||||
}
|
||||
widget.onSubmit(formData);
|
||||
}
|
||||
}
|
||||
}
|
||||
476
base_project/lib/BuilderField/shared/ui/entity_list.dart
Normal file
476
base_project/lib/BuilderField/shared/ui/entity_list.dart
Normal file
@@ -0,0 +1,476 @@
|
||||
import 'package:base_project/core/providers/dynamic_theme_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../../core/constants/ui_constants.dart';
|
||||
import '../../../shared/widgets/app_bar/modern_app_bar.dart';
|
||||
import 'entity_card.dart';
|
||||
|
||||
/// Generic list component for displaying entities with search, pagination, and refresh
|
||||
/// This component works with any entity type and provides consistent UI/UX
|
||||
class EntityList extends StatefulWidget {
|
||||
final List<Map<String, dynamic>> entities;
|
||||
final bool isLoading;
|
||||
final String? errorMessage;
|
||||
final bool hasMoreData;
|
||||
final String searchQuery;
|
||||
final Function(String) onSearchChanged;
|
||||
final Function(Map<String, dynamic>) onEdit;
|
||||
final Function(Map<String, dynamic>) onDelete;
|
||||
final Function(Map<String, dynamic>)? onTap;
|
||||
final Function() onRefresh;
|
||||
final Function() onLoadMore;
|
||||
final String title;
|
||||
final Function()? onAddNew;
|
||||
final List<Map<String, dynamic>> displayFields;
|
||||
|
||||
const EntityList({
|
||||
super.key,
|
||||
required this.entities,
|
||||
required this.isLoading,
|
||||
this.errorMessage,
|
||||
required this.hasMoreData,
|
||||
required this.searchQuery,
|
||||
required this.onSearchChanged,
|
||||
required this.onEdit,
|
||||
required this.onDelete,
|
||||
this.onTap,
|
||||
required this.onRefresh,
|
||||
required this.onLoadMore,
|
||||
required this.title,
|
||||
this.onAddNew,
|
||||
this.displayFields = const [],
|
||||
});
|
||||
|
||||
@override
|
||||
State<EntityList> createState() => _EntityListState();
|
||||
}
|
||||
|
||||
class _EntityListState extends State<EntityList> with TickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeAnimations();
|
||||
_searchController.text = widget.searchQuery;
|
||||
_scrollController.addListener(_scrollListener);
|
||||
}
|
||||
|
||||
void _initializeAnimations() {
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
_animationController.forward();
|
||||
}
|
||||
|
||||
void _scrollListener() {
|
||||
if (_scrollController.position.pixels >=
|
||||
_scrollController.position.maxScrollExtent - 200) {
|
||||
if (widget.hasMoreData && !widget.isLoading) {
|
||||
widget.onLoadMore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
_searchController.dispose();
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<DynamicThemeProvider>(
|
||||
builder: (context, dynamicThemeProvider, child) {
|
||||
final colorScheme = dynamicThemeProvider.getCurrentColorScheme(
|
||||
Theme.of(context).brightness == Brightness.dark,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: colorScheme.surface,
|
||||
appBar: ModernAppBar(
|
||||
title: widget.title,
|
||||
showBackButton: true,
|
||||
actions: [
|
||||
if (widget.onAddNew != null)
|
||||
IconButton(
|
||||
onPressed: widget.onAddNew,
|
||||
icon: Icon(
|
||||
Icons.add_rounded,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
tooltip: 'Add New',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Search Bar
|
||||
_buildSearchBar(colorScheme),
|
||||
|
||||
// Content
|
||||
Expanded(
|
||||
child: _buildContent(colorScheme),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchBar(ColorScheme colorScheme) {
|
||||
return Container(
|
||||
margin: UIConstants.getResponsivePadding(
|
||||
context,
|
||||
mobile: UIConstants.screenPaddingMedium,
|
||||
tablet: UIConstants.screenPaddingLarge,
|
||||
desktop: UIConstants.screenPaddingLarge,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
colorScheme.surface,
|
||||
colorScheme.surface.withOpacity(0.95),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius16),
|
||||
border: Border.all(
|
||||
color: colorScheme.primary.withOpacity(0.1),
|
||||
width: 1.5,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.primary.withOpacity(0.08),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 5),
|
||||
spreadRadius: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
onChanged: widget.onSearchChanged,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search ${widget.title.toLowerCase()}...',
|
||||
hintStyle: TextStyle(
|
||||
color: colorScheme.onSurface.withOpacity(0.6),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search_rounded,
|
||||
color: colorScheme.primary,
|
||||
size: 22,
|
||||
),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
widget.onSearchChanged('');
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.clear_rounded,
|
||||
color: colorScheme.onSurface.withOpacity(0.6),
|
||||
size: 20,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: UIConstants.spacing16,
|
||||
vertical: UIConstants.spacing12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: widget.onRefresh,
|
||||
icon: Icon(
|
||||
Icons.refresh_rounded,
|
||||
color: colorScheme.onSurface.withOpacity(0.8),
|
||||
size: 22,
|
||||
),
|
||||
tooltip: 'Refresh',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(ColorScheme colorScheme) {
|
||||
if (widget.isLoading && widget.entities.isEmpty) {
|
||||
return _buildLoadingState(colorScheme);
|
||||
}
|
||||
|
||||
if (widget.errorMessage != null && widget.entities.isEmpty) {
|
||||
return _buildErrorState(colorScheme);
|
||||
}
|
||||
|
||||
if (widget.entities.isEmpty) {
|
||||
return _buildEmptyState(colorScheme);
|
||||
}
|
||||
|
||||
return _buildEntityGrid(colorScheme);
|
||||
}
|
||||
|
||||
Widget _buildLoadingState(ColorScheme colorScheme) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
color: colorScheme.primary,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing16),
|
||||
Text(
|
||||
'Loading ${widget.title.toLowerCase()}...',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface.withOpacity(0.7),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorState(ColorScheme colorScheme) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.errorContainer.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius20),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.error_outline_rounded,
|
||||
color: colorScheme.error,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing16),
|
||||
Text(
|
||||
'Error Loading Data',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing8),
|
||||
Text(
|
||||
widget.errorMessage ?? 'Something went wrong',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface.withOpacity(0.7),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: widget.onRefresh,
|
||||
icon: Icon(
|
||||
Icons.refresh_rounded,
|
||||
color: colorScheme.onPrimary,
|
||||
),
|
||||
label: Text(
|
||||
'Retry',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: colorScheme.primary,
|
||||
foregroundColor: colorScheme.onPrimary,
|
||||
elevation: 4,
|
||||
shadowColor: colorScheme.primary.withOpacity(0.3),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UIConstants.spacing24,
|
||||
vertical: UIConstants.spacing12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius12),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(ColorScheme colorScheme) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primaryContainer.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius20),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.inbox_outlined,
|
||||
color: colorScheme.primary,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing16),
|
||||
Text(
|
||||
'No ${widget.title.toLowerCase()} found',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing8),
|
||||
Text(
|
||||
widget.searchQuery.isNotEmpty
|
||||
? 'Try adjusting your search terms'
|
||||
: 'Start by adding your first ${widget.title.toLowerCase()}',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface.withOpacity(0.7),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (widget.onAddNew != null) ...[
|
||||
const SizedBox(height: UIConstants.spacing24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: widget.onAddNew,
|
||||
icon: Icon(
|
||||
Icons.add_rounded,
|
||||
color: colorScheme.onPrimary,
|
||||
),
|
||||
label: Text(
|
||||
'Add ${widget.title}',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: colorScheme.primary,
|
||||
foregroundColor: colorScheme.onPrimary,
|
||||
elevation: 4,
|
||||
shadowColor: colorScheme.primary.withOpacity(0.3),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UIConstants.spacing24,
|
||||
vertical: UIConstants.spacing12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius12),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEntityGrid(ColorScheme colorScheme) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async => widget.onRefresh(),
|
||||
color: colorScheme.primary,
|
||||
child: GridView.builder(
|
||||
controller: _scrollController,
|
||||
padding: UIConstants.getResponsivePadding(
|
||||
context,
|
||||
mobile: UIConstants.screenPaddingMedium,
|
||||
tablet: UIConstants.screenPaddingLarge,
|
||||
desktop: UIConstants.screenPaddingLarge,
|
||||
),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: UIConstants.getResponsiveInt(
|
||||
context,
|
||||
mobile: 1,
|
||||
tablet: 2,
|
||||
desktop: 3,
|
||||
),
|
||||
childAspectRatio: 0.85,
|
||||
crossAxisSpacing: UIConstants.spacing16,
|
||||
mainAxisSpacing: UIConstants.spacing16,
|
||||
),
|
||||
itemCount: widget.entities.length + (widget.isLoading ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index < widget.entities.length) {
|
||||
return AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return FadeTransition(
|
||||
opacity: _animationController,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, 20 * (1 - _animationController.value)),
|
||||
child: EntityCard(
|
||||
entity: widget.entities[index],
|
||||
onEdit: widget.onEdit,
|
||||
onDelete: widget.onDelete,
|
||||
onTap: widget.onTap,
|
||||
displayFields: widget.displayFields,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return _buildLoadingCard(colorScheme);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingCard(ColorScheme colorScheme) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
colorScheme.surface,
|
||||
colorScheme.surface.withOpacity(0.95),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius20),
|
||||
border: Border.all(
|
||||
color: colorScheme.primary.withOpacity(0.1),
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: colorScheme.primary,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
608
base_project/lib/BuilderField/shared/ui/entity_screens.dart
Normal file
608
base_project/lib/BuilderField/shared/ui/entity_screens.dart
Normal file
@@ -0,0 +1,608 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../../core/providers/dynamic_theme_provider.dart';
|
||||
import '../../../core/constants/ui_constants.dart';
|
||||
import '../../../shared/widgets/app_bar/modern_app_bar.dart';
|
||||
import '../../../shared/widgets/buttons/modern_button.dart';
|
||||
import '../fields/base_field.dart';
|
||||
import 'entity_form.dart';
|
||||
|
||||
/// Generic CRUD screens for entities
|
||||
/// This provides consistent create and update screens for all entity types
|
||||
|
||||
/// Generic Create Entity Screen
|
||||
class EntityCreateScreen extends StatefulWidget {
|
||||
final List<BaseField> fields;
|
||||
final Function(Map<String, dynamic>) onSubmit;
|
||||
final String title;
|
||||
final bool isLoading;
|
||||
final String? errorMessage;
|
||||
|
||||
const EntityCreateScreen({
|
||||
super.key,
|
||||
required this.fields,
|
||||
required this.onSubmit,
|
||||
required this.title,
|
||||
this.isLoading = false,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EntityCreateScreen> createState() => _EntityCreateScreenState();
|
||||
}
|
||||
|
||||
class _EntityCreateScreenState extends State<EntityCreateScreen>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _fadeAnimation;
|
||||
late Animation<Offset> _slideAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeAnimations();
|
||||
}
|
||||
|
||||
void _initializeAnimations() {
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_fadeAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.0, 0.6, curve: Curves.easeOut),
|
||||
));
|
||||
|
||||
_slideAnimation = Tween<Offset>(
|
||||
begin: const Offset(0, 0.3),
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.2, 1.0, curve: Curves.easeOutCubic),
|
||||
));
|
||||
|
||||
_animationController.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<DynamicThemeProvider>(
|
||||
builder: (context, dynamicThemeProvider, child) {
|
||||
final colorScheme = dynamicThemeProvider.getCurrentColorScheme(
|
||||
Theme.of(context).brightness == Brightness.dark,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: colorScheme.surface,
|
||||
appBar: ModernAppBar(
|
||||
title: 'Create ${widget.title}',
|
||||
showBackButton: true,
|
||||
),
|
||||
body: widget.isLoading
|
||||
? _buildLoadingState(colorScheme)
|
||||
: _buildContent(colorScheme),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingState(ColorScheme colorScheme) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
color: colorScheme.primary,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing16),
|
||||
Text(
|
||||
'Creating ${widget.title.toLowerCase()}...',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface.withOpacity(0.7),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(ColorScheme colorScheme) {
|
||||
return SingleChildScrollView(
|
||||
padding: UIConstants.getResponsivePadding(
|
||||
context,
|
||||
mobile: UIConstants.screenPaddingMedium,
|
||||
tablet: UIConstants.screenPaddingLarge,
|
||||
desktop: UIConstants.screenPaddingLarge,
|
||||
),
|
||||
child: AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
_buildHeader(colorScheme),
|
||||
|
||||
const SizedBox(height: UIConstants.spacing24),
|
||||
|
||||
// Form
|
||||
_buildForm(colorScheme),
|
||||
|
||||
if (widget.errorMessage != null) ...[
|
||||
const SizedBox(height: UIConstants.spacing16),
|
||||
_buildErrorMessage(colorScheme),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(ColorScheme colorScheme) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
colorScheme.primary,
|
||||
colorScheme.primary.withOpacity(0.8),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.primary.withOpacity(0.3),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
spreadRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: UIConstants.cardPaddingLarge,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.onPrimary.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius12),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.add_rounded,
|
||||
color: colorScheme.onPrimary,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UIConstants.spacing16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Create New ${widget.title}',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onPrimary,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing4),
|
||||
Text(
|
||||
'Fill in the details below to create a new ${widget.title.toLowerCase()}',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onPrimary.withOpacity(0.8),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildForm(ColorScheme colorScheme) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
colorScheme.surface,
|
||||
colorScheme.surface.withOpacity(0.95),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius20),
|
||||
border: Border.all(
|
||||
color: colorScheme.primary.withOpacity(0.1),
|
||||
width: 1.5,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.primary.withOpacity(0.08),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
spreadRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: UIConstants.cardPaddingLarge,
|
||||
child: EntityForm(
|
||||
fields: widget.fields,
|
||||
onSubmit: widget.onSubmit,
|
||||
submitButtonText: 'Create ${widget.title}',
|
||||
isLoading: widget.isLoading,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorMessage(ColorScheme colorScheme) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(UIConstants.spacing16),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.errorContainer.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius12),
|
||||
border: Border.all(
|
||||
color: colorScheme.error.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline_rounded,
|
||||
color: colorScheme.error,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: UIConstants.spacing12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.errorMessage!,
|
||||
style: TextStyle(
|
||||
color: colorScheme.error,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic Update Entity Screen
|
||||
class EntityUpdateScreen extends StatefulWidget {
|
||||
final List<BaseField> fields;
|
||||
final Map<String, dynamic> initialData;
|
||||
final Function(Map<String, dynamic>) onSubmit;
|
||||
final String title;
|
||||
final bool isLoading;
|
||||
final String? errorMessage;
|
||||
|
||||
const EntityUpdateScreen({
|
||||
super.key,
|
||||
required this.fields,
|
||||
required this.initialData,
|
||||
required this.onSubmit,
|
||||
required this.title,
|
||||
this.isLoading = false,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EntityUpdateScreen> createState() => _EntityUpdateScreenState();
|
||||
}
|
||||
|
||||
class _EntityUpdateScreenState extends State<EntityUpdateScreen>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _fadeAnimation;
|
||||
late Animation<Offset> _slideAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeAnimations();
|
||||
}
|
||||
|
||||
void _initializeAnimations() {
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_fadeAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.0, 0.6, curve: Curves.easeOut),
|
||||
));
|
||||
|
||||
_slideAnimation = Tween<Offset>(
|
||||
begin: const Offset(0, 0.3),
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.2, 1.0, curve: Curves.easeOutCubic),
|
||||
));
|
||||
|
||||
_animationController.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<DynamicThemeProvider>(
|
||||
builder: (context, dynamicThemeProvider, child) {
|
||||
final colorScheme = dynamicThemeProvider.getCurrentColorScheme(
|
||||
Theme.of(context).brightness == Brightness.dark,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: colorScheme.surface,
|
||||
appBar: ModernAppBar(
|
||||
title: 'Update ${widget.title}',
|
||||
showBackButton: true,
|
||||
),
|
||||
body: widget.isLoading
|
||||
? _buildLoadingState(colorScheme)
|
||||
: _buildContent(colorScheme),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingState(ColorScheme colorScheme) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
color: colorScheme.primary,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing16),
|
||||
Text(
|
||||
'Updating ${widget.title.toLowerCase()}...',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface.withOpacity(0.7),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(ColorScheme colorScheme) {
|
||||
return SingleChildScrollView(
|
||||
padding: UIConstants.getResponsivePadding(
|
||||
context,
|
||||
mobile: UIConstants.screenPaddingMedium,
|
||||
tablet: UIConstants.screenPaddingLarge,
|
||||
desktop: UIConstants.screenPaddingLarge,
|
||||
),
|
||||
child: AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
return FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
_buildHeader(colorScheme),
|
||||
|
||||
const SizedBox(height: UIConstants.spacing24),
|
||||
|
||||
// Form
|
||||
_buildForm(colorScheme),
|
||||
|
||||
if (widget.errorMessage != null) ...[
|
||||
const SizedBox(height: UIConstants.spacing16),
|
||||
_buildErrorMessage(colorScheme),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(ColorScheme colorScheme) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
colorScheme.secondary,
|
||||
colorScheme.secondary.withOpacity(0.8),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.secondary.withOpacity(0.3),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
spreadRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: UIConstants.cardPaddingLarge,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.onSecondary.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius12),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.edit_rounded,
|
||||
color: colorScheme.onSecondary,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UIConstants.spacing16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Update ${widget.title}',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSecondary,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UIConstants.spacing4),
|
||||
Text(
|
||||
'Modify the details below to update this ${widget.title.toLowerCase()}',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSecondary.withOpacity(0.8),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildForm(ColorScheme colorScheme) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
colorScheme.surface,
|
||||
colorScheme.surface.withOpacity(0.95),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius20),
|
||||
border: Border.all(
|
||||
color: colorScheme.primary.withOpacity(0.1),
|
||||
width: 1.5,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.primary.withOpacity(0.08),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
spreadRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: UIConstants.cardPaddingLarge,
|
||||
child: EntityForm(
|
||||
fields: widget.fields,
|
||||
initialData: widget.initialData,
|
||||
onSubmit: widget.onSubmit,
|
||||
submitButtonText: 'Update ${widget.title}',
|
||||
isLoading: widget.isLoading,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorMessage(ColorScheme colorScheme) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(UIConstants.spacing16),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.errorContainer.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(UIConstants.radius12),
|
||||
border: Border.all(
|
||||
color: colorScheme.error.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline_rounded,
|
||||
color: colorScheme.error,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: UIConstants.spacing12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.errorMessage!,
|
||||
style: TextStyle(
|
||||
color: colorScheme.error,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user