Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions frontend/lib/features/customers/presentation/customer_form_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../data/customers_repository.dart';
import 'customers_provider.dart';

class CustomerFormDialog extends ConsumerStatefulWidget {
final CustomerModel? customer;

const CustomerFormDialog({super.key, this.customer});

@override
ConsumerState<CustomerFormDialog> createState() => _CustomerFormDialogState();
}

class _CustomerFormDialogState extends ConsumerState<CustomerFormDialog> {
final _formKey = GlobalKey<FormState>();
late final TextEditingController _nameController;
late final TextEditingController _phoneController;
late final TextEditingController _addressController;
late final TextEditingController _creditLimitController;
late final TextEditingController _paymentTermsController;
late final TextEditingController _notesController;
bool _isLoading = false;

bool get isEditing => widget.customer != null;

@override
void initState() {
super.initState();
final c = widget.customer;
_nameController = TextEditingController(text: c?.customerName ?? '');
_phoneController = TextEditingController(text: c?.phoneNumber ?? '');
_addressController = TextEditingController(text: c?.address ?? '');
_creditLimitController = TextEditingController(text: c?.creditLimit ?? '0');
_paymentTermsController = TextEditingController(text: c?.paymentTerms.toString() ?? '0');
_notesController = TextEditingController();
}

@override
void dispose() {
_nameController.dispose();
_phoneController.dispose();
_addressController.dispose();
_creditLimitController.dispose();
_paymentTermsController.dispose();
_notesController.dispose();
super.dispose();
}

Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isLoading = true);

final data = {
'customer_name': _nameController.text.trim(),
'phone_number': _phoneController.text.trim().isEmpty ? null : _phoneController.text.trim(),
'address': _addressController.text.trim().isEmpty ? null : _addressController.text.trim(),
'credit_limit': _creditLimitController.text.trim(),
'payment_terms': int.tryParse(_paymentTermsController.text.trim()) ?? 0,
'notes': _notesController.text.trim().isEmpty ? null : _notesController.text.trim(),
};

try {
final repo = ref.read(customersRepositoryProvider);
if (isEditing) {
await repo.update(widget.customer!.customerId, data);
} else {
await repo.create(data);
}
ref.invalidate(customersProvider);
if (mounted) Navigator.of(context).pop(true);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red),
);
}
} finally {
if (mounted) setState(() => _isLoading = false);
}
}

@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(isEditing ? 'Edit Customer' : 'Add Customer'),
content: SizedBox(
width: 450,
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(labelText: 'Customer Name *'),
validator: (v) => (v == null || v.trim().isEmpty) ? 'Required' : null,
),
const SizedBox(height: 16),
TextFormField(
controller: _phoneController,
decoration: const InputDecoration(labelText: 'Phone Number'),
),
const SizedBox(height: 16),
TextFormField(
controller: _addressController,
decoration: const InputDecoration(labelText: 'Address'),
maxLines: 2,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextFormField(
controller: _creditLimitController,
decoration: const InputDecoration(labelText: 'Credit Limit'),
keyboardType: TextInputType.number,
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: _paymentTermsController,
decoration: const InputDecoration(labelText: 'Payment Terms (days)'),
keyboardType: TextInputType.number,
),
),
],
),
const SizedBox(height: 16),
TextFormField(
controller: _notesController,
decoration: const InputDecoration(labelText: 'Notes'),
maxLines: 2,
),
],
),
),
),
),
actions: [
TextButton(
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: _isLoading ? null : _submit,
child: _isLoading
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
: Text(isEditing ? 'Update' : 'Create'),
),
],
);
}
}
35 changes: 30 additions & 5 deletions frontend/lib/features/customers/presentation/customers_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,21 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/widgets/empty_state.dart';
import '../../../core/widgets/skeleton_loader.dart';
import '../data/customers_repository.dart';
import 'customers_provider.dart';
import 'customer_form_dialog.dart';

class CustomersPage extends ConsumerWidget {
const CustomersPage({super.key});

void _showAddDialog(BuildContext context) {
showDialog(context: context, builder: (_) => const CustomerFormDialog());
}

void _showEditDialog(BuildContext context, CustomerModel customer) {
showDialog(context: context, builder: (_) => CustomerFormDialog(customer: customer));
}

@override
Widget build(BuildContext context, WidgetRef ref) {
final customersAsync = ref.watch(customersProvider);
Expand All @@ -22,7 +32,11 @@ class CustomersPage extends ConsumerWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Customers', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700)),
ElevatedButton.icon(onPressed: () {}, icon: const Icon(Icons.add, size: 18), label: const Text('Add Customer')),
ElevatedButton.icon(
onPressed: () => _showAddDialog(context),
icon: const Icon(Icons.add, size: 18),
label: const Text('Add Customer'),
),
],
),
const SizedBox(height: 24),
Expand Down Expand Up @@ -52,10 +66,21 @@ class CustomersPage extends ConsumerWidget {
return ListTile(
title: Text(c.customerName, style: const TextStyle(fontWeight: FontWeight.w500)),
subtitle: Text(c.phoneNumber ?? 'No phone'),
trailing: Text('\$${c.currentBalance}', style: TextStyle(
fontWeight: FontWeight.w600,
color: double.parse(c.currentBalance) > 0 ? AppColors.warning : AppColors.success,
)),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('\$${c.currentBalance}', style: TextStyle(
fontWeight: FontWeight.w600,
color: double.tryParse(c.currentBalance) != null && double.parse(c.currentBalance) > 0 ? AppColors.warning : AppColors.success,
)),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.edit, size: 18),
onPressed: () => _showEditDialog(context, c),
tooltip: 'Edit',
),
],
),
);
},
);
Expand Down
180 changes: 180 additions & 0 deletions frontend/lib/features/products/presentation/product_form_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../data/products_repository.dart';
import 'products_provider.dart';

