This commit is contained in:
2026-04-07 11:30:22 +02:00
parent 4bbd1edf48
commit 130780cbb8
20 changed files with 426 additions and 131 deletions

View File

@@ -12,14 +12,26 @@ class CompanyBloc extends Bloc<CompanyEvent, CompanyState> {
CompanyBloc() : super(const CompanyState(status: CompanyStatus.initial)) { CompanyBloc() : super(const CompanyState(status: CompanyStatus.initial)) {
on<SaveCompanyRequested>((event, emit) async { on<SaveCompanyRequested>((event, emit) async {
emit(const CompanyState(status: CompanyStatus.loading)); emit(const CompanyState(status: CompanyStatus.loading));
try { try {
// Recuperiamo l'ID utente corrente da Supabase Auth
final userId = _supabase.auth.currentUser!.id; final userId = _supabase.auth.currentUser!.id;
await _supabase.from('companies').insert({ await _supabase.from('company').insert({
'owner_id': userId, 'user_id': userId,
'ragione_sociale': event.ragioneSociale, 'ragione_sociale': event.ragioneSociale,
'partita_iva': event.partitaIva, 'partita_iva': event.partitaIva,
// Se il CF è vuoto, usa la P.IVA (logica salva-tempo per ditte individuali)
'codice_fiscale': event.codiceFiscale.isEmpty
? event.partitaIva
: event.codiceFiscale,
'codice_univoco': event.codiceUnivoco, 'codice_univoco': event.codiceUnivoco,
'indirizzo': event.indirizzo,
'cap': event.cap,
'citta': event.citta,
'provincia': event.provincia,
'company_logo': event.companyLogo,
'is_paid': false, // Di default partono con trial/non pagato
}); });
emit(const CompanyState(status: CompanyStatus.success)); emit(const CompanyState(status: CompanyStatus.success));

View File

@@ -1,16 +1,47 @@
part of 'company_bloc.dart'; part of 'company_bloc.dart';
abstract class CompanyEvent { // lib/blocs/company/company_event.dart
abstract class CompanyEvent extends Equatable {
const CompanyEvent(); const CompanyEvent();
@override
List<Object?> get props => [];
} }
final class SaveCompanyRequested extends CompanyEvent { class SaveCompanyRequested extends CompanyEvent {
final String ragioneSociale; final String ragioneSociale;
final String partitaIva; final String partitaIva;
final String codiceFiscale;
final String codiceUnivoco; final String codiceUnivoco;
const SaveCompanyRequested( final String indirizzo;
this.ragioneSociale, final String cap;
this.partitaIva, final String citta;
this.codiceUnivoco, final String provincia;
); final String companyLogo;
const SaveCompanyRequested({
required this.ragioneSociale,
required this.partitaIva,
required this.codiceFiscale,
required this.codiceUnivoco,
required this.indirizzo,
required this.cap,
required this.citta,
required this.provincia,
this.companyLogo = '', // Default vuoto come da schema SQL
});
@override
List<Object?> get props => [
ragioneSociale,
partitaIva,
codiceFiscale,
codiceUnivoco,
indirizzo,
cap,
citta,
provincia,
companyLogo,
];
} }

View File

@@ -0,0 +1,51 @@
// lib/ui/common/flux_text_field.dart
import 'package:flutter/material.dart';
import 'package:flux/core/theme/theme.dart';
class FluxTextField extends StatelessWidget {
final String label;
final IconData icon;
final bool isPassword;
final TextEditingController? controller;
final TextInputType? keyboardType; // Aggiunto per flessibilità
const FluxTextField({
super.key, // Usiamo super.key per Flutter moderno
required this.label,
required this.icon,
this.isPassword = false,
this.controller,
this.keyboardType,
});
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
obscureText: isPassword,
keyboardType: keyboardType,
style: TextStyle(color: context.primaryText),
decoration: InputDecoration(
prefixIcon: Icon(icon, color: context.accent.withValues(alpha: 0.6)),
labelText: label,
labelStyle: TextStyle(color: context.secondaryText, fontSize: 14),
filled: true,
fillColor: context.surface.withValues(alpha: 0.5),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: context.secondaryText.withValues(alpha: 0.1),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: context.accent, width: 1.5),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
),
);
}
}

View File

@@ -1,8 +1,9 @@
// lib/ui/auth/auth_screen.dart // lib/ui/auth/auth_screen.dart
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/blocs/auth/auth_bloc.dart'; import 'package:flux/features/auth/bloc/auth_bloc.dart';
import 'package:flux/theme/theme.dart'; import 'package:flux/core/theme/theme.dart';
import 'package:flux/core/widgets/flux_text_field.dart';
class AuthScreen extends StatefulWidget { class AuthScreen extends StatefulWidget {
const AuthScreen({super.key}); const AuthScreen({super.key});
@@ -30,19 +31,19 @@ class _AuthScreenState extends State<AuthScreen> {
builder: (context, state) { builder: (context, state) {
return Column( return Column(
children: [ children: [
_AuthTextField( FluxTextField(
label: 'Email', label: 'Email',
icon: Icons.email, icon: Icons.email,
controller: _emailController, controller: _emailController,
), ),
_AuthTextField( FluxTextField(
label: 'Password', label: 'Password',
icon: Icons.lock, icon: Icons.lock,
isPassword: true, isPassword: true,
controller: _passwordController, controller: _passwordController,
), ),
if (!state.isLoginMode) if (!state.isLoginMode)
_AuthTextField( FluxTextField(
label: 'Codice Negozio', label: 'Codice Negozio',
icon: Icons.store, icon: Icons.store,
controller: _storeController, controller: _storeController,
@@ -100,77 +101,3 @@ class _FluxLogo extends StatelessWidget {
); );
} }
} }
class _AuthTextField extends StatefulWidget {
final String label;
final IconData icon;
final bool isPassword;
final TextEditingController? controller; // Aggiunto per recuperare i dati
const _AuthTextField({
required this.label,
required this.icon,
this.isPassword = false,
this.controller,
});
@override
State<_AuthTextField> createState() => _AuthTextFieldState();
}
class _AuthTextFieldState extends State<_AuthTextField> {
bool _obscureText = true; // Stato interno per la visibilità
@override
Widget build(BuildContext context) {
return TextField(
controller: widget.controller,
obscureText: widget.isPassword ? _obscureText : false,
style: TextStyle(color: context.primaryText),
decoration: InputDecoration(
prefixIcon: Icon(
widget.icon,
color: context.accent.withValues(alpha: 0.6),
),
labelText: widget.label,
labelStyle: TextStyle(color: context.secondaryText, fontSize: 14),
filled: true,
fillColor: context.surface.withValues(alpha: 0.5),
// --- LOGICA OCCHIO PASSWORD ---
suffixIcon: widget.isPassword
? IconButton(
icon: Icon(
_obscureText
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
color: context.secondaryText,
size: 20,
),
onPressed: () {
setState(() {
_obscureText = !_obscureText;
});
},
)
: null,
// --- BORDI STILE FLUX ---
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: context.secondaryText.withValues(alpha: 0.1),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: context.accent, width: 1.5),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
),
);
}
}

View File

@@ -14,7 +14,7 @@ class CompanyModel extends Equatable {
final String codiceUnivoco; final String codiceUnivoco;
final bool isPaid; final bool isPaid;
final DateTime? paymentExpiration; final DateTime? paymentExpiration;
final String? companyLogo; final String companyLogo;
const CompanyModel({ const CompanyModel({
required this.id, required this.id,
@@ -30,10 +30,9 @@ class CompanyModel extends Equatable {
required this.codiceUnivoco, required this.codiceUnivoco,
required this.isPaid, required this.isPaid,
this.paymentExpiration, this.paymentExpiration,
this.companyLogo, this.companyLogo = '',
}); });
// --- FROM JSON (Dall'input di Supabase a Dart) ---
factory CompanyModel.fromJson(Map<String, dynamic> json) { factory CompanyModel.fromJson(Map<String, dynamic> json) {
return CompanyModel( return CompanyModel(
id: json['id'], id: json['id'],
@@ -51,11 +50,10 @@ class CompanyModel extends Equatable {
paymentExpiration: json['payment_expiration'] != null paymentExpiration: json['payment_expiration'] != null
? DateTime.parse(json['payment_expiration']) ? DateTime.parse(json['payment_expiration'])
: null, : null,
companyLogo: json['company_logo'], companyLogo: json['company_logo'] ?? '',
); );
} }
// --- TO JSON (Da Dart a Supabase) ---
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'ragione_sociale': ragioneSociale, 'ragione_sociale': ragioneSociale,
@@ -69,12 +67,13 @@ class CompanyModel extends Equatable {
'is_paid': isPaid, 'is_paid': isPaid,
'payment_expiration': paymentExpiration?.toIso8601String(), 'payment_expiration': paymentExpiration?.toIso8601String(),
'company_logo': companyLogo, 'company_logo': companyLogo,
// 'id', 'created_at' e 'user_id' di solito sono gestiti dal DB in fase di insert
}; };
} }
// --- COPY WITH (Per aggiornamenti parziali) ---
CompanyModel copyWith({ CompanyModel copyWith({
String? id,
DateTime? createdAt,
String? userId,
String? ragioneSociale, String? ragioneSociale,
String? indirizzo, String? indirizzo,
String? cap, String? cap,
@@ -86,11 +85,10 @@ class CompanyModel extends Equatable {
bool? isPaid, bool? isPaid,
DateTime? paymentExpiration, DateTime? paymentExpiration,
String? companyLogo, String? companyLogo,
}) { }) => CompanyModel(
return CompanyModel( id: id ?? this.id,
id: id, createdAt: createdAt ?? this.createdAt,
createdAt: createdAt, userId: userId ?? this.userId,
userId: userId,
ragioneSociale: ragioneSociale ?? this.ragioneSociale, ragioneSociale: ragioneSociale ?? this.ragioneSociale,
indirizzo: indirizzo ?? this.indirizzo, indirizzo: indirizzo ?? this.indirizzo,
cap: cap ?? this.cap, cap: cap ?? this.cap,
@@ -103,16 +101,7 @@ class CompanyModel extends Equatable {
paymentExpiration: paymentExpiration ?? this.paymentExpiration, paymentExpiration: paymentExpiration ?? this.paymentExpiration,
companyLogo: companyLogo ?? this.companyLogo, companyLogo: companyLogo ?? this.companyLogo,
); );
}
@override @override
List<Object?> get props => [ List<Object?> get props => [id, userId, ragioneSociale, partitaIva, isPaid];
id,
userId,
ragioneSociale,
partitaIva,
isPaid,
paymentExpiration,
companyLogo,
];
} }

View File

@@ -0,0 +1,273 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/blocs/company/company_bloc.dart';
import 'package:flux/blocs/session/session_bloc.dart';
import 'package:flux/core/theme/theme.dart';
import 'package:flux/core/widgets/flux_text_field.dart';
class CreateCompanyScreen extends StatefulWidget {
const CreateCompanyScreen({super.key});
@override
State<CreateCompanyScreen> createState() => _CreateCompanyScreenState();
}
// lib/ui/setup/create_company_screen.dart
class _CreateCompanyScreenState extends State<CreateCompanyScreen> {
final _formKey = GlobalKey<FormState>();
// Controller per ogni campo dello schema SQL
final _ragioneSocialeController = TextEditingController();
final _pIvaController = TextEditingController();
final _cfController = TextEditingController();
final _sdiController = TextEditingController();
final _indirizzoController = TextEditingController();
final _capController = TextEditingController();
final _cittaController = TextEditingController();
final _provinciaController = TextEditingController();
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CompanyBloc(),
child: Scaffold(
body: BlocConsumer<CompanyBloc, CompanyState>(
listener: (context, state) {
if (state.status == CompanyStatus.success) {
context.read<SessionBloc>().add(AppStarted());
}
},
builder: (context, state) {
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(context),
const SizedBox(height: 32),
// --- SEZIONE 1: IDENTITÀ FISCALE ---
_SectionTitle(title: 'DATI FISCALI'),
const SizedBox(height: 16),
FluxTextField(
label: 'Ragione Sociale',
icon: Icons.business,
controller: _ragioneSocialeController,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: FluxTextField(
label: 'Partita IVA',
icon: Icons.numbers,
controller: _pIvaController,
),
),
const SizedBox(width: 12),
Expanded(
child: FluxTextField(
label: 'Codice Fiscale',
icon: Icons.badge_outlined,
controller: _cfController,
),
),
],
),
const SizedBox(height: 16),
FluxTextField(
label: 'Codice Univoco (SDI) / PEC',
icon: Icons.send_and_archive_outlined,
controller: _sdiController,
),
const SizedBox(height: 32),
// --- SEZIONE 2: SEDE LEGALE ---
_SectionTitle(title: 'SEDE LEGALE'),
const SizedBox(height: 16),
FluxTextField(
label: 'Indirizzo e n. civico',
icon: Icons.home_work_outlined,
controller: _indirizzoController,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
flex: 2,
child: FluxTextField(
label: 'Città',
icon: Icons.location_city,
controller: _cittaController,
),
),
const SizedBox(width: 12),
Expanded(
child: FluxTextField(
label: 'CAP',
icon: Icons.map_outlined,
controller: _capController,
),
),
const SizedBox(width: 12),
Expanded(
child: FluxTextField(
label: 'Prov',
icon: Icons.explore_outlined,
controller: _provinciaController,
),
),
],
),
const SizedBox(height: 32),
// --- SEZIONE 3: LOGO AZIENDALE ---
_SectionTitle(title: 'BRANDING'),
const SizedBox(height: 16),
_buildLogoPicker(context),
const SizedBox(height: 48),
// --- BOTTONE INVIO ---
_buildSubmitButton(context, state),
],
),
),
),
);
},
),
),
);
}
// Placeholder per il futuro caricamento logo
Widget _buildLogoPicker(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: context.accent.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(16),
// Bordo continuo ma sottile e semitrasparente per un look pulito
border: Border.all(
color: context.accent.withValues(alpha: 0.3),
width: 1,
),
),
child: Column(
children: [
Icon(Icons.cloud_upload_outlined, color: context.accent, size: 32),
const SizedBox(height: 12),
Text(
'Carica Logo Aziendale',
style: TextStyle(
color: context.primaryText,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'Verrà usato per le tue stampe e ricevute',
textAlign: TextAlign.center,
style: TextStyle(color: context.secondaryText, fontSize: 12),
),
],
),
);
}
Widget _buildSubmitButton(BuildContext context, CompanyState state) {
return SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: state.status == CompanyStatus.loading
? null
: () => _submit(context),
child: state.status == CompanyStatus.loading
? const CircularProgressIndicator()
: const Text('SALVA AZIENDA'),
),
);
}
void _submit(BuildContext context) {
// Qui chiameremo il Bloc passando tutti i dati raccolti dai controller
context.read<CompanyBloc>().add(
SaveCompanyRequested(
ragioneSociale: _ragioneSocialeController.text,
partitaIva: _pIvaController.text,
codiceFiscale: _cfController.text,
codiceUnivoco: _sdiController.text,
indirizzo: _indirizzoController.text,
cap: _capController.text,
citta: _cittaController.text,
provincia: _provinciaController.text,
companyLogo: '', // Per ora vuoto come da accordi
),
);
}
Widget _buildHeader(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: context.accent.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
),
child: Icon(
Icons.domain_add_rounded,
color: context.accent,
size: 32,
),
),
const SizedBox(height: 24),
Text(
'Configura la tua Azienda',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: context.primaryText,
),
),
const SizedBox(height: 12),
Text(
'FLUX ha bisogno dei tuoi dati fiscali per gestire correttamente le fatturazioni e le attivazioni dei tuoi negozi.',
style: TextStyle(
color: context.secondaryText,
fontSize: 15,
height: 1.5,
),
),
],
);
}
}
// Widget di supporto per i titoli delle sezioni
class _SectionTitle extends StatelessWidget {
final String title;
const _SectionTitle({required this.title});
@override
Widget build(BuildContext context) {
return Text(
title,
style: TextStyle(
color: context.accent,
fontWeight: FontWeight.w800,
letterSpacing: 1.2,
fontSize: 13,
),
);
}
}

