Initial commit

This commit is contained in:
Super User
2026-06-14 15:03:29 -04:00
commit 9ac9e9e953
46 changed files with 2655 additions and 0 deletions

45
.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

30
.metadata Normal file
View File

@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "d8a9f9a52e5af486f80d932e838ee93861ffd863"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863
base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863
- platform: android
create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863
base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

57
AGENTS.md Normal file
View File

@@ -0,0 +1,57 @@
# HogarControl
Flutter app for tracking home bills and recurring expenses. All data stored locally via SQLite — no internet required, no accounts.
## Build & install
```bash
export JAVA_HOME=/root/.sdkman/candidates/java/21.0.11-amzn
flutter pub get
flutter build apk --debug
adb install build/app/outputs/flutter-apk/app-debug.apk
```
## Prerequisites
- **JDK 21** via SDKMAN: `sdk use java 21.0.11-amzn`
- **Android SDK** at `/opt/android-sdk` (set via `flutter config --android-sdk /opt/android-sdk`)
- **Flutter 3.27.4** at `/opt/flutter/bin`
- NDK `27.0.12077973` (set in `android/app/build.gradle`)
## Key config
- `android/app/build.gradle` — compileSdk 35, minSdk 26, targetSdk 35, coreLibraryDesugaringEnabled, AGP 8.7.3
- `android/settings.gradle` — AGP + Kotlin plugin versions
- Permissions: `INTERNET`, `POST_NOTIFICATIONS`, `RECEIVE_BOOT_COMPLETED`, `SCHEDULE_EXACT_ALARM`
## Stack
- **State**: Provider (`ChangeNotifierProvider` for `Servicio`, `Factura`, `Configuracion`)
- **DB**: sqflite (single DB `hogarcontrol.db`, tables: `servicios`, `facturas`, `configuracion`)
- **Notifications**: `flutter_local_notifications` with `zonedSchedule` + timezone
- **Charts**: fl_chart (bar chart) + table_calendar
- **UI**: Material 3, no router (manual Navigator pushes)
## Project layout
| Path | Purpose |
|---|---|
| `lib/main.dart` | App entry, MultiProvider setup, theme |
| `lib/models/` | Data classes (`Servicio`, `Factura`, `Configuracion`) |
| `lib/database/database_helper.dart` | Singleton DB with CRUD helpers |
| `lib/providers/` | ChangeNotifier state classes |
| `lib/screens/` | 9 screens (Splash → Dashboard → ...) |
| `lib/services/notification_service.dart` | Local notification scheduling |
| `lib/widgets/` | Reusable UI components |
## DB schema
- `servicios` — id, nombre, icono, color, dia_vencimiento, fecha_creacion
- `facturas` — id, servicio_id (FK), monto, fecha_emision, fecha_vencimiento, estado, nota, fecha_pago
- `configuracion` — id, dias_recordatorio (comma-separated), tema, moneda
## Warnings
- `dart analyze` may flag unused imports/variables in screens — safe to ignore for widget-tree watchers
- Running as root causes flutter warnings but doesn't break builds
- Flutter 3.27.4 has known warnings about x86 target deprecation

16
README.md Normal file
View File

@@ -0,0 +1,16 @@
# hogarcontrol
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

28
analysis_options.yaml Normal file
View File

@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

13
android/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

46
android/app/build.gradle Normal file
View File

@@ -0,0 +1,46 @@
plugins {
id "com.android.application"
id "kotlin-android"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin"
}
android {
namespace = "com.hogarcontrol.hogarcontrol"
compileSdk = 35
ndkVersion = "27.0.12077973"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
coreLibraryDesugaringEnabled true
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17
}
defaultConfig {
applicationId = "com.hogarcontrol.hogarcontrol"
minSdk = 26
targetSdk = 35
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.debug
}
}
}
flutter {
source = "../.."
}
dependencies {
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.4"
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,48 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<application
android:label="hogarcontrol"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,5 @@
package com.hogarcontrol.hogarcontrol
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity()

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

18
android/build.gradle Normal file
View File

@@ -0,0 +1,18 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-all.zip

25
android/settings.gradle Normal file
View File

@@ -0,0 +1,25 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.7.3" apply false
id "org.jetbrains.kotlin.android" version "2.0.21" apply false
}
include ":app"

View File