class ProductFormDialog extends ConsumerStatefulWidget {
final ProductModel? product;

const ProductFormDialog({super.key, this.product});

@override
ConsumerState<ProductFormDialog> createState() => _ProductFormDialogState();
}

class _ProductFormDialogState extends ConsumerState<ProductFormDialog> {
final _formKey = GlobalKey<FormState>();
late final TextEditingController _nameController;
late final TextEditingController _purchaseCostController;
late final TextEditingController _sellingPriceController;
late final TextEditingController _barcodeController;
late final TextEditingController _notesController;
late bool _isMeterBased;
late bool _allowPieceSale;
late bool _activeStatus;
bool _isLoading = false;

bool get isEditing => widget.product != null;

@override
void initState() {
super.initState();
final p = widget.product;
_nameController = TextEditingController(text: p?.productName ?? '');
_purchaseCostController = TextEditingController(text: p?.purchaseCost ?? '0');
_sellingPriceController = TextEditingController(text: p?.sellingPrice ?? '0');
_barcodeController = TextEditingController(text: p?.barcode ?? '');
_notesController = TextEditingController();
_isMeterBased = p?.isMeterBased ?? true;
_allowPieceSale = p?.allowPieceSale ?? false;
_activeStatus = p?.activeStatus ?? true;
}

@override
void dispose() {
_nameController.dispose();
_purchaseCostController.dispose();
_sellingPriceController.dispose();
_barcodeController.dispose();
_notesController.dispose();
super.dispose();
}

Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isLoading = true);

final data = <String, dynamic>{
'product_name': _nameController.text.trim(),
'is_meter_based': _isMeterBased,
'allow_piece_sale': _allowPieceSale,
'base_unit': _isMeterBased ? 'meter' : 'piece',
'purchase_cost_per_meter': _purchaseCostController.text.trim(),
'selling_price': _sellingPriceController.text.trim(),
'barcode': _barcodeController.text.trim().isEmpty ? null : _barcodeController.text.trim(),
'notes': _notesController.text.trim().isEmpty ? null : _notesController.text.trim(),
};

if (isEditing) {
data['active_status'] = _activeStatus;
}

try {
final repo = ref.read(productsRepositoryProvider);
if (isEditing) {
await repo.update(widget.product!.productId, data);
} else {
await repo.create(data);
}
ref.invalidate(productsProvider);
if (mounted) Navigator.of(context).pop(true);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red),
);
}
} finally {
if (mounted) setState(() => _isLoading = false);
}
}

@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(isEditing ? 'Edit Product' : 'Add Product'),
content: SizedBox(
width: 500,
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(labelText: 'Product Name *'),
validator: (v) => (v == null || v.trim().isEmpty) ? 'Required' : null,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextFormField(
controller: _purchaseCostController,
decoration: const InputDecoration(labelText: 'Purchase Cost'),
keyboardType: TextInputType.number,
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: _sellingPriceController,
decoration: const InputDecoration(labelText: 'Selling Price'),
keyboardType: TextInputType.number,
),
),
],
),
const SizedBox(height: 16),
TextFormField(
controller: _barcodeController,
decoration: const InputDecoration(labelText: 'Barcode'),
),
const SizedBox(height: 16),
SwitchListTile(
title: const Text('Meter-based measurement'),
subtitle: Text(_isMeterBased ? 'Measured in meters' : 'Measured in pieces'),
value: _isMeterBased,
onChanged: (v) => setState(() => _isMeterBased = v),
contentPadding: EdgeInsets.zero,
),
SwitchListTile(
title: const Text('Allow piece sale'),
value: _allowPieceSale,
onChanged: (v) => setState(() => _allowPieceSale = v),
contentPadding: EdgeInsets.zero,
),
if (isEditing)
SwitchListTile(
title: const Text('Active'),
value: _activeStatus,
onChanged: (v) => setState(() => _activeStatus = v),
contentPadding: EdgeInsets.zero,
),
const SizedBox(height: 16),
TextFormField(
controller: _notesController,
decoration: const InputDecoration(labelText: 'Notes'),
maxLines: 2,
),
],
),
),
),
),
actions: [
TextButton(
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: _isLoading ? null : _submit,
child: _isLoading
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
: Text(isEditing ? 'Update' : 'Create'),
),
],
);
}
}
Loading