Examples
Complete production-ready examples demonstrating best practices and patterns
This guide presents complete, production-ready examples from actual features built with the Vyuh Entity System. These examples demonstrate best practices and common patterns you can adapt for your own applications.
Example 1: User Management System
A complete user management implementation with roles, permissions, and activity tracking.
Entity Model
// lib/models/user.dart
import 'package:json_annotation/json_annotation.dart';
import 'package:vyuh_entity_system/vyuh_entity_system.dart';
part 'user.g.dart';
@JsonSerializable()
class User extends EntityBase {
final String name;
final String email;
final String? phone;
final UserRole role;
final String? department;
final String? locationId;
final bool isActive;
final DateTime? lastLogin;
final Map<String, dynamic>? preferences;
User({
required super.id,
required super.schemaType,
required this.name,
required this.email,
this.phone,
required this.role,
this.department,
this.locationId,
this.isActive = true,
this.lastLogin,
this.preferences,
super.createdAt,
super.layout,
super.modifiers,
});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
@override
Map<String, dynamic> toJson() => _$UserToJson(this);
User copyWith({
String? name,
String? email,
String? phone,
UserRole? role,
String? department,
String? locationId,
bool? isActive,
DateTime? lastLogin,
Map<String, dynamic>? preferences,
}) {
return User(
id: id,
schemaType: schemaType,
name: name ?? this.name,
email: email ?? this.email,
phone: phone ?? this.phone,
role: role ?? this.role,
department: department ?? this.department,
locationId: locationId ?? this.locationId,
isActive: isActive ?? this.isActive,
lastLogin: lastLogin ?? this.lastLogin,
preferences: preferences ?? this.preferences,
createdAt: createdAt,
layout: layout,
modifiers: modifiers,
);
}
}
enum UserRole {
@JsonValue('admin')
admin,
@JsonValue('manager')
manager,
@JsonValue('user')
user,
@JsonValue('viewer')
viewer,
}
extension UserRoleExtension on UserRole {
String get displayName {
switch (this) {
case UserRole.admin:
return 'Administrator';
case UserRole.manager:
return 'Manager';
case UserRole.user:
return 'User';
case UserRole.viewer:
return 'Viewer';
}
}
Color get color {
switch (this) {
case UserRole.admin:
return Colors.red;
case UserRole.manager:
return Colors.orange;
case UserRole.user:
return Colors.blue;
case UserRole.viewer:
return Colors.grey;
}
}
}API Implementation
// lib/api/user_api.dart
import 'package:vyuh_entity_system/vyuh_entity_system.dart';
import '../models/user.dart';
class UserApi extends EntityApi<User> {
UserApi({required super.client});
@override
User fromJson(Map<String, dynamic> json) => User.fromJson(json);
Future<bool> emailExists(String email) async {
final response = await client.get(
'/api/v1/users/check-email',
queryParameters: {'email': email},
);
return response.data['exists'] ?? false;
}
Future<User> updatePassword(String userId, String newPassword) async {
final response = await client.post(
'/api/v1/users/$userId/update-password',
data: {'password': newPassword},
);
return fromJson(response.data);
}
Future<void> resetPassword(String email) async {
await client.post(
'/api/v1/users/reset-password',
data: {'email': email},
);
}
Future<List<User>> getByLocation(String locationId) async {
return list(filters: {'locationId': locationId});
}
Future<List<User>> getByRole(UserRole role) async {
return list(filters: {'role': role.name});
}
Future<Map<String, int>> getUserStatistics() async {
final response = await client.get('/api/v1/users/statistics');
return Map<String, int>.from(response.data);
}
}Layout Implementation
// lib/layouts/user_layouts.dart
import 'package:flutter/material.dart';
import 'package:vyuh_entity_system/vyuh_entity_system.dart';
import '../models/user.dart';
class UserLayouts {
static EntityLayoutDescriptor<User> create() {
return EntityLayoutDescriptor<User>(
list: [
UserTableLayout(),
UserGridLayout(),
],
details: [
UserDetailLayout(),
],
summary: [
UserCardLayout(),
],
);
}
}
class UserTableLayout extends TableListLayout<User> {
UserTableLayout() : super(
title: 'Users',
columns: [
TableColumn<User>(
key: 'name',
label: 'Name',
getValue: (user) => user.name,
sortable: true,
searchable: true,
buildCell: (context, user) =>
Row(
children: [
CircleAvatar(
radius: 16,
backgroundColor: user.role.color,
child: Text(
user.name[0].toUpperCase(),
style: const TextStyle(color: Colors.white, fontSize: 12),
),
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(user.name),
Text(
user.role.displayName,
style: Theme
.of(context)
.textTheme
.bodySmall,
),
],
),
),
],
),
),
TableColumn<User>(
key: 'email',
label: 'Email',
getValue: (user) => user.email,
sortable: true,
searchable: true,
copyable: true,
),
TableColumn<User>(
key: 'department',
label: 'Department',
getValue: (user) => user.department ?? '-',
sortable: true,
),
TableColumn<User>(
key: 'status',
label: 'Status',
getValue: (user) => user.isActive ? 'Active' : 'Inactive',
width: 100,
buildCell: (context, user) =>
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: user.isActive
? Colors.green.withOpacity(0.1)
: Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
user.isActive ? Icons.check_circle : Icons.cancel,
size: 14,
color: user.isActive ? Colors.green : Colors.grey,
),
const SizedBox(width: 4),
Text(
user.isActive ? 'Active' : 'Inactive',
style: TextStyle(
fontSize: 12,
color: user.isActive ? Colors.green : Colors.grey,
),
),
],
),
),
),
TableColumn<User>(
key: 'lastLogin',
label: 'Last Login',
getValue: (user) =>
user.lastLogin != null
? _formatLastLogin(user.lastLogin!)
: 'Never',
sortable: true,
),
],
filters: [
Filter<User>(
key: 'role',
label: 'Role',
options: UserRole.values.map((role) =>
FilterOption(
value: role.name,
label: role.displayName,
)).toList(),
apply: (users, value) =>
users.where((user) =>
user.role.name == value
).toList(),
),
Filter<User>(
key: 'status',
label: 'Status',
options: [
FilterOption(value: 'active', label: 'Active'),
FilterOption(value: 'inactive', label: 'Inactive'),
],
apply: (users, value) =>
users.where((user) =>
value == 'active' ? user.isActive : !user.isActive
).toList(),
),
],
bulkActions: [
BulkAction(
label: 'Activate',
icon: Icons.check_circle,
onTap: (context, users) async {
final api = context
.read<EntityProvider<User>>()
.api as UserApi;
for (final user in users) {
await api.update(user.id, user.copyWith(isActive: true));
}
},
isEnabled: (users) => users.any((u) => !u.isActive),
),
BulkAction(
label: 'Deactivate',
icon: Icons.cancel,
onTap: (context, users) async {
final api = context
.read<EntityProvider<User>>()
.api as UserApi;
for (final user in users) {
await api.update(user.id, user.copyWith(isActive: false));
}
},
isEnabled: (users) => users.any((u) => u.isActive),
),
],
);
static String _formatLastLogin(DateTime lastLogin) {
final now = DateTime.now();
final difference = now.difference(lastLogin);
if (difference.inDays == 0) {
if (difference.inHours == 0) {
return '${difference.inMinutes} minutes ago';
}
return '${difference.inHours} hours ago';
} else if (difference.inDays < 7) {
return '${difference.inDays} days ago';
} else {
return DateFormat('MMM d, y').format(lastLogin);
}
}
}
class UserDetailLayout extends EntityLayout<User> {
@override
Widget build(BuildContext context, User user) {
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header Card
Card(
child: Padding(
padding: const EdgeInsets.all(24),
child: Row(
children: [
CircleAvatar(
radius: 40,
backgroundColor: user.role.color,
child: Text(
user.name[0].toUpperCase(),
style: const TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 24),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user.name,
style: Theme
.of(context)
.textTheme
.headlineSmall,
),
const SizedBox(height: 4),
Text(
user.email,
style: Theme
.of(context)
.textTheme
.bodyLarge,
),
const SizedBox(height: 8),
Row(
children: [
Chip(
label: Text(user.role.displayName),
backgroundColor: user.role.color.withOpacity(0.1),
labelStyle: TextStyle(color: user.role.color),
),
const SizedBox(width: 8),
if (!user.isActive)
const Chip(
label: Text('Inactive'),
backgroundColor: Colors.red,
labelStyle: TextStyle(color: Colors.white),
),
],
),
],
),
),
Column(
children: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => context.go('/users/${user.id}/edit'),
tooltip: 'Edit User',
),
IconButton(
icon: const Icon(Icons.history),
onPressed: () => _showActivityHistory(context, user),
tooltip: 'Activity History',
),
],
),
],
),
),
),
const SizedBox(height: 24),
// Information Grid
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: _buildInfoSection(
context,
title: 'Contact Information',
children: [
_buildInfoRow('Email', user.email, copyable: true),
_buildInfoRow('Phone', user.phone ?? 'Not provided'),
],
),
),
const SizedBox(width: 24),
Expanded(
child: _buildInfoSection(
context,
title: 'Organization',
children: [
_buildInfoRow('Department', user.department ?? '-'),
if (user.locationId != null)
FutureBuilder<Location?>(
future: _loadLocation(user.locationId!),
builder: (context, snapshot) {
return _buildInfoRow(
'Location',
snapshot.data?.name ?? 'Loading...',
);
},
),
],
),
),
],
),
const SizedBox(height: 24),
// Activity Summary
_buildActivitySummary(context, user),
const SizedBox(height: 24),
// Permissions
_buildPermissionsSection(context, user),
],
),
);
}
Widget _buildInfoSection(BuildContext context, {
required String title,
required List<Widget> children,
}) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme
.of(context)
.textTheme
.titleMedium,
),
const SizedBox(height: 16),
...children,
],
),
),
);
}
Widget _buildInfoRow(String label, String value, {bool copyable = false}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
SizedBox(
width: 120,
child: Text(
label,
style: const TextStyle(fontWeight: FontWeight.w500),
),
),
Expanded(
child: copyable
? SelectableText(value)
: Text(value),
),
],
),
);
}
Widget _buildActivitySummary(BuildContext context, User user) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Recent Activity',
style: Theme
.of(context)
.textTheme
.titleMedium,
),
TextButton(
onPressed: () => _showActivityHistory(context, user),
child: const Text('View All'),
),
],
),
const SizedBox(height: 16),
if (user.lastLogin != null)
ListTile(
leading: const Icon(Icons.login),
title: const Text('Last Login'),
subtitle: Text(
DateFormat('MMM d, y at h:mm a').format(user.lastLogin!),
),
contentPadding: EdgeInsets.zero,
),
ListTile(
leading: const Icon(Icons.calendar_today),
title: const Text('Account Created'),
subtitle: Text(
DateFormat('MMM d, y').format(user.createdAt!),
),
contentPadding: EdgeInsets.zero,
),
],
),
),
);
}
Widget _buildPermissionsSection(BuildContext context, User user) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Permissions',
style: Theme
.of(context)
.textTheme
.titleMedium,
),
if (context.read<EntityPermissionService>()
.hasPermission(Permission.update('users', user.id)))
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => _editPermissions(context, user),
tooltip: 'Edit Permissions',
),
],
),
const SizedBox(height: 16),
FutureBuilder<List<Permission>>(
future: _loadUserPermissions(user.id),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const CircularProgressIndicator();
}
final permissions = snapshot.data!;
if (permissions.isEmpty) {
return Text(
'No specific permissions assigned',
style: Theme
.of(context)
.textTheme
.bodySmall,
);
}
return Wrap(
spacing: 8,
runSpacing: 8,
children: permissions.map((permission) =>
Chip(
label: Text(
'${permission.resource}.${permission.actions.join(",")}',
),
)).toList(),
);
},
),
],
),
),
);
}
Future<Location?> _loadLocation(String locationId) async {
// Load location details
return null; // Placeholder
}
Future<List<Permission>> _loadUserPermissions(String userId) async {
// Load user permissions
return []; // Placeholder
}
void _showActivityHistory(BuildContext context, User user) {
showDialog(
context: context,
builder: (_) =>
Dialog(
child: Container(
width: 800,
height: 600,
padding: const EdgeInsets.all(24),
child: Column(
children: [
Text(
'Activity History - ${user.name}',
style: Theme
.of(context)
.textTheme
.titleLarge,
),
const SizedBox(height: 16),
Expanded(
child: ActivityTimeline(
userId: user.id,
),
),
],
),
),
),
);
}
void _editPermissions(BuildContext context, User user) {
// Show permission editor
}
}Form Implementation
// lib/forms/user_form.dart
import 'package:vyuh_entity_system/vyuh_entity_system.dart';
import 'package:vyuh_feature_forms/vyuh_feature_forms.dart';
import '../models/user.dart';
class UserFormDescriptor extends EntityFormDescriptor<User> {
@override
StepForm prepare(User? entity) {
final isEdit = entity != null;
return StepForm(
title: isEdit ? 'Edit User' : 'Create User',
steps: [
FormStep(
title: 'Basic Information',
form: FormBuilder(
fields: [
TextField(
name: 'name',
label: 'Full Name',
value: entity?.name,
validators: [
required(),
minLength(3),
maxLength(100),
],
helperText: 'Enter the user\'s full name',
),
TextField(
name: 'email',
label: 'Email Address',
value: entity?.email,
validators: [
required(),
email(),
AsyncValidator(
validator: (value) async {
if (isEdit && value == entity.email) return null;
final api = UserApi(client: SharedApiClient());
final exists = await api.emailExists(value!);
return exists ? 'Email already in use' : null;
},
),
],
keyboardType: TextInputType.emailAddress,
enabled: !isEdit, // Can't change email after creation
),
TextField(
name: 'phone',
label: 'Phone Number',
value: entity?.phone,
validators: [
pattern(
r'^\+?[\d\s-()]+$',
'Please enter a valid phone number',
),
],
keyboardType: TextInputType.phone,
),
],
),
),
FormStep(
title: 'Role & Organization',
form: FormBuilder(
fields: [
SelectField(
name: 'role',
label: 'Role',
value: entity?.role,
options: UserRole.values.map((role) =>
Option(
value: role,
label: role.displayName,
description: _getRoleDescription(role),
)).toList(),
validators: [required()],
),
SelectField(
name: 'department',
label: 'Department',
value: entity?.department,
optionsBuilder: () async {
final departments = await _loadDepartments();
return departments.map((dept) =>
Option(
value: dept.id,
label: dept.name,
)).toList();
},
),
SelectField(
name: 'locationId',
label: 'Location',
value: entity?.locationId,
dependsOn: ['department'],
optionsBuilder: (formData) async {
final deptId = formData['department'] as String?;
if (deptId == null) return [];
final locations = await _loadLocationsForDepartment(deptId);
return locations.map((loc) =>
Option(
value: loc.id,
label: loc.name,
)).toList();
},
),
],
),
),
FormStep(
title: 'Account Settings',
form: FormBuilder(
fields: [
if (!isEdit)
ToggleField(
name: 'sendWelcomeEmail',
label: 'Send Welcome Email',
value: true,
description: 'Send login credentials to the user',
),
ToggleField(
name: 'isActive',
label: 'Active Account',
value: entity?.isActive ?? true,
description: 'Inactive users cannot log in',
),
if (isEdit)
InfoField(
label: 'Last Login',
value: entity.lastLogin != null
? DateFormat('MMM d, y at h:mm a').format(entity.lastLogin!)
: 'Never',
),
],
),
),
],
actions: [
if (isEdit)
FormAction(
label: 'Reset Password',
icon: Icons.lock_reset,
onTap: (context) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (_) =>
AlertDialog(
title: const Text('Reset Password?'),
content: const Text(
'This will send a password reset email to the user.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Reset'),
),
],
),
);
if (confirmed == true) {
final api = UserApi(client: SharedApiClient());
await api.resetPassword(entity.email);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Password reset email sent'),
),
);
}
}
},
),
],
);
}
@override
Map<String, dynamic> toFormData(User entity) {
return {
'name': entity.name,
'email': entity.email,
'phone': entity.phone,
'role': entity.role,
'department': entity.department,
'locationId': entity.locationId,
'isActive': entity.isActive,
};
}
@override
User fromFormData(Map<String, dynamic> data, {User? entity}) {
return User(
id: entity?.id ?? const Uuid().v4(),
schemaType: 'users',
name: data['name'],
email: data['email'],
phone: data['phone'],
role: data['role'],
department: data['department'],
locationId: data['locationId'],
isActive: data['isActive'],
lastLogin: entity?.lastLogin,
preferences: entity?.preferences,
createdAt: entity?.createdAt ?? DateTime.now(),
);
}
String _getRoleDescription(UserRole role) {
switch (role) {
case UserRole.admin:
return 'Full system access';
case UserRole.manager:
return 'Manage users and content';
case UserRole.user:
return 'Standard user access';
case UserRole.viewer:
return 'Read-only access';
}
}
Future<List<Department>> _loadDepartments() async {
// Load departments
return []; // Placeholder
}
Future<List<Location>> _loadLocationsForDepartment(String deptId) async {
// Load locations for department
return []; // Placeholder
}
}Complete Configuration
// lib/config/user_config.dart
import 'package:flutter/material.dart';
import 'package:vyuh_entity_system/vyuh_entity_system.dart';
import '../models/user.dart';
import '../api/user_api.dart';
import '../layouts/user_layouts.dart';
import '../forms/user_form.dart';
class UserConfig {
static final instance = EntityConfiguration<User>(
metadata: EntityMetadata(
identifier: 'users',
name: 'User',
pluralName: 'Users',
description: 'System users and administrators',
icon: Icons.people,
themeColor: Colors.indigo,
category: 'Administration',
route: EntityRouteBuilder.fromIdentifier('users'),
getHelp: (context) async => '''
# User Management
Manage system users, their roles, and permissions.
## Roles
- **Administrator**: Full system access
- **Manager**: Can manage users and content
- **User**: Standard access to assigned features
- **Viewer**: Read-only access
## Common Tasks
1. **Add a new user**: Click the + button and fill in user details
2. **Reset password**: Edit a user and click "Reset Password"
3. **Deactivate user**: Edit user and toggle "Active Account" off
## Permissions
User permissions are managed through roles and specific permission grants.
Contact your system administrator to modify permissions.
''',
),
api: (client) => UserApi(client: client),
layouts: UserLayouts.create(),
form: UserFormDescriptor(),
actions: EntityActions<User>(
list: [
EntityAction(
label: 'Import Users',
icon: Icons.upload_file,
onTap: (context, _) async {
// Show import dialog
final file = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['csv'],
);
if (file != null) {
// Import users from CSV
await _importUsers(context, file.files.first);
}
},
),
EntityAction(
label: 'Export Users',
icon: Icons.download,
onTap: (context, users) async {
final csv = _generateUsersCsv(users);
// Save file
final fileName = 'users_${DateTime
.now()
.millisecondsSinceEpoch}.csv';
await FileSaver.instance.saveFile(
fileName,
Uint8List.fromList(csv.codeUnits),
'csv',
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Exported ${users.length} users')),
);
}
},
),
],
single: [
EntityAction(
label: 'View Activity',
icon: Icons.history,
onTap: (context, [user]) async {
showDialog(
context: context,
builder: (_) =>
ActivityDialog(
title: 'Activity - ${user.name}',
userId: user.id,
),
);
},
),
EntityAction(
label: 'Manage Permissions',
icon: Icons.security,
onTap: (context, [user]) async {
final hasPermission = await context
.read<EntityPermissionService>()
.hasPermission(Permission.update('permissions'));
if (!hasPermission) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('You do not have permission to manage permissions'),
),
);
return;
}
if (context.mounted) {
context.go('/users/${user.id}/permissions');
}
},
),
],
),
);
static Future<void> _importUsers(BuildContext context,
PlatformFile file,) async {
// Implementation for importing users from CSV
}
static String _generateUsersCsv(List<User> users) {
final csv = StringBuffer();
// Header
csv.writeln('Name,Email,Role,Department,Status,Last Login');
// Data
for (final user in users) {
csv.writeln(
'${user.name},'
'${user.email},'
'${user.role.displayName},'
'${user.department ?? ""},'
'${user.isActive ? "Active" : "Inactive"},'
'${user.lastLogin?.toIso8601String() ?? "Never"}',
);
}
return csv.toString();
}
}Example 2: Reference Standards Management
A laboratory reference standards system with expiration tracking and usage history.
Entity Model
// lib/models/reference_standard.dart
import 'package:json_annotation/json_annotation.dart';
import 'package:vyuh_entity_system/vyuh_entity_system.dart';
part 'reference_standard.g.dart';
@JsonSerializable()
class ReferenceStandard extends EntityBase {
final String name;
final String catalogNumber;
final String? lotNumber;
final String manufacturer;
final ReferenceStandardType type;
final DateTime? expiryDate;
final StorageCondition storageCondition;
final String? locationId;
final double? concentration;
final String? concentrationUnit;
final double currentQuantity;
final String quantityUnit;
final Map<String, dynamic>? specifications;
final bool isExpired;
final bool isLowStock;
ReferenceStandard({
required super.id,
required super.schemaType,
required this.name,
required this.catalogNumber,
this.lotNumber,
required this.manufacturer,
required this.type,
this.expiryDate,
required this.storageCondition,
this.locationId,
this.concentration,
this.concentrationUnit,
required this.currentQuantity,
required this.quantityUnit,
this.specifications,
super.createdAt,
super.layout,
super.modifiers,
})
: isExpired = expiryDate != null && expiryDate.isBefore(DateTime.now()),
isLowStock = currentQuantity < 10; // Simple threshold
factory ReferenceStandard.fromJson(Map<String, dynamic> json) =>
_$ReferenceStandardFromJson(json);
@override
Map<String, dynamic> toJson() => _$ReferenceStandardToJson(this);
int get daysUntilExpiry {
if (expiryDate == null) return -1;
return expiryDate!.difference(DateTime.now()).inDays;
}
String get status {
if (isExpired) return 'Expired';
if (daysUntilExpiry <= 30) return 'Expiring Soon';
if (isLowStock) return 'Low Stock';
return 'Available';
}
Color get statusColor {
switch (status) {
case 'Expired':
return Colors.red;
case 'Expiring Soon':
return Colors.orange;
case 'Low Stock':
return Colors.amber;
default:
return Colors.green;
}
}
}
enum ReferenceStandardType {
@JsonValue('chemical')
chemical,
@JsonValue('biological')
biological,
@JsonValue('physical')
physical,
}
enum StorageCondition {
@JsonValue('room_temperature')
roomTemperature,
@JsonValue('refrigerated')
refrigerated,
@JsonValue('frozen')
frozen,
@JsonValue('ultra_low')
ultraLow,
}Dashboard Widget
// lib/widgets/reference_standard_dashboard.dart
import 'package:flutter/material.dart';
import 'package:vyuh_entity_system/vyuh_entity_system.dart';
import '../models/reference_standard.dart';
class ReferenceStandardDashboard extends EntityDashboardWidget<ReferenceStandard> {
ReferenceStandardDashboard() : super(
title: 'Reference Standards Overview',
icon: Icons.science,
gridWidth: 4,
gridHeight: 2,
);
@override
Widget build(BuildContext context, DashboardState state) {
return FutureBuilder<List<ReferenceStandard>>(
future: _loadStandards(context),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final standards = snapshot.data!;
final stats = _calculateStats(standards);
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Summary Cards
SizedBox(
height: 100,
child: Row(
children: [
Expanded(
child: _StatCard(
title: 'Total Standards',
value: stats['total'].toString(),
icon: Icons.inventory,
color: Colors.blue,
),
),
const SizedBox(width: 16),
Expanded(
child: _StatCard(
title: 'Expiring Soon',
value: stats['expiringSoon'].toString(),
icon: Icons.warning,
color: Colors.orange,
onTap: () => _showExpiringStandards(context),
),
),
const SizedBox(width: 16),
Expanded(
child: _StatCard(
title: 'Low Stock',
value: stats['lowStock'].toString(),
icon: Icons.inventory_2,
color: Colors.amber,
onTap: () => _showLowStockStandards(context),
),
),
const SizedBox(width: 16),
Expanded(
child: _StatCard(
title: 'Expired',
value: stats['expired'].toString(),
icon: Icons.dangerous,
color: Colors.red,
onTap: () => _showExpiredStandards(context),
),
),
],
),
),
const SizedBox(height: 24),
// Charts
Expanded(
child: Row(
children: [
// By Type Chart
Expanded(
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Standards by Type',
style: Theme
.of(context)
.textTheme
.titleMedium,
),
const SizedBox(height: 16),
Expanded(
child: _TypeDistributionChart(
standards: standards,
),
),
],
),
),
),
),
const SizedBox(width: 16),
// Expiry Timeline
Expanded(
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Upcoming Expirations',
style: Theme
.of(context)
.textTheme
.titleMedium,
),
const SizedBox(height: 16),
Expanded(
child: _ExpiryTimeline(
standards: standards
.where((s) => s.expiryDate != null && !s.isExpired)
.toList()
..sort((a, b) =>
a.expiryDate!.compareTo(b.expiryDate!)),
),
),
],
),
),
),
),
],
),
),
],
),
);
},
);
}
Future<List<ReferenceStandard>> _loadStandards(BuildContext context) async {
final api = context
.read<EntityProvider<ReferenceStandard>>()
.api;
return api.list();
}
Map<String, int> _calculateStats(List<ReferenceStandard> standards) {
final now = DateTime.now();
final thirtyDaysFromNow = now.add(const Duration(days: 30));
return {
'total': standards.length,
'expiringSoon': standards.where((s) =>
s.expiryDate != null &&
s.expiryDate!.isAfter(now) &&
s.expiryDate!.isBefore(thirtyDaysFromNow)
).length,
'lowStock': standards
.where((s) => s.isLowStock && !s.isExpired)
.length,
'expired': standards
.where((s) => s.isExpired)
.length,
};
}
void _showExpiringStandards(BuildContext context) {
context.go('/reference-standards?filter=expiring');
}
void _showLowStockStandards(BuildContext context) {
context.go('/reference-standards?filter=low-stock');
}
void _showExpiredStandards(BuildContext context) {
context.go('/reference-standards?filter=expired');
}
}
class _StatCard extends StatelessWidget {
final String title;
final String value;
final IconData icon;
final Color color;
final VoidCallback? onTap;
const _StatCard({
required this.title,
required this.value,
required this.icon,
required this.color,
this.onTap,
});
@override
Widget build(BuildContext context) {
return Card(
color: color.withOpacity(0.1),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color, size: 32),
const SizedBox(height: 8),
Text(
value,
style: Theme
.of(context)
.textTheme
.headlineMedium
?.copyWith(
color: color,
fontWeight: FontWeight.bold,
),
),
Text(
title,
style: Theme
.of(context)
.textTheme
.bodySmall,
textAlign: TextAlign.center,
),
],
),
),
),
);
}
}Example 3: Equipment Management with Areas
Complex entity relationships with equipment assigned to areas.
Area-Equipment Relationship Layout
// lib/layouts/equipment_areas_layout.dart
import 'package:flutter/material.dart';
import 'package:vyuh_entity_system/vyuh_entity_system.dart';
import '../models/equipment.dart';
import '../models/area.dart';
class EquipmentAreasLayout extends EntityLayout<Equipment> {
@override
Widget build(BuildContext context, EntityListViewState<Equipment> state) {
return FutureBuilder<List<Area>>(
future: _loadAreas(context),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final areas = snapshot.data!;
final equipmentByArea = _groupEquipmentByArea(state.entities, areas);
return DefaultTabController(
length: areas.length + 1,
child: Column(
children: [
TabBar(
isScrollable: true,
tabs: [
const Tab(text: 'All Equipment'),
...areas.map((area) =>
Tab(
text: area.name,
icon: _buildAreaIcon(area, equipmentByArea[area.id]?.length ?? 0),
)),
],
),
Expanded(
child: TabBarView(
children: [
// All equipment view
_buildEquipmentGrid(context, state.entities),
// Area-specific views
...areas.map((area) =>
_buildAreaView(
context,
area,
equipmentByArea[area.id] ?? [],
)),
],
),
),
],
),
);
},
);
}
Future<List<Area>> _loadAreas(BuildContext context) async {
final api = context
.read<EntityProvider<Area>>()
.api;
return api.list();
}
Map<String, List<Equipment>> _groupEquipmentByArea(List<Equipment> equipment,
List<Area> areas,) {
final grouped = <String, List<Equipment>>{};
for (final area in areas) {
grouped[area.id] = equipment
.where((e) => e.areaId == area.id)
.toList();
}
return grouped;
}
Widget _buildAreaIcon(Area area, int equipmentCount) {
return Badge(
label: Text(equipmentCount.toString()),
child: Icon(_getIconForAreaType(area.type)),
);
}
IconData _getIconForAreaType(AreaType type) {
switch (type) {
case AreaType.laboratory:
return Icons.science;
case AreaType.storage:
return Icons.warehouse;
case AreaType.office:
return Icons.business;
case AreaType.production:
return Icons.factory;
default:
return Icons.domain;
}
}
Widget _buildEquipmentGrid(BuildContext context, List<Equipment> equipment) {
return GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1.5,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: equipment.length,
itemBuilder: (context, index) {
final item = equipment[index];
return _EquipmentCard(equipment: item);
},
);
}
Widget _buildAreaView(BuildContext context,
Area area,
List<Equipment> equipment,) {
if (equipment.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inventory_2_outlined,
size: 64,
color: Theme
.of(context)
.disabledColor,
),
const SizedBox(height: 16),
Text(
'No equipment in ${area.name}',
style: Theme
.of(context)
.textTheme
.titleMedium,
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: () => _assignEquipmentToArea(context, area),
icon: const Icon(Icons.add),
label: const Text('Assign Equipment'),
),
],
),
);
}
return Column(
children: [
// Area summary
Container(
padding: const EdgeInsets.all(16),
color: Theme
.of(context)
.primaryColor
.withOpacity(0.1),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
area.name,
style: Theme
.of(context)
.textTheme
.titleLarge,
),
if (area.description != null)
Text(
area.description!,
style: Theme
.of(context)
.textTheme
.bodyMedium,
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${equipment.length} Equipment',
style: Theme
.of(context)
.textTheme
.titleMedium,
),
Text(
'${equipment
.where((e) => e.status == EquipmentStatus.operational)
.length} Operational',
style: Theme
.of(context)
.textTheme
.bodySmall,
),
],
),
],
),
),
// Equipment grid
Expanded(
child: _buildEquipmentGrid(context, equipment),
),
],
);
}
void _assignEquipmentToArea(BuildContext context, Area area) {
showDialog(
context: context,
builder: (_) => AssignEquipmentDialog(area: area),
);
}
}
class _EquipmentCard extends StatelessWidget {
final Equipment equipment;
const _EquipmentCard({required this.equipment});
@override
Widget build(BuildContext context) {
return Card(
child: InkWell(
onTap: () => context.go('/equipment/${equipment.id}'),
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
_getEquipmentIcon(equipment.type),
size: 32,
color: equipment.statusColor,
),
const Spacer(),
_StatusBadge(status: equipment.status),
],
),
const SizedBox(height: 8),
Text(
equipment.name,
style: Theme
.of(context)
.textTheme
.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
equipment.serialNumber,
style: Theme
.of(context)
.textTheme
.bodySmall,
),
const Spacer(),
if (equipment.nextCalibrationDate != null)
Row(
children: [
Icon(
Icons.schedule,
size: 16,
color: _getCalibrationColor(equipment.nextCalibrationDate!),
),
const SizedBox(width: 4),
Text(
'Cal: ${DateFormat('MMM d').format(equipment.nextCalibrationDate!)}',
style: Theme
.of(context)
.textTheme
.bodySmall
?.copyWith(
color: _getCalibrationColor(equipment.nextCalibrationDate!),
),
),
],
),
],
),
),
),
);
}
IconData _getEquipmentIcon(EquipmentType type) {
switch (type) {
case EquipmentType.analytical:
return Icons.analytics;
case EquipmentType.measurement:
return Icons.straighten;
case EquipmentType.safety:
return Icons.health_and_safety;
case EquipmentType.general:
return Icons.inventory;
}
}
Color _getCalibrationColor(DateTime date) {
final daysUntil = date
.difference(DateTime.now())
.inDays;
if (daysUntil < 0) return Colors.red;
if (daysUntil < 30) return Colors.orange;
return Colors.green;
}
}
class _StatusBadge extends StatelessWidget {
final EquipmentStatus status;
const _StatusBadge({required this.status});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: status.color.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
status.displayName,
style: TextStyle(
fontSize: 12,
color: status.color,
fontWeight: FontWeight.bold,
),
),
);
}
}Summary
These examples demonstrate:
- Complete Entity Implementation - From models to UI
- Complex Relationships - Managing related entities
- Advanced UI Patterns - Custom layouts and interactions
- Real-World Features - Import/export, dashboards, activity tracking
- Production-Ready Code - Error handling, validation, permissions
Key takeaways:
- Use the entity system's built-in features before creating custom solutions
- Leverage composition for reusable configurations
- Implement proper error handling and validation
- Design with relationships in mind
- Create rich, interactive UIs that enhance productivity
The Vyuh Entity System provides a powerful foundation for building sophisticated business applications with consistent patterns and minimal boilerplate code.