@@ -0,0 +1,193 @@
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import '../models/servicio.dart';
import '../models/factura.dart';
import '../models/configuracion.dart';
class DatabaseHelper {
static final DatabaseHelper instance = DatabaseHelper._init();
static Database? _database;
DatabaseHelper._init();
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDB('hogarcontrol.db');
return _database!;
}
Future<Database> _initDB(String filePath) async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, filePath);
return await openDatabase(path, version: 1, onCreate: _createDB, onConfigure: _onConfigure);
}
Future _onConfigure(Database db) async {
await db.execute('PRAGMA foreign_keys = ON');
}
Future _createDB(Database db, int version) async {
await db.execute('''
CREATE TABLE servicios (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nombre TEXT NOT NULL,
icono TEXT NOT NULL,
color INTEGER NOT NULL,
dia_vencimiento INTEGER NOT NULL,
fecha_creacion TEXT NOT NULL
)
''');
await db.execute('''
CREATE TABLE facturas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
servicio_id INTEGER NOT NULL,
monto REAL NOT NULL,
fecha_emision TEXT NOT NULL,
fecha_vencimiento TEXT NOT NULL,
estado TEXT NOT NULL DEFAULT 'Pendiente',
nota TEXT,
fecha_pago TEXT,
FOREIGN KEY (servicio_id) REFERENCES servicios(id) ON DELETE CASCADE
)
''');
await db.execute('''
CREATE TABLE configuracion (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dias_recordatorio TEXT NOT NULL DEFAULT '7,3,1,0',
tema TEXT NOT NULL DEFAULT 'claro',
moneda TEXT NOT NULL DEFAULT 'USD'
)
''');
}
Future<int> insertServicio(Servicio servicio) async {
final db = await database;
return await db.insert('servicios', servicio.toMap()..remove('id'));
}
Future<List<Servicio>> getServicios() async {
final db = await database;
final maps = await db.query('servicios', orderBy: 'nombre ASC');
return maps.map((map) => Servicio.fromMap(map)).toList();
}
Future<int> updateServicio(Servicio servicio) async {
final db = await database;
return await db.update('servicios', servicio.toMap(), where: 'id = ?', whereArgs: [servicio.id]);
}
Future<int> deleteServicio(int id) async {
final db = await database;
return await db.delete('servicios', where: 'id = ?', whereArgs: [id]);
}
Future<int> insertFactura(Factura factura) async {
final db = await database;
return await db.insert('facturas', factura.toMap()..remove('id'));
}
Future<List<Factura>> getFacturas({String? estado, int? servicioId}) async {
final db = await database;
String? where;
List<dynamic>? whereArgs;
if (estado != null && servicioId != null) {
where = 'estado = ? AND servicio_id = ?';
whereArgs = [estado, servicioId];
} else if (estado != null) {
where = 'estado = ?';
whereArgs = [estado];
} else if (servicioId != null) {
where = 'servicio_id = ?';
whereArgs = [servicioId];
}
final maps = await db.query('facturas', where: where, whereArgs: whereArgs, orderBy: 'fecha_vencimiento DESC');
return maps.map((map) => Factura.fromMap(map)).toList();
}
Future<List<Factura>> getFacturasVencidas() async {
final db = await database;
final hoy = DateTime.now().toIso8601String().split('T')[0];
final maps = await db.query('facturas',
where: 'estado = ? AND fecha_vencimiento < ?', whereArgs: ['Pendiente', hoy], orderBy: 'fecha_vencimiento ASC');
return maps.map((map) => Factura.fromMap(map)).toList();
}
Future<List<Factura>> getFacturasPorMes(int year, int month) async {
final db = await database;
final mes = month.toString().padLeft(2, '0');
final maps = await db.query('facturas',
where: "fecha_vencimiento LIKE ?", whereArgs: ['$year-$mes%'], orderBy: 'fecha_vencimiento ASC');
return maps.map((map) => Factura.fromMap(map)).toList();
}
Future<List<Factura>> getFacturasPorRango(String inicio, String fin) async {
final db = await database;
final maps = await db.query('facturas',
where: 'fecha_vencimiento BETWEEN ? AND ?', whereArgs: [inicio, fin], orderBy: 'fecha_vencimiento ASC');
return maps.map((map) => Factura.fromMap(map)).toList();
}
Future<double> getTotalGastadoMes(int year, int month) async {
final db = await database;
final mes = month.toString().padLeft(2, '0');
final result = await db.rawQuery(
"SELECT COALESCE(SUM(monto), 0) as total FROM facturas WHERE estado = 'Pagada' AND fecha_pago LIKE ?",
['$year-$mes%']);
return (result.first['total'] as num).toDouble();
}
Future<Map<String, double>> getGastosPorServicio(int year, int month) async {
final db = await database;
final mes = month.toString().padLeft(2, '0');
final result = await db.rawQuery('''
SELECT s.nombre, COALESCE(SUM(f.monto), 0) as total
FROM facturas f
JOIN servicios s ON f.servicio_id = s.id
WHERE f.estado = 'Pagada' AND f.fecha_pago LIKE ?
GROUP BY s.nombre
''', ['$year-$mes%']);
final map = <String, double>{};
for (final row in result) {
map[row['nombre'] as String] = (row['total'] as num).toDouble();
}
return map;
}
Future<List<Map<String, dynamic>>> getGastoMensual(int year) async {
final db = await database;
return await db.rawQuery('''
SELECT strftime('%m', fecha_pago) as mes, COALESCE(SUM(monto), 0) as total
FROM facturas
WHERE estado = 'Pagada' AND fecha_pago LIKE ?
GROUP BY mes
ORDER BY mes
''', ['$year-%']);
}
Future<int> updateFactura(Factura factura) async {
final db = await database;
return await db.update('facturas', factura.toMap(), where: 'id = ?', whereArgs: [factura.id]);
}
Future<int> deleteFactura(int id) async {
final db = await database;
return await db.delete('facturas', where: 'id = ?', whereArgs: [id]);
}
Future<Configuracion?> getConfiguracion() async {
final db = await database;
final maps = await db.query('configuracion', limit: 1);
if (maps.isEmpty) return null;
return Configuracion.fromMap(maps.first);
}
Future<int> saveConfiguracion(Configuracion config) async {
final db = await database;
final existing = await getConfiguracion();
if (existing != null) {
return await db.update('configuracion', config.toMap()..remove('id'),
where: 'id = ?', whereArgs: [existing.id]);
}
return await db.insert('configuracion', config.toMap()..remove('id'));
}
}

38
lib/main.dart Normal file
View File

@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/servicio_provider.dart';
import 'providers/factura_provider.dart';
import 'providers/configuracion_provider.dart';
import 'screens/splash_screen.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => ServicioProvider()),
ChangeNotifierProvider(create: (_) => FacturaProvider()),
ChangeNotifierProvider(create: (_) => ConfiguracionProvider()),
],
child: const HogarControlApp(),
),
);
}
class HogarControlApp extends StatelessWidget {
const HogarControlApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'HogarControl',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: Colors.teal,
useMaterial3: true,
brightness: Brightness.light,
),
home: const SplashScreen(),
);
}
}

View File

@@ -0,0 +1,27 @@
class Configuracion {
final int? id;
final String diasRecordatorio;
final String tema;
final String moneda;
Configuracion({
this.id,
required this.diasRecordatorio,
this.tema = 'claro',
this.moneda = 'USD',
});
Map<String, dynamic> toMap() => {
'id': id,
'dias_recordatorio': diasRecordatorio,
'tema': tema,
'moneda': moneda,
};
factory Configuracion.fromMap(Map<String, dynamic> map) => Configuracion(
id: map['id'] as int?,
diasRecordatorio: map['dias_recordatorio'] as String,
tema: map['tema'] as String? ?? 'claro',
moneda: map['moneda'] as String? ?? 'USD',
);
}

64
lib/models/factura.dart Normal file
View File

@@ -0,0 +1,64 @@
class Factura {
final int? id;
final int servicioId;
final double monto;
final String fechaEmision;
final String fechaVencimiento;
final String estado;
final String? nota;
final String? fechaPago;
Factura({
this.id,
required this.servicioId,
required this.monto,
required this.fechaEmision,
required this.fechaVencimiento,
this.estado = 'Pendiente',
this.nota,
this.fechaPago,
});
Factura copyWith({
int? id,
int? servicioId,
double? monto,
String? fechaEmision,
String? fechaVencimiento,
String? estado,
String? nota,
String? fechaPago,
}) =>
Factura(
id: id ?? this.id,
servicioId: servicioId ?? this.servicioId,
monto: monto ?? this.monto,
fechaEmision: fechaEmision ?? this.fechaEmision,
fechaVencimiento: fechaVencimiento ?? this.fechaVencimiento,
estado: estado ?? this.estado,
nota: nota ?? this.nota,
fechaPago: fechaPago ?? this.fechaPago,
);
Map<String, dynamic> toMap() => {
'id': id,
'servicio_id': servicioId,
'monto': monto,
'fecha_emision': fechaEmision,
'fecha_vencimiento': fechaVencimiento,
'estado': estado,
'nota': nota,
'fecha_pago': fechaPago,
};
factory Factura.fromMap(Map<String, dynamic> map) => Factura(
id: map['id'] as int?,
servicioId: map['servicio_id'] as int,
monto: (map['monto'] as num).toDouble(),
fechaEmision: map['fecha_emision'] as String,
fechaVencimiento: map['fecha_vencimiento'] as String,
estado: map['estado'] as String? ?? 'Pendiente',
nota: map['nota'] as String?,
fechaPago: map['fecha_pago'] as String?,
);
}

35
lib/models/servicio.dart Normal file
View File

@@ -0,0 +1,35 @@
class Servicio {
final int? id;
final String nombre;
final String icono;
final int color;
final int diaVencimiento;
final String fechaCreacion;
Servicio({
this.id,
required this.nombre,
required this.icono,
required this.color,
required this.diaVencimiento,
required this.fechaCreacion,
});
Map<String, dynamic> toMap() => {
'id': id,
'nombre': nombre,
'icono': icono,
'color': color,
'dia_vencimiento': diaVencimiento,
'fecha_creacion': fechaCreacion,
};
factory Servicio.fromMap(Map<String, dynamic> map) => Servicio(
id: map['id'] as int?,
nombre: map['nombre'] as String,
icono: map['icono'] as String,
color: map['color'] as int,
diaVencimiento: map['dia_vencimiento'] as int,
fechaCreacion: map['fecha_creacion'] as String,
);
}

