Agrego frontend app

This commit is contained in:
Pablo
2026-08-22 19:08:19 -03:00
parent 824e092b29
commit 2d0797e627
254 changed files with 37231 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
# 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-dependencies
.pub-cache/
.pub/
/build/
# Regenerados por `flutter pub get`/build a partir de los plugins en
# pubspec.yaml; versionarlos solo genera diffs falsos entre corridas.
windows/flutter/generated_plugin_registrant.cc
windows/flutter/generated_plugin_registrant.h
windows/flutter/generated_plugins.cmake
linux/flutter/generated_plugin_registrant.cc
linux/flutter/generated_plugin_registrant.h
linux/flutter/generated_plugins.cmake
macos/Flutter/GeneratedPluginRegistrant.swift
/coverage/
# 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
# Environment variables
.env
# Claude Code files
CLAUDE.md
.claude
.claude/
+45
View File
@@ -0,0 +1,45 @@
# 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: "68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
- platform: android
create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
- platform: ios
create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
- platform: linux
create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
- platform: macos
create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
- platform: web
create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
- platform: windows
create_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
base_revision: 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3
# 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'
+46
View File
@@ -0,0 +1,46 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
//
// El entorno se elige al compilar con --dart-define-from-file. Cada
// configuración apunta a config/dev.json o config/prod.json. Si falta el
// archivo, la app corta el arranque (AppEnv.assertConfigured).
"version": "0.2.0",
"configurations": [
{
"name": "SOMA (dev)",
"request": "launch",
"type": "dart",
"args": [
"--dart-define-from-file=config/dev.json"
]
},
{
"name": "SOMA (prod)",
"request": "launch",
"type": "dart",
"args": [
"--dart-define-from-file=config/prod.json"
]
},
{
"name": "SOMA (dev · profile)",
"request": "launch",
"type": "dart",
"flutterMode": "profile",
"args": [
"--dart-define-from-file=config/dev.json"
]
},
{
"name": "SOMA (dev · release)",
"request": "launch",
"type": "dart",
"flutterMode": "release",
"args": [
"--dart-define-from-file=config/dev.json"
]
}
]
}
+52
View File
@@ -0,0 +1,52 @@
# SOMA - Gym Manager
App de gestión de gimnasio con Flutter y Supabase.
## Setup
### 1. Configurar Supabase
Editá el archivo `.env` con tus credenciales de Supabase:
```env
SUPABASE_URL=https://TU-PROYECTO.supabase.co
SUPABASE_ANON_KEY=tu-anon-key-aqui
```
### 2. Instalar dependencias
```bash
flutter pub get
```
### 3. Correr la app
```bash
flutter run
```
## Roles
- **admin/superadmin**: Acceso total
- **profesor**: Gestión de rutinas, horarios propios
- **usuario**: Ver pagos, rutina y perfil
## Estructura
```
lib/
├── core/ # Configuración, tema, widgets compartidos
├── features/ # Features por dominio (auth, payments, etc)
└── main.dart
```
## Stack
- Flutter + Riverpod
- Supabase (backend)
- GoRouter (navegación)
- Clean Architecture
## Documentación
Ver [PROJECT.md](PROJECT.md) para detalles técnicos completos.
+1
View File
@@ -0,0 +1 @@
include: package:flutter_lints/flutter.yaml
+14
View File
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
@@ -0,0 +1,44 @@
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.example.gimnasio_soma"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.gimnasio_soma"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
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.getByName("debug")
}
}
}
flutter {
source = "../.."
}
@@ -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>
@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="gimnasio_soma"
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>
@@ -0,0 +1,5 @@
package com.example.gimnasio_soma
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -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>
@@ -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

@@ -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>
@@ -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>
@@ -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>
+24
View File
@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
@@ -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.14-all.zip
@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
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.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

