This commit is contained in:
string 2025-09-09 08:45:46 +05:30
parent 01be9df2ed
commit f9043173c0
25 changed files with 3632 additions and 4 deletions

View File

@ -0,0 +1,86 @@
import 'dart:typed_data';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'base_field.dart';
import '../utils/entity_field_store.dart';
class AudioUploadField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
AudioUploadField({
required this.fieldKey,
required this.label,
this.hint = 'Select audio file',
this.isRequired = false,
});
@override
String? Function(String?)? get validator => (_) {
final items =
EntityFieldStore.instance.get<List<UploadItem>>(fieldKey) ??
const [];
if (isRequired && items.isEmpty) return '$label is required';
return null;
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
final items = EntityFieldStore.instance.get<List<UploadItem>>(fieldKey) ??
<UploadItem>[];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(label,
style: TextStyle(
fontWeight: FontWeight.w600, color: colorScheme.onSurface)),
const Spacer(),
TextButton.icon(
onPressed: () async {
final result =
await FilePicker.platform.pickFiles(type: FileType.audio);
if (result != null && result.files.isNotEmpty) {
final f = result.files.single;
final Uint8List? bytes = f.bytes ??
(f.path != null
? await File(f.path!).readAsBytes()
: null);
if (bytes != null) {
final updated = List<UploadItem>.from(items)
..add(UploadItem(fileName: f.name, bytes: bytes));
EntityFieldStore.instance.set(fieldKey, updated);
if (onChanged != null) onChanged();
}
}
},
icon: const Icon(Icons.audiotrack_rounded),
label: const Text('Add Audio'),
),
],
),
const SizedBox(height: 8),
...items.map((u) => ListTile(
leading: const Icon(Icons.music_note_rounded),
title: Text(u.fileName, overflow: TextOverflow.ellipsis),
trailing: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () {
final updated = List<UploadItem>.from(items)..remove(u);
EntityFieldStore.instance.set(fieldKey, updated);
if (onChanged != null) onChanged();
},
),
)),
],
);
}
}

View File

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
/// Base interface for all field types in the Entity system
/// This allows UI to be independent of field types and enables reusability
abstract class BaseField {
/// Unique identifier for the field (e.g., 'name', 'phone')
String get fieldKey;
/// Display label for the field (e.g., 'Name', 'Phone Number')
String get label;
/// Placeholder text for the field
String get hint;
/// Whether the field is required for validation
bool get isRequired;
/// Validation function for the field
String? Function(String?)? get validator;
/// Main method - each field type provides its own implementation
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
});
/// Optional: Field-specific styling properties
Map<String, dynamic>? get customProperties => null;
/// Optional: Field-specific validation rules
String? Function(String?)? get customValidator => null;
}

View File