View File

@@ -0,0 +1,40 @@
import 'package:flutter/foundation.dart';
import '../database/database_helper.dart';
import '../models/configuracion.dart';
import '../services/notification_service.dart';
class ConfiguracionProvider extends ChangeNotifier {
final DatabaseHelper _db = DatabaseHelper.instance;
Configuracion? _config;
bool _cargando = false;
Configuracion? get config => _config;
bool get cargando => _cargando;
Future<void> cargarConfiguracion() async {
_cargando = true;
notifyListeners();
_config = await _db.getConfiguracion();
if (_config == null) {
_config = Configuracion(diasRecordatorio: '7,3,1,0');
await _db.saveConfiguracion(_config!);
}
_cargando = false;
notifyListeners();
}
Future<void> guardarConfiguracion(Configuracion config) async {
await _db.saveConfiguracion(config);
_config = config;
await NotificationService.instance.scheduleRecordatorios();
notifyListeners();
}
List<int> get diasRecordatorioList {
if (_config == null) return [7, 3, 1, 0];
return _config!.diasRecordatorio
.split(',')
.map((e) => int.tryParse(e.trim()) ?? 0)
.toList();
}
}

View File

@@ -0,0 +1,61 @@
import 'package:flutter/foundation.dart';
import '../database/database_helper.dart';
import '../models/factura.dart';
class FacturaProvider extends ChangeNotifier {
final DatabaseHelper _db = DatabaseHelper.instance;
List<Factura> _facturas = [];
List<Factura> _facturasVencidas = [];
bool _cargando = false;
List<Factura> get facturas => _facturas;
List<Factura> get facturasVencidas => _facturasVencidas;
bool get cargando => _cargando;
Future<void> cargarFacturas({String? estado, int? servicioId}) async {
_cargando = true;
notifyListeners();
_facturas = await _db.getFacturas(estado: estado, servicioId: servicioId);
_facturasVencidas = await _db.getFacturasVencidas();
_cargando = false;
notifyListeners();
}
Future<void> agregarFactura(Factura factura) async {
await _db.insertFactura(factura);
await cargarFacturas();
}
Future<void> marcarPagada(int id) async {
final factura = _facturas.firstWhere((f) => f.id == id);
final hoy = DateTime.now().toIso8601String().split('T')[0];
final actualizada = factura.copyWith(estado: 'Pagada', fechaPago: hoy);
await _db.updateFactura(actualizada);
await cargarFacturas();
}
Future<void> eliminarFactura(int id) async {
await _db.deleteFactura(id);
await cargarFacturas();
}
Future<double> totalGastadoMes(int year, int month) async {
return await _db.getTotalGastadoMes(year, month);
}
Future<Map<String, double>> gastosPorServicio(int year, int month) async {
return await _db.getGastosPorServicio(year, month);
}
Future<List<Map<String, dynamic>>> gastoMensual(int year) async {
return await _db.getGastoMensual(year);
}
Future<List<Factura>> facturasPorMes(int year, int month) async {
return await _db.getFacturasPorMes(year, month);
}
Future<List<Factura>> facturasPorRango(String inicio, String fin) async {
return await _db.getFacturasPorRango(inicio, fin);
}
}

View File

@@ -0,0 +1,35 @@
import 'package:flutter/foundation.dart';
import '../database/database_helper.dart';
import '../models/servicio.dart';
class ServicioProvider extends ChangeNotifier {
final DatabaseHelper _db = DatabaseHelper.instance;
List<Servicio> _servicios = [];
bool _cargando = false;
List<Servicio> get servicios => _servicios;
bool get cargando => _cargando;
Future<void> cargarServicios() async {
_cargando = true;
notifyListeners();
_servicios = await _db.getServicios();
_cargando = false;
notifyListeners();
}
Future<void> agregarServicio(Servicio servicio) async {
await _db.insertServicio(servicio);
await cargarServicios();
}
Future<void> actualizarServicio(Servicio servicio) async {
await _db.updateServicio(servicio);
await cargarServicios();
}
Future<void> eliminarServicio(int id) async {
await _db.deleteServicio(id);
await cargarServicios();
}
}

View File