+5
View File
@@ -0,0 +1,5 @@
{
"ENV": "dev",
"SUPABASE_URL": "https://TU-PROYECTO.supabase.co",
"SUPABASE_ANON_KEY": "tu-anon-key-aqui"
}
+3
View File
@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
+34
View File
@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
+43
View File
@@ -0,0 +1,43 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end
@@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.gimnasioSoma;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.gimnasioSoma.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.gimnasioSoma.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.gimnasioSoma.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.gimnasioSoma;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.gimnasioSoma;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
+49
View File
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Gimnasio Soma</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>gimnasio_soma</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>
@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"
@@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}
@@ -0,0 +1,17 @@
import 'package:gimnasio_soma/core/config/app_constants.dart';
class AppConfig {
final int pagosVentanaEdicionMinutos;
const AppConfig({required this.pagosVentanaEdicionMinutos});
factory AppConfig.fromMap(Map<String, dynamic> map) => AppConfig(
pagosVentanaEdicionMinutos:
(map['pagos.ventana_edicion_minutos'] as num?)?.toInt() ??
AppConstants.pagosVentanaEdicionMinutosDefault,
);
static const AppConfig defaults = AppConfig(
pagosVentanaEdicionMinutos: AppConstants.pagosVentanaEdicionMinutosDefault,
);
}
@@ -0,0 +1,18 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:gimnasio_soma/core/config/app_config.dart';
import 'package:gimnasio_soma/core/config/app_constants.dart';
import 'package:gimnasio_soma/core/config/supabase_config.dart';
final appConfigProvider = FutureProvider<AppConfig>((ref) async {
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString(AppConstants.tokenKey);
final client = SupabaseConfig.client;
final params = <String, dynamic>{};
if (token != null) params['p_token'] = token;
final response = await client.rpc(AppConstants.rpcGetConfig, params: params);
if (response is Map) {
return AppConfig.fromMap(Map<String, dynamic>.from(response));
}
return AppConfig.defaults;
});
@@ -0,0 +1,86 @@
class AppConstants {
AppConstants._();
static const String appName = 'SOMA PRO';
// Layout breakpoints
static const double kDesktopBreakpoint = 800.0;
static const double kWideBreakpoint = 1200.0;
// Avatar radii
static const double kAvatarRadiusSm = 16.0;
static const double kAvatarRadiusMd = 20.0;
static const double kAvatarRadiusLg = 28.0;
// SharedPreferences keys
static const String tokenKey = 'session_token';
static const String usuariosViewModeKey = 'usuarios_view_mode';
static const String pagosViewModeKey = 'pagos_view_mode';
// Supabase RPC names - Auth
static const String rpcLogin = 'fc_ingresar';
static const String rpcIniciarSesionPorToken = 'fc_iniciar_sesion_por_token';
static const String rpcGetUserByToken = 'fc_obtener_usuario_por_token';
static const String rpcDestroySession = 'fc_eliminar_sesion';
// Supabase RPC names - Usuarios
static const String rpcGetUsuarios = 'fc_obtener_usuarios';
static const String rpcInsertUsuario = 'fc_insertar_usuario';
static const String rpcUpdateUsuario = 'fc_modificar_usuario';
static const String rpcToggleUsuarioStatus = 'fc_modificar_estado_usuario';
static const String rpcDeleteUsuario = 'fc_eliminar_usuario';
static const String rpcCambiarPropiaContrasena = 'fc_cambiar_propia_contrasena';
static const String rpcResetearContrasenaUsuario = 'fc_resetear_contrasena_usuario';
// Supabase RPC names - Config
static const String rpcGetConfig = 'fc_obtener_config';
// Supabase RPC names - Pagos
static const String rpcGetPagos = 'fc_obtener_pagos';
static const String rpcGetMisPagos = 'fc_obtener_mis_pagos';
static const String rpcInsertPago = 'fc_insertar_pago';
static const String rpcEditarPago = 'fc_editar_pago';
static const String rpcAnularPago = 'fc_anular_pago';
static const String rpcGetMetodosPago = 'fc_obtener_metodos_pago';
static const String rpcUpdateMetodoPago = 'fc_modificar_metodos_pago';
// Fallback local cuando appConfigProvider aún no resolvió o falló.
static const int pagosVentanaEdicionMinutosDefault = 30;
// Supabase RPC names - Actividades
static const String rpcGetActividadesTipoCuota = 'fc_obtener_actividades_tipo_cuota';
static const String rpcGetActividades = 'fc_obtener_actividades';
static const String rpcInsertActividad = 'fc_insertar_actividad';
static const String rpcUpdateActividad = 'fc_modificar_actividad';
static const String rpcDeleteActividad = 'fc_eliminar_actividad';
// Supabase RPC names - Horarios
static const String rpcObtenerHorarios = 'fc_obtener_horarios';
static const String rpcObtenerPlanificacionesFuturas = 'fc_obtener_planificaciones_futuras';
static const String rpcInsertHorarioConActividades = 'fc_insertar_horario_con_actividades';
static const String rpcEliminarDiaEspecial = 'fc_eliminar_dia_especial';
// Supabase RPC names - Huerfanas
static const String rpcObtenerReservasHuerfanas = 'fc_obtener_reservas_huerfanas';
static const String rpcResolverHuerfana = 'fc_resolver_huerfana';
static const String rpcMoverReservaHuerfana = 'fc_mover_reserva_huerfana';
// Supabase RPC names - Turnos
static const String rpcObtenerTurnos = 'fc_obtener_turnos';
static const String rpcUpsertTurno = 'fc_upsert_turno';
static const String rpcLimpiarTurnosAntiguos = 'fc_limpiar_turnos_antiguos';
static const String rpcObtenerReservasTurno = 'fc_obtener_reservas_turno';
static const String rpcReservarTurnoAdmin = 'fc_reservar_turno_admin';
static const String rpcCancelarReservaAdmin = 'fc_cancelar_reserva_admin';
static const String rpcObtenerEstadoCupo = 'fc_obtener_estado_cupo';
// Supabase RPC names - Usuarios Cuota
static const String rpcGetUsuarioTipoCuota = 'fc_obtener_usuario_tipo_cuota';
// Supabase RPC names - Tipos de Cuota
static const String rpcGetTiposCuota = 'fc_obtener_tipo_cuota';
static const String rpcInsertTipoCuota = 'fc_insertar_tipo_cuota';
static const String rpcUpdateTipoCuota = 'fc_modificar_tipo_cuota';
static const String rpcDeleteTipoCuota = 'fc_eliminar_tipo_cuota';
}
@@ -0,0 +1,52 @@
/// Configuración de entorno horneada en tiempo de compilación.
///
/// Los valores entran al binario por `--dart-define-from-file=config/<env>.json`
/// (ver `config/dev.json` y `config/prod.json` en la raíz del proyecto). Son
/// constantes de compilación: se leen con `String.fromEnvironment` y quedan
/// fijas en el binario. NO hay archivo `.env` ni carga en runtime.
///
/// Si la app se compila/corre sin pasar el archivo de config, estas constantes
/// quedan vacías y [assertConfigured] corta el arranque con un mensaje claro,
/// en vez de dejar que la app intente conectarse a una URL vacía.
class AppEnv {
AppEnv._();
/// Identidad del entorno: `'dev'` o `'prod'`. Vacío si no se pasó el archivo.
static const String env = String.fromEnvironment('ENV');
/// URL del proyecto Supabase del entorno.
static const String supabaseUrl = String.fromEnvironment('SUPABASE_URL');
/// Clave pública (anon) del entorno. Es pública por diseño (protegida por RLS),
/// por eso puede vivir en `config/*.json` versionado. El secreto real
/// (service_role / DATABASE_URL) nunca entra acá.
static const String supabaseAnonKey =
String.fromEnvironment('SUPABASE_ANON_KEY');
static bool get isDev => env == 'dev';
static bool get isProd => env == 'prod';
/// Falla temprano y con un mensaje accionable si la app se compiló sin
/// `--dart-define-from-file=config/<env>.json`, o con un `ENV` inválido.
static void assertConfigured() {
final faltantes = <String>[
if (env.isEmpty) 'ENV',
if (supabaseUrl.isEmpty) 'SUPABASE_URL',
if (supabaseAnonKey.isEmpty) 'SUPABASE_ANON_KEY',
];
if (faltantes.isNotEmpty) {
throw StateError(
'Configuración de entorno ausente: ${faltantes.join(', ')}. '
'Compilá/corré pasando el archivo de entorno, por ejemplo:\n'
' flutter run --dart-define-from-file=config/dev.json\n'
' flutter build windows --dart-define-from-file=config/prod.json',
);
}
if (env != 'dev' && env != 'prod') {
throw StateError(
'ENV inválido: "$env". Debe ser "dev" o "prod" '
'(ver config/dev.json y config/prod.json).',
);
}
}
}
@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:gimnasio_soma/core/config/app_env.dart';
/// Envuelve la app en una cinta diagonal con el nombre del entorno cuando NO es
/// prod (ej. "DEV"), para no confundir el entorno de pruebas con el real. En
/// prod devuelve el hijo sin envolver: no se ve ninguna cinta.
class EnvBanner extends StatelessWidget {
const EnvBanner({super.key, required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
if (AppEnv.isProd) return child;
return Banner(
message: AppEnv.env.toUpperCase(),
location: BannerLocation.topStart,
color: Colors.red.shade700,
child: child,
);
}
}
@@ -0,0 +1,23 @@
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:gimnasio_soma/core/config/app_env.dart';
import 'package:gimnasio_soma/core/services/soma_logger.dart';
class SupabaseConfig {
static SupabaseClient get client => Supabase.instance.client;
static Future<void> initialize() async {
await Supabase.initialize(
url: AppEnv.supabaseUrl,
anonKey: AppEnv.supabaseAnonKey,
);
}
/// RPC con logging automático de request, params, duración y resultado.
static Future<dynamic> rpc(String fn, {Map<String, dynamic>? params}) {
return SomaLogger.instance.logRpc(
fn,
params ?? {},
() async => await client.rpc(fn, params: params),
);
}
}
@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
enum SomaRole { staff, usuario }
class NavigationItem {
final String label;
final IconData icon;
final IconData activeIcon;
final String path;
final Set<SomaRole> allowedRoles;
const NavigationItem({
required this.label,
required this.icon,
required this.activeIcon,
required this.path,
required this.allowedRoles,
});
bool isVisibleTo(String userRole) {
// Mapear múltiples roles de BD al rol unificado 'staff'
if (userRole == 'superadmin' || userRole == 'admin' || userRole == 'profesor') {
return allowedRoles.contains(SomaRole.staff);
}
return allowedRoles.contains(SomaRole.usuario);
}
}
@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'navigation_item.dart';
const List<NavigationItem> kNavigationItems = [
NavigationItem(
label: 'Usuarios',
icon: Icons.people_outline,
activeIcon: Icons.people,
path: '/usuarios',
allowedRoles: {SomaRole.staff},
),
NavigationItem(
label: 'Pagos',
icon: Icons.payment_outlined,
activeIcon: Icons.payment,
path: '/pagos',
allowedRoles: {SomaRole.staff, SomaRole.usuario},
),
// Rutinas: módulo aún no desarrollado. Se oculta de la navegación hasta
// implementarlo. La ruta /rutinas sigue definida en app_router (el guard
// redirige al primer ítem visible si se accede directamente).
NavigationItem(
label: 'Actividades',
icon: Icons.sports_gymnastics_outlined,
activeIcon: Icons.sports_gymnastics,
path: '/actividades',
allowedRoles: {SomaRole.staff},
),
NavigationItem(
label: 'Horarios',
icon: Icons.schedule_outlined,
activeIcon: Icons.schedule,
path: '/horarios',
allowedRoles: {SomaRole.staff, SomaRole.usuario},
),
NavigationItem(
label: 'Turnos',
icon: Icons.event_available_outlined,
activeIcon: Icons.event_available,
path: '/turnos',
allowedRoles: {SomaRole.staff},
),
NavigationItem(
label: 'Planes',
icon: Icons.card_membership_outlined,
activeIcon: Icons.card_membership,
path: '/planes',
allowedRoles: {SomaRole.staff},
),
NavigationItem(
label: 'Sin turno',
icon: Icons.warning_amber_outlined,
activeIcon: Icons.warning_amber,
path: '/huerfanas',
allowedRoles: {SomaRole.staff},
),
];
List<NavigationItem> getVisibleItems(String userRole) {
return kNavigationItems.where((item) => item.isVisibleTo(userRole)).toList();
}
@@ -0,0 +1,157 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:gimnasio_soma/core/navigation/navigation_item.dart';
import 'package:gimnasio_soma/core/navigation/navigation_items.dart';
import 'package:gimnasio_soma/core/navigation/widgets/soma_drawer.dart';
import 'package:gimnasio_soma/core/navigation/widgets/soma_sidebar.dart';
import 'package:gimnasio_soma/features/auth/presentation/providers/auth_provider.dart';
const _kDesktopBreakpoint = 800.0;
class NavigationShell extends ConsumerWidget {
final Widget child;
const NavigationShell({super.key, required this.child});
static const _subPageTitles = {
'/pagos/metodos': 'Métodos de pago',
};
@override
Widget build(BuildContext context, WidgetRef ref) {
final width = MediaQuery.of(context).size.width;
final isDesktop = width >= _kDesktopBreakpoint;
final user = ref.watch(authStateProvider).valueOrNull;
final currentPath = GoRouterState.of(context).matchedLocation;
final effectiveRole = user?.role ?? '';
final visibleItems =
user != null ? getVisibleItems(effectiveRole) : <NavigationItem>[];
// Detectar si estamos en una sub-página
final isSubPage = !visibleItems.any((i) => i.path == currentPath) &&
!{'/gallery', '/logs', '/perfil'}.contains(currentPath);
final parentItem = isSubPage
? visibleItems
.where((i) => currentPath.startsWith(i.path))
.firstOrNull
: null;
final theme = Theme.of(context);
if (isDesktop) {
return Scaffold(
body: Row(
children: [
SomaSidebar(
items: visibleItems,
currentPath: currentPath,
user: user,
),
Expanded(
child: Column(
children: [
if (isSubPage && parentItem != null)
_BackBar(
parentLabel: parentItem.label,
onBack: () => context.go(parentItem.path),
theme: theme,
),
Expanded(child: child),
],
),
),
],
),
);
}
// Mobile
return Scaffold(
appBar: AppBar(
title: Text(_titleForPath(currentPath)),
leading: isSubPage && parentItem != null
? IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.go(parentItem.path),
)
: Builder(
builder: (ctx) => IconButton(
icon: const Icon(Icons.menu),
onPressed: () => Scaffold.of(ctx).openDrawer(),
),
),
),
drawer: isSubPage
? null
: SomaDrawer(
items: visibleItems,
currentPath: currentPath,
user: user,
),
body: child,
);
}
String _titleForPath(String path) {
// Primero buscar título específico de sub-página
final subTitle = _subPageTitles[path];
if (subTitle != null) return subTitle;
final item =
kNavigationItems.where((i) => path.startsWith(i.path)).firstOrNull;
return item?.label ?? 'SOMA PRO';
}
}
class _BackBar extends StatelessWidget {
final String parentLabel;
final VoidCallback onBack;
final ThemeData theme;
const _BackBar({
required this.parentLabel,
required this.onBack,
required this.theme,
});
@override
Widget build(BuildContext context) {
return Container(
height: 44,
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest.withAlpha(80),
border: Border(
bottom: BorderSide(
color: theme.colorScheme.surfaceContainerHighest,
width: 0.5,
),
),
),
child: InkWell(
onTap: onBack,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Icon(
Icons.arrow_back,
size: 18,
color: theme.colorScheme.onSurface.withAlpha(178),
),
const SizedBox(width: 8),
Text(
'Volver a $parentLabel',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: theme.colorScheme.onSurface.withAlpha(178),
),
),
],
),
),
),
);
}
}
@@ -0,0 +1,112 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:gimnasio_soma/core/navigation/navigation_item.dart';
import 'package:gimnasio_soma/core/navigation/widgets/soma_nav_tile.dart';
import 'package:gimnasio_soma/core/navigation/widgets/soma_user_card.dart';
import 'package:gimnasio_soma/core/widgets/soma_logo.dart';
import 'package:gimnasio_soma/features/auth/domain/entities/user_session.dart';
import 'package:gimnasio_soma/features/auth/presentation/providers/auth_provider.dart';
class SomaDrawer extends ConsumerWidget {
final List<NavigationItem> items;
final String currentPath;
final UserSession? user;
const SomaDrawer({
super.key,
required this.items,
required this.currentPath,
required this.user,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Drawer(
backgroundColor: Theme.of(context).colorScheme.surface,
child: SafeArea(
child: Column(
children: [
const SizedBox(height: 20),
// Logo
const SomaLogo(width: 100),
const SizedBox(height: 24),
Divider(
height: 1,
thickness: 0.5,
indent: 16,
endIndent: 16,
color: Theme.of(context).colorScheme.surfaceContainerHighest,
),
const SizedBox(height: 8),
// Nav items
Expanded(
child: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 12),
itemCount: items.length,
separatorBuilder: (_, _) => const SizedBox(height: 4),
itemBuilder: (context, index) {
final item = items[index];
final isActive = currentPath.startsWith(item.path);
return SomaNavTile(
item: item,
isActive: isActive,
onTap: () {
Navigator.of(context).pop();
context.go(item.path);
},
);
},
),
),
// Footer
Divider(
height: 1,
thickness: 0.5,
indent: 16,
endIndent: 16,
color: Theme.of(context).colorScheme.surfaceContainerHighest,
),
SomaUserCard(
user: user,
onTap: () {
Navigator.of(context).pop();
context.go('/perfil');
},
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: Row(
children: [
const Spacer(),
IconButton(
icon: const Icon(Icons.logout, size: 20),
tooltip: 'Cerrar sesión',
onPressed: () {
Navigator.of(context).pop();
ref.read(authStateProvider.notifier).logout();
},
color: Theme.of(context)
.colorScheme
.onSurface
.withAlpha(153),
),
],
),
),
const SizedBox(height: 8),
],
),
),
);
}
}
@@ -0,0 +1,147 @@
import 'package:flutter/material.dart';
import 'package:gimnasio_soma/core/navigation/navigation_item.dart';
import 'package:gimnasio_soma/core/theme/soma_colors.dart';
class SomaNavTile extends StatelessWidget {
final NavigationItem item;
final bool isActive;
final bool isCollapsed;
final VoidCallback onTap;
final int? badge;
const SomaNavTile({
super.key,
required this.item,
required this.isActive,
required this.onTap,
this.isCollapsed = false,
this.badge,
});
@override
Widget build(BuildContext context) {
final tile = InkWell(
onTap: onTap,
mouseCursor: SystemMouseCursors.click,
borderRadius: BorderRadius.circular(8),
child: Container(
height: 44,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: isActive
? SomaColors.primary.withAlpha(18)
: Colors.transparent,
),
child: Row(
children: [
// Franja izquierda de acento: reemplaza Border(left:...) no uniforme
Container(
width: 3,
color: isActive ? SomaColors.primary : Colors.transparent,
),
Expanded(
child: Padding(
padding: EdgeInsets.fromLTRB(isCollapsed ? 0 : 9, 0, isCollapsed ? 0 : 12, 0),
child: Row(
mainAxisAlignment: isCollapsed
? MainAxisAlignment.center
: MainAxisAlignment.start,
children: [
Stack(
clipBehavior: Clip.none,
children: [
Icon(
isActive ? item.activeIcon : item.icon,
size: 20,
color: isActive
? SomaColors.primary
: Theme.of(context)
.colorScheme
.onSurface
.withAlpha(153),
),
if (badge != null && badge! > 0)
Positioned(
top: -4,
right: -5,
child: Container(
padding: const EdgeInsets.all(2),
constraints: const BoxConstraints(
minWidth: 14,
minHeight: 14,
),
decoration: BoxDecoration(
color: SomaColors.error,
borderRadius: BorderRadius.circular(7),
),
child: Text(
badge! > 99 ? '99+' : '$badge',
style: const TextStyle(
fontSize: 8,
fontWeight: FontWeight.w700,
color: Colors.white,
height: 1,
),
textAlign: TextAlign.center,
),
),
),
],
),
if (!isCollapsed) ...[
const SizedBox(width: 12),
Expanded(
child: Text(
item.label,
style: TextStyle(
fontSize: 14,
fontWeight:
isActive ? FontWeight.w600 : FontWeight.w500,
color: isActive
? Theme.of(context).colorScheme.onSurface
: Theme.of(context)
.colorScheme
.onSurface
.withAlpha(178),
),
),
),
if (badge != null && badge! > 0)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: SomaColors.error,
borderRadius: BorderRadius.circular(10),
),
child: Text(
badge! > 99 ? '99+' : '$badge',
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
),
],
],
),
),
),
],
),
),
);
if (isCollapsed) {
return Tooltip(
message: item.label,
preferBelow: false,
child: tile,
);
}
return tile;
}
}
@@ -0,0 +1,240 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:gimnasio_soma/core/navigation/navigation_item.dart';
import 'package:gimnasio_soma/core/navigation/widgets/soma_nav_tile.dart';
import 'package:gimnasio_soma/core/navigation/widgets/soma_user_card.dart';
import 'package:gimnasio_soma/core/theme/soma_colors.dart';
import 'package:gimnasio_soma/core/widgets/soma_logo.dart';
import 'package:gimnasio_soma/features/auth/domain/entities/user_session.dart';
import 'package:gimnasio_soma/features/auth/presentation/providers/auth_provider.dart';
import 'package:gimnasio_soma/features/huerfanas/presentation/providers/huerfanas_provider.dart';
final sidebarCollapsedProvider = StateProvider<bool>((ref) => false);
class SomaSidebar extends ConsumerWidget {
final List<NavigationItem> items;
final String currentPath;
final UserSession? user;
const SomaSidebar({
super.key,
required this.items,
required this.currentPath,
required this.user,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isCollapsed = ref.watch(sidebarCollapsedProvider);
Future<void> confirmLogout() async {
final confirm = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Cerrar sesión'),
content: const Text('¿Estás seguro de que querés cerrar sesión?'),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('Cancelar'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(minimumSize: const Size(0, 40)),
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text('Cerrar sesión'),
),
],
),
);
if (confirm != true) return;
ref.read(authStateProvider.notifier).logout();
}
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
width: isCollapsed ? 64 : 240,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border(
right: BorderSide(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
width: 1,
),
),
),
child: Column(
children: [
// Logo
Padding(
padding: EdgeInsets.fromLTRB(
isCollapsed ? 8 : 16,
20,
isCollapsed ? 8 : 16,
8,
),
child: AnimatedCrossFade(
duration: const Duration(milliseconds: 200),
crossFadeState: isCollapsed
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
firstChild: const SomaLogo(width: 90),
secondChild: const SomaLogo(width: 32),
),
),
const SizedBox(height: 16),
// Nav items
Expanded(
child: ListView.separated(
padding: EdgeInsets.symmetric(
horizontal: isCollapsed ? 8 : 12,
),
itemCount: items.length,
separatorBuilder: (_, _) => const SizedBox(height: 4),
itemBuilder: (context, index) {
final item = items[index];
final isActive = currentPath.startsWith(item.path);
int? badge;
if (item.path == '/huerfanas') {
badge = ref.watch(huerfanasPendienteCountProvider).valueOrNull;
}
return SomaNavTile(
item: item,
isActive: isActive,
isCollapsed: isCollapsed,
badge: badge,
onTap: () => context.go(item.path),
);
},
),
),
// Footer
Divider(
height: 1,
thickness: 0.5,
indent: isCollapsed ? 8 : 16,
endIndent: isCollapsed ? 8 : 16,
),
SomaUserCard(
user: user,
isCollapsed: isCollapsed,
onTap: () => context.go('/perfil'),
),
// Actions
Padding(
padding: EdgeInsets.symmetric(
horizontal: isCollapsed ? 8 : 12,
vertical: 4,
),
child: _SidebarActionTile(
isCollapsed: isCollapsed,
icon: Icons.logout,
label: 'Cerrar sesión',
color: SomaColors.error,
onTap: confirmLogout,
),
),
const SizedBox(height: 2),
// Collapse toggle
Padding(
padding: EdgeInsets.symmetric(
horizontal: isCollapsed ? 8 : 12,
vertical: 4,
),
child: _SidebarActionTile(
isCollapsed: isCollapsed,
icon: isCollapsed
? Icons.keyboard_double_arrow_right
: Icons.keyboard_double_arrow_left,
label: isCollapsed ? 'Expandir panel' : 'Colapsar panel',
onTap: () => ref.read(sidebarCollapsedProvider.notifier).state =
!isCollapsed,
),
),
const SizedBox(height: 8),
],
),
);
}
}
class _SidebarActionTile extends StatefulWidget {
final bool isCollapsed;
final IconData icon;
final String label;
final VoidCallback onTap;
final Color? color;
const _SidebarActionTile({
required this.isCollapsed,
required this.icon,
required this.label,
required this.onTap,
this.color,
});
@override
State<_SidebarActionTile> createState() => _SidebarActionTileState();
}
class _SidebarActionTileState extends State<_SidebarActionTile> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
final baseColor = widget.color ?? Theme.of(context).colorScheme.onSurface;
if (widget.isCollapsed) {
return Tooltip(
message: widget.label,
waitDuration: const Duration(milliseconds: 600),
child: IconButton(
icon: Icon(widget.icon, size: 20),
onPressed: widget.onTap,
color: baseColor.withAlpha(178),
splashRadius: 18,
),
);
}
return MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: InkWell(
onTap: widget.onTap,
borderRadius: BorderRadius.circular(8),
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
height: 38,
padding: const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: _hovered ? baseColor.withAlpha(15) : Colors.transparent,
),
child: Row(
children: [
Icon(widget.icon, size: 18, color: baseColor.withAlpha(190)),
const SizedBox(width: 10),
Text(
widget.label,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: baseColor.withAlpha(190),
),
),
],
),
),
),
);
}
}
@@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:gimnasio_soma/core/theme/soma_colors.dart';
import 'package:gimnasio_soma/features/auth/domain/entities/user_session.dart';
class SomaUserCard extends StatelessWidget {
final UserSession? user;
final bool isCollapsed;
final VoidCallback? onTap;
const SomaUserCard({
super.key,
required this.user,
this.isCollapsed = false,
this.onTap,
});
String _getInitials() {
if (user == null) return '?';
final n = user!.nombre;
final a = user!.apellido;
if (n.isNotEmpty && a.isNotEmpty) {
return '${n[0]}${a[0]}'.toUpperCase();
}
if (n.isNotEmpty) return n[0].toUpperCase();
if (user!.dni.length >= 2) return user!.dni.substring(0, 2);
return '?';
}
@override
Widget build(BuildContext context) {
final avatar = CircleAvatar(
radius: 16,
backgroundColor: SomaColors.primary.withAlpha(40),
child: Text(
_getInitials(),
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
);
if (isCollapsed) {
return Tooltip(
message: user?.displayName ?? '',
child: InkWell(
onTap: onTap,
mouseCursor: SystemMouseCursors.click,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Center(child: avatar),
),
),
);
}
return InkWell(
onTap: onTap,
mouseCursor: SystemMouseCursors.click,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
avatar,
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
user?.displayName ?? '',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
user?.role ?? '',
style: TextStyle(
fontSize: 11,
color: Theme.of(context)
.colorScheme
.onSurface
.withAlpha(130),
),
),
],
),
),
],
),
),
);
}
}
@@ -0,0 +1,157 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:gimnasio_soma/core/navigation/navigation_items.dart';
import 'package:gimnasio_soma/core/navigation/navigation_shell.dart';
import 'package:gimnasio_soma/features/auth/presentation/providers/auth_provider.dart';
import 'package:gimnasio_soma/features/auth/presentation/screens/login_screen.dart';
import 'package:gimnasio_soma/features/auth/presentation/screens/splash_screen.dart';
import 'package:gimnasio_soma/features/dev/logs_screen.dart';
import 'package:gimnasio_soma/features/dev/widget_gallery_screen.dart';
import 'package:gimnasio_soma/features/actividades/presentation/screens/actividades_screen.dart';
import 'package:gimnasio_soma/features/perfil/presentation/screens/cambiar_contrasena_screen.dart';
import 'package:gimnasio_soma/features/perfil/presentation/screens/perfil_screen.dart';
import 'package:gimnasio_soma/features/horarios/presentation/screens/horarios_screen.dart';
import 'package:gimnasio_soma/features/pagos/presentation/screens/metodos_pago_screen.dart';
import 'package:gimnasio_soma/features/pagos/presentation/screens/pagos_screen.dart';
import 'package:gimnasio_soma/features/tipos_cuota/presentation/screens/tipos_cuota_screen.dart';
import 'package:gimnasio_soma/features/huerfanas/presentation/screens/huerfanas_screen.dart';
import 'package:gimnasio_soma/features/turnos/presentation/screens/turnos_screen.dart';
import 'package:gimnasio_soma/features/usuarios/presentation/screens/usuarios_screen.dart';
final routerProvider = Provider<GoRouter>((ref) {
final authChangeNotifier = ref.watch(authChangeNotifierProvider);
return GoRouter(
initialLocation: '/splash',
refreshListenable: authChangeNotifier,
redirect: (context, state) {
final authState = ref.read(authStateProvider);
final user = authState.valueOrNull;
final isLoggedIn = user != null;
final location = state.matchedLocation;
final isAuthScreen = location == '/login' || location == '/splash';
// No logueado y no está en login/splash → ir a login
if (!isLoggedIn && !isAuthScreen) return '/login';
final effectiveRole = user?.role ?? '';
// Logueado y está en login o splash → ir al primer item visible
if (isLoggedIn && isAuthScreen) {
final items = getVisibleItems(effectiveRole);
return items.isNotEmpty ? items.first.path : '/usuarios';
}
// Guard: si el usuario navega a una ruta sin permiso
if (isLoggedIn) {
final visiblePaths =
getVisibleItems(effectiveRole).map((i) => i.path).toSet();
final matched = state.matchedLocation;
final isStaff = user.isStaff;
// Rutas internas/de staff que no figuran en el menú pero deben
// quedar bloqueadas para clientes.
const staffOnlyPaths = {
'/gallery',
'/logs',
'/perfil/cambiar-contrasena',
};
if (staffOnlyPaths.contains(matched) && !isStaff) {
return visiblePaths.isNotEmpty ? visiblePaths.first : '/usuarios';
}
if (matched != '/gallery' &&
matched != '/perfil' &&
matched != '/perfil/cambiar-contrasena' &&
matched != '/logs' &&
!visiblePaths.any((p) => matched.startsWith(p))) {
return visiblePaths.isNotEmpty ? visiblePaths.first : '/usuarios';
}
}
return null;
},
routes: [
// Splash - validación de sesión
GoRoute(
path: '/splash',
builder: (context, state) => const SplashScreen(),
),
// Login - fuera del shell
GoRoute(
path: '/login',
builder: (context, state) => const LoginScreen(),
),
// Shell con navegación
ShellRoute(
builder: (context, state, child) => NavigationShell(child: child),
routes: [
GoRoute(
path: '/usuarios',
pageBuilder: (c, s) => _fade(s, const UsuariosScreen()),
),
GoRoute(
path: '/pagos',
pageBuilder: (c, s) => _fade(s, const PagosScreen()),
),
GoRoute(
path: '/pagos/metodos',
pageBuilder: (c, s) => _fade(s, const MetodosPagoScreen()),
),
GoRoute(
path: '/actividades',
pageBuilder: (c, s) => _fade(s, const ActividadesScreen()),
),
GoRoute(
path: '/horarios',
pageBuilder: (c, s) => _fade(s, const HorariosScreen()),
),
GoRoute(
path: '/turnos',
pageBuilder: (c, s) => _fade(s, const TurnosScreen()),
),
GoRoute(
path: '/planes',
pageBuilder: (c, s) => _fade(s, const TiposCuotaScreen()),
),
GoRoute(
path: '/huerfanas',
pageBuilder: (c, s) => _fade(s, const HuerfanasScreen()),
),
GoRoute(
path: '/perfil',
pageBuilder: (c, s) => _fade(s, const PerfilScreen()),
),
GoRoute(
path: '/perfil/cambiar-contrasena',
pageBuilder: (c, s) => _fade(s, const CambiarContrasenaScreen()),
),
GoRoute(
path: '/gallery',
pageBuilder: (c, s) => _fade(s, const WidgetGalleryScreen()),
),
GoRoute(
path: '/logs',
pageBuilder: (c, s) => _fade(s, const LogsScreen()),
),
],
),
],
);
});
CustomTransitionPage<void> _fade(GoRouterState state, Widget child) =>
CustomTransitionPage<void>(
key: state.pageKey,
child: child,
transitionDuration: const Duration(milliseconds: 180),
reverseTransitionDuration: const Duration(milliseconds: 140),
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
FadeTransition(
opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut),
child: child,
),
);
@@ -0,0 +1,192 @@
import 'dart:collection';
import 'dart:developer' as developer;
import 'package:flutter/foundation.dart';
enum LogLevel { debug, info, warning, error }
class LogEntry {
final DateTime timestamp;
final LogLevel level;
final String tag;
final String message;
final String? detail;
final Duration? duration;
LogEntry({
required this.timestamp,
required this.level,
required this.tag,
required this.message,
this.detail,
this.duration,
});
String get formatted {
final ts =
'${timestamp.hour.toString().padLeft(2, '0')}:'
'${timestamp.minute.toString().padLeft(2, '0')}:'
'${timestamp.second.toString().padLeft(2, '0')}.'
'${timestamp.millisecond.toString().padLeft(3, '0')}';
final lvl = level.name.toUpperCase().padRight(5);
final dur = duration != null ? ' (${duration!.inMilliseconds}ms)' : '';
return '[$ts] $lvl [$tag] $message$dur';
}
}
class SomaLogger {
SomaLogger._();
static final SomaLogger instance = SomaLogger._();
static const int _maxEntries = 300;
final Queue<LogEntry> _entries = Queue<LogEntry>();
/// Plataforma detectada al iniciar.
String platform = 'unknown';
List<LogEntry> get entries => _entries.toList();
int get count => _entries.length;
void _log(LogLevel level, String tag, String message,
{String? detail, Duration? duration}) {
final entry = LogEntry(
timestamp: DateTime.now(),
level: level,
tag: tag,
message: message,
detail: detail,
duration: duration,
);
_entries.addLast(entry);
while (_entries.length > _maxEntries) {
_entries.removeFirst();
}
// Salida a consola multiplataforma
developer.log(
entry.formatted,
name: 'SOMA',
level: _levelToInt(level),
);
if (detail != null) {
developer.log(' $detail', name: 'SOMA', level: _levelToInt(level));
}
}
int _levelToInt(LogLevel level) {
switch (level) {
case LogLevel.debug:
return 500;
case LogLevel.info:
return 800;
case LogLevel.warning:
return 900;
case LogLevel.error:
return 1000;
}
}
void debug(String tag, String message,
{String? detail, Duration? duration}) =>
_log(LogLevel.debug, tag, message, detail: detail, duration: duration);
void info(String tag, String message,
{String? detail, Duration? duration}) =>
_log(LogLevel.info, tag, message, detail: detail, duration: duration);
void warning(String tag, String message,
{String? detail, Duration? duration}) =>
_log(LogLevel.warning, tag, message, detail: detail, duration: duration);
void error(String tag, String message,
{String? detail, Duration? duration}) =>
_log(LogLevel.error, tag, message, detail: detail, duration: duration);
/// Wrapper para llamadas RPC con logging automático de request y timing.
Future<T> logRpc<T>(
String rpcName,
Map<String, dynamic> params,
Future<T> Function() call,
) async {
final sanitized = _sanitizeParams(params);
debug('RPC', '-> $rpcName', detail: sanitized);
final sw = Stopwatch()..start();
try {
final result = await call();
sw.stop();
info('RPC', '<- $rpcName OK',
duration: sw.elapsed, detail: _summarizeResult(result));
return result;
} catch (e) {
sw.stop();
error('RPC', 'x $rpcName FAIL',
duration: sw.elapsed, detail: e.toString());
rethrow;
}
}
String _sanitizeParams(Map<String, dynamic> params) {
final safe = <String, dynamic>{};
for (final entry in params.entries) {
if (entry.key.contains('token') || entry.key.contains('password')) {
safe[entry.key] = '***';
} else {
safe[entry.key] = entry.value;
}
}
return safe.toString();
}
String _summarizeResult(dynamic result) {
if (result is List) return '${result.length} items';
if (result is Map) return '${result.length} keys';
if (result is bool) return result.toString();
if (result == null) return 'null';
if (result is String && result.length > 80) {
return '${result.substring(0, 80)}...';
}
return result.toString();
}
/// Detecta plataforma actual.
void detectPlatform() {
if (kIsWeb) {
platform = 'Web';
} else {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
platform = 'Android';
case TargetPlatform.iOS:
platform = 'iOS';
case TargetPlatform.windows:
platform = 'Windows';
case TargetPlatform.macOS:
platform = 'macOS';
case TargetPlatform.linux:
platform = 'Linux';
case TargetPlatform.fuchsia:
platform = 'Fuchsia';
}
}
info('App', 'Plataforma: $platform');
}
void clear() => _entries.clear();
/// Exporta logs como texto plano.
String export() {
final buf = StringBuffer();
buf.writeln('=== SOMA Logs ===');
buf.writeln('Plataforma: $platform');
buf.writeln('Exportado: ${DateTime.now()}');
buf.writeln('Entradas: ${_entries.length}');
buf.writeln('');
for (final e in _entries) {
buf.writeln(e.formatted);
if (e.detail != null) buf.writeln(' ${e.detail}');
}
return buf.toString();
}
}
@@ -0,0 +1,32 @@
import 'package:url_launcher/url_launcher.dart';
class WhatsAppService {
WhatsAppService._();
/// Normaliza un número argentino a formato internacional sin +.
/// Retorna null si el número es inválido (vacío o menos de 8 dígitos).
static String? normalizarNumeroAr(String? rawPhone) {
if (rawPhone == null || rawPhone.trim().isEmpty) return null;
final digits = rawPhone.replaceAll(RegExp(r'[^\d]'), '');
if (digits.length < 8) return null;
if (digits.startsWith('0054')) return '54${digits.substring(4)}';
if (digits.startsWith('54') && digits.length >= 10) return digits;
if (digits.startsWith('0')) return '54${digits.substring(1)}';
return '54$digits';
}
/// Abre WhatsApp con un mensaje pre-armado.
/// Retorna true si se abrió correctamente, false si el número es inválido
/// o si el sistema no pudo lanzar la URL.
static Future<bool> abrirChat({
required String? telefono,
required String mensaje,
}) async {
final number = normalizarNumeroAr(telefono);
if (number == null) return false;
final uri = Uri.parse(
'https://wa.me/$number?text=${Uri.encodeComponent(mensaje)}',
);
return launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
@@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
abstract final class ActivityColors {
static const List<Color> _palette = [
Color(0xFFE07272), // coral
Color(0xFFD4904A), // ámbar terroso
Color(0xFF9FC44A), // lima
Color(0xFF4AC49A), // menta
Color(0xFF4A9ED4), // cielo
Color(0xFF7A6ED4), // lavanda
Color(0xFFC46EC4), // malva
Color(0xFFD46E96), // rosa
];
static Color forId(int actividadId) => _palette[actividadId % _palette.length];
}
@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
class SomaColors {
SomaColors._();
// Primary
static const Color primary = Color(0xFFFFD600);
static const Color primaryDark = Color(0xFFFFC800);
static const Color primaryLight = Color(0xFFFFE24D);
// Variante legible de primary para texto sobre fondo claro
static const Color primaryText = Color(0xFF8B7000);
// Dark theme
static const Color darkBackground = Color(0xFF121212);
static const Color darkSurface = Color(0xFF1E1E1E);
static const Color darkSurfaceVariant = Color(0xFF2C2C2C);
static const Color darkOnBackground = Color(0xFFFFFFFF);
static const Color darkOnSurface = Color(0xFFE0E0E0);
static const Color darkOnSurfaceVariant = Color(0xFF9E9E9E);
// Semantic
static const Color success = Color(0xFF4CAF50);
static const Color error = Color(0xFFEF5350);
static const Color info = Color(0xFFFFD600);
// On primary (text on yellow)
static const Color onPrimary = Color(0xFF1A1A1A);
}
@@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'soma_colors.dart';
class SomaTheme {
SomaTheme._();
static ThemeData get dark => ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
scaffoldBackgroundColor: SomaColors.darkBackground,
colorScheme: const ColorScheme.dark(
primary: SomaColors.primary,
onPrimary: SomaColors.onPrimary,
surface: SomaColors.darkSurface,
onSurface: SomaColors.darkOnSurface,
surfaceContainerHighest: SomaColors.darkSurfaceVariant,
error: SomaColors.error,
),
appBarTheme: const AppBarTheme(
backgroundColor: SomaColors.darkBackground,
foregroundColor: SomaColors.darkOnBackground,
elevation: 0,
systemOverlayStyle: SystemUiOverlayStyle.light,
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: SomaColors.darkSurfaceVariant,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: SomaColors.primary, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: SomaColors.error, width: 1),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: SomaColors.error, width: 2),
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
hintStyle: const TextStyle(color: SomaColors.darkOnSurfaceVariant),
labelStyle: const TextStyle(color: SomaColors.darkOnSurfaceVariant),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: SomaColors.primary,
foregroundColor: SomaColors.onPrimary,
minimumSize: const Size(double.infinity, 52),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: SomaColors.primary,
minimumSize: const Size(double.infinity, 52),
side: const BorderSide(color: SomaColors.primary, width: 1.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: SomaColors.primary,
textStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
tabBarTheme: const TabBarThemeData(
labelColor: SomaColors.darkOnSurface,
unselectedLabelColor: SomaColors.darkOnSurfaceVariant,
indicatorColor: SomaColors.primary,
),
segmentedButtonTheme: SegmentedButtonThemeData(
style: ButtonStyle(
backgroundColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) return SomaColors.primary;
return Colors.transparent;
}),
foregroundColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) return SomaColors.onPrimary;
return null;
}),
iconColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) return SomaColors.onPrimary;
return null;
}),
),
),
snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
contentTextStyle: const TextStyle(fontSize: 14),
),
);
}
@@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:gimnasio_soma/core/theme/soma_colors.dart';
class SomaHelpItem {
final IconData icon;
final String text;
const SomaHelpItem({required this.icon, required this.text});
}
/// Botón "?" para poner al lado del título de una pantalla. Al tocarlo
/// muestra un globito explicando qué hace cada ícono/acción visible.
class SomaHeaderHelp extends StatelessWidget {
final List<SomaHelpItem> items;
const SomaHeaderHelp({super.key, required this.items});
void _show(BuildContext context) {
final button = context.findRenderObject() as RenderBox;
final overlay = Overlay.of(context).context.findRenderObject() as RenderBox;
final position = RelativeRect.fromRect(
Rect.fromPoints(
button.localToGlobal(
Offset(0, button.size.height + 6),
ancestor: overlay,
),
button.localToGlobal(
button.size.bottomRight(Offset(0, button.size.height + 6)),
ancestor: overlay,
),
),
Offset.zero & overlay.size,
);
showMenu<void>(
context: context,
position: position,
constraints: const BoxConstraints(maxWidth: 300),
items: [
PopupMenuItem<void>(
enabled: false,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
for (int i = 0; i < items.length; i++) ...[
if (i > 0) const SizedBox(height: 10),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(items[i].icon, size: 16, color: SomaColors.primary),
const SizedBox(width: 10),
Expanded(
child: Text(
items[i].text,
style: const TextStyle(fontSize: 12.5, height: 1.3),
),
),
],
),
],
],
),
),
],
);
}
@override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(
Icons.help_outline,
size: 18,
color: Theme.of(context).colorScheme.onSurface.withAlpha(120),
),
tooltip: 'Ayuda',
visualDensity: VisualDensity.compact,
onPressed: () => _show(context),
);
}
}
@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
class SomaLogo extends StatelessWidget {
final double? width;
final double? height;
const SomaLogo({
super.key,
this.width,
this.height,
});
@override
Widget build(BuildContext context) {
return Image.asset(
'assets/logo.png',
width: width,
height: height,
fit: BoxFit.contain,
);
}
}
@@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
class SomaPrimaryButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final bool isLoading;
final IconData? icon;
const SomaPrimaryButton({
super.key,
required this.text,
this.onPressed,
this.isLoading = false,
this.icon,
});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: isLoading ? null : onPressed,
child: isLoading
? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5,
color: Colors.black87,
),
)
: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon, size: 20),
const SizedBox(width: 8),
],
Text(text),
],
),
);
}
}
@@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
class SomaSecondaryButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final bool isLoading;
final IconData? icon;
const SomaSecondaryButton({
super.key,
required this.text,
this.onPressed,
this.isLoading = false,
this.icon,
});
@override
Widget build(BuildContext context) {
return OutlinedButton(
onPressed: isLoading ? null : onPressed,
child: isLoading
? SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5,
color: Theme.of(context).colorScheme.primary,
),
)
: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon, size: 20),
const SizedBox(width: 8),
],
Text(text),
],
),
);
}
}
@@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class SomaTextField extends StatelessWidget {
final TextEditingController? controller;
final String? hintText;
final String? labelText;
final IconData? prefixIcon;
final Widget? suffixIcon;
final bool obscureText;
final TextInputType keyboardType;
final List<TextInputFormatter>? inputFormatters;
final String? Function(String?)? validator;
final void Function(String)? onChanged;
final bool enabled;
final int maxLines;
const SomaTextField({
super.key,
this.controller,
this.hintText,
this.labelText,
this.prefixIcon,
this.suffixIcon,
this.obscureText = false,
this.keyboardType = TextInputType.text,
this.inputFormatters,
this.validator,
this.onChanged,
this.enabled = true,
this.maxLines = 1,
});
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
obscureText: obscureText,
keyboardType: keyboardType,
inputFormatters: inputFormatters,
validator: validator,
onChanged: onChanged,
enabled: enabled,
maxLines: maxLines,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontSize: 16,
),
decoration: InputDecoration(
hintText: hintText,
labelText: labelText,
prefixIcon: prefixIcon != null
? Icon(
prefixIcon,
color: Theme.of(context).colorScheme.onSurface.withAlpha(153),
size: 22,
)
: null,
suffixIcon: suffixIcon,
),
);
}
}
@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:gimnasio_soma/core/theme/soma_colors.dart';
enum ToastType { success, error, info }
class SomaToast {
SomaToast._();
static void show(
BuildContext context, {
required String message,
ToastType type = ToastType.info,
SnackBarAction? action,
}) {
ScaffoldMessenger.of(context).clearSnackBars();
final (Color bg, Color text, IconData icon) = switch (type) {
ToastType.success => (SomaColors.success, Colors.white, Icons.check_circle_outline),
ToastType.error => (SomaColors.error, Colors.white, Icons.error_outline),
ToastType.info => (SomaColors.primary, SomaColors.onPrimary, Icons.info_outline),
};
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(icon, color: text, size: 20),
const SizedBox(width: 10),
Expanded(
child: Text(
message,
style: TextStyle(
color: text,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
],
),
backgroundColor: bg,
duration: Duration(seconds: action != null ? 5 : 3),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
action: action,
),
);
}
}
@@ -0,0 +1,65 @@
import 'package:shared_preferences/shared_preferences.dart';
import 'package:gimnasio_soma/core/config/app_constants.dart';
import 'package:gimnasio_soma/core/config/supabase_config.dart';
import 'package:gimnasio_soma/features/actividades/domain/entities/actividad.dart';
import 'package:gimnasio_soma/features/actividades/domain/repositories/actividades_repository.dart';
class ActividadesRepositoryImpl implements ActividadesRepository {
Future<String> _getToken() async {
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString(AppConstants.tokenKey);
if (token == null) throw Exception('Sin sesión activa');
return token;
}
@override
Future<List<Actividad>> getActividades({bool? soloActivas}) async {
final token = await _getToken();
final params = <String, dynamic>{'p_token': token};
if (soloActivas != null) params['p_solo_activas'] = soloActivas;
final response = await SupabaseConfig.rpc(
AppConstants.rpcGetActividades,
params: params,
);
if (response is List) {
return response
.map((e) => Actividad.fromMap(e as Map<String, dynamic>))
.toList();
}
return [];
}
@override
Future<void> insertActividad(Map<String, dynamic> datos) async {
final token = await _getToken();
await SupabaseConfig.rpc(
AppConstants.rpcInsertActividad,
params: {'p_token': token, 'p_datos': datos},
);
}
@override
Future<void> updateActividad(int id, Map<String, dynamic> datos) async {
final token = await _getToken();
await SupabaseConfig.rpc(
AppConstants.rpcUpdateActividad,
params: {'p_token': token, 'p_actividad_id': id, 'p_datos': datos},
);
}
@override
Future<bool> deleteActividad(int id) async {
final token = await _getToken();
final response = await SupabaseConfig.rpc(
AppConstants.rpcDeleteActividad,
params: {'p_token': token, 'p_actividad_id': id},
);
return response == true;
}
}
@@ -0,0 +1,47 @@
class Actividad {
final int id;
final String nombre;
final int duracion; // minutos
final int capacidadPorDefecto;
final bool libre;
final bool activo;
const Actividad({
required this.id,
required this.nombre,
required this.duracion,
required this.capacidadPorDefecto,
this.libre = false,
this.activo = true,
});
factory Actividad.fromMap(Map<String, dynamic> map) {
return Actividad(
id: map['id'] as int,
nombre: map['nombre'] as String? ?? '',
duracion: (map['duracion'] as num?)?.toInt() ?? 0,
capacidadPorDefecto: (map['capacidad_por_defecto'] as num?)?.toInt() ?? 0,
libre: map['libre'] as bool? ?? false,
activo: map['activo'] as bool? ?? true,
);
}
Map<String, dynamic> toMap() {
return {
'nombre': nombre,
'duracion': duracion,
'capacidad_por_defecto': capacidadPorDefecto,
'libre': libre,
'activo': activo,
};
}
String get duracionDisplay {
if (duracion >= 60) {
final h = duracion ~/ 60;
final m = duracion % 60;
return m > 0 ? '${h}h ${m}min' : '${h}h';
}
return '${duracion}min';
}
}
@@ -0,0 +1,8 @@
import 'package:gimnasio_soma/features/actividades/domain/entities/actividad.dart';
abstract class ActividadesRepository {
Future<List<Actividad>> getActividades({bool? soloActivas});
Future<void> insertActividad(Map<String, dynamic> datos);
Future<void> updateActividad(int id, Map<String, dynamic> datos);
Future<bool> deleteActividad(int id);
}
@@ -0,0 +1,67 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:gimnasio_soma/features/actividades/data/repositories/actividades_repository_impl.dart';
import 'package:gimnasio_soma/features/actividades/domain/entities/actividad.dart';
import 'package:gimnasio_soma/features/actividades/domain/repositories/actividades_repository.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
String _errorMessage(Object e) {
if (e is PostgrestException) return e.message;
return e.toString().replaceFirst('Exception: ', '');
}
final actividadesRepositoryProvider = Provider<ActividadesRepository>((ref) {
return ActividadesRepositoryImpl();
});
final actividadesProvider =
StateNotifierProvider<ActividadesNotifier, AsyncValue<List<Actividad>>>((ref) {
return ActividadesNotifier(ref.read(actividadesRepositoryProvider));
});
class ActividadesNotifier extends StateNotifier<AsyncValue<List<Actividad>>> {
final ActividadesRepository _repository;
ActividadesNotifier(this._repository) : super(const AsyncValue.loading()) {
loadActividades();
}
Future<void> loadActividades() async {
state = const AsyncValue.loading();
try {
final actividades = await _repository.getActividades();
state = AsyncValue.data(actividades);
} catch (e, st) {
state = AsyncValue.error(e, st);
}
}
Future<String?> insertActividad(Map<String, dynamic> datos) async {
try {
await _repository.insertActividad(datos);
await loadActividades();
return null;
} catch (e) {
return _errorMessage(e);
}
}
Future<String?> updateActividad(int id, Map<String, dynamic> datos) async {
try {
await _repository.updateActividad(id, datos);
await loadActividades();
return null;
} catch (e) {
return _errorMessage(e);
}
}
Future<String?> deleteActividad(int id) async {
try {
await _repository.deleteActividad(id);
await loadActividades();
return null;
} catch (e) {
return _errorMessage(e);
}
}
}
@@ -0,0 +1,445 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:gimnasio_soma/core/theme/activity_colors.dart';
import 'package:gimnasio_soma/core/theme/soma_colors.dart';
import 'package:gimnasio_soma/core/widgets/soma_toast.dart';
import 'package:gimnasio_soma/features/actividades/domain/entities/actividad.dart';
import 'package:gimnasio_soma/features/actividades/presentation/providers/actividades_provider.dart';
import 'package:gimnasio_soma/features/actividades/presentation/widgets/actividad_form_dialog.dart';
class ActividadesScreen extends ConsumerStatefulWidget {
const ActividadesScreen({super.key});
@override
ConsumerState<ActividadesScreen> createState() => _ActividadesScreenState();
}
class _ActividadesScreenState extends ConsumerState<ActividadesScreen> {
Future<void> _showCreateDialog() async {
final result = await showDialog<Map<String, dynamic>>(
context: context,
builder: (_) => const ActividadFormDialog(),
);
if (result == null || !mounted) return;
final error =
await ref.read(actividadesProvider.notifier).insertActividad(result);
if (!mounted) return;
if (error != null) {
SomaToast.show(context, message: error, type: ToastType.error);
} else {
SomaToast.show(context,
message: 'Actividad creada', type: ToastType.success);
}
}
Future<void> _showEditDialog(Actividad actividad) async {
final result = await showDialog<Map<String, dynamic>>(
context: context,
builder: (_) => ActividadFormDialog(actividad: actividad),
);
if (result == null || !mounted) return;
final error = await ref
.read(actividadesProvider.notifier)
.updateActividad(actividad.id, result);
if (!mounted) return;
if (error != null) {
SomaToast.show(context, message: error, type: ToastType.error);
} else {
SomaToast.show(context,
message: 'Actividad actualizada', type: ToastType.success);
}
}
Future<void> _deleteActividad(Actividad actividad) async {
final confirm = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Eliminar actividad'),
content: Text(
'¿Estás seguro de que querés eliminar "${actividad.nombre}"?\n'
'Esta acción no se puede deshacer.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('Cancelar'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: SomaColors.error,
foregroundColor: Colors.white,
minimumSize: const Size(0, 40),
),
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text('Eliminar'),
),
],
),
);
if (confirm != true || !mounted) return;
final error = await ref
.read(actividadesProvider.notifier)
.deleteActividad(actividad.id);
if (!mounted) return;
if (error != null) {
SomaToast.show(context, message: error, type: ToastType.error);
} else {
SomaToast.show(context,
message: 'Actividad eliminada', type: ToastType.success);
}
}
@override
Widget build(BuildContext context) {
final state = ref.watch(actividadesProvider);
final isWide = MediaQuery.of(context).size.width >= 800;
return Scaffold(
body: Column(
children: [
// Header
Padding(
padding: EdgeInsets.fromLTRB(
isWide ? 32 : 16,
isWide ? 28 : 16,
isWide ? 32 : 16,
12,
),
child: Row(
children: [
const Text(
'Actividades',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700),
),
const Spacer(),
_AddButton(isWide: isWide, onTap: _showCreateDialog),
],
),
),
// Lista
Expanded(
child: state.when(
loading: () => const Center(
child: CircularProgressIndicator(color: SomaColors.primary),
),
error: (e, _) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error_outline,
size: 48,
color: Theme.of(context)
.colorScheme
.onSurface
.withAlpha(100)),
const SizedBox(height: 12),
Text(
e.toString().replaceFirst('Exception: ', ''),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurface
.withAlpha(153),
),
),
const SizedBox(height: 16),
TextButton.icon(
onPressed: () => ref
.read(actividadesProvider.notifier)
.loadActividades(),
icon: const Icon(Icons.refresh, size: 18),
label: const Text('Reintentar'),
),
],
),
),
data: (actividades) {
if (actividades.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.fitness_center_outlined,
size: 56,
color: Theme.of(context)
.colorScheme
.onSurface
.withAlpha(60)),
const SizedBox(height: 12),
Text(
'No hay actividades',
style: TextStyle(
fontSize: 15,
color: Theme.of(context)
.colorScheme
.onSurface
.withAlpha(130),
),
),
],
),
);
}
return RefreshIndicator(
color: SomaColors.primary,
onRefresh: () => ref
.read(actividadesProvider.notifier)
.loadActividades(),
child: ListView.separated(
padding: EdgeInsets.fromLTRB(
isWide ? 32 : 16, 4, isWide ? 32 : 16, 80,
),
itemCount: actividades.length,
separatorBuilder: (_, _) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final act = actividades[index];
return _ActividadCard(
actividad: act,
onEdit: () => _showEditDialog(act),
onDelete: () => _deleteActividad(act),
);
},
),
);
},
),
),
],
),
);
}
}
class _ActividadCard extends StatelessWidget {
final Actividad actividad;
final VoidCallback onEdit;
final VoidCallback onDelete;
const _ActividadCard({
required this.actividad,
required this.onEdit,
required this.onDelete,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return InkWell(
onTap: onEdit,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: theme.colorScheme.surface,
border: Border.all(
color: theme.colorScheme.surfaceContainerHighest,
width: 0.5,
),
),
child: Row(
children: [
// Ícono
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: actividad.activo
? ActivityColors.forId(actividad.id).withAlpha(30)
: theme.colorScheme.onSurface.withAlpha(15),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
Icons.fitness_center,
size: 20,
color: actividad.activo
? theme.colorScheme.onSurface
: theme.colorScheme.onSurface.withAlpha(100),
),
),
const SizedBox(width: 14),
// Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
actividad.nombre,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
if (!actividad.activo)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: theme.colorScheme.onSurface.withAlpha(20),
borderRadius: BorderRadius.circular(6),
),
child: Text(
'Inactiva',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color:
theme.colorScheme.onSurface.withAlpha(130),
),
),
),
if (actividad.libre)
Padding(
padding: const EdgeInsets.only(left: 6),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: SomaColors.success.withAlpha(20),
borderRadius: BorderRadius.circular(6),
),
child: const Text(
'Libre',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: SomaColors.success,
),
),
),
),
],
),
const SizedBox(height: 3),
Row(
children: [
Icon(Icons.timer_outlined,
size: 14,
color:
theme.colorScheme.onSurface.withAlpha(100)),
const SizedBox(width: 4),
Text(
actividad.duracionDisplay,
style: TextStyle(
fontSize: 12,
color:
theme.colorScheme.onSurface.withAlpha(130),
),
),
Text(
'',
style: TextStyle(
fontSize: 12,
color:
theme.colorScheme.onSurface.withAlpha(80),
),
),
Icon(Icons.group_outlined,
size: 14,
color:
theme.colorScheme.onSurface.withAlpha(100)),
const SizedBox(width: 4),
Text(
'${actividad.capacidadPorDefecto} personas',
style: TextStyle(
fontSize: 12,
color:
theme.colorScheme.onSurface.withAlpha(130),
),
),
],
),
],
),
),
// Actions
PopupMenuButton<String>(
icon: Icon(
Icons.more_vert,
size: 18,
color: theme.colorScheme.onSurface.withAlpha(100),
),
itemBuilder: (_) => [
const PopupMenuItem(
value: 'edit',
child: Row(
children: [
Icon(Icons.edit_outlined, size: 18),
SizedBox(width: 8),
Text('Editar'),
],
),
),
const PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete_outline, size: 18, color: SomaColors.error),
SizedBox(width: 8),
Text('Eliminar',
style: TextStyle(color: SomaColors.error)),
],
),
),
],
onSelected: (v) {
if (v == 'edit') onEdit();
if (v == 'delete') onDelete();
},
),
],
),
),
);
}
}
class _AddButton extends StatelessWidget {
final bool isWide;
final VoidCallback onTap;
const _AddButton({required this.isWide, required this.onTap});
@override
Widget build(BuildContext context) {
if (isWide) {
return ElevatedButton.icon(
onPressed: onTap,
icon: const Icon(Icons.add, size: 20),
label: const Text('Nueva'),
style: ElevatedButton.styleFrom(
minimumSize: const Size(0, 42),
),
);
}
return SizedBox(
height: 42,
width: 42,
child: IconButton.filled(
onPressed: onTap,
icon: const Icon(Icons.add, size: 22),
style: IconButton.styleFrom(
backgroundColor: SomaColors.primary,
foregroundColor: SomaColors.onPrimary,
),
),
);
}
}
@@ -0,0 +1,218 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gimnasio_soma/core/theme/soma_colors.dart';
import 'package:gimnasio_soma/core/widgets/soma_text_field.dart';
import 'package:gimnasio_soma/features/actividades/domain/entities/actividad.dart';
class ActividadFormDialog extends StatefulWidget {
final Actividad? actividad;
const ActividadFormDialog({super.key, this.actividad});
@override
State<ActividadFormDialog> createState() => _ActividadFormDialogState();
}
class _ActividadFormDialogState extends State<ActividadFormDialog> {
final _formKey = GlobalKey<FormState>();
late final TextEditingController _nombreCtrl;
late final TextEditingController _duracionCtrl;
late final TextEditingController _capacidadCtrl;
late bool _libre;
bool get _isEditing => widget.actividad != null;
@override
void initState() {
super.initState();
_nombreCtrl = TextEditingController(text: widget.actividad?.nombre ?? '');
_duracionCtrl = TextEditingController(
text: widget.actividad != null ? widget.actividad!.duracion.toString() : '',
);
_capacidadCtrl = TextEditingController(
text: widget.actividad != null
? widget.actividad!.capacidadPorDefecto.toString()
: '',
);
_libre = widget.actividad?.libre ?? false;
}
@override
void dispose() {
_nombreCtrl.dispose();
_duracionCtrl.dispose();
_capacidadCtrl.dispose();
super.dispose();
}
void _submit() {
if (!_formKey.currentState!.validate()) return;
final data = <String, dynamic>{
'nombre': _nombreCtrl.text.trim(),
'duracion': int.tryParse(_duracionCtrl.text.trim()) ?? 0,
'capacidad_por_defecto': int.tryParse(_capacidadCtrl.text.trim()) ?? 0,
'libre': _libre,
};
Navigator.of(context).pop(data);
}
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
final isWide = width >= 600;
return Dialog(
insetPadding: EdgeInsets.symmetric(
horizontal: isWide ? (width - 440) / 2 : 20,
vertical: 24,
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 440),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
Padding(
padding: const EdgeInsets.fromLTRB(24, 20, 16, 0),
child: Row(
children: [
Text(
_isEditing ? 'Editar Actividad' : 'Nueva Actividad',
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.w700),
),
const Spacer(),
IconButton(
icon: const Icon(Icons.close, size: 20),
onPressed: () => Navigator.of(context).pop(),
),
],
),
),
const Divider(height: 20),
// Form
Flexible(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 8),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SomaTextField(
controller: _nombreCtrl,
labelText: 'Nombre *',
prefixIcon: Icons.fitness_center,
validator: (v) => v == null || v.trim().isEmpty
? 'Nombre requerido'
: null,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: SomaTextField(
controller: _duracionCtrl,
labelText: 'Duración (min) *',
prefixIcon: Icons.timer_outlined,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(4),
],
validator: (v) {
if (v == null || v.trim().isEmpty) {
return 'Requerido';
}
final n = int.tryParse(v.trim());
if (n == null || n <= 0) return 'Inválido';
return null;
},
),
),
const SizedBox(width: 12),
Expanded(
child: SomaTextField(
controller: _capacidadCtrl,
labelText: 'Capacidad *',
prefixIcon: Icons.group_outlined,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(4),
],
validator: (v) {
if (v == null || v.trim().isEmpty) {
return 'Requerido';
}
final n = int.tryParse(v.trim());
if (n == null || n <= 0) return 'Inválido';
return null;
},
),
),
],
),
const SizedBox(height: 16),
SwitchListTile(
title: const Text(
'Actividad libre',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
subtitle: const Text(
'No requiere turno previo',
style: TextStyle(fontSize: 12),
),
value: _libre,
activeThumbColor: SomaColors.primary,
contentPadding: EdgeInsets.zero,
onChanged: (v) => setState(() => _libre = v),
),
const SizedBox(height: 8),
],
),
),
),
),
const Divider(height: 1),
// Actions
Padding(
padding: const EdgeInsets.fromLTRB(24, 12, 24, 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
'Cancelar',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurface
.withAlpha(178),
),
),
),
const SizedBox(width: 12),
ElevatedButton(
onPressed: _submit,
style: ElevatedButton.styleFrom(
minimumSize: const Size(0, 42),
),
child: Text(_isEditing ? 'Guardar' : 'Crear'),
),
],
),
),
],
),
),
);
}
}

Some files were not shown because too many files have changed in this diff Show More