View File

@@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class CreateStoreScreen extends StatelessWidget {
const CreateStoreScreen({super.key});
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

View File

@@ -1,9 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/blocs/session/session_bloc.dart'; import 'package:flux/blocs/session/session_bloc.dart';
import 'package:flux/theme/theme.dart'; import 'package:flux/core/theme/theme.dart';
import 'package:flux/theme/theme_bloc.dart'; import 'package:flux/core/theme/bloc/theme_bloc.dart';
import 'package:flux/ui/auth/auth_screen.dart'; import 'package:flux/features/auth/ui/auth_screen.dart';
import 'package:flux/features/company/ui/create_company_screen.dart';
import 'package:flux/features/store/ui/create_store_screen.dart';
import 'package:flux/ui/home_screen.dart'; import 'package:flux/ui/home_screen.dart';
import 'package:flux/ui/settings/settings.dart'; import 'package:flux/ui/settings/settings.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flux/theme/theme.dart'; import 'package:flux/core/theme/theme.dart';
class AnagraficheMainView extends StatelessWidget { class AnagraficheMainView extends StatelessWidget {
const AnagraficheMainView({super.key}); const AnagraficheMainView({super.key});

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flux/theme/theme.dart'; import 'package:flux/core/theme/theme.dart';
class DashboardView extends StatelessWidget { class DashboardView extends StatelessWidget {
const DashboardView({super.key}); const DashboardView({super.key});

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flux/theme/theme.dart'; import 'package:flux/core/theme/theme.dart';
import 'package:flux/ui/anagrafiche/anagrafiche_main_view.dart'; import 'package:flux/ui/anagrafiche/anagrafiche_main_view.dart';
import 'package:flux/ui/dashboard/dashboard_view.dart'; import 'package:flux/ui/dashboard/dashboard_view.dart';
import 'package:flux/ui/settings/settings_view.dart'; import 'package:flux/ui/settings/settings_view.dart';

View File

@@ -1,6 +1,6 @@
// lib/ui/impostazioni/impostazioni_view.dart // lib/ui/impostazioni/impostazioni_view.dart
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flux/theme/theme.dart'; import 'package:flux/core/theme/theme.dart';
import 'package:flux/ui/settings/theme_settings_view.dart'; import 'package:flux/ui/settings/theme_settings_view.dart';
class SettingsView extends StatelessWidget { class SettingsView extends StatelessWidget {

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/data/enums.dart'; import 'package:flux/data/enums.dart';
import 'package:flux/theme/theme.dart'; import 'package:flux/core/theme/theme.dart';
import 'package:flux/theme/theme_bloc.dart'; import 'package:flux/core/theme/bloc/theme_bloc.dart';
class ThemeSettingsView extends StatelessWidget { class ThemeSettingsView extends StatelessWidget {
const ThemeSettingsView({super.key}); const ThemeSettingsView({super.key});