@@ -0,0 +1,123 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import 'package:table_calendar/table_calendar.dart';
import '../providers/factura_provider.dart';
import '../providers/servicio_provider.dart';
import '../providers/configuracion_provider.dart';
class CalendarioScreen extends StatefulWidget {
const CalendarioScreen({super.key});
@override
State<CalendarioScreen> createState() => _CalendarioScreenState();
}
class _CalendarioScreenState extends State<CalendarioScreen> {
DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay;
Map<DateTime, List<Map<String, dynamic>>> _eventos = {};
@override
void initState() {
super.initState();
_cargarEventos();
}
void _cargarEventos() {
final facturaProv = context.read<FacturaProvider>();
final servicioProv = context.read<ServicioProvider>();
facturaProv.cargarFacturas().then((_) {
_construirEventos(facturaProv, servicioProv);
});
}
void _construirEventos(FacturaProvider facturaProv, ServicioProvider servicioProv) {
final eventos = <DateTime, List<Map<String, dynamic>>>{};
for (final f in facturaProv.facturas) {
final fecha = DateTime.tryParse(f.fechaVencimiento);
if (fecha == null) continue;
final servicio = servicioProv.servicios.where((s) => s.id == f.servicioId).firstOrNull;
final day = DateTime(fecha.year, fecha.month, fecha.day);
eventos.putIfAbsent(day, () => []).add({
'servicio': servicio?.nombre ?? 'Desconocido',
'monto': f.monto,
'estado': f.estado,
'color': Color(servicio?.color ?? 0xFF6200EE),
});
}
setState(() => _eventos = eventos);
}
@override
Widget build(BuildContext context) {
context.watch<FacturaProvider>();
context.watch<ServicioProvider>();
final configProv = context.watch<ConfiguracionProvider>();
final symbol = configProv.config?.moneda == 'USD' ? '\$' : configProv.config?.moneda == 'EUR' ? '' : 'MX\$';
return Scaffold(
appBar: AppBar(title: const Text('Calendario de pagos')),
body: Column(
children: [
TableCalendar(
firstDay: DateTime(2024),
lastDay: DateTime(2035),
focusedDay: _focusedDay,
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
eventLoader: (day) {
final d = DateTime(day.year, day.month, day.day);
return _eventos[d] ?? [];
},
calendarStyle: CalendarStyle(
todayDecoration: BoxDecoration(color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5), shape: BoxShape.circle),
selectedDecoration: BoxDecoration(color: Theme.of(context).colorScheme.primary, shape: BoxShape.circle),
markerDecoration: BoxDecoration(color: Theme.of(context).colorScheme.secondary, shape: BoxShape.circle),
),
headerStyle: const HeaderStyle(formatButtonVisible: false, titleCentered: true),
onDaySelected: (selected, focused) {
setState(() {
_selectedDay = selected;
_focusedDay = focused;
});
},
onPageChanged: (focused) => _focusedDay = focused,
calendarBuilders: CalendarBuilders(
markerBuilder: (context, date, events) {
if (events.isEmpty) return null;
final pendientes = events.where((e) => (e as Map)['estado'] == 'Pendiente').length;
final pagadas = events.where((e) => (e as Map)['estado'] == 'Pagada').length;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (pendientes > 0)
Container(margin: const EdgeInsets.only(right: 2), width: 6, height: 6, decoration: BoxDecoration(color: Colors.red, shape: BoxShape.circle)),
if (pagadas > 0)
Container(width: 6, height: 6, decoration: BoxDecoration(color: Colors.green, shape: BoxShape.circle)),
],
);
},
),
),
const Divider(),
Expanded(
child: _selectedDay == null || (_eventos[DateTime(_selectedDay!.year, _selectedDay!.month, _selectedDay!.day)]?.isEmpty ?? true)
? Center(child: Text('Sin facturas en esta fecha', style: Theme.of(context).textTheme.bodyLarge))
: ListView(
children: (_eventos[DateTime(_selectedDay!.year, _selectedDay!.month, _selectedDay!.day)] ?? []).map((e) {
return Card(
child: ListTile(
leading: CircleAvatar(backgroundColor: e['color'] as Color, child: Text((e['servicio'] as String).substring(0, 2).toUpperCase(), style: const TextStyle(color: Colors.white, fontSize: 12))),
title: Text(e['servicio'] as String),
subtitle: Text('$symbol${NumberFormat('#,##0.00').format(e['monto'])}'),
trailing: Chip(label: Text(e['estado'] as String, style: TextStyle(fontSize: 12, color: e['estado'] == 'Pagada' ? Colors.green : Colors.orange))),
),
);
}).toList(),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/configuracion.dart';
import '../providers/configuracion_provider.dart';
class ConfiguracionScreen extends StatefulWidget {
const ConfiguracionScreen({super.key});
@override
State<ConfiguracionScreen> createState() => _ConfiguracionScreenState();
}
class _ConfiguracionScreenState extends State<ConfiguracionScreen> {
late List<bool> _diasSeleccionados;
late String _moneda;
final List<Map<String, dynamic>> _opcionesDias = [
{'label': '7 días antes', 'value': 7},
{'label': '3 días antes', 'value': 3},
{'label': '1 día antes', 'value': 1},
{'label': 'Día de vencimiento', 'value': 0},
];
final List<String> _monedas = ['USD', 'EUR', 'MXN', 'COP', 'ARS', 'PEN', 'CLP', 'BRL'];
@override
void initState() {
super.initState();
final config = context.read<ConfiguracionProvider>().config;
final dias = config?.diasRecordatorio ?? '7,3,1,0';
final lista = dias.split(',').map((e) => int.tryParse(e.trim()) ?? 0).toList();
_diasSeleccionados = _opcionesDias.map((o) => lista.contains(o['value'])).toList();
_moneda = config?.moneda ?? 'USD';
}
@override
Widget build(BuildContext context) {
final provider = context.read<ConfiguracionProvider>();
return Scaffold(
appBar: AppBar(title: const Text('Configuración')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Text('Recordatorios', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
...List.generate(_opcionesDias.length, (i) {
return CheckboxListTile(
title: Text(_opcionesDias[i]['label'] as String),
value: _diasSeleccionados[i],
onChanged: (v) {
setState(() => _diasSeleccionados[i] = v ?? false);
},
);
}),
const SizedBox(height: 20),
Text('Moneda', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
value: _moneda,
decoration: const InputDecoration(border: OutlineInputBorder()),
items: _monedas.map((m) => DropdownMenuItem(value: m, child: Text(m))).toList(),
onChanged: (v) => setState(() => _moneda = v!),
),
const SizedBox(height: 32),
FilledButton(
onPressed: () async {
final diasStr = _opcionesDias
.asMap()
.entries
.where((e) => _diasSeleccionados[e.key])
.map((e) => e.value['value'].toString())
.join(',');
final config = Configuracion(
id: provider.config?.id,
diasRecordatorio: diasStr.isEmpty ? '0' : diasStr,
tema: provider.config?.tema ?? 'claro',
moneda: _moneda,
);
await provider.guardarConfiguracion(config);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Configuración guardada')),
);
}
},
child: const Text('Guardar configuración'),
),
],
),
);
}
}

View File

@@ -0,0 +1,126 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import '../models/factura.dart';
import '../providers/factura_provider.dart';
import '../providers/servicio_provider.dart';
class CrearFacturaScreen extends StatefulWidget {
const CrearFacturaScreen({super.key});
@override
State<CrearFacturaScreen> createState() => _CrearFacturaScreenState();
}
class _CrearFacturaScreenState extends State<CrearFacturaScreen> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _montoCtrl;
late TextEditingController _notaCtrl;
int? _servicioId;
DateTime _fechaEmision = DateTime.now();
DateTime _fechaVencimiento = DateTime.now().add(const Duration(days: 30));
String _estado = 'Pendiente';
@override
void initState() {
super.initState();
_montoCtrl = TextEditingController();
_notaCtrl = TextEditingController();
context.read<ServicioProvider>().cargarServicios();
}
@override
void dispose() {
_montoCtrl.dispose();
_notaCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final servicioProv = context.watch<ServicioProvider>();
final facturaProv = context.read<FacturaProvider>();
return Scaffold(
appBar: AppBar(title: const Text('Nueva factura')),
body: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
DropdownButtonFormField<int>(
value: _servicioId,
decoration: const InputDecoration(labelText: 'Servicio', border: OutlineInputBorder()),
items: servicioProv.servicios.map((s) => DropdownMenuItem(value: s.id, child: Text(s.nombre))).toList(),
onChanged: (v) => setState(() => _servicioId = v),
validator: (v) => v == null ? 'Selecciona un servicio' : null,
),
const SizedBox(height: 16),
TextFormField(
controller: _montoCtrl,
decoration: const InputDecoration(labelText: 'Monto', border: OutlineInputBorder(), prefixText: '\$ '),
keyboardType: TextInputType.numberWithOptions(decimal: true),
validator: (v) {
if (v == null || v.isEmpty) return 'Ingresa el monto';
if (double.tryParse(v) == null) return 'Monto inválido';
return null;
},
),
const SizedBox(height: 16),
ListTile(
title: const Text('Fecha de emisión'),
subtitle: Text(DateFormat('yyyy-MM-dd').format(_fechaEmision)),
trailing: const Icon(Icons.calendar_today),
onTap: () async {
final date = await showDatePicker(context: context, firstDate: DateTime(2020), lastDate: DateTime(2035), initialDate: _fechaEmision);
if (date != null) setState(() => _fechaEmision = date);
},
),
ListTile(
title: const Text('Fecha de vencimiento'),
subtitle: Text(DateFormat('yyyy-MM-dd').format(_fechaVencimiento)),
trailing: const Icon(Icons.calendar_today),
onTap: () async {
final date = await showDatePicker(context: context, firstDate: DateTime(2020), lastDate: DateTime(2035), initialDate: _fechaVencimiento);
if (date != null) setState(() => _fechaVencimiento = date);
},
),
const SizedBox(height: 16),
TextFormField(
controller: _notaCtrl,
decoration: const InputDecoration(labelText: 'Nota (opcional)', border: OutlineInputBorder()),
maxLines: 2,
),
const SizedBox(height: 16),
SegmentedButton<String>(
segments: const [
ButtonSegment(value: 'Pendiente', label: Text('Pendiente')),
ButtonSegment(value: 'Pagada', label: Text('Pagada')),
],
selected: {_estado},
onSelectionChanged: (v) => setState(() => _estado = v.first),
),
const SizedBox(height: 32),
FilledButton(
onPressed: () async {
if (!_formKey.currentState!.validate()) return;
final factura = Factura(
servicioId: _servicioId!,
monto: double.parse(_montoCtrl.text),
fechaEmision: DateFormat('yyyy-MM-dd').format(_fechaEmision),
fechaVencimiento: DateFormat('yyyy-MM-dd').format(_fechaVencimiento),
estado: _estado,
nota: _notaCtrl.text.isEmpty ? null : _notaCtrl.text,
fechaPago: _estado == 'Pagada' ? DateFormat('yyyy-MM-dd').format(DateTime.now()) : null,
);
await facturaProv.agregarFactura(factura);
if (context.mounted) Navigator.pop(context);
},
child: const Text('Guardar factura'),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,149 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import '../models/servicio.dart';
import '../providers/servicio_provider.dart';
class CrearServicioScreen extends StatefulWidget {
final Servicio? servicio;
const CrearServicioScreen({super.key, this.servicio});
@override
State<CrearServicioScreen> createState() => _CrearServicioScreenState();
}
class _CrearServicioScreenState extends State<CrearServicioScreen> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _nombreCtrl;
String _iconoSeleccionado = 'recibo';
Color _colorSeleccionado = Colors.purple;
int _diaVencimiento = 15;
final List<Map<String, dynamic>> _iconos = [
{'key': 'electricidad', 'icon': Icons.bolt, 'label': 'Electricidad'},
{'key': 'agua', 'icon': Icons.water_drop, 'label': 'Agua'},
{'key': 'internet', 'icon': Icons.wifi, 'label': 'Internet'},
{'key': 'cable', 'icon': Icons.tv, 'label': 'Cable'},
{'key': 'telefono', 'icon': Icons.phone, 'label': 'Teléfono'},
{'key': 'gas', 'icon': Icons.local_fire_department, 'label': 'Gas'},
{'key': 'alquiler', 'icon': Icons.home, 'label': 'Alquiler'},
{'key': 'recibo', 'icon': Icons.receipt, 'label': 'Otros'},
];
final List<Color> _colores = [
Colors.red, Colors.pink, Colors.purple, Colors.deepPurple,
Colors.indigo, Colors.blue, Colors.lightBlue, Colors.cyan,
Colors.teal, Colors.green, Colors.lightGreen, Colors.lime,
Colors.yellow, Colors.amber, Colors.orange, Colors.deepOrange,
Colors.brown, Colors.grey, Colors.blueGrey,
];
@override
void initState() {
super.initState();
_nombreCtrl = TextEditingController(text: widget.servicio?.nombre ?? '');
if (widget.servicio != null) {
_iconoSeleccionado = widget.servicio!.icono;
_colorSeleccionado = Color(widget.servicio!.color);
_diaVencimiento = widget.servicio!.diaVencimiento;
}
}
@override
void dispose() {
_nombreCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final provider = context.read<ServicioProvider>();
return Scaffold(
appBar: AppBar(title: Text(widget.servicio != null ? 'Editar servicio' : 'Nuevo servicio')),
body: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
TextFormField(
controller: _nombreCtrl,
decoration: const InputDecoration(labelText: 'Nombre del servicio', border: OutlineInputBorder()),
validator: (v) => v == null || v.trim().isEmpty ? 'Ingresa el nombre' : null,
),
const SizedBox(height: 20),
Text('Icono', style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: _iconos.map((item) {
final selected = _iconoSeleccionado == item['key'];
return ChoiceChip(
selected: selected,
avatar: Icon(item['icon'] as IconData, size: 20),
label: Text(item['label'] as String, style: const TextStyle(fontSize: 12)),
onSelected: (_) => setState(() => _iconoSeleccionado = item['key'] as String),
);
}).toList(),
),
const SizedBox(height: 20),
Text('Color', style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 8),
Wrap(
spacing: 6,
runSpacing: 6,
children: _colores.map((c) {
final selected = _colorSeleccionado == c;
return GestureDetector(
onTap: () => setState(() => _colorSeleccionado = c),
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: c,
shape: BoxShape.circle,
border: selected ? Border.all(color: Colors.white, width: 3) : null,
boxShadow: selected ? [BoxShadow(color: c.withValues(alpha: 0.5), blurRadius: 8)] : null,
),
child: selected ? const Icon(Icons.check, color: Colors.white, size: 18) : null,
),
);
}).toList(),
),
const SizedBox(height: 20),
Text('Día de vencimiento', style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 8),
DropdownButtonFormField<int>(
value: _diaVencimiento,
decoration: const InputDecoration(border: OutlineInputBorder()),
items: List.generate(28, (i) => DropdownMenuItem(value: i + 1, child: Text('${i + 1}'))),
onChanged: (v) => setState(() => _diaVencimiento = v!),
),
const SizedBox(height: 32),
FilledButton(
onPressed: () async {
if (!_formKey.currentState!.validate()) return;
final servicio = Servicio(
id: widget.servicio?.id,
nombre: _nombreCtrl.text.trim(),
icono: _iconoSeleccionado,
color: _colorSeleccionado.value,
diaVencimiento: _diaVencimiento,
fechaCreacion: widget.servicio?.fechaCreacion ?? DateFormat('yyyy-MM-dd').format(DateTime.now()),
);
if (widget.servicio != null) {
await provider.actualizarServicio(servicio);
} else {
await provider.agregarServicio(servicio);
}
if (context.mounted) Navigator.pop(context);
},
child: Text(widget.servicio != null ? 'Guardar cambios' : 'Crear servicio'),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,142 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import '../providers/servicio_provider.dart';
import '../providers/factura_provider.dart';
import '../providers/configuracion_provider.dart';
import '../widgets/stat_card.dart';
import 'servicios_screen.dart';
import 'facturas_screen.dart';
import 'calendario_screen.dart';
import 'estadisticas_screen.dart';
import 'configuracion_screen.dart';
import 'crear_factura_screen.dart';
class DashboardScreen extends StatefulWidget {
const DashboardScreen({super.key});
@override
State<DashboardScreen> createState() => _DashboardScreenState();
}
class _DashboardScreenState extends State<DashboardScreen> {
@override
void initState() {
super.initState();
_cargarDatos();
}
void _cargarDatos() {
context.read<ServicioProvider>().cargarServicios();
context.read<FacturaProvider>().cargarFacturas();
}
@override
Widget build(BuildContext context) {
final facturaProv = context.watch<FacturaProvider>();
final servicioProv = context.watch<ServicioProvider>();
final configProv = context.watch<ConfiguracionProvider>();
final moneda = configProv.config?.moneda ?? 'USD';
final symbol = moneda == 'USD' ? '\$' : moneda == 'EUR' ? '' : moneda == 'MXN' ? 'MX\$' : '\$';
final pendientes = facturaProv.facturas.where((f) => f.estado == 'Pendiente').length;
final vencidas = facturaProv.facturasVencidas.length;
return Scaffold(
appBar: AppBar(title: const Text('HogarControl'), centerTitle: true, actions: [
IconButton(icon: const Icon(Icons.settings), onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ConfiguracionScreen()))),
]),
body: RefreshIndicator(
onRefresh: () async => _cargarDatos(),
child: ListView(
padding: const EdgeInsets.all(16),
children: [
Row(
children: [
Expanded(child: StatCard(
title: 'Pendientes',
value: '$pendientes',
icon: Icons.pending_actions,
color: Colors.orange,
)),
const SizedBox(width: 12),
Expanded(child: StatCard(
title: 'Vencidas',
value: '$vencidas',
icon: Icons.warning_rounded,
color: Colors.red,
)),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(child: StatCard(
title: 'Pagadas este mes',
value: facturaProv.facturas.where((f) => f.estado == 'Pagada').length.toString(),
icon: Icons.check_circle,
color: Colors.green,
)),
const SizedBox(width: 12),
Expanded(child: StatCard(
title: 'Total gastado',
value: '$symbol${NumberFormat('#,##0.00').format(facturaProv.facturas.where((f) => f.estado == 'Pagada').fold(0.0, (sum, f) => sum + f.monto))}',
icon: Icons.attach_money,
color: Colors.blue,
)),
],
),
const SizedBox(height: 20),
Text('Próximos vencimientos', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
...facturaProv.facturas
.where((f) => f.estado == 'Pendiente')
.take(5)
.map((f) {
final servicio = servicioProv.servicios.where((s) => s.id == f.servicioId).firstOrNull;
return Card(
child: ListTile(
leading: CircleAvatar(
backgroundColor: Color(servicio?.color ?? 0xFF6200EE),
child: Text(servicio?.nombre.substring(0, 2).toUpperCase() ?? '??',
style: const TextStyle(color: Colors.white, fontSize: 12)),
),
title: Text(servicio?.nombre ?? 'Desconocido'),
subtitle: Text('Ven: ${f.fechaVencimiento} $symbol${NumberFormat('#,##0.00').format(f.monto)}'),
trailing: TextButton(
onPressed: () => facturaProv.marcarPagada(f.id!),
child: const Text('Pagar'),
),
),
);
}),
],
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const CrearFacturaScreen())).then((_) => _cargarDatos()),
icon: const Icon(Icons.add),
label: const Text('Nueva factura'),
),
bottomNavigationBar: NavigationBar(
selectedIndex: 0,
onDestinationSelected: (i) {
switch (i) {
case 0: break;
case 1: Navigator.push(context, MaterialPageRoute(builder: (_) => const ServiciosScreen()));
case 2: Navigator.push(context, MaterialPageRoute(builder: (_) => const FacturasScreen()));
case 3: Navigator.push(context, MaterialPageRoute(builder: (_) => const CalendarioScreen()));
case 4: Navigator.push(context, MaterialPageRoute(builder: (_) => const EstadisticasScreen()));
}
},
destinations: const [
NavigationDestination(icon: Icon(Icons.dashboard), label: 'Dashboard'),
NavigationDestination(icon: Icon(Icons.electrical_services), label: 'Servicios'),
NavigationDestination(icon: Icon(Icons.receipt_long), label: 'Facturas'),
NavigationDestination(icon: Icon(Icons.calendar_month), label: 'Calendario'),
NavigationDestination(icon: Icon(Icons.bar_chart), label: 'Stats'),
],
),
);
}
}

View File

@@ -0,0 +1,145 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import 'package:fl_chart/fl_chart.dart';
import '../providers/factura_provider.dart';
import '../providers/configuracion_provider.dart';
class EstadisticasScreen extends StatefulWidget {
const EstadisticasScreen({super.key});
@override
State<EstadisticasScreen> createState() => _EstadisticasScreenState();
}
class _EstadisticasScreenState extends State<EstadisticasScreen> {
late int _year;
@override
void initState() {
super.initState();
_year = DateTime.now().year;
}
@override
Widget build(BuildContext context) {
final facturaProv = context.watch<FacturaProvider>();
final configProv = context.watch<ConfiguracionProvider>();
final moneda = configProv.config?.moneda ?? 'USD';
final symbol = moneda == 'USD' ? '\$' : moneda == 'EUR' ? '' : moneda == 'MXN' ? 'MX\$' : '\$';
return Scaffold(
appBar: AppBar(title: const Text('Estadísticas')),
body: FutureBuilder(
future: Future.wait([
facturaProv.gastoMensual(_year),
facturaProv.gastosPorServicio(_year, DateTime.now().month),
facturaProv.totalGastadoMes(_year, DateTime.now().month),
]),
builder: (context, AsyncSnapshot<List<dynamic>> snapshot) {
if (!snapshot.hasData) return const Center(child: CircularProgressIndicator());
final gastoMensual = snapshot.data![0] as List<Map<String, dynamic>>;
final gastoPorServicio = snapshot.data![1] as Map<String, double>;
final totalMes = snapshot.data![2] as double;
return ListView(
padding: const EdgeInsets.all(16),
children: [
Text('Resumen $_year', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 8),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Gasto mensual actual', style: Theme.of(context).textTheme.titleSmall),
Text('$symbol${NumberFormat('#,##0.00').format(totalMes)}',
style: Theme.of(context).textTheme.headlineMedium),
],
),
),
),
const SizedBox(height: 20),
Text('Gasto mensual ($_year)', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
SizedBox(
height: 200,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: (gastoMensual.isEmpty ? 1000 : gastoMensual.map((e) => (e['total'] as num).toDouble()).reduce((a, b) => a > b ? a : b)) * 1.2,
barGroups: List.generate(12, (i) {
final mes = (i + 1).toString().padLeft(2, '0');
final data = gastoMensual.where((e) => e['mes'] == mes).firstOrNull;
return BarChartGroupData(
x: i,
barRods: [
BarChartRodData(
toY: data != null ? (data['total'] as num).toDouble() : 0,
color: Theme.of(context).colorScheme.primary,
width: 16,
borderRadius: const BorderRadius.vertical(top: Radius.circular(4)),
),
],
);
}),
titlesData: FlTitlesData(
leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: true, reservedSize: 40, getTitlesWidget: (v, _) => Text('$symbol${v.toInt()}', style: const TextStyle(fontSize: 10)))),
bottomTitles: AxisTitles(sideTitles: SideTitles(showTitles: true, getTitlesWidget: (v, _) {
const meses = ['E', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'];
return Text(meses[v.toInt()], style: const TextStyle(fontSize: 10));
})),
rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)),
),
gridData: FlGridData(show: true, drawVerticalLine: false, horizontalInterval: (gastoMensual.isEmpty ? 1000 : gastoMensual.map((e) => (e['total'] as num).toDouble()).reduce((a, b) => a > b ? a : b)) / 4),
borderData: FlBorderData(show: false),
),
),
),
const SizedBox(height: 20),
Text('Distribución por servicio', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
if (gastoPorServicio.isEmpty)
Card(child: Padding(padding: const EdgeInsets.all(16), child: Text('Sin datos este mes')))
else
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: gastoPorServicio.entries.map((e) {
final total = gastoPorServicio.values.fold(0.0, (a, b) => a + b);
final pct = total > 0 ? e.value / total * 100 : 0.0;
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
SizedBox(width: 100, child: Text(e.key, style: const TextStyle(fontSize: 13))),
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: pct / 100,
backgroundColor: Colors.grey[200],
minHeight: 12,
),
),
),
const SizedBox(width: 8),
SizedBox(width: 70, child: Text('$symbol${NumberFormat('#,##0.00').format(e.value)}', style: const TextStyle(fontSize: 12), textAlign: TextAlign.right)),
],
),
);
}).toList(),
),
),
),
],
);
},
),
);
}
}

