ripristinato codice stabile da branch staff

This commit is contained in:
2026-04-14 11:29:49 +02:00
parent f33b63c0c6
commit 541000390e
28 changed files with 2260 additions and 13 deletions

View File

@@ -0,0 +1,105 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_bloc.dart';
import 'package:flux/features/master_data/products/data/product_repository.dart';
import 'package:flux/features/master_data/products/models/brand_model.dart';
import 'package:flux/features/master_data/products/models/model_model.dart';
import 'package:get_it/get_it.dart';
part 'product_state.dart';
class ProductCubit extends Cubit<ProductState> {
final ProductRepository _repository = GetIt.I<ProductRepository>();
final SessionBloc _sessionBloc;
ProductCubit(this._sessionBloc) : super(const ProductState());
// Caricamento iniziale dei Brand
Future<void> loadBrands() async {
emit(state.copyWith(status: ProductStatus.loading));
try {
final brands = await _repository.getBrands(
_sessionBloc.state.company!.id,
);
emit(state.copyWith(status: ProductStatus.success, brands: brands));
} catch (e) {
emit(
state.copyWith(status: ProductStatus.error, errorMessage: e.toString()),
);
}
}
// Selezione Brand e caricamento Modelli
Future<void> selectBrand(BrandModel? brand) async {
if (brand == null) {
emit(state.copyWith(selectedBrand: null, models: []));
return;
}
emit(state.copyWith(status: ProductStatus.loading, selectedBrand: brand));
try {
final models = await _repository.getModelsByBrand(brand.id!);
emit(state.copyWith(status: ProductStatus.success, models: models));
} catch (e) {
emit(
state.copyWith(status: ProductStatus.error, errorMessage: e.toString()),
);
}
}
// Aggiungi/Modifica Brand
Future<void> saveBrand(String name, {String? id}) async {
try {
final brand = BrandModel(
id: id,
name: name,
companyId: _sessionBloc.state.company!.id,
);
final newBrand = await _repository.upsertBrand(brand);
await loadBrands(); // Ricarichiamo la lista aggiornata
selectBrand(newBrand);
} catch (e) {
emit(
state.copyWith(status: ProductStatus.error, errorMessage: e.toString()),
);
}
}
// Aggiungi/Modifica Modello
Future<void> saveModel(String name, {String? id}) async {
if (state.selectedBrand == null) return;
try {
final model = ModelModel(
id: id,
name: name,
brandId: state.selectedBrand!.id!,
nameWithBrand: '', // Gestito dal trigger SQL
);
await _repository.upsertModel(model);
await selectBrand(
state.selectedBrand,
); // Ricarichiamo i modelli del brand
} catch (e) {
emit(
state.copyWith(status: ProductStatus.error, errorMessage: e.toString()),
);
}
}
// Disattivazione (Soft Delete)
Future<void> toggleStatus(String table, String id, bool currentStatus) async {
try {
await _repository.toggleActiveStatus(table, id, !currentStatus);
if (table == 'brand') {
await loadBrands();
} else {
await selectBrand(state.selectedBrand);
}
} catch (e) {
emit(
state.copyWith(status: ProductStatus.error, errorMessage: e.toString()),
);
}
}
}

View File

