Files
2026-05-27 03:11:45 -04:00

279 lines
8.7 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../providers/dashboard_providers.dart';
import '../../../core/models/reservation.dart';
class ContractsScreen extends ConsumerStatefulWidget {
const ContractsScreen({super.key});
@override
ConsumerState<ContractsScreen> createState() => _ContractsScreenState();
}
class _ContractsScreenState extends ConsumerState<ContractsScreen> {
final _searchCtrl = TextEditingController();
String _search = '';
List<Reservation> _visibleContracts(List<Reservation> reservations) {
final query = _search.trim().toLowerCase();
return reservations.where((reservation) {
if (reservation.contractNumber == null) {
return false;
}
if (query.isEmpty) {
return true;
}
final customer = reservation.customer;
final vehicle = reservation.vehicle;
final haystack = [
customer?.fullName,
customer?.email,
vehicle?.displayName,
vehicle?.licensePlate,
reservation.contractNumber,
reservation.invoiceNumber,
].whereType<String>().join(' ').toLowerCase();
return haystack.contains(query);
}).toList();
}
@override
void dispose() {
_searchCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final ReservationQuery params = (
page: 1,
pageSize: 100,
status: null,
source: null,
search: null,
customerId: null,
);
final asyncData = ref.watch(reservationsProvider(params));
return Scaffold(
appBar: AppBar(
title: const Text('Contracts'),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(56),
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 8),
child: TextField(
controller: _searchCtrl,
decoration: InputDecoration(
hintText: 'Search by customer, vehicle, contract…',
prefixIcon: const Icon(Icons.search, size: 20),
suffixIcon: _search.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear, size: 20),
onPressed: () {
_searchCtrl.clear();
setState(() => _search = '');
},
)
: null,
isDense: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Theme.of(context).colorScheme.surface,
),
onChanged: (v) => setState(() => _search = v),
),
),
),
),
body: asyncData.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Error: $e')),
data: (response) {
final contracts = _visibleContracts(response.data);
if (contracts.isEmpty) {
return const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.description_outlined,
size: 64, color: Color(0xFF9CA3AF)),
SizedBox(height: 12),
Text(
'No contracts yet',
style: TextStyle(
fontSize: 16,
color: Color(0xFF6B7280),
),
),
],
),
);
}
return RefreshIndicator(
onRefresh: () async =>
ref.invalidate(reservationsProvider(params)),
child: ListView.separated(
padding: const EdgeInsets.all(12),
itemCount: contracts.length,
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemBuilder: (_, i) => _ContractCard(reservation: contracts[i]),
),
);
},
),
);
}
}
class _ContractCard extends StatelessWidget {
final Reservation reservation;
const _ContractCard({required this.reservation});
@override
Widget build(BuildContext context) {
final r = reservation;
final vehicle = r.vehicle;
final customer = r.customer;
return Card(
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
child: InkWell(
borderRadius: BorderRadius.circular(10),
onTap: () =>
context.push('/dashboard/reservations/${r.id}/contract'),
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.description_outlined,
size: 18, color: Color(0xFF1A56DB)),
const SizedBox(width: 6),
Text(
r.contractNumber!,
style: const TextStyle(
fontWeight: FontWeight.w700,
fontSize: 15,
color: Color(0xFF1A56DB),
),
),
const Spacer(),
_StatusChip(status: r.status),
],
),
if (r.invoiceNumber != null) ...[
const SizedBox(height: 4),
Text(
'Invoice: ${r.invoiceNumber}',
style: const TextStyle(
fontSize: 12, color: Color(0xFF6B7280)),
),
],
const SizedBox(height: 10),
Row(
children: [
const Icon(Icons.person_outline,
size: 16, color: Color(0xFF6B7280)),
const SizedBox(width: 4),
Expanded(
child: Text(
customer?.fullName ?? '',
style: const TextStyle(fontSize: 13),
),
),
],
),
const SizedBox(height: 4),
Row(
children: [
const Icon(Icons.directions_car_outlined,
size: 16, color: Color(0xFF6B7280)),
const SizedBox(width: 4),
Expanded(
child: Text(
vehicle?.displayName ?? '',
style: const TextStyle(fontSize: 13),
),
),
],
),
const SizedBox(height: 10),
Row(
children: [
const Icon(Icons.calendar_today_outlined,
size: 14, color: Color(0xFF9CA3AF)),
const SizedBox(width: 4),
Text(
'${_fmt(r.startDate)}${_fmt(r.endDate)}',
style: const TextStyle(
fontSize: 12, color: Color(0xFF6B7280)),
),
const Spacer(),
Text(
'${(r.totalAmount / 100).toStringAsFixed(2)} DA',
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 13,
),
),
],
),
],
),
),
),
);
}
String _fmt(DateTime d) =>
'${d.day.toString().padLeft(2, '0')}/${d.month.toString().padLeft(2, '0')}/${d.year}';
}
class _StatusChip extends StatelessWidget {
final String status;
const _StatusChip({required this.status});
@override
Widget build(BuildContext context) {
final (label, bg, fg) = switch (status) {
'CONFIRMED' => ('Confirmed', const Color(0xFFEBF5FB), const Color(0xFF1A56DB)),
'ACTIVE' => ('Active', const Color(0xFFECFDF5), const Color(0xFF059669)),
'COMPLETED' => ('Completed', const Color(0xFFF0FDF4), const Color(0xFF16A34A)),
'CLOSED' => ('Closed', const Color(0xFFF9FAFB), const Color(0xFF6B7280)),
_ => (status, const Color(0xFFF9FAFB), const Color(0xFF6B7280)),
};
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(6),
),
child: Text(
label,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: fg,
),
),
);
}
}