View File

@@ -0,0 +1,110 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import '../providers/factura_provider.dart';
import '../providers/servicio_provider.dart';
import '../providers/configuracion_provider.dart';
class FacturasScreen extends StatefulWidget {
const FacturasScreen({super.key});
@override
State<FacturasScreen> createState() => _FacturasScreenState();
}
class _FacturasScreenState extends State<FacturasScreen> {
String? _filtroEstado;
int? _filtroServicio;
@override
void initState() {
super.initState();
context.read<FacturaProvider>().cargarFacturas();
}
@override
Widget build(BuildContext context) {
final facturaProv = context.watch<FacturaProvider>();
final servicioProv = context.watch<ServicioProvider>();
final configProv = context.watch<ConfiguracionProvider>();
final moneda = configProv.config?.moneda ?? 'USD';
final symbol = moneda == 'USD' ? '\$' : moneda == 'EUR' ? '' : moneda == 'MXN' ? 'MX\$' : '\$';
return Scaffold(
appBar: AppBar(title: const Text('Facturas')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
FilterChip(label: const Text('Todas'), selected: _filtroEstado == null && _filtroServicio == null, onSelected: (_) => setState(() { _filtroEstado = null; _filtroServicio = null; facturaProv.cargarFacturas(); })),
const SizedBox(width: 8),
FilterChip(label: const Text('Pendientes'), selected: _filtroEstado == 'Pendiente', onSelected: (_) => setState(() { _filtroEstado = 'Pendiente'; _filtroServicio = null; facturaProv.cargarFacturas(estado: 'Pendiente'); })),
const SizedBox(width: 8),
FilterChip(label: const Text('Pagadas'), selected: _filtroEstado == 'Pagada', onSelected: (_) => setState(() { _filtroEstado = 'Pagada'; _filtroServicio = null; facturaProv.cargarFacturas(estado: 'Pagada'); })),
const SizedBox(width: 8),
FilterChip(label: const Text('Vencidas'), selected: _filtroEstado == 'vencidas', onSelected: (_) => setState(() { _filtroEstado = 'vencidas'; _filtroServicio = null; facturaProv.facturasVencidas; })),
],
),
),
),
Expanded(
child: facturaProv.cargando
? const Center(child: CircularProgressIndicator())
: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: facturaProv.facturas.length + (facturaProv.facturas.isEmpty ? 1 : 0),
itemBuilder: (_, i) {
if (facturaProv.facturas.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(48),
child: Column(
children: [
Icon(Icons.receipt_long, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text('No hay facturas', style: Theme.of(context).textTheme.bodyLarge),
],
),
),
);
}
final f = facturaProv.facturas[i];
final servicio = servicioProv.servicios.where((s) => s.id == f.servicioId).firstOrNull;
final vencida = f.estado == 'Pendiente' && DateTime.tryParse(f.fechaVencimiento)?.isBefore(DateTime.now()) == true;
return Card(
child: ListTile(
leading: CircleAvatar(
backgroundColor: Color(servicio?.color ?? 0xFF6200EE),
child: Text(servicio?.nombre.substring(0, 2).toUpperCase() ?? '??',
style: const TextStyle(color: Colors.white, fontSize: 12)),
),
title: Text('${servicio?.nombre ?? 'Desconocido'} - $symbol${NumberFormat('#,##0.00').format(f.monto)}'),
subtitle: Text('Vence: ${f.fechaVencimiento}${f.estado == 'Pagada' ? ' Pagada: ${f.fechaPago ?? ''}' : ''}'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (f.estado == 'Pendiente')
IconButton(
icon: Icon(Icons.check_circle, color: vencida ? Colors.red : Colors.green),
onPressed: () => facturaProv.marcarPagada(f.id!),
),
IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.red),
onPressed: () => facturaProv.eliminarFactura(f.id!),
),
],
),
),
);
},
),
),
],
),
);
}
}