@@ -0,0 +1,44 @@
part of 'product_cubit.dart';
enum ProductStatus { initial, loading, success, error }
class ProductState extends Equatable {
final ProductStatus status;
final List<BrandModel> brands;
final List<ModelModel> models;
final BrandModel? selectedBrand; // Il brand attualmente selezionato
final String? errorMessage;
const ProductState({
this.status = ProductStatus.initial,
this.brands = const [],
this.models = const [],
this.selectedBrand,
this.errorMessage,
});
ProductState copyWith({
ProductStatus? status,
List<BrandModel>? brands,
List<ModelModel>? models,
BrandModel? selectedBrand,
String? errorMessage,
}) {
return ProductState(
status: status ?? this.status,
brands: brands ?? this.brands,
models: models ?? this.models,
selectedBrand: selectedBrand ?? this.selectedBrand,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [
status,
brands,
models,
selectedBrand,
errorMessage,
];
}

View File

@@ -0,0 +1,86 @@
import 'package:get_it/get_it.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../models/brand_model.dart';
import '../models/model_model.dart';
class ProductRepository {
final SupabaseClient _client = GetIt.I<SupabaseClient>();
// --- BRAND ---
/// Recupera tutti i brand dell'azienda
Future<List<BrandModel>> getBrands(String companyId) async {
try {
final response = await _client
.from('brand')
.select()
.eq('company_id', companyId)
.eq('is_active', true)
.order('name');
return (response as List).map((b) => BrandModel.fromJson(b)).toList();
} catch (e) {
throw 'Errore nel recupero dei Brand';
}
}
/// Crea o aggiorna un brand
Future<BrandModel> upsertBrand(BrandModel brand) async {
try {
final response = await _client
.from('brand')
.upsert(brand.toJson())
.select()
.single();
return BrandModel.fromJson(response);
} catch (e) {
throw 'Errore nel salvataggio del Brand';
}
}
// --- MODEL ---
/// Recupera i modelli di un brand specifico
Future<List<ModelModel>> getModelsByBrand(String brandId) async {
try {
final response = await _client
.from('model')
.select()
.eq('brand_id', brandId)
.eq('is_active', true)
.order('name');
return (response as List).map((m) => ModelModel.fromJson(m)).toList();
} catch (e) {
throw 'Errore nel recupero dei modelli';
}
}
/// Crea o aggiorna un modello
/// NOTA: name_with_brand verrà gestito dal trigger SQL che hai lanciato!
Future<ModelModel> upsertModel(ModelModel model) async {
try {
final response = await _client
.from('model')
.upsert(model.toJson())
.select()
.single();
return ModelModel.fromJson(response);
} catch (e) {
throw 'Errore nel salvataggio del modello';
}
}
// --- DELETE (LOGICA) ---
/// Disattiva un brand o un modello (Soft Delete per non rompere le FK delle operazioni passate)
Future<void> toggleActiveStatus(String table, String id, bool status) async {
try {
await _client.from(table).update({'is_active': status}).eq('id', id);
} catch (e) {
throw 'Errore durante la modifica dello stato';
}
}
}

View File

@@ -0,0 +1,58 @@
import 'package:equatable/equatable.dart';
import 'package:flux/core/utils/string_extensions.dart';
class BrandModel extends Equatable {
final String? id;
final String name;
final String companyId;
final bool isActive;
final DateTime? createdAt;
const BrandModel({
this.id,
required this.name,
required this.companyId,
this.isActive = true,
this.createdAt,
});
factory BrandModel.fromJson(Map<String, dynamic> json) {
return BrandModel(
id: json['id'] as String,
name: (json['name'] as String).myFormat(),
companyId: json['company_id'] as String,
isActive: json['is_active'] as bool? ?? true,
createdAt: json['created_at'] != null
? DateTime.parse(json['created_at'])
: null,
);
}
Map<String, dynamic> toJson() {
return {
if (id != null) 'id': id,
'name': name.toLowerCase().trim(),
'company_id': companyId,
'is_active': isActive,
};
}
BrandModel copyWith({
String? id,
String? name,
String? companyId,
bool? isActive,
DateTime? createdAt,
}) {
return BrandModel(
id: id ?? this.id,
name: name ?? this.name,
companyId: companyId ?? this.companyId,
isActive: isActive ?? this.isActive,
createdAt: createdAt ?? this.createdAt,
);
}
@override
List<Object?> get props => [id, name, companyId, isActive, createdAt];
}

View File

@@ -0,0 +1,70 @@
import 'package:equatable/equatable.dart';
import 'package:flux/core/utils/string_extensions.dart';
class ModelModel extends Equatable {
final String? id;
final String name;
final String nameWithBrand;
final String brandId;
final bool isActive;
final DateTime? createdAt;
const ModelModel({
this.id,
required this.name,
required this.nameWithBrand,
required this.brandId,
this.isActive = true,
this.createdAt,
});
factory ModelModel.fromJson(Map<String, dynamic> json) {
return ModelModel(
id: json['id'] as String,
name: (json['name'] as String).myFormat(),
nameWithBrand: (json['name_with_brand'] as String).myFormat(),
brandId: json['brand_id'] as String,
isActive: json['is_active'] as bool? ?? true,
createdAt: json['created_at'] != null
? DateTime.parse(json['created_at'])
: null,
);
}
Map<String, dynamic> toJson() {
return {
if (id != null) 'id': id,
'name': name.toLowerCase().trim(),
'brand_id': brandId,
'is_active': isActive,
};
}
ModelModel copyWith({
String? id,
String? name,
String? nameWithBrand,
String? brandId,
bool? isActive,
DateTime? createdAt,
}) {
return ModelModel(
id: id ?? this.id,
name: name ?? this.name,
nameWithBrand: nameWithBrand ?? this.nameWithBrand,
brandId: brandId ?? this.brandId,
isActive: isActive ?? this.isActive,
createdAt: createdAt ?? this.createdAt,
);
}
@override
List<Object?> get props => [
id,
name,
nameWithBrand,
brandId,
isActive,
createdAt,
];
}

View File

@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/theme/theme.dart';
import 'package:flux/features/master_data/products/blocs/product_cubit.dart';
import 'package:flux/features/master_data/products/models/brand_model.dart';
import 'package:flux/features/master_data/products/ui/product_dialogs.dart';
import 'package:flux/features/master_data/products/ui/round_action_button.dart';
class BrandSelector extends StatelessWidget {
final ProductState state;
const BrandSelector(this.state, {super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: context.background,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: context.accent.withValues(alpha: 0.1)),
),
child: Row(
children: [
Expanded(
child: DropdownButtonFormField<BrandModel>(
initialValue: state.selectedBrand,
//value: state.selectedBrand,
decoration: const InputDecoration(
labelText: "Seleziona Brand",
prefixIcon: Icon(Icons.branding_watermark_outlined),
),
items: state.brands.map((brand) {
return DropdownMenuItem(value: brand, child: Text(brand.name));
}).toList(),
onChanged: (brand) =>
context.read<ProductCubit>().selectBrand(brand),
),
),
const SizedBox(width: 16),
// Pulsanti rapidi Brand
RoundActionButton(
icon: Icons.add,
onTap: () => showBrandDialog(context),
tooltip: "Nuovo Brand",
),
if (state.selectedBrand != null) ...[
const SizedBox(width: 8),
RoundActionButton(
icon: Icons.edit_outlined,
onTap: () => showBrandDialog(context, brand: state.selectedBrand),
tooltip: "Modifica Brand",
),
],
],
),
);
}
}

