Files
senaro_dealer_app/lib/screens/admin_screen.dart
T
2026-10-01 16:03:32 +05:30

388 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../labels.dart';
import '../models/models.dart';
import '../services/auth_controller.dart';
import '../ui/feedback.dart';
class AdminScreen extends StatelessWidget {
const AdminScreen({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Column(
children: [
const TabBar(
tabs: [
Tab(text: 'Dealers'),
Tab(text: 'Sales reps'),
],
),
const Expanded(
child: TabBarView(
children: [
_DealersTab(),
_RepsTab(),
],
),
),
],
),
);
}
}
class _DealersTab extends StatefulWidget {
const _DealersTab();
@override
State<_DealersTab> createState() => _DealersTabState();
}
class _DealersTabState extends State<_DealersTab> {
final _name = TextEditingController();
final _code = TextEditingController();
final _address = TextEditingController();
final _phone = TextEditingController();
String? _district;
int _districtField = 0;
List<Dealer> _dealers = [];
bool _loading = true;
bool _saving = false;
@override
void initState() {
super.initState();
_load();
}
@override
void dispose() {
_name.dispose();
_code.dispose();
_address.dispose();
_phone.dispose();
super.dispose();
}
Future<void> _load() async {
try {
final dealers = await context.read<AuthController>().api.adminDealers();
if (!mounted) return;
setState(() {
_dealers = dealers;
_loading = false;
});
} catch (error) {
if (!mounted) return;
setState(() => _loading = false);
showError(context, error);
}
}
Future<void> _delete(Dealer dealer) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete dealer'),
content: Text(
'Delete ${dealer.name}? Its login, stock, and plans will be removed.',
),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
FilledButton(onPressed: () => Navigator.pop(context, true), child: const Text('Delete')),
],
),
);
if (confirmed != true || !mounted) return;
try {
await context.read<AuthController>().api.deleteDealer(dealer.id);
await _load();
} catch (error) {
if (mounted) showError(context, error);
}
}
Future<void> _save() async {
setState(() => _saving = true);
try {
await context.read<AuthController>().api.createDealer({
'name': _name.text.trim(),
'code': _code.text.trim(),
'district': _district ?? '',
'address': _address.text.trim(),
'phone': _phone.text.trim(),
});
_name.clear();
_code.clear();
_address.clear();
_phone.clear();
_district = null;
_districtField++;
await _load();
} catch (error) {
if (mounted) showError(context, error);
} finally {
if (mounted) setState(() => _saving = false);
}
}
@override
Widget build(BuildContext context) {
if (_loading) return const Center(child: CircularProgressIndicator());
return ListView(
padding: const EdgeInsets.all(16),
children: [
Text('New dealer', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 12),
TextField(controller: _name, decoration: const InputDecoration(labelText: 'Name')),
const SizedBox(height: 12),
TextField(controller: _code, decoration: const InputDecoration(labelText: 'Code')),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
key: ValueKey(_districtField),
initialValue: _district,
isExpanded: true,
decoration: const InputDecoration(labelText: 'District'),
items: [
for (final district in sriLankaDistricts)
DropdownMenuItem(value: district, child: Text(district)),
],
onChanged: (value) => setState(() => _district = value),
),
const SizedBox(height: 12),
TextField(controller: _address, decoration: const InputDecoration(labelText: 'Address')),
const SizedBox(height: 12),
TextField(
controller: _phone,
keyboardType: TextInputType.phone,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: const InputDecoration(
labelText: 'Contact phone',
hintText: '0715127342',
helperText: 'This number becomes the dealer login',
),
),
const SizedBox(height: 12),
FilledButton(
onPressed: _saving ? null : _save,
child: Text(_saving ? 'Saving' : 'Create dealer'),
),
const SizedBox(height: 20),
for (final dealer in _dealers)
ListTile(
contentPadding: EdgeInsets.zero,
title: Text(dealer.name),
subtitle: Text('${dealer.code}${dealer.district == null ? '' : ' · ${dealer.district}'}'),
trailing: IconButton(
tooltip: 'Delete',
onPressed: () => _delete(dealer),
icon: const Icon(Icons.delete_outline),
),
),
],
);
}
}
class _RepsTab extends StatefulWidget {
const _RepsTab();
@override
State<_RepsTab> createState() => _RepsTabState();
}
class _RepsTabState extends State<_RepsTab> {
final _name = TextEditingController();
final _phone = TextEditingController();
final _password = TextEditingController();
List<Dealer> _dealers = [];
List<AppUser> _reps = [];
final Set<String> _selected = {};
bool _loading = true;
bool _saving = false;
@override
void initState() {
super.initState();
_load();
}
@override
void dispose() {
_name.dispose();
_phone.dispose();
_password.dispose();
super.dispose();
}
Future<void> _load() async {
final api = context.read<AuthController>().api;
try {
final results = await Future.wait([api.adminDealers(), api.salesReps()]);
if (!mounted) return;
setState(() {
_dealers = results[0] as List<Dealer>;
_reps = results[1] as List<AppUser>;
_loading = false;
});
} catch (error) {
if (!mounted) return;
setState(() => _loading = false);
showError(context, error);
}
}
Future<void> _save() async {
setState(() => _saving = true);
try {
await context.read<AuthController>().api.createSalesRep({
'name': _name.text.trim(),
'phone': _phone.text.trim(),
'password': _password.text,
'dealerIds': _selected.toList(),
});
_name.clear();
_phone.clear();
_password.clear();
_selected.clear();
await _load();
} catch (error) {
if (mounted) showError(context, error);
} finally {
if (mounted) setState(() => _saving = false);
}
}
Future<void> _changeAssignment(AppUser rep, {required bool assign}) async {
final choices = _dealers.where((dealer) {
final has = rep.assignedDealerIds.contains(dealer.id);
return assign ? !has : has;
}).toList();
if (choices.isEmpty) {
showError(context, assign ? 'Every dealer is already assigned' : 'No dealers to remove');
return;
}
String? dealerId = choices.first.id;
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(assign ? 'Assign dealer' : 'Remove dealer'),
content: DropdownButtonFormField<String>(
initialValue: dealerId,
items: [
for (final dealer in choices)
DropdownMenuItem(value: dealer.id, child: Text(dealer.name)),
],
onChanged: (value) => dealerId = value,
),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
FilledButton(onPressed: () => Navigator.pop(context, true), child: const Text('Save')),
],
),
);
if (confirmed != true || dealerId == null || !mounted) return;
try {
await context.read<AuthController>().api.assignDealer(
salesRepId: rep.id,
dealerId: dealerId!,
assigned: assign,
);
await _load();
} catch (error) {
if (mounted) showError(context, error);
}
}
String _dealerNames(AppUser rep) {
if (rep.assignedDealerIds.isEmpty) return 'No dealers assigned';
return rep.assignedDealerIds
.map((id) {
for (final dealer in _dealers) {
if (dealer.id == id) return dealer.name;
}
return id;
})
.join(', ');
}
@override
Widget build(BuildContext context) {
if (_loading) return const Center(child: CircularProgressIndicator());
return ListView(
padding: const EdgeInsets.all(16),
children: [
Text('New sales rep', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 12),
TextField(controller: _name, decoration: const InputDecoration(labelText: 'Name')),
const SizedBox(height: 12),
TextField(
controller: _phone,
keyboardType: TextInputType.phone,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: const InputDecoration(labelText: 'Phone', hintText: '0715127342'),
),
const SizedBox(height: 12),
TextField(
controller: _password,
obscureText: true,
decoration: const InputDecoration(labelText: 'Password'),
),
const SizedBox(height: 8),
for (final dealer in _dealers)
CheckboxListTile(
contentPadding: EdgeInsets.zero,
title: Text(dealer.name),
value: _selected.contains(dealer.id),
onChanged: (checked) {
setState(() {
if (checked == true) {
_selected.add(dealer.id);
} else {
_selected.remove(dealer.id);
}
});
},
),
FilledButton(
onPressed: _saving ? null : _save,
child: Text(_saving ? 'Saving' : 'Create sales rep'),
),
const SizedBox(height: 20),
for (final rep in _reps)
Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(rep.name, style: Theme.of(context).textTheme.titleMedium),
Text(rep.phone),
const SizedBox(height: 4),
Text(_dealerNames(rep)),
Row(
children: [
TextButton(
onPressed: () => _changeAssignment(rep, assign: true),
child: const Text('Assign'),
),
TextButton(
onPressed: () => _changeAssignment(rep, assign: false),
child: const Text('Remove'),
),
],
),
],
),
),
),
],
);
}
}