@ -0,0 +1,162 @@
import 'dart:math';
import 'package:flutter/material.dart';
import '../../../shared/widgets/inputs/modern_text_field.dart';
import 'base_field.dart';
/// Generic Captcha field: shows generated code and asks user to type it
class CaptchaField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final int length;
CaptchaField({
required this.fieldKey,
required this.label,
this.hint = 'Enter CAPTCHA',
this.isRequired = true,
this.length = 6,
});
@override
String? Function(String?)? get validator => (value) {
if (isRequired && (value == null || value.isEmpty)) {
return '$label is required';
}
return null;
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
return _CaptchaFieldWidget(
key: ValueKey(fieldKey),
label: label,
hint: hint,
length: length,
colorScheme: colorScheme,
controller: controller,
baseValidator: validator,
onChanged: onChanged,
);
}
String _generateCaptcha(int length) {
final random = Random();
const chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
return String.fromCharCodes(Iterable.generate(
length,
(_) => chars.codeUnitAt(random.nextInt(chars.length)),
));
}
}
class _CaptchaFieldWidget extends StatefulWidget {
final String label;
final String hint;
final int length;
final ColorScheme colorScheme;
final TextEditingController controller;
final String? Function(String?)? baseValidator;
final VoidCallback? onChanged;
const _CaptchaFieldWidget({
super.key,
required this.label,
required this.hint,
required this.length,
required this.colorScheme,
required this.controller,
required this.baseValidator,
required this.onChanged,
});
@override
State<_CaptchaFieldWidget> createState() => _CaptchaFieldWidgetState();
}
class _CaptchaFieldWidgetState extends State<_CaptchaFieldWidget> {
late String _captcha;
@override
void initState() {
super.initState();
_captcha = _generateCaptcha(widget.length);
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: widget.colorScheme.primaryContainer.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
border:
Border.all(color: widget.colorScheme.primary.withOpacity(0.2)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_captcha,
style: TextStyle(
color: widget.colorScheme.primary,
fontSize: 20,
fontWeight: FontWeight.w700,
letterSpacing: 3,
),
),
IconButton(
tooltip: 'Refresh',
onPressed: () {
setState(() {
_captcha = _generateCaptcha(widget.length);
});
if (widget.onChanged != null) widget.onChanged!();
},
icon: Icon(Icons.refresh_rounded,
color: widget.colorScheme.primary),
),
],
),
),
const SizedBox(height: 12),
ModernTextField(
controller: widget.controller,
hint: widget.hint,
validator: (value) {
final base = widget.baseValidator;
final err = base != null ? base(value) : null;
if (err != null) return err;
if ((value ?? '').trim() != _captcha) {
return 'CAPTCHA does not match';
}
return null;
},
onChanged:
widget.onChanged != null ? (_) => widget.onChanged!() : null,
),
],
);
}
String _generateCaptcha(int length) {
final random = Random();
const chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
return String.fromCharCodes(Iterable.generate(
length,
(_) => chars.codeUnitAt(random.nextInt(chars.length)),
));
}
}

View File

@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'base_field.dart';
class CheckboxField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
CheckboxField({
required this.fieldKey,
required this.label,
this.hint = '',
this.isRequired = false,
});
@override
String? Function(String?)? get validator => (value) {
if (isRequired && (value == null || value.isEmpty)) {
return '$label is required';
}
return null;
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
final bool checked = controller.text.toLowerCase() == 'true';
return Row(
children: [
Checkbox(
value: checked,
onChanged: (value) {
controller.text = (value ?? false).toString();
if (onChanged != null) onChanged();
},
),
const SizedBox(width: 8),
Expanded(
child: Text(
label,
style: TextStyle(
color: colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
),
],
);
}
}

View File

@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'base_field.dart';
import '../../../shared/widgets/inputs/modern_text_field.dart';
/// Custom text input field implementation (preferred over legacy TextField)
class CustomTextField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final int? maxLength;
final TextInputType? keyboardType;
final Map<String, dynamic>? customProperties;
final String? Function(String?)? customValidator;
CustomTextField({
required this.fieldKey,
required this.label,
required this.hint,
this.isRequired = false,
this.maxLength,
this.keyboardType,
this.customProperties,
this.customValidator,
});
@override
String? Function(String?)? get validator => (value) {
// Custom validator (e.g., range checks)
if (customValidator != null) {
final err = customValidator!(value);
if (err != null) return err;
}
if (isRequired && (value == null || value.isEmpty)) {
return '$label is required';
}
if (maxLength != null && value != null && value.length > maxLength!) {
return '$label must be less than $maxLength characters';
}
return null;
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
final bool isPassword = customProperties?['isPassword'] == true;
return ModernTextField(
controller: controller,
hint: hint,
maxLength: maxLength,
keyboardType: keyboardType,
obscureText: isPassword,
onChanged: onChanged != null ? (_) => onChanged() : null,
validator: validator,
);
}
}

View File