View File

@@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/servicio_provider.dart';
import 'crear_servicio_screen.dart';
class ServiciosScreen extends StatelessWidget {
const ServiciosScreen({super.key});
@override
Widget build(BuildContext context) {
final provider = context.watch<ServicioProvider>();
return Scaffold(
appBar: AppBar(title: const Text('Servicios')),
body: provider.cargando
? const Center(child: CircularProgressIndicator())
: provider.servicios.isEmpty
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.electrical_services, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text('No hay servicios registrados', style: Theme.of(context).textTheme.bodyLarge),
const SizedBox(height: 8),
Text('Agrega tu primer servicio', style: Theme.of(context).textTheme.bodyMedium),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: provider.servicios.length,
itemBuilder: (_, i) {
final s = provider.servicios[i];
return Card(
child: ListTile(
leading: CircleAvatar(
backgroundColor: Color(s.color),
child: Icon(_getIcon(s.icono), color: Colors.white),
),
title: Text(s.nombre),
subtitle: Text('Vence día ${s.diaVencimiento}'),
trailing: IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.red),
onPressed: () async {
final ok = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: const Text('Eliminar servicio'),
content: Text('¿Eliminar ${s.nombre}? Se eliminarán sus facturas.'),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancelar')),
TextButton(onPressed: () => Navigator.pop(context, true), child: const Text('Eliminar')),
],
),
);
if (ok == true) provider.eliminarServicio(s.id!);
},
),
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => CrearServicioScreen(servicio: s)),
).then((_) => provider.cargarServicios()),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const CrearServicioScreen())).then((_) => provider.cargarServicios()),
child: const Icon(Icons.add),
),
);
}
IconData _getIcon(String name) {
switch (name) {
case 'electricidad': return Icons.bolt;
case 'agua': return Icons.water_drop;
case 'internet': return Icons.wifi;
case 'cable': return Icons.tv;
case 'telefono': return Icons.phone;
case 'gas': return Icons.local_fire_department;
case 'alquiler': return Icons.home;
default: return Icons.receipt;
}
}
}