View File

@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/theme/theme.dart';
import 'package:flux/features/master_data/products/blocs/product_cubit.dart';
import 'package:flux/features/master_data/products/ui/product_dialogs.dart';
class ModelsList extends StatelessWidget {
final ProductState state;
const ModelsList(this.state, {super.key});
@override
Widget build(BuildContext context) {
if (state.selectedBrand == null) {
return const Center(
child: Text("Seleziona un brand per gestire i modelli"),
);
}
if (state.status == ProductStatus.loading && state.models.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Modelli di ${state.selectedBrand!.name}",
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
ElevatedButton.icon(
onPressed: () => showModelDialog(context),
icon: const Icon(Icons.add),
label: const Text("NUOVO MODELLO"),
),
],
),
const SizedBox(height: 16),
Expanded(
child: ListView.separated(
itemCount: state.models.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final model = state.models[index];
return ListTile(
title: Text(model.name),
subtitle: Text(
model.nameWithBrand,
), // Quello gestito dal trigger!
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit_outlined),
onPressed: () => showModelDialog(context, model: model),
),
IconButton(
icon: Icon(
model.isActive
? Icons.visibility_outlined
: Icons.visibility_off_outlined,
color: model.isActive ? context.accent : Colors.grey,
),
onPressed: () => context
.read<ProductCubit>()
.toggleStatus('model', model.id!, model.isActive),
),
],
),
);
},
),
),
],
);
}
}