@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import '../../../shared/widgets/inputs/modern_text_field.dart';
import 'base_field.dart';
/// Date input field implementation with native date picker
class DateField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final DateTime? initialDate;
final DateTime firstDate;
final DateTime lastDate;
DateField({
required this.fieldKey,
required this.label,
required this.hint,
this.isRequired = false,
this.initialDate,
DateTime? firstDate,
DateTime? lastDate,
}) : firstDate = firstDate ?? DateTime(2000),
lastDate = lastDate ?? DateTime(2101);
@override
String? Function(String?)? get validator => (value) {
if (isRequired && (value == null || value.isEmpty)) {
return '$label is required';
}
return null;
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
return Builder(
builder: (context) {
return ModernTextField(
controller: controller,
hint: hint.isNotEmpty ? hint : label,
readOnly: true,
validator: validator,
suffixIcon: const Icon(Icons.calendar_today_rounded),
onTap: () async {
FocusScope.of(context).unfocus();
final now = DateTime.now();
final picked = await showDatePicker(
context: context,
initialDate: initialDate ?? now,
firstDate: firstDate,
lastDate: lastDate,
helpText: label,
);
if (picked != null) {
final String value = _formatDate(picked);
controller.text = value;
if (onChanged != null) onChanged();
}
},
);
},
);
}
String _formatDate(DateTime date) {
String two(int n) => n < 10 ? '0$n' : '$n';
return '${date.year}-${two(date.month)}-${two(date.day)}';
}
}
// No global navigator key required; Builder provides local context

View File

@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'base_field.dart';
import 'package:flutter/material.dart';
import 'base_field.dart';
import '../../../shared/widgets/inputs/modern_text_field.dart';
/// DateTime input field implementation with native date & time pickers
class DateTimeField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final DateTime? initialDateTime;
final DateTime firstDate;
final DateTime lastDate;
DateTimeField({
required this.fieldKey,
required this.label,
required this.hint,
this.isRequired = false,
this.initialDateTime,
DateTime? firstDate,
DateTime? lastDate,
}) : firstDate = firstDate ?? DateTime(2000),
lastDate = lastDate ?? DateTime(2101);
@override
String? Function(String?)? get validator => (value) {
if (isRequired && (value == null || value.isEmpty)) {
return '$label is required';
}
return null;
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
return Builder(
builder: (context) {
return ModernTextField(
controller: controller,
hint: hint.isNotEmpty ? hint : label,
readOnly: true,
validator: validator,
suffixIcon: const Icon(Icons.access_time_rounded),
onTap: () async {
FocusScope.of(context).unfocus();
final now = DateTime.now();
final datePicked = await showDatePicker(
context: context,
initialDate: (initialDateTime ?? now),
firstDate: firstDate,
lastDate: lastDate,
helpText: label,
);
if (datePicked == null) return;
final timePicked = await showTimePicker(
context: context,
initialTime: TimeOfDay.fromDateTime(initialDateTime ?? now),
helpText: label,
);
if (timePicked == null) return;
final combined = DateTime(
datePicked.year,
datePicked.month,
datePicked.day,
timePicked.hour,
timePicked.minute,
);
controller.text = _formatDateTime(combined);
if (onChanged != null) onChanged();
},
);
},
);
}
String _formatDateTime(DateTime dt) {
String two(int n) => n < 10 ? '0$n' : '$n';
return '${dt.year}-${two(dt.month)}-${two(dt.day)} ${two(dt.hour)}:${two(dt.minute)}';
}
}

View File

@ -0,0 +1,50 @@
// import 'package:flutter/material.dart';
// import 'base_field.dart';
// import '../../../Reuseable/reusable_dropdown_field.dart';
// /// Dropdown selection field implementation
// class DropdownField extends BaseField {
// final String fieldKey;
// final String label;
// final String hint;
// final bool isRequired;
// final List<Map<String, dynamic>> options;
// final String valueKey;
// final String displayKey;
// DropdownField({
// required this.fieldKey,
// required this.label,
// required this.hint,
// this.isRequired = false,
// required this.options,
// this.valueKey = 'id',
// this.displayKey = 'name',
// });
// @override
// String? Function(String?)? get validator => (value) {
// if (isRequired && (value == null || value.isEmpty)) {
// return '$label is required';
// }
// return null;
// };
// @override
// Widget buildField({
// required TextEditingController controller,
// required ColorScheme colorScheme,
// VoidCallback? onChanged,
// }) {
// return ReusableDropdownField(
// controller: controller,
// label: label,
// hint: hint,
// items: options,
// valueKey: valueKey,
// displayKey: displayKey,
// onChanged: onChanged != null ? (_) => onChanged() : null,
// validator: validator,
// );
// }
// }