View File

@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/configuracion_provider.dart';
import '../services/notification_service.dart';
import 'dashboard_screen.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
@override
void initState() {
super.initState();
_init();
}
Future<void> _init() async {
await NotificationService.instance.init();
await context.read<ConfiguracionProvider>().cargarConfiguracion();
await NotificationService.instance.scheduleRecordatorios();
if (mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const DashboardScreen()),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.primary,
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.home_rounded, size: 80, color: Theme.of(context).colorScheme.onPrimary),
const SizedBox(height: 16),
Text('HogarControl',
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onPrimary)),
const SizedBox(height: 24),
CircularProgressIndicator(color: Theme.of(context).colorScheme.onPrimary),
],
),
),
);
}
}

View File

@@ -0,0 +1,74 @@
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone/data/latest.dart' as tz_data;
import '../database/database_helper.dart';
class NotificationService {
static final NotificationService instance = NotificationService._init();
final FlutterLocalNotificationsPlugin _plugin = FlutterLocalNotificationsPlugin();
bool _initialized = false;
NotificationService._init();
Future<void> init() async {
if (_initialized) return;
tz_data.initializeTimeZones();
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
const settings = InitializationSettings(android: androidSettings);
await _plugin.initialize(settings);
_initialized = true;
}
Future<void> scheduleRecordatorios() async {
await _plugin.cancelAll();
final db = DatabaseHelper.instance;
final facturas = await db.getFacturas(estado: 'Pendiente');
final config = await db.getConfiguracion();
final diasStr = config?.diasRecordatorio ?? '7,3,1,0';
final dias = diasStr.split(',').map((e) => int.tryParse(e.trim()) ?? 0).toList();
for (final factura in facturas) {
final vencimiento = DateTime.tryParse(factura.fechaVencimiento);
if (vencimiento == null) continue;
for (final dia in dias) {
final notifDate = vencimiento.subtract(Duration(days: dia));
final now = DateTime.now();
if (notifDate.isAfter(now) || notifDate.day == now.day) {
await _scheduleNotification(
id: factura.id! * 10 + dia,
title: dia == 0 ? 'Factura vence hoy' : 'Recordatorio de factura',
body: 'La factura vence en $dia día(s)',
scheduledDate: notifDate,
);
}
}
}
}
Future<void> _scheduleNotification({
required int id,
required String title,
required String body,
required DateTime scheduledDate,
}) async {
const androidDetails = AndroidNotificationDetails(
'hogarcontrol_recordatorios',
'Recordatorios de facturas',
channelDescription: 'Notificaciones de recordatorio de pago',
importance: Importance.high,
priority: Priority.high,
);
const details = NotificationDetails(android: androidDetails);
final tzDate = tz.TZDateTime.from(scheduledDate, tz.local);
await _plugin.zonedSchedule(
id,
title,
body,
tzDate,
details,
androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle,
matchDateTimeComponents: DateTimeComponents.dayOfMonthAndTime,
uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime,
);
}
}

