modern
This commit is contained in:
parent
01be9df2ed
commit
f9043173c0
@ -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();
|
||||
},
|
||||
),
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
33
base_project/lib/BuilderField/shared/fields/base_field.dart
Normal file
33
base_project/lib/BuilderField/shared/fields/base_field.dart
Normal 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;
|
||||
}
|
||||
162
base_project/lib/BuilderField/shared/fields/captcha_field.dart
Normal file
162
base_project/lib/BuilderField/shared/fields/captcha_field.dart
Normal 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)),
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
76
base_project/lib/BuilderField/shared/fields/date_field.dart
Normal file
76
base_project/lib/BuilderField/shared/fields/date_field.dart
Normal 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
|
||||
@ -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)}';
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
48
base_project/lib/BuilderField/shared/fields/email_field.dart
Normal file
48
base_project/lib/BuilderField/shared/fields/email_field.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
},
|
||||
),
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
51
base_project/lib/BuilderField/shared/fields/phone_field.dart
Normal file
51
base_project/lib/BuilderField/shared/fields/phone_field.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
65
base_project/lib/BuilderField/shared/fields/radio_field.dart
Normal file
65
base_project/lib/BuilderField/shared/fields/radio_field.dart
Normal 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(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
48
base_project/lib/BuilderField/shared/fields/url_field.dart
Normal file
48
base_project/lib/BuilderField/shared/fields/url_field.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
},
|
||||
),
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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});
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../../../../core/constants/ui_constants.dart';
|
||||
import '../../../core/constants/ui_constants.dart';
|
||||
|
||||
class ModernTextField extends StatefulWidget {
|
||||
final String? label;
|
||||
@ -71,6 +71,7 @@ class _ModernTextFieldState extends State<ModernTextField>
|
||||
late Animation<double> _scaleAnimation;
|
||||
bool _isFocused = false;
|
||||
bool _hasError = false;
|
||||
late bool _obscureText;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -79,6 +80,7 @@ class _ModernTextFieldState extends State<ModernTextField>
|
||||
duration: UIConstants.durationFast,
|
||||
vsync: this,
|
||||
);
|
||||
_obscureText = widget.obscureText;
|
||||
|
||||
_fadeAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
@ -183,7 +185,7 @@ class _ModernTextFieldState extends State<ModernTextField>
|
||||
controller: widget.controller,
|
||||
focusNode: widget.focusNode,
|
||||
keyboardType: widget.keyboardType,
|
||||
obscureText: widget.obscureText,
|
||||
obscureText: _obscureText,
|
||||
enabled: widget.enabled,
|
||||
readOnly: widget.readOnly,
|
||||
maxLines: widget.maxLines,
|
||||
@ -229,7 +231,7 @@ class _ModernTextFieldState extends State<ModernTextField>
|
||||
),
|
||||
)
|
||||
: null,
|
||||
suffixIcon: widget.suffixIcon != null
|
||||
suffixIcon: (widget.suffixIcon != null)
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: UIConstants.spacing16),
|
||||
@ -243,7 +245,20 @@ class _ModernTextFieldState extends State<ModernTextField>
|
||||
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,
|
||||
contentPadding: widget.contentPadding ??
|
||||
const EdgeInsets.symmetric(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user