View File

@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'base_field.dart';
import '../../../shared/widgets/inputs/modern_text_field.dart';
/// Email input field implementation
class EmailField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
EmailField({
required this.fieldKey,
required this.label,
required this.hint,
this.isRequired = false,
});
@override
String? Function(String?)? get validator => (value) {
if (isRequired && (value == null || value.isEmpty)) {
return '$label is required';
}
if (value != null && value.isNotEmpty) {
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(value)) {
return 'Please enter a valid email address';
}
}
return null;
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
return ModernTextField(
controller: controller,
hint: hint,
keyboardType: TextInputType.emailAddress,
prefixIcon: const Icon(Icons.email_outlined),
onChanged: onChanged != null ? (_) => onChanged() : null,
validator: validator,
);
}
}

View File

@ -0,0 +1,85 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'base_field.dart';
import '../utils/entity_field_store.dart';
/// Multi-file upload field; stores files in EntityFieldStore under fieldKey
class FileUploadField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
FileUploadField({
required this.fieldKey,
required this.label,
this.hint = 'Select files',
this.isRequired = false,
});
@override
String? Function(String?)? get validator => (_) {
final items =
EntityFieldStore.instance.get<List<UploadItem>>(fieldKey) ??
const [];
if (isRequired && items.isEmpty) {
return '$label is required';
}
return null;
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
final items = EntityFieldStore.instance.get<List<UploadItem>>(fieldKey) ??
<UploadItem>[];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(label,
style: TextStyle(
fontWeight: FontWeight.w600, color: colorScheme.onSurface)),
const Spacer(),
TextButton.icon(
onPressed: () async {
final result =
await FilePicker.platform.pickFiles(allowMultiple: true);
if (result != null) {
final List<UploadItem> updated = List<UploadItem>.from(items);
for (final f in result.files) {
final Uint8List? bytes = f.bytes;
if (bytes == null) continue;
updated.add(UploadItem(fileName: f.name, bytes: bytes));
}
EntityFieldStore.instance.set(fieldKey, updated);
if (onChanged != null) onChanged();
}
},
icon: const Icon(Icons.attach_file_rounded),
label: const Text('Add Files'),
),
],
),
const SizedBox(height: 8),
...items.map((u) => ListTile(
leading: const Icon(Icons.insert_drive_file_rounded),
title: Text(u.fileName, overflow: TextOverflow.ellipsis),
trailing: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () {
final updated = List<UploadItem>.from(items)..remove(u);
EntityFieldStore.instance.set(fieldKey, updated);
if (onChanged != null) onChanged();
},
),
)),
],
);
}
}

View File

@ -0,0 +1,103 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'base_field.dart';
import '../utils/entity_field_store.dart';
class ImageUploadField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
ImageUploadField({
required this.fieldKey,
required this.label,
this.hint = 'Select images',
this.isRequired = false,
});
@override
String? Function(String?)? get validator => (_) {
final items =
EntityFieldStore.instance.get<List<UploadItem>>(fieldKey) ??
const [];
if (isRequired && items.isEmpty) return '$label is required';
return null;
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
final items = EntityFieldStore.instance.get<List<UploadItem>>(fieldKey) ??
<UploadItem>[];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(label,
style: TextStyle(
fontWeight: FontWeight.w600, color: colorScheme.onSurface)),
const Spacer(),
TextButton.icon(
onPressed: () async {
final ImagePicker picker = ImagePicker();
final XFile? picked =
await picker.pickImage(source: ImageSource.gallery);
if (picked != null) {
final Uint8List bytes = await picked.readAsBytes();
final updated = List<UploadItem>.from(items)
..add(UploadItem(fileName: picked.name, bytes: bytes));
EntityFieldStore.instance.set(fieldKey, updated);
if (onChanged != null) onChanged();
}
},
icon: const Icon(Icons.image_rounded),
label: const Text('Add Image'),
),
],
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: items
.map((u) => Stack(
alignment: Alignment.topRight,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.memory(u.bytes,
width: 72, height: 72, fit: BoxFit.cover),
),
Positioned(
right: 0,
child: InkWell(
onTap: () {
final updated = List<UploadItem>.from(items)
..remove(u);
EntityFieldStore.instance.set(fieldKey, updated);
if (onChanged != null) onChanged();
},
child: Container(
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.close_rounded,
color: Colors.white, size: 18),
),
),
),
],
))
.toList(),
),
],
);
}
}