View File

@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
class StatCard extends StatelessWidget {
final String title;
final String value;
final IconData icon;
final Color color;
const StatCard({
super.key,
required this.title,
required this.value,
required this.icon,
required this.color,
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, color: color, size: 20),
const SizedBox(width: 8),
Text(title, style: Theme.of(context).textTheme.bodySmall),
],
),
const SizedBox(height: 8),
Text(value, style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold)),
],
),
),
);
}
}

493
pubspec.lock Normal file
View File

@@ -0,0 +1,493 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.dev"
source: hosted
version: "2.11.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
characters:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
clock:
dependency: transitive
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
url: "https://pub.dev"
source: hosted
version: "1.1.1"
collection:
dependency: transitive
description:
name: collection
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
url: "https://pub.dev"
source: hosted
version: "1.19.0"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
source: hosted
version: "1.0.8"
dbus:
dependency: transitive
description:
name: dbus
sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270
url: "https://pub.dev"
source: hosted
version: "0.7.12"
equatable:
dependency: transitive
description:
name: equatable
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
url: "https://pub.dev"
source: hosted
version: "2.0.8"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
fl_chart:
dependency: "direct main"
description:
name: fl_chart
sha256: "74959b99b92b9eebeed1a4049426fd67c4abc3c5a0f4d12e2877097d6a11ae08"
url: "https://pub.dev"
source: hosted
version: "0.69.2"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: ef41ae901e7529e52934feba19ed82827b11baa67336829564aeab3129460610
url: "https://pub.dev"
source: hosted
version: "18.0.1"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
sha256: "8f685642876742c941b29c32030f6f4f6dacd0e4eaecb3efbb187d6a3812ca01"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: "6c5b83c86bf819cdb177a9247a3722067dd8cc6313827ce7c77a4b238a26fd52"
url: "https://pub.dev"
source: hosted
version: "8.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
http:
dependency: transitive
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.19.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
url: "https://pub.dev"
source: hosted
version: "10.0.7"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
url: "https://pub.dev"
source: hosted
version: "3.0.8"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
lints:
dependency: transitive
description:
name: lints
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
url: "https://pub.dev"
source: hosted
version: "5.1.1"
matcher:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
url: "https://pub.dev"
source: hosted
version: "1.15.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
path:
dependency: "direct main"
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
url: "https://pub.dev"
source: hosted
version: "2.2.17"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
url: "https://pub.dev"
source: hosted
version: "6.0.2"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
provider:
dependency: "direct main"
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
simple_gesture_detector:
dependency: transitive
description:
name: simple_gesture_detector
sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3
url: "https://pub.dev"
source: hosted
version: "0.2.1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev"
source: hosted
version: "1.10.0"
sqflite:
dependency: "direct main"
description:
name: sqflite
sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709"
url: "https://pub.dev"
source: hosted
version: "2.5.4+6"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c"
url: "https://pub.dev"
source: hosted
version: "2.4.1+1"
sqflite_platform_interface:
dependency: transitive
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
source: hosted
version: "2.1.2"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225"
url: "https://pub.dev"
source: hosted
version: "3.3.0+3"
table_calendar:
dependency: "direct main"
description:
name: table_calendar
sha256: b2896b7c86adf3a4d9c911d860120fe3dbe03c85db43b22fd61f14ee78cdbb63
url: "https://pub.dev"
source: hosted
version: "3.1.3"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.dev"
source: hosted
version: "1.2.1"
test_api:
dependency: transitive
description:
name: test_api
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
url: "https://pub.dev"
source: hosted
version: "0.7.3"
timezone:
dependency: "direct main"
description:
name: timezone
sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
url: "https://pub.dev"
source: hosted
version: "0.10.1"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
url: "https://pub.dev"
source: hosted
version: "14.3.0"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.5.0"
sdks:
dart: ">=3.6.2 <4.0.0"
flutter: ">=3.27.0"

95
pubspec.yaml Normal file
View File

@@ -0,0 +1,95 @@
name: hogarcontrol
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: ^3.6.2
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
sqflite: ^2.4.1
path_provider: ^2.1.5
path: ^1.9.0
intl: ^0.19.0
flutter_local_notifications: ^18.0.1
timezone: ^0.10.1
table_calendar: ^3.1.3
fl_chart: ^0.69.2
provider: ^6.1.2
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package

22
test/widget_test.dart Normal file
View File

@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:hogarcontrol/main.dart';
import 'package:hogarcontrol/providers/servicio_provider.dart';
import 'package:hogarcontrol/providers/factura_provider.dart';
import 'package:hogarcontrol/providers/configuracion_provider.dart';
void main() {
testWidgets('App shows splash screen', (WidgetTester tester) async {
await tester.pumpWidget(MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => ServicioProvider()),
ChangeNotifierProvider(create: (_) => FacturaProvider()),
ChangeNotifierProvider(create: (_) => ConfiguracionProvider()),
],
child: const HogarControlApp(),
));
expect(find.text('HogarControl'), findsOneWidget);
});
}