Files
network-app-mobile/lib/features/clients/presentation/clients_screen.dart

243 lines
8.3 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../shared/services/api_client.dart';
import '../../../shared/providers/auth_provider.dart';
final clientsProvider = FutureProvider.autoDispose
.family<List<Map<String, dynamic>>, String?>((ref, search) async {
final apiClient = ref.watch(apiClientProvider);
return apiClient.getClients(search: search);
});
class ClientsScreen extends ConsumerStatefulWidget {
const ClientsScreen({super.key});
@override
ConsumerState<ClientsScreen> createState() => _ClientsScreenState();
}
class _ClientsScreenState extends ConsumerState<ClientsScreen> {
final _searchController = TextEditingController();
String? _searchQuery;
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final clientsAsync = ref.watch(clientsProvider(_searchQuery));
return Scaffold(
appBar: AppBar(
title: const Text('Clients'),
),
body: Column(
children: [
// Search bar
Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Search clients...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchQuery != null
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
setState(() {
_searchQuery = null;
});
},
)
: null,
),
onSubmitted: (value) {
setState(() {
_searchQuery = value.isEmpty ? null : value;
});
},
),
),
// Client list
Expanded(
child: clientsAsync.when(
data: (clients) {
if (clients.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.people_outline,
size: 64,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
_searchQuery != null
? 'No clients found'
: 'No clients yet',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Colors.grey,
),
),
const SizedBox(height: 8),
if (_searchQuery == null)
FilledButton.icon(
onPressed: () => context.go('/clients/new'),
icon: const Icon(Icons.add),
label: const Text('Add Client'),
),
],
),
);
}
return RefreshIndicator(
onRefresh: () async {
ref.invalidate(clientsProvider(_searchQuery));
},
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: clients.length,
itemBuilder: (context, index) {
final client = clients[index];
return _ClientCard(client: client);
},
),
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stack) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: Colors.red.shade300,
),
const SizedBox(height: 16),
Text(
'Failed to load clients',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
TextButton(
onPressed: () {
ref.invalidate(clientsProvider(_searchQuery));
},
child: const Text('Retry'),
),
],
),
),
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.go('/clients/new'),
child: const Icon(Icons.add),
),
);
}
}
class _ClientCard extends StatelessWidget {
final Map<String, dynamic> client;
const _ClientCard({required this.client});
@override
Widget build(BuildContext context) {
final name = '${client['firstName']} ${client['lastName']}';
final company = client['company'] as String?;
final tags = (client['tags'] as List?)?.cast<String>() ?? [];
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: InkWell(
onTap: () => context.go('/clients/${client['id']}'),
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
child: Text(
'${client['firstName'][0]}${client['lastName'][0]}',
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
if (company != null && company.isNotEmpty) ...[
const SizedBox(height: 2),
Text(
company,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
),
],
if (tags.isNotEmpty) ...[
const SizedBox(height: 8),
Wrap(
spacing: 4,
runSpacing: 4,
children: tags.take(3).map((tag) => Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Text(
tag,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
),
)).toList(),
),
],
],
),
),
Icon(
Icons.chevron_right,
color: Colors.grey.shade400,
),
],
),
),
),
);
}
}