View File

@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'base_field.dart';
import '../../../shared/widgets/inputs/modern_text_field.dart';
/// Number input field implementation
class NumberField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final double? min;
final double? max;
final int? decimalPlaces;
NumberField({
required this.fieldKey,
required this.label,
required this.hint,
this.isRequired = false,
this.min,
this.max,
this.decimalPlaces,
});
@override
String? Function(String?)? get validator => (value) {
if (isRequired && (value == null || value.isEmpty)) {
return '$label is required';
}
if (value != null && value.isNotEmpty) {
final number = double.tryParse(value);
if (number == null) {
return 'Please enter a valid number';
}
if (min != null && number < min!) {
return '$label must be at least $min';
}
if (max != null && number > max!) {
return '$label must be at most $max';
}
}
return null;
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
return ModernTextField(
controller: controller,
hint: hint,
keyboardType: TextInputType.numberWithOptions(
decimal: decimalPlaces != null && decimalPlaces! > 0,
),
onChanged: onChanged != null ? (_) => onChanged() : null,
validator: validator,
);
}
}

View File

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'base_field.dart';
import '../../../shared/widgets/inputs/modern_text_field.dart';
/// Reusable password field supporting dynamic pairing with confirm password
/// via a shared groupId. Use two instances with the same groupId, one with
/// isConfirm=true. EntityForm will handle cross-field validation and exclude
/// confirm entries from submission.
class PasswordField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final int? maxLength;
final String? groupId; // Pair password/confirm dynamically
final bool isConfirm; // Mark this instance as a confirm field
PasswordField({
required this.fieldKey,
required this.label,
required this.hint,
this.isRequired = false,
this.maxLength,
this.groupId,
this.isConfirm = false,
});
@override
String? Function(String?)? get validator => (value) {
if (isRequired && (value == null || value.isEmpty)) {
return '$label is required';
}
if (maxLength != null && value != null && value.length > maxLength!) {
return '$label must be less than $maxLength characters';
}
// Cross-field match is handled centrally in EntityForm using groupId
return null;
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
return ModernTextField(
controller: controller,
hint: hint,
maxLength: maxLength,
keyboardType: TextInputType.visiblePassword,
obscureText: true,
onChanged: onChanged != null ? (_) => onChanged() : null,
validator: validator,
);
}
@override
Map<String, dynamic>? get customProperties => {
if (groupId != null) 'groupId': groupId,
'isConfirm': isConfirm,
'isPassword': true,
};
}

View File

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'base_field.dart';
import '../../../shared/widgets/inputs/modern_text_field.dart';
/// Phone number input field implementation
class PhoneField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final String? countryCode;
PhoneField({
required this.fieldKey,
required this.label,
required this.hint,
this.isRequired = false,
this.countryCode,
});
@override
String? Function(String?)? get validator => (value) {
if (isRequired && (value == null || value.isEmpty)) {
return '$label is required';
}
if (value != null && value.isNotEmpty) {
// Phone number validation
final phoneRegex = RegExp(r'^\+?[\d\s\-\(\)]+$');
if (!phoneRegex.hasMatch(value)) {
return 'Please enter a valid phone number';
}
}
return null;
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
return ModernTextField(
controller: controller,
hint: hint,
keyboardType: TextInputType.phone,
prefixIcon: countryCode != null ? Text(countryCode!) : null,
onChanged: onChanged != null ? (_) => onChanged() : null,
validator: validator,
);
}
}

