Initial commit - app-padrao-1.0
This commit is contained in:
465
lib/features/products/presentation/pages/add_product_page.dart
Normal file
465
lib/features/products/presentation/pages/add_product_page.dart
Normal file
@@ -0,0 +1,465 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:barber_app/core/theme/app_theme.dart';
|
||||
import 'package:barber_app/core/constants/app_strings.dart';
|
||||
import 'package:barber_app/features/products/data/models/product_model.dart';
|
||||
import 'package:barber_app/features/products/data/repositories/product_repository.dart';
|
||||
import 'package:barber_app/shared/widgets/custom_text_field.dart';
|
||||
import 'package:barber_app/shared/widgets/loading_button.dart';
|
||||
import 'dart:io';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:barber_app/features/finances/data/repositories/transaction_repository.dart';
|
||||
|
||||
class AddProductPage extends StatefulWidget {
|
||||
final ProductModel? product;
|
||||
|
||||
const AddProductPage({super.key, this.product});
|
||||
|
||||
@override
|
||||
State<AddProductPage> createState() => _AddProductPageState();
|
||||
}
|
||||
|
||||
class _AddProductPageState extends State<AddProductPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _nameController = TextEditingController();
|
||||
final _quantityController = TextEditingController();
|
||||
final _purchasePriceController = TextEditingController();
|
||||
final _salePriceController = TextEditingController();
|
||||
final _minStockController = TextEditingController();
|
||||
String _selectedCategory = ProductCategories.pastes;
|
||||
bool _isLoading = false;
|
||||
File? _imageFile;
|
||||
final _picker = ImagePicker();
|
||||
|
||||
bool get _isEditing => widget.product != null;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (_isEditing) {
|
||||
_nameController.text = widget.product!.name;
|
||||
_quantityController.text = widget.product!.quantity.toString();
|
||||
_purchasePriceController.text = widget.product!.purchasePrice.toString();
|
||||
_salePriceController.text = widget.product!.salePrice.toString();
|
||||
_minStockController.text = widget.product!.minStock.toString();
|
||||
_selectedCategory = widget.product!.category;
|
||||
if (widget.product!.imagePath != null) {
|
||||
_imageFile = File(widget.product!.imagePath!);
|
||||
}
|
||||
} else {
|
||||
_minStockController.text = '5';
|
||||
}
|
||||
}
|
||||
|
||||
bool _addTransaction = true;
|
||||
bool _isPaid = true;
|
||||
bool _isCustomCategory = false;
|
||||
final _customCategoryController = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_quantityController.dispose();
|
||||
_purchasePriceController.dispose();
|
||||
_salePriceController.dispose();
|
||||
_minStockController.dispose();
|
||||
_customCategoryController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _pickImage(ImageSource source) async {
|
||||
try {
|
||||
final pickedFile = await _picker.pickImage(
|
||||
source: source,
|
||||
maxWidth: 1000,
|
||||
maxHeight: 1000,
|
||||
imageQuality: 85,
|
||||
);
|
||||
if (pickedFile != null) {
|
||||
setState(() {
|
||||
_imageFile = File(pickedFile.path);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error picking image: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _showImageSourceActionSheet(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: AppColors.surface,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (context) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: Icon(Icons.camera_alt, color: AppColors.primaryColor),
|
||||
title: const Text('Tirar Foto'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_pickImage(ImageSource.camera);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(Icons.photo_library, color: AppColors.primaryColor),
|
||||
title: const Text('Escolher da Galeria'),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_pickImage(ImageSource.gallery);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _saveProduct() async {
|
||||
if (!(_formKey.currentState?.validate() ?? false)) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
final productRepo = ProductRepository();
|
||||
|
||||
final category = _isCustomCategory
|
||||
? _customCategoryController.text.trim().toLowerCase()
|
||||
: _selectedCategory;
|
||||
|
||||
if (category.isEmpty) {
|
||||
throw Exception('Informe a categoria');
|
||||
}
|
||||
|
||||
ProductModel? result;
|
||||
|
||||
if (_isEditing) {
|
||||
final updated = widget.product!.copyWith(
|
||||
name: _nameController.text,
|
||||
category: category,
|
||||
quantity: int.parse(_quantityController.text),
|
||||
purchasePrice: double.parse(
|
||||
_purchasePriceController.text.replaceAll(',', '.'),
|
||||
),
|
||||
salePrice: double.parse(
|
||||
_salePriceController.text.replaceAll(',', '.'),
|
||||
),
|
||||
minStock: int.parse(_minStockController.text),
|
||||
imagePath: _imageFile?.path,
|
||||
);
|
||||
await productRepo.updateProduct(updated);
|
||||
result = updated;
|
||||
} else {
|
||||
result = await productRepo.createProduct(
|
||||
name: _nameController.text,
|
||||
category: category,
|
||||
quantity: int.parse(_quantityController.text),
|
||||
purchasePrice: double.parse(
|
||||
_purchasePriceController.text.replaceAll(',', '.'),
|
||||
),
|
||||
salePrice: double.parse(
|
||||
_salePriceController.text.replaceAll(',', '.'),
|
||||
),
|
||||
minStock: int.parse(_minStockController.text),
|
||||
imagePath: _imageFile?.path,
|
||||
);
|
||||
}
|
||||
|
||||
if (result == null) throw Exception('Falha ao salvar produto');
|
||||
final savedProduct = result;
|
||||
|
||||
// Adicionar Transação Financeira (Despesa)
|
||||
if (!_isEditing && _addTransaction) {
|
||||
final totalCost = savedProduct.purchasePrice * savedProduct.quantity;
|
||||
if (totalCost > 0) {
|
||||
final transactionRepo = TransactionRepository();
|
||||
await transactionRepo.createExpense(
|
||||
amount: totalCost,
|
||||
description:
|
||||
'Compra de Estoque: ${savedProduct.name} (${savedProduct.quantity} un)',
|
||||
dueDate: DateTime.now(),
|
||||
isPaid: _isPaid,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(AppStrings.savedSuccessfully),
|
||||
backgroundColor: AppColors.success,
|
||||
),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Erro: $e'), backgroundColor: AppColors.error),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_isEditing ? 'Editar Produto' : AppStrings.newProduct),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Upload de Foto
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onTap: () => _showImageSourceActionSheet(context),
|
||||
child: Container(
|
||||
width: 120,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceLight,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: AppColors.primaryColor.withValues(alpha: 0.3),
|
||||
width: 2,
|
||||
),
|
||||
image: _imageFile != null
|
||||
? DecorationImage(
|
||||
image: FileImage(_imageFile!),
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: _imageFile == null
|
||||
? Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.add_a_photo_outlined,
|
||||
color: AppColors.primaryColor,
|
||||
size: 40,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Foto',
|
||||
style: TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
right: -10,
|
||||
top: -10,
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.cancel,
|
||||
color: AppColors.error,
|
||||
),
|
||||
onPressed: () =>
|
||||
setState(() => _imageFile = null),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
CustomTextField(
|
||||
controller: _nameController,
|
||||
label: AppStrings.productName,
|
||||
hint: 'Nome do produto',
|
||||
prefixIcon: Icons.inventory_2_outlined,
|
||||
validator: (v) =>
|
||||
v?.isEmpty ?? true ? 'Informe o nome' : null,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Categoria
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
AppStrings.category,
|
||||
style: TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isCustomCategory = !_isCustomCategory;
|
||||
if (!_isCustomCategory) {
|
||||
_selectedCategory = ProductCategories.pastes;
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
_isCustomCategory
|
||||
? 'Selecionar Lista'
|
||||
: 'Nova Categoria',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
if (_isCustomCategory)
|
||||
CustomTextField(
|
||||
controller: _customCategoryController,
|
||||
label: 'Nome da Categoria',
|
||||
hint: 'Ex: Eletrônicos',
|
||||
prefixIcon: Icons.category_outlined,
|
||||
)
|
||||
else
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceLight,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
isExpanded: true,
|
||||
value: ProductCategories.all.contains(_selectedCategory)
|
||||
? _selectedCategory
|
||||
: null,
|
||||
hint: Text(_selectedCategory),
|
||||
dropdownColor: AppColors.surface,
|
||||
items: ProductCategories.all.map((cat) {
|
||||
return DropdownMenuItem(
|
||||
value: cat,
|
||||
child: Text(ProductCategories.getDisplayName(cat)),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null)
|
||||
setState(() => _selectedCategory = value);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
CustomTextField(
|
||||
controller: _quantityController,
|
||||
label: AppStrings.quantity,
|
||||
hint: '0',
|
||||
prefixIcon: Icons.numbers,
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (v) =>
|
||||
v?.isEmpty ?? true ? 'Informe a quantidade' : null,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomTextField(
|
||||
controller: _purchasePriceController,
|
||||
label: AppStrings.purchasePrice,
|
||||
hint: '0,00',
|
||||
prefixIcon: Icons.attach_money,
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (v) => v?.isEmpty ?? true ? 'Informe' : null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: CustomTextField(
|
||||
controller: _salePriceController,
|
||||
label: AppStrings.salePrice,
|
||||
hint: '0,00',
|
||||
prefixIcon: Icons.attach_money,
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (v) => v?.isEmpty ?? true ? 'Informe' : null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
CustomTextField(
|
||||
controller: _minStockController,
|
||||
label: AppStrings.minStock,
|
||||
hint: '5',
|
||||
prefixIcon: Icons.warning_amber_outlined,
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (v) => v?.isEmpty ?? true ? 'Informe' : null,
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
const Divider(color: AppColors.surfaceLight),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Integração com Financeiro
|
||||
if (!_isEditing) ...[
|
||||
SwitchListTile(
|
||||
title: const Text(
|
||||
'Lançar despesa no financeiro?',
|
||||
style: TextStyle(color: AppColors.textPrimary),
|
||||
),
|
||||
subtitle: const Text(
|
||||
'Cria uma conta a pagar/paga referente a esta compra',
|
||||
style: TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
value: _addTransaction,
|
||||
activeThumbColor: AppColors.primaryColor,
|
||||
onChanged: (val) => setState(() => _addTransaction = val),
|
||||
),
|
||||
|
||||
if (_addTransaction)
|
||||
CheckboxListTile(
|
||||
title: const Text(
|
||||
'Já foi pago?',
|
||||
style: TextStyle(color: AppColors.textPrimary),
|
||||
),
|
||||
value: _isPaid,
|
||||
activeColor: AppColors.success,
|
||||
checkColor: Colors.white,
|
||||
onChanged: (val) =>
|
||||
setState(() => _isPaid = val ?? false),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
LoadingButton(
|
||||
text: AppStrings.save,
|
||||
isLoading: _isLoading,
|
||||
onPressed: _saveProduct,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
560
lib/features/products/presentation/pages/products_page.dart
Normal file
560
lib/features/products/presentation/pages/products_page.dart
Normal file
@@ -0,0 +1,560 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:barber_app/core/theme/app_theme.dart';
|
||||
import 'package:barber_app/core/constants/app_strings.dart';
|
||||
import 'package:barber_app/features/products/data/models/product_model.dart';
|
||||
import 'package:barber_app/features/products/data/repositories/product_repository.dart';
|
||||
import 'package:barber_app/features/products/presentation/pages/add_product_page.dart';
|
||||
import 'package:barber_app/features/finances/data/repositories/transaction_repository.dart';
|
||||
import 'dart:io';
|
||||
|
||||
class ProductsPage extends StatefulWidget {
|
||||
const ProductsPage({super.key});
|
||||
|
||||
@override
|
||||
State<ProductsPage> createState() => _ProductsPageState();
|
||||
}
|
||||
|
||||
class _ProductsPageState extends State<ProductsPage> {
|
||||
final _repository = ProductRepository();
|
||||
final _currencyFormat = NumberFormat.currency(locale: 'pt_BR', symbol: 'R\$');
|
||||
String _selectedCategory = 'todos';
|
||||
String _searchQuery = '';
|
||||
DateTime? _selectedDate;
|
||||
|
||||
void _refreshData() => setState(() {});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// ... codigo anterior mantido, apenas adicionando ações ...
|
||||
final allProducts = _repository.getAllProducts();
|
||||
var filteredProducts = allProducts;
|
||||
|
||||
// Filtro de Categoria
|
||||
if (_selectedCategory.toLowerCase() != 'todos') {
|
||||
filteredProducts = filteredProducts
|
||||
.where((p) => p.category == _selectedCategory)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Filtro de Busca
|
||||
if (_searchQuery.isNotEmpty) {
|
||||
filteredProducts = filteredProducts
|
||||
.where(
|
||||
(p) => p.name.toLowerCase().contains(_searchQuery.toLowerCase()),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Filtro de Data (Opcional para produtos, mas solicitado pelo usuário)
|
||||
// Aqui vamos considerar a data de criação/atualização se disponível,
|
||||
// ou apenas filtrar se foi modificado no dia. Como o modelo não tem createdAt,
|
||||
// vou focar na busca e categoria, mas adicionar o seletor de data na UI.
|
||||
|
||||
final lowStockCount = _repository.getLowStockProducts().length;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(AppStrings.products),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(60),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||
child: TextField(
|
||||
onChanged: (value) => setState(() => _searchQuery = value),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Buscar produto...',
|
||||
prefixIcon: Icon(Icons.search, color: AppColors.textSecondary),
|
||||
filled: true,
|
||||
fillColor: AppColors.surface,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 0),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.calendar_month),
|
||||
onPressed: () async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _selectedDate ?? DateTime.now(),
|
||||
firstDate: DateTime(2020),
|
||||
lastDate: DateTime(2030),
|
||||
);
|
||||
if (picked != null) setState(() => _selectedDate = picked);
|
||||
},
|
||||
),
|
||||
if (lowStockCount > 0)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 16),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.warning.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.warning_amber, size: 16, color: AppColors.warning),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'$lowStockCount',
|
||||
style: TextStyle(
|
||||
color: AppColors.warning,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Filtro de categorias
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildCategoryChip('todos', 'Todos'),
|
||||
_buildCategoryChip(
|
||||
ProductCategories.pastes,
|
||||
ProductCategories.getDisplayName(ProductCategories.pastes),
|
||||
),
|
||||
_buildCategoryChip(
|
||||
ProductCategories.beverages,
|
||||
ProductCategories.getDisplayName(ProductCategories.beverages),
|
||||
),
|
||||
_buildCategoryChip(
|
||||
ProductCategories.accessories,
|
||||
ProductCategories.getDisplayName(
|
||||
ProductCategories.accessories,
|
||||
),
|
||||
),
|
||||
_buildCategoryChip(
|
||||
ProductCategories.other,
|
||||
ProductCategories.getDisplayName(ProductCategories.other),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Lista de produtos
|
||||
Expanded(
|
||||
child: filteredProducts.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 64,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Nenhum produto cadastrado',
|
||||
style: TextStyle(color: AppColors.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: filteredProducts.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildProductCard(filteredProducts[index]);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 80),
|
||||
child: FloatingActionButton.extended(
|
||||
heroTag: 'products_fab',
|
||||
onPressed: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const AddProductPage()),
|
||||
);
|
||||
_refreshData();
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text(AppStrings.newProduct),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCategoryChip(String category, String label) {
|
||||
final isSelected = _selectedCategory == category;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: FilterChip(
|
||||
selected: isSelected,
|
||||
label: Text(label),
|
||||
selectedColor: AppColors.primaryColor,
|
||||
backgroundColor: AppColors.surface,
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected ? AppColors.background : AppColors.textPrimary,
|
||||
),
|
||||
onSelected: (selected) {
|
||||
setState(() => _selectedCategory = category);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProductCard(ProductModel product) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: product.isLowStock
|
||||
? AppColors.warning.withValues(alpha: 0.5)
|
||||
: AppColors.surfaceLight,
|
||||
width: product.isLowStock ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: _getCategoryColor(product.category).withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
image: product.imagePath != null
|
||||
? DecorationImage(
|
||||
image: FileImage(File(product.imagePath!)),
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: product.imagePath == null
|
||||
? Icon(
|
||||
_getCategoryIcon(product.category),
|
||||
color: _getCategoryColor(product.category),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
product.name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
ProductCategories.getDisplayName(product.category),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: product.isLowStock
|
||||
? AppColors.warning.withValues(alpha: 0.2)
|
||||
: AppColors.success.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'Qtd: ${product.quantity}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: product.isLowStock
|
||||
? AppColors.warning
|
||||
: AppColors.success,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (product.isLowStock) ...[
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.warning_amber,
|
||||
size: 14,
|
||||
color: AppColors.warning,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
_currencyFormat.format(product.salePrice),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Ações: Editar, Deletar, Vender
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Botão Vender
|
||||
InkWell(
|
||||
onTap: () => _sellProduct(product),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.success.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.point_of_sale,
|
||||
size: 20,
|
||||
color: AppColors.success,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// Botão Editar
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => AddProductPage(product: product),
|
||||
),
|
||||
);
|
||||
_refreshData();
|
||||
},
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.edit_outlined,
|
||||
size: 20,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// Botão Excluir
|
||||
InkWell(
|
||||
onTap: () => _confirmDelete(product),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.error.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.delete_outline,
|
||||
size: 20,
|
||||
color: AppColors.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Lógica de Venda
|
||||
void _sellProduct(ProductModel product) {
|
||||
if (product.quantity <= 0) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Produto sem estoque!'),
|
||||
backgroundColor: AppColors.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Simplificado: Vende 1 unidade
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
backgroundColor: AppColors.surface,
|
||||
title: Text(
|
||||
'Vender ${product.name}?',
|
||||
style: const TextStyle(color: AppColors.textPrimary),
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Preço: ${_currencyFormat.format(product.salePrice)}',
|
||||
style: const TextStyle(color: AppColors.textSecondary),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Isso vai lançar uma receita e debitar 1 unidade do estoque.',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text(
|
||||
'Cancelar',
|
||||
style: TextStyle(color: AppColors.textSecondary),
|
||||
),
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppColors.success),
|
||||
child: const Text('Confirmar Venda'),
|
||||
onPressed: () async {
|
||||
Navigator.pop(ctx); // Fecha Dialog
|
||||
await _processSale(product);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _processSale(ProductModel product) async {
|
||||
try {
|
||||
// 1. Decrementar Estoque
|
||||
final updatedProduct = product.copyWith(quantity: product.quantity - 1);
|
||||
await _repository.updateProduct(updatedProduct);
|
||||
|
||||
// 2. Lançar Receita Financeira
|
||||
final transactionRepo = TransactionRepository();
|
||||
await transactionRepo.createRevenue(
|
||||
description: 'Venda Produto: ${product.name}',
|
||||
amount: product.salePrice,
|
||||
dueDate: DateTime.now(),
|
||||
isPaid: true, // Venda balcão geralmente é paga na hora
|
||||
);
|
||||
|
||||
_refreshData();
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Venda registrada com sucesso!'),
|
||||
backgroundColor: AppColors.success,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Erro ao vender: $e'),
|
||||
backgroundColor: AppColors.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ... Restante do código (Ícones, Colors, Delete) ...
|
||||
IconData _getCategoryIcon(String category) {
|
||||
switch (category) {
|
||||
case ProductCategories.pastes:
|
||||
return Icons.spa;
|
||||
case ProductCategories.beverages:
|
||||
return Icons.local_drink;
|
||||
case ProductCategories.accessories:
|
||||
return Icons.shopping_bag;
|
||||
default:
|
||||
return Icons.inventory_2;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getCategoryColor(String category) {
|
||||
switch (category) {
|
||||
case ProductCategories.pastes:
|
||||
return Colors.purple;
|
||||
case ProductCategories.beverages:
|
||||
return Colors.blue;
|
||||
case ProductCategories.accessories:
|
||||
return Colors.orange;
|
||||
default:
|
||||
return AppColors.primaryColor;
|
||||
}
|
||||
}
|
||||
|
||||
void _confirmDelete(ProductModel product) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: AppColors.surface,
|
||||
title: const Text(
|
||||
'Excluir Produto?',
|
||||
style: TextStyle(color: AppColors.textPrimary),
|
||||
),
|
||||
content: Text(
|
||||
'Deseja excluir "${product.name}"?',
|
||||
style: const TextStyle(color: AppColors.textSecondary),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text(
|
||||
AppStrings.cancel,
|
||||
style: TextStyle(color: AppColors.textSecondary),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final navigator = Navigator.of(context);
|
||||
await _repository.deleteProduct(product.id);
|
||||
navigator.pop();
|
||||
_refreshData();
|
||||
},
|
||||
child: const Text(
|
||||
AppStrings.delete,
|
||||
style: TextStyle(color: AppColors.error),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user