View File

@@ -0,0 +1,110 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/features/master_data/products/blocs/product_cubit.dart';
import 'package:flux/features/master_data/products/models/brand_model.dart';
import 'package:flux/features/master_data/products/models/model_model.dart';
void showBrandDialog(BuildContext context, {BrandModel? brand}) {
final controller = TextEditingController(text: brand?.name);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(brand == null ? "Nuovo Brand" : "Modifica Brand"),
content: TextField(
controller: controller,
autofocus: true,
decoration: const InputDecoration(
labelText: "Nome Brand",
hintText: "es. Apple, Samsung...",
),
onSubmitted: (_) => _submitBrand(controller, context, brand),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Annulla"),
),
ElevatedButton(
onPressed: () => _submitBrand(controller, context, brand),
child: const Text("Salva"),
),
],
),
);
}
void _submitBrand(
TextEditingController controller,
BuildContext context,
BrandModel? brand,
) {
if (controller.text.trim().isNotEmpty) {
context.read<ProductCubit>().saveBrand(controller.text, id: brand?.id);
Navigator.pop(context);
}
}
void showModelDialog(BuildContext context, {ModelModel? model}) {
final controller = TextEditingController(text: model?.name);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(model == null ? "Nuovo Modello" : "Modifica Modello"),
content: TextField(
controller: controller,
autofocus: true,
decoration: const InputDecoration(
labelText: "Nome Modello",
hintText: "es. iPhone 15, Galaxy S24...",
),
onSubmitted: (_) => _submitModel(controller, context, model),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Annulla"),
),
ElevatedButton(
onPressed: () => _submitModel(controller, context, model),
child: const Text("Salva"),
),
],
),
);
}
void _submitModel(
TextEditingController controller,
BuildContext context,
ModelModel? model,
) {
if (controller.text.isNotEmpty) {
context.read<ProductCubit>().saveModel(controller.text, id: model?.id);
Navigator.pop(context);
}
}
void confirmToggle(BuildContext context, String title, VoidCallback onConfirm) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("Sei sicuro?"),
content: Text("Stai per cambiare lo stato di: $title"),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Annulla"),
),
TextButton(
onPressed: () {
onConfirm();
Navigator.pop(context);
},
child: const Text("Conferma", style: TextStyle(color: Colors.red)),
),
],
),
);
}

View File

@@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/theme/theme.dart';
import 'package:flux/features/master_data/products/blocs/product_cubit.dart';
import 'package:flux/features/master_data/products/ui/brand_selector.dart';
import 'package:flux/features/master_data/products/ui/models_list.dart';
import 'package:go_router/go_router.dart';
class ProductsScreen extends StatelessWidget {
const ProductsScreen({super.key});
@override
Widget build(BuildContext context) {
// Carichiamo i brand appena la pagina viene creata
context.read<ProductCubit>().loadBrands();
return Scaffold(
backgroundColor: context.background,
appBar: AppBar(
backgroundColor: context.background,
elevation: 0,
centerTitle: false,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.pop(), // Torna alla Dashboard
),
title: Text(
"Anagrafica Prodotti",
style: TextStyle(
color: context.primaryText,
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
),
body: BlocConsumer<ProductCubit, ProductState>(
listener: (context, state) {
if (state.status == ProductStatus.error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage ?? 'Errore')),
);
}
},
builder: (context, state) {
return Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Marche e Modelli",
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: context.accent,
),
),
const SizedBox(height: 24),
// SEZIONE BRAND
BrandSelector(state),
const SizedBox(height: 32),
// SEZIONE MODELLI
Expanded(child: ModelsList(state)),
],
),
);
},
),
);
}
}

View File

@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
class RoundActionButton extends StatelessWidget {
final IconData icon;
final VoidCallback onTap;
final String tooltip;
const RoundActionButton({
super.key,
required this.icon,
required this.onTap,
required this.tooltip,
});
@override
Widget build(BuildContext context) {
return IconButton.filledTonal(
onPressed: onTap,
icon: Icon(icon),
tooltip: tooltip,
);
}
}