View File

@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'base_field.dart';
class RadioField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
final List<String> options; // e.g., ['yes','no']
RadioField({
required this.fieldKey,
required this.label,
this.hint = '',
this.isRequired = false,
required this.options,
});
@override
String? Function(String?)? get validator => (value) {
if (isRequired && (value == null || value.isEmpty)) {
return '$label is required';
}
if (value != null && value.isNotEmpty && !options.contains(value)) {
return 'Invalid selection';
}
return null;
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
final String current = controller.text;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
color: colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
Wrap(
spacing: 12,
children: options.map((opt) {
return ChoiceChip(
label: Text(opt),
selected: current == opt,
selectedColor: colorScheme.primary.withOpacity(0.2),
onSelected: (_) {
controller.text = opt;
if (onChanged != null) onChanged();
},
);
}).toList(),
),
],
);
}
}

View File

@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'base_field.dart';
/// Boolean switch field implementation
class SwitchField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
SwitchField({
required this.fieldKey,
required this.label,
this.hint = '',
this.isRequired = false,
});
@override
String? Function(String?)? get validator => (_) => null;
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
final bool current = (controller.text.toLowerCase() == 'true');
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
label,
style: TextStyle(
color: colorScheme.onSurface,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
Switch(
value: current,
activeColor: colorScheme.onPrimary,
activeTrackColor: colorScheme.primary,
onChanged: (value) {
controller.text = value.toString();
if (onChanged != null) onChanged();
},
),
],
);
}
}

View File

@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'base_field.dart';
import '../../../shared/widgets/inputs/modern_text_field.dart';
/// URL input field implementation using ModernTextField
class UrlField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
UrlField({
required this.fieldKey,
required this.label,
required this.hint,
this.isRequired = false,
});
@override
String? Function(String?)? get validator => (value) {
if (isRequired && (value == null || value.isEmpty)) {
return '$label is required';
}
if (value != null && value.isNotEmpty) {
final uri = Uri.tryParse(value);
if (uri == null || !(uri.hasScheme && uri.hasAuthority)) {
return 'Please enter a valid URL';
}
}
return null;
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
return ModernTextField(
controller: controller,
hint: hint,
keyboardType: TextInputType.url,
prefixIcon: const Icon(Icons.link_rounded),
validator: validator,
onChanged: onChanged != null ? (_) => onChanged() : null,
);
}
}

View File

@ -0,0 +1,86 @@
import 'dart:typed_data';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'base_field.dart';
import '../utils/entity_field_store.dart';
class VideoUploadField extends BaseField {
final String fieldKey;
final String label;
final String hint;
final bool isRequired;
VideoUploadField({
required this.fieldKey,
required this.label,
this.hint = 'Select video file',
this.isRequired = false,
});
@override
String? Function(String?)? get validator => (_) {
final items =
EntityFieldStore.instance.get<List<UploadItem>>(fieldKey) ??
const [];
if (isRequired && items.isEmpty) return '$label is required';
return null;
};
@override
Widget buildField({
required TextEditingController controller,
required ColorScheme colorScheme,
VoidCallback? onChanged,
}) {
final items = EntityFieldStore.instance.get<List<UploadItem>>(fieldKey) ??
<UploadItem>[];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(label,
style: TextStyle(
fontWeight: FontWeight.w600, color: colorScheme.onSurface)),
const Spacer(),
TextButton.icon(
onPressed: () async {
final result =
await FilePicker.platform.pickFiles(type: FileType.video);
if (result != null && result.files.isNotEmpty) {
final f = result.files.single;
final Uint8List? bytes = f.bytes ??
(f.path != null
? await File(f.path!).readAsBytes()
: null);
if (bytes != null) {
final updated = List<UploadItem>.from(items)
..add(UploadItem(fileName: f.name, bytes: bytes));
EntityFieldStore.instance.set(fieldKey, updated);
if (onChanged != null) onChanged();
}
}
},
icon: const Icon(Icons.video_library_rounded),
label: const Text('Add Video'),
),
],
),
const SizedBox(height: 8),
...items.map((u) => ListTile(
leading: const Icon(Icons.videocam_rounded),
title: Text(u.fileName, overflow: TextOverflow.ellipsis),
trailing: IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () {
final updated = List<UploadItem>.from(items)..remove(u);
EntityFieldStore.instance.set(fieldKey, updated);
if (onChanged != null) onChanged();
},
),
)),
],
);
}
}

