Done Category, Product Catalogue, Customer, Vendor, Purchase Invoice Module

This commit is contained in:
2026-08-28 11:30:18 +05:30
parent 0d13833679
commit 189f49ed07
118 changed files with 12624 additions and 4004 deletions

View File

@@ -56,131 +56,185 @@ class _AddTaskSheetState extends ConsumerState<AddTaskSheet> {
),
child: SafeArea(
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(_isEditing ? 'Edit Task' : 'New Task', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
IconButton(icon: const Icon(LucideIcons.x), onPressed: () => Navigator.pop(context)),
],
),
const SizedBox(height: 16),
PremiumTextField(
labelText: 'Task Title',
initialValue: _isEditing ? _title : null,
prefixIcon: const Icon(LucideIcons.checkSquare),
validator: (val) => val == null || val.isEmpty ? 'Required' : null,
onSaved: (val) => _title = val!,
),
const SizedBox(height: 16),
PremiumTextField(
maxLines: 3,
labelText: 'Description (Optional)',
initialValue: _isEditing ? _description : null,
prefixIcon: const Icon(LucideIcons.alignLeft),
onSaved: (val) => _description = val ?? '',
),
const SizedBox(height: 16),
Autocomplete<User>(
initialValue: _isEditing && _assigneeName.isNotEmpty
? TextEditingValue(text: _assigneeName)
: null,
optionsBuilder: (TextEditingValue textEditingValue) async {
if (textEditingValue.text.isEmpty) {
return const Iterable<User>.empty();
}
try {
return await ref.read(userSearchProvider(textEditingValue.text).future);
} catch (e) {
return const Iterable<User>.empty();
}
},
displayStringForOption: (User option) => '${option.name} (${option.email})',
onSelected: (User selection) {
_assigneeName = selection.name;
},
optionsViewBuilder: (context, onSelected, options) {
return Align(
alignment: Alignment.topLeft,
child: Material(
elevation: 4,
borderRadius: BorderRadius.circular(12),
color: Colors.white,
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 200, maxWidth: 300),
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final User option = options.elementAt(index);
return ListTile(
title: Text(option.name, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(option.email, style: const TextStyle(color: Colors.grey)),
onTap: () => onSelected(option),
);
},
),
key: _formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_isEditing ? 'Edit Task' : 'New Task',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
);
},
fieldViewBuilder: (context, textEditingController, focusNode, onFieldSubmitted) {
return PremiumTextField(
controller: textEditingController,
focusNode: focusNode,
labelText: 'Assignee Name (Optional)',
prefixIcon: const Icon(LucideIcons.user),
onSaved: (val) {
if (_assigneeName.isEmpty) _assigneeName = val ?? '';
},
);
},
),
const SizedBox(height: 16),
SwitchListTile(
title: const Text('Is Billable?'),
subtitle: const Text('Task time will be invoiced'),
value: _isBillable,
onChanged: (val) {
setState(() {
_isBillable = val;
});
},
),
if (_isBillable) ...[
IconButton(
icon: const Icon(LucideIcons.x),
onPressed: () => Navigator.pop(context),
),
],
),
const SizedBox(height: 16),
PremiumTextField(
keyboardType: const TextInputType.numberWithOptions(decimal: true),
labelText: 'Hourly Rate',
initialValue: _isEditing && _hourlyRate > 0 ? _hourlyRate.toString() : null,
prefixIcon: const Icon(LucideIcons.indianRupee),
validator: (val) => _isBillable && (val == null || val.isEmpty) ? 'Required for billable tasks' : null,
onSaved: (val) => _hourlyRate = double.tryParse(val ?? '0') ?? 0,
labelText: 'Task Title',
initialValue: _isEditing ? _title : null,
prefixIcon: const Icon(LucideIcons.checkSquare),
validator: (val) =>
val == null || val.isEmpty ? 'Required' : null,
onSaved: (val) => _title = val!,
),
const SizedBox(height: 16),
PremiumTextField(
maxLines: 3,
labelText: 'Description (Optional)',
initialValue: _isEditing ? _description : null,
prefixIcon: const Icon(LucideIcons.alignLeft),
onSaved: (val) => _description = val ?? '',
),
const SizedBox(height: 16),
Autocomplete<User>(
initialValue: _isEditing && _assigneeName.isNotEmpty
? TextEditingValue(text: _assigneeName)
: null,
optionsBuilder: (TextEditingValue textEditingValue) async {
if (textEditingValue.text.isEmpty) {
return const Iterable<User>.empty();
}
try {
return await ref.read(
userSearchProvider(textEditingValue.text).future,
);
} catch (e) {
return const Iterable<User>.empty();
}
},
displayStringForOption: (User option) =>
'${option.name} (${option.email})',
onSelected: (User selection) {
_assigneeName = selection.name;
},
optionsViewBuilder: (context, onSelected, options) {
return Align(
alignment: Alignment.topLeft,
child: Material(
elevation: 4,
borderRadius: BorderRadius.circular(12),
color: Colors.white,
child: ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 200,
maxWidth: 300,
),
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final User option = options.elementAt(index);
return ListTile(
title: Text(
option.name,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
subtitle: Text(
option.email,
style: const TextStyle(color: Colors.grey),
),
onTap: () => onSelected(option),
);
},
),
),
),
);
},
fieldViewBuilder:
(
context,
textEditingController,
focusNode,
onFieldSubmitted,
) {
return PremiumTextField(
controller: textEditingController,
focusNode: focusNode,
labelText: 'Assignee Name (Optional)',
prefixIcon: const Icon(LucideIcons.user),
onSaved: (val) {
if (_assigneeName.isEmpty)
_assigneeName = val ?? '';
},
);
},
),
const SizedBox(height: 16),
SwitchListTile(
title: const Text('Is Billable?'),
subtitle: const Text('Task time will be invoiced'),
value: _isBillable,
onChanged: (val) {
setState(() {
_isBillable = val;
});
},
),
if (_isBillable) ...[
const SizedBox(height: 16),
PremiumTextField(
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
labelText: 'Hourly Rate',
initialValue: _isEditing && _hourlyRate > 0
? _hourlyRate.toString()
: null,
prefixIcon: const Icon(LucideIcons.indianRupee),
validator: (val) =>
_isBillable && (val == null || val.isEmpty)
? 'Required for billable tasks'
: null,
onSaved: (val) =>
_hourlyRate = double.tryParse(val ?? '0') ?? 0,
),
],
const SizedBox(height: 24),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
onPressed: _isLoading ? null : _submit,
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: Text(
_isEditing ? 'Save Changes' : 'Create Task',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 24),
],
const SizedBox(height: 24),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: _isLoading ? null : _submit,
child: _isLoading
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
: Text(_isEditing ? 'Save Changes' : 'Create Task', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
),
const SizedBox(height: 24),
],
),
),
),
),
),
);
}
@@ -216,7 +270,9 @@ class _AddTaskSheetState extends ConsumerState<AddTaskSheet> {
if (mounted) Navigator.pop(context);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $e')));
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Error: $e')));
}
} finally {
if (mounted) setState(() => _isLoading = false);