388 lines
11 KiB
Dart
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'),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|