View 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';
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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,
),
),
);
}
}

View 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,
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,37 @@
import 'dart:typed_data';
/// Temporary in-memory store for complex field data (e.g., uploads)
/// Keyed by fieldKey. Values are dynamic but expected shapes are documented.
class EntityFieldStore {
EntityFieldStore._();
static final EntityFieldStore instance = EntityFieldStore._();
final Map<String, dynamic> _data = {};
void set(String fieldKey, dynamic value) {
_data[fieldKey] = value;
}
T? get<T>(String fieldKey) {
final value = _data[fieldKey];
if (value is T) return value as T;
return null;
}
void remove(String fieldKey) {
_data.remove(fieldKey);
}
void clear() {
_data.clear();
}
}
/// Expected value shapes:
/// - For multi-file uploads: List<UploadItem>
class UploadItem {
final String fileName;
final Uint8List bytes;
UploadItem({required this.fileName, required this.bytes});
}

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../../../../../core/constants/ui_constants.dart'; import '../../../core/constants/ui_constants.dart';
class ModernTextField extends StatefulWidget { class ModernTextField extends StatefulWidget {
final String? label; final String? label;
@ -71,6 +71,7 @@ class _ModernTextFieldState extends State<ModernTextField>
late Animation<double> _scaleAnimation; late Animation<double> _scaleAnimation;
bool _isFocused = false; bool _isFocused = false;
bool _hasError = false; bool _hasError = false;
late bool _obscureText;
@override @override
void initState() { void initState() {
@ -79,6 +80,7 @@ class _ModernTextFieldState extends State<ModernTextField>
duration: UIConstants.durationFast, duration: UIConstants.durationFast,
vsync: this, vsync: this,
); );
_obscureText = widget.obscureText;
_fadeAnimation = Tween<double>( _fadeAnimation = Tween<double>(
begin: 0.0, begin: 0.0,
@ -183,7 +185,7 @@ class _ModernTextFieldState extends State<ModernTextField>
controller: widget.controller, controller: widget.controller,
focusNode: widget.focusNode, focusNode: widget.focusNode,
keyboardType: widget.keyboardType, keyboardType: widget.keyboardType,
obscureText: widget.obscureText, obscureText: _obscureText,
enabled: widget.enabled, enabled: widget.enabled,
readOnly: widget.readOnly, readOnly: widget.readOnly,
maxLines: widget.maxLines, maxLines: widget.maxLines,
@ -229,7 +231,7 @@ class _ModernTextFieldState extends State<ModernTextField>
), ),
) )
: null, : null,
suffixIcon: widget.suffixIcon != null suffixIcon: (widget.suffixIcon != null)
? Padding( ? Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
right: UIConstants.spacing16), right: UIConstants.spacing16),
@ -243,7 +245,20 @@ class _ModernTextFieldState extends State<ModernTextField>
child: widget.suffixIcon!, child: widget.suffixIcon!,
), ),
) )
: null, : (widget.obscureText
? IconButton(
onPressed: () {
setState(() {
_obscureText = !_obscureText;
});
},
icon: Icon(
_obscureText
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
),
)
: null),
border: InputBorder.none, border: InputBorder.none,
contentPadding: widget.contentPadding ?? contentPadding: widget.contentPadding ??
const EdgeInsets.symmetric( const EdgeInsets.symmetric(