Initial commit - app-padrao-1.0
This commit is contained in:
43
lib/features/services/data/models/service_model.dart
Normal file
43
lib/features/services/data/models/service_model.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'package:hive/hive.dart';
|
||||
|
||||
part 'service_model.g.dart';
|
||||
|
||||
@HiveType(typeId: 6) // TypeId 0=User, 1=Haircut, 2=Product, 3=TransactionType/4=Transaction, 5=Settings
|
||||
class ServiceModel {
|
||||
@HiveField(0)
|
||||
final String id;
|
||||
|
||||
@HiveField(1)
|
||||
final String name; // Ex: Corte Máquina, Barba
|
||||
|
||||
@HiveField(2)
|
||||
final double price; // Preço padrão
|
||||
|
||||
@HiveField(3)
|
||||
final int durationMinutes; // Duração estimada (opcional, bom ter)
|
||||
|
||||
@HiveField(4)
|
||||
final String userId; // Para segregar por usuário
|
||||
|
||||
ServiceModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.price,
|
||||
this.durationMinutes = 30,
|
||||
required this.userId,
|
||||
});
|
||||
|
||||
ServiceModel copyWith({
|
||||
String? name,
|
||||
double? price,
|
||||
int? durationMinutes,
|
||||
}) {
|
||||
return ServiceModel(
|
||||
id: id,
|
||||
name: name ?? this.name,
|
||||
price: price ?? this.price,
|
||||
durationMinutes: durationMinutes ?? this.durationMinutes,
|
||||
userId: userId,
|
||||
);
|
||||
}
|
||||
}
|
||||
53
lib/features/services/data/models/service_model.g.dart
Normal file
53
lib/features/services/data/models/service_model.g.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'service_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class ServiceModelAdapter extends TypeAdapter<ServiceModel> {
|
||||
@override
|
||||
final int typeId = 6;
|
||||
|
||||
@override
|
||||
ServiceModel read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return ServiceModel(
|
||||
id: fields[0] as String,
|
||||
name: fields[1] as String,
|
||||
price: fields[2] as double,
|
||||
durationMinutes: fields[3] as int,
|
||||
userId: fields[4] as String,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, ServiceModel obj) {
|
||||
writer
|
||||
..writeByte(5)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
..write(obj.name)
|
||||
..writeByte(2)
|
||||
..write(obj.price)
|
||||
..writeByte(3)
|
||||
..write(obj.durationMinutes)
|
||||
..writeByte(4)
|
||||
..write(obj.userId);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ServiceModelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:barber_app/core/database/database_service.dart';
|
||||
import 'package:barber_app/features/services/data/models/service_model.dart';
|
||||
|
||||
class ServiceRepository {
|
||||
final _uuid = const Uuid();
|
||||
|
||||
String? get _currentUserId => DatabaseService.getCurrentUserId();
|
||||
|
||||
// Criar Serviço
|
||||
Future<ServiceModel?> createService({
|
||||
required String name,
|
||||
required double price,
|
||||
int durationMinutes = 30,
|
||||
}) async {
|
||||
if (_currentUserId == null) return null;
|
||||
|
||||
final service = ServiceModel(
|
||||
id: _uuid.v4(),
|
||||
name: name,
|
||||
price: price,
|
||||
durationMinutes: durationMinutes,
|
||||
userId: _currentUserId!,
|
||||
);
|
||||
|
||||
// ATENÇÃO: Precisamos adicionar servicesBoxInstance no DatabaseService
|
||||
await DatabaseService.servicesBoxInstance.put(service.id, service);
|
||||
return service;
|
||||
}
|
||||
|
||||
// Editar
|
||||
Future<void> updateService(ServiceModel service) async {
|
||||
await DatabaseService.servicesBoxInstance.put(service.id, service);
|
||||
}
|
||||
|
||||
// Deletar
|
||||
Future<void> deleteService(String id) async {
|
||||
await DatabaseService.servicesBoxInstance.delete(id);
|
||||
}
|
||||
|
||||
// Listar Todos
|
||||
List<ServiceModel> getAllServices() {
|
||||
if (_currentUserId == null) return [];
|
||||
|
||||
return DatabaseService.servicesBoxInstance.values
|
||||
.where((s) => s.userId == _currentUserId)
|
||||
.toList()
|
||||
..sort((a, b) => a.name.compareTo(b.name));
|
||||
}
|
||||
}
|
||||
170
lib/features/services/presentation/pages/add_service_page.dart
Normal file
170
lib/features/services/presentation/pages/add_service_page.dart
Normal file
@@ -0,0 +1,170 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:barber_app/core/theme/app_theme.dart';
|
||||
import 'package:barber_app/features/services/data/models/service_model.dart';
|
||||
import 'package:barber_app/features/services/data/repositories/service_repository.dart';
|
||||
import 'package:barber_app/shared/widgets/custom_text_field.dart';
|
||||
import 'package:barber_app/shared/widgets/loading_button.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class AddServicePage extends StatefulWidget {
|
||||
final ServiceModel? service;
|
||||
|
||||
const AddServicePage({super.key, this.service});
|
||||
|
||||
@override
|
||||
State<AddServicePage> createState() => _AddServicePageState();
|
||||
}
|
||||
|
||||
class _AddServicePageState extends State<AddServicePage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _nameController = TextEditingController();
|
||||
final _priceController = TextEditingController(); // Valor numérico raw
|
||||
final _displayPriceController = TextEditingController(); // Valor formatado R$
|
||||
final _durationController = TextEditingController(text: '30');
|
||||
|
||||
final _repository = ServiceRepository();
|
||||
bool _isLoading = false;
|
||||
|
||||
final _currencyFormat = NumberFormat.currency(locale: 'pt_BR', symbol: 'R\$');
|
||||
|
||||
bool get _isEditing => widget.service != null;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (_isEditing) {
|
||||
_nameController.text = widget.service!.name;
|
||||
_priceController.text = widget.service!.price.toStringAsFixed(2);
|
||||
_displayPriceController.text = _currencyFormat.format(widget.service!.price);
|
||||
_durationController.text = widget.service!.durationMinutes.toString();
|
||||
}
|
||||
}
|
||||
|
||||
void _formatPrice(String value) {
|
||||
if (value.isEmpty) return;
|
||||
|
||||
// Remove tudo que não é número
|
||||
String numbers = value.replaceAll(RegExp(r'[^0-9]'), '');
|
||||
if (numbers.isEmpty) return;
|
||||
|
||||
double val = double.parse(numbers) / 100;
|
||||
_priceController.text = val.toStringAsFixed(2);
|
||||
_displayPriceController.text = _currencyFormat.format(val);
|
||||
|
||||
// Mantém cursor no final
|
||||
_displayPriceController.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: _displayPriceController.text.length),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _saveService() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
if (_priceController.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Por favor, informe o preço')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
final price = double.parse(_priceController.text);
|
||||
final duration = int.tryParse(_durationController.text) ?? 30;
|
||||
|
||||
if (_isEditing) {
|
||||
final updatedService = widget.service!.copyWith(
|
||||
name: _nameController.text.trim(),
|
||||
price: price,
|
||||
durationMinutes: duration,
|
||||
);
|
||||
await _repository.updateService(updatedService);
|
||||
} else {
|
||||
await _repository.createService(
|
||||
name: _nameController.text.trim(),
|
||||
price: price,
|
||||
durationMinutes: duration,
|
||||
);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(_isEditing
|
||||
? 'Serviço atualizado com sucesso!'
|
||||
: 'Serviço criado com sucesso!'
|
||||
),
|
||||
backgroundColor: AppColors.success,
|
||||
),
|
||||
);
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Erro ao salvar: $e'),
|
||||
backgroundColor: AppColors.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_isEditing ? 'Editar Serviço' : 'Novo Serviço'),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
CustomTextField(
|
||||
label: 'Nome do Serviço',
|
||||
controller: _nameController,
|
||||
hint: 'Ex: Corte Degradê, Barba Terapia...',
|
||||
validator: (v) => v?.isEmpty == true ? 'Informe o nome' : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
CustomTextField(
|
||||
label: 'Preço',
|
||||
controller: _displayPriceController,
|
||||
hint: 'R\$ 0,00',
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: _formatPrice,
|
||||
validator: (v) => v?.isEmpty == true ? 'Informe o preço' : null,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
CustomTextField(
|
||||
label: 'Duração Média (minutos)',
|
||||
controller: _durationController,
|
||||
keyboardType: TextInputType.number,
|
||||
hint: '30',
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
LoadingButton(
|
||||
text: 'Salvar Serviço',
|
||||
onPressed: _saveService,
|
||||
isLoading: _isLoading,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
165
lib/features/services/presentation/pages/services_page.dart
Normal file
165
lib/features/services/presentation/pages/services_page.dart
Normal file
@@ -0,0 +1,165 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:barber_app/core/theme/app_theme.dart';
|
||||
import 'package:barber_app/features/services/data/models/service_model.dart';
|
||||
import 'package:barber_app/features/services/data/repositories/service_repository.dart';
|
||||
import 'package:barber_app/features/services/presentation/pages/add_service_page.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class ServicesPage extends StatefulWidget {
|
||||
const ServicesPage({super.key});
|
||||
|
||||
@override
|
||||
State<ServicesPage> createState() => _ServicesPageState();
|
||||
}
|
||||
|
||||
class _ServicesPageState extends State<ServicesPage> {
|
||||
final _repository = ServiceRepository();
|
||||
List<ServiceModel> _services = [];
|
||||
final _currencyFormat = NumberFormat.currency(locale: 'pt_BR', symbol: 'R\$');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadServices();
|
||||
}
|
||||
|
||||
void _loadServices() {
|
||||
setState(() {
|
||||
_services = _repository.getAllServices();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _deleteService(String id) async {
|
||||
await _repository.deleteService(id);
|
||||
_loadServices();
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Serviço removido com sucesso!')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Meus Serviços'), centerTitle: true),
|
||||
body: _services.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.content_cut_outlined,
|
||||
size: 64,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Nenhum serviço cadastrado',
|
||||
style: TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: () => _navigateToAddService(),
|
||||
child: const Text('Cadastrar Primeiro Serviço'),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _services.length,
|
||||
itemBuilder: (context, index) {
|
||||
final service = _services[index];
|
||||
return Dismissible(
|
||||
key: Key(service.id),
|
||||
direction: DismissDirection.endToStart,
|
||||
background: Container(
|
||||
alignment: Alignment.centerRight,
|
||||
padding: const EdgeInsets.only(right: 20),
|
||||
color: AppColors.error,
|
||||
child: const Icon(Icons.delete, color: Colors.white),
|
||||
),
|
||||
confirmDismiss: (direction) async {
|
||||
return await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Confirmar exclusão'),
|
||||
content: Text(
|
||||
'Deseja remover o serviço "${service.name}"?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Cancelar'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text(
|
||||
'Excluir',
|
||||
style: TextStyle(color: AppColors.error),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
onDismissed: (direction) => _deleteService(service.id),
|
||||
child: Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
leading: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(Icons.cut, color: AppColors.primaryColor),
|
||||
),
|
||||
title: Text(
|
||||
service.name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'Duração média: ${service.durationMinutes} min',
|
||||
style: const TextStyle(color: AppColors.textSecondary),
|
||||
),
|
||||
trailing: Text(
|
||||
_currencyFormat.format(service.price),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: AppColors.success,
|
||||
),
|
||||
),
|
||||
onTap: () => _navigateToAddService(service: service),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => _navigateToAddService(),
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _navigateToAddService({ServiceModel? service}) async {
|
||||
final result = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => AddServicePage(service: service)),
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
_loadServices();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user