diff --git a/CHANGELOG.md b/CHANGELOG.md index db9b17e..e634d8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,71 @@ O formato segue [Keep a Changelog](https://keepachangelog.com/pt-BR/). Este projeto adota [Versionamento Semântico](https://semver.org/pt-BR/). +## [1.53.0] - 2025-12-17 + +### Fixed +- 🔧 **Correção Middleware Admin** - Alterado de `admin` para `admin.only` nas rotas site-settings +- 🔧 **Correção Deploy cnxifly.com** - Controller atualizado para usar arquivos do servidor + - Páginas armazenadas em `/var/www/cnxifly-pages/` + - Deploy direto via `File::copy()` ao invés de instruções manuais + +### Added +- 📁 **Diretório de páginas no servidor** (`/var/www/cnxifly-pages/`) + - `index.html` - Página institucional live + - `maintenance.html` - Página de manutenção + - Permite trocar páginas via painel admin + +### Technical Details +- Middleware correto: `admin.only` (registrado em bootstrap/app.php) +- SiteSettingsController agora copia arquivos diretamente no servidor +- Permissões ajustadas para www-data no cnxifly-pages + +--- + +## [1.52.0] - 2025-12-17 + +### Added +- 🌐 **Landing Page Institucional ConneXiFly** - Página completa para cnxifly.com + - **Produtos destacados**: + - WebMoney: Gestão financeira inteligente (Disponível) + - EZPool: Software de mantenimiento de piscinas (Próximamente) + - **Seções**: + - Hero com animações e gradientes + - Cards de produtos com preços + - Features do ConneXiFly + - CTA para registro + - Footer completo + - **Modal de notificação** para EZPool (pré-registro) + - **Design responsivo** e dark theme consistente + +- 🔧 **Página de Manutenção** alternativa (`landing/maintenance.html`) + - Design simples para modo manutenção + - Links para produtos disponíveis + +- ⚙️ **Painel Admin - Configurações do Site** + - **Backend**: + - `SiteSetting` model para configurações persistentes + - `SiteSettingsController` para gerenciar cnxifly.com + - Rotas admin para toggle de modo (live/maintenance) + - Migration para tabela `site_settings` + + - **Frontend**: + - Nova página `/site-settings` (admin only) + - Toggle entre modo Live e Manutenção + - Preview links para todos os sites + - Instruções de deploy + +- 📝 **Script de Deploy Landing** (`deploy-landing.sh`) + - Deploy automático para cnxifly.com + - Suporte a modos: live, maintenance + +### Technical Details +- Arquivos criados em `/landing/` (index.html, maintenance.html) +- Bootstrap 5.3 + Bootstrap Icons para a landing +- Integração com menu do WebMoney admin + +--- + ## [1.51.0] - 2025-12-17 ### Added diff --git a/VERSION b/VERSION index ba0a719..3f48301 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.51.0 +1.53.0 diff --git a/backend/app/Http/Controllers/Api/FinancialHealthController.php b/backend/app/Http/Controllers/Api/FinancialHealthController.php index c337aeb..ec7aaef 100644 --- a/backend/app/Http/Controllers/Api/FinancialHealthController.php +++ b/backend/app/Http/Controllers/Api/FinancialHealthController.php @@ -30,6 +30,9 @@ public function index(Request $request) $this->setPrimaryCurrency(); $this->loadExchangeRates(); + // Verificar si hay datos suficientes para análisis + $dataStatus = $this->checkDataSufficiency(); + // Obtener datos base $financialSummary = $this->getFinancialSummary(); $cashFlowAnalysis = $this->analyzeCashFlow(); @@ -77,6 +80,9 @@ public function index(Request $request) 'last_updated' => now()->toIso8601String(), 'currency' => $this->primaryCurrency, + // Estado de datos + 'data_status' => $dataStatus, + // Resumen financiero 'summary' => [ 'total_assets' => $financialSummary['total_assets'], @@ -118,6 +124,56 @@ public function index(Request $request) ]); } + /** + * Verificar si hay datos suficientes para análisis + */ + private function checkDataSufficiency() + { + $accountsCount = Account::where('user_id', $this->userId)->count(); + $transactionsCount = Transaction::where('user_id', $this->userId)->count(); + $categoriesCount = Category::where('user_id', $this->userId)->count(); + + // Transacciones de los últimos 30 días + $recentTransactions = Transaction::where('user_id', $this->userId) + ->where('effective_date', '>=', now()->subDays(30)) + ->count(); + + // Determinar nivel de suficiencia + $hasSufficientData = $accountsCount >= 1 && $transactionsCount >= 10; + $hasMinimalData = $accountsCount >= 1 || $transactionsCount >= 1; + + // Mensaje apropiado + $message = null; + $level = 'sufficient'; + + if ($accountsCount === 0 && $transactionsCount === 0) { + $level = 'no_data'; + $message = 'No hay datos registrados. Añade cuentas y transacciones para comenzar el análisis.'; + } elseif ($accountsCount === 0) { + $level = 'insufficient'; + $message = 'Añade al menos una cuenta bancaria para un análisis más preciso.'; + } elseif ($transactionsCount < 10) { + $level = 'limited'; + $message = 'Hay pocos datos para un análisis completo. Registra más transacciones para mejorar la precisión.'; + } elseif ($recentTransactions === 0) { + $level = 'outdated'; + $message = 'No hay transacciones recientes. Los datos pueden estar desactualizados.'; + } + + return [ + 'has_sufficient_data' => $hasSufficientData, + 'has_minimal_data' => $hasMinimalData, + 'level' => $level, + 'message' => $message, + 'counts' => [ + 'accounts' => $accountsCount, + 'transactions' => $transactionsCount, + 'categories' => $categoriesCount, + 'recent_transactions' => $recentTransactions, + ], + ]; + } + /** * Establecer moneda principal del usuario */ diff --git a/backend/app/Http/Controllers/Api/SiteSettingsController.php b/backend/app/Http/Controllers/Api/SiteSettingsController.php new file mode 100644 index 0000000..cf874cd --- /dev/null +++ b/backend/app/Http/Controllers/Api/SiteSettingsController.php @@ -0,0 +1,192 @@ +mapWithKeys(function ($setting) { + return [$setting->key => [ + 'value' => $setting->value['value'] ?? $setting->value, + 'description' => $setting->description, + 'updated_at' => $setting->updated_at, + ]]; + }); + + return response()->json([ + 'success' => true, + 'settings' => $settings, + ]); + } + + /** + * Get a specific setting + */ + public function show(string $key) + { + $setting = SiteSetting::where('key', $key)->first(); + + if (!$setting) { + return response()->json([ + 'success' => false, + 'message' => 'Configuración no encontrada', + ], 404); + } + + return response()->json([ + 'success' => true, + 'key' => $setting->key, + 'value' => $setting->value['value'] ?? $setting->value, + 'description' => $setting->description, + ]); + } + + /** + * Update a setting + */ + public function update(Request $request, string $key) + { + $request->validate([ + 'value' => 'required', + 'description' => 'nullable|string', + ]); + + $setting = SiteSetting::setValue( + $key, + $request->value, + $request->description + ); + + return response()->json([ + 'success' => true, + 'message' => 'Configuración actualizada', + 'setting' => [ + 'key' => $setting->key, + 'value' => $setting->value['value'] ?? $setting->value, + 'description' => $setting->description, + ], + ]); + } + + /** + * Toggle cnxifly.com page mode + */ + public function toggleCnxiflyPage(Request $request) + { + $request->validate([ + 'mode' => 'required|in:live,maintenance,construction', + ]); + + $mode = $request->mode; + + // Update database setting + SiteSetting::setValue('cnxifly_page_type', $mode, 'Tipo de página cnxifly.com'); + SiteSetting::setValue('cnxifly_maintenance_mode', $mode !== 'live', 'Modo de mantenimiento'); + + // Get paths + $serverPath = '/var/www/cnxifly'; + $localLandingPath = base_path('../landing'); + + // Log the action + Log::info("CnxiFly page mode changed to: {$mode}"); + + return response()->json([ + 'success' => true, + 'message' => "Modo de página cambiado a: {$mode}", + 'current_mode' => $mode, + 'instructions' => $this->getInstructions($mode), + ]); + } + + /** + * Get cnxifly page status + */ + public function getCnxiflyStatus() + { + $mode = SiteSetting::getValue('cnxifly_page_type', 'live'); + $maintenanceMode = SiteSetting::getValue('cnxifly_maintenance_mode', false); + + return response()->json([ + 'success' => true, + 'mode' => $mode, + 'maintenance_mode' => $maintenanceMode, + 'modes_available' => [ + 'live' => 'Página institucional completa', + 'maintenance' => 'Página de mantenimiento', + 'construction' => 'Página en construcción', + ], + ]); + } + + /** + * Deploy cnxifly landing page to server + */ + public function deployCnxiflyPage(Request $request) + { + $request->validate([ + 'mode' => 'required|in:live,maintenance', + ]); + + $mode = $request->mode; + + // Source files are in /var/www/cnxifly-pages/ on the server + $sourceDir = '/var/www/cnxifly-pages'; + $targetDir = '/var/www/cnxifly'; + $sourceFile = $mode === 'live' ? 'index.html' : 'maintenance.html'; + $sourcePath = "{$sourceDir}/{$sourceFile}"; + $targetPath = "{$targetDir}/index.html"; + + // Check if source file exists + if (!File::exists($sourcePath)) { + return response()->json([ + 'success' => false, + 'message' => "Archivo de origen no encontrado: {$sourcePath}. Ejecute deploy-landing.sh primero.", + ], 404); + } + + // Copy the file + try { + File::copy($sourcePath, $targetPath); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => "Error al copiar archivo: " . $e->getMessage(), + ], 500); + } + + // Update setting + SiteSetting::setValue('cnxifly_page_type', $mode); + SiteSetting::setValue('cnxifly_maintenance_mode', $mode === 'maintenance'); + + return response()->json([ + 'success' => true, + 'message' => "Página cnxifly.com actualizada a modo: {$mode}", + 'current_mode' => $mode, + ]); + } + + private function getInstructions(string $mode): array + { + $sourceFile = match($mode) { + 'live' => 'index.html', + 'maintenance' => 'maintenance.html', + 'construction' => 'maintenance.html', + }; + + return [ + 'description' => "Para aplicar el modo '{$mode}', copie el archivo {$sourceFile} al servidor", + 'source_file' => $sourceFile, + 'target_path' => '/var/www/cnxifly/index.html', + ]; + } +} diff --git a/backend/app/Http/Controllers/Api/UserManagementController.php b/backend/app/Http/Controllers/Api/UserManagementController.php index be49e7f..1e1e46e 100644 --- a/backend/app/Http/Controllers/Api/UserManagementController.php +++ b/backend/app/Http/Controllers/Api/UserManagementController.php @@ -6,8 +6,10 @@ use App\Models\User; use App\Models\Plan; use App\Models\Subscription; +use App\Mail\WelcomeNewUser; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Mail; use Illuminate\Validation\Rules\Password; use Carbon\Carbon; @@ -22,15 +24,17 @@ public function store(Request $request) $validated = $request->validate([ 'name' => 'required|string|max:255', 'email' => 'required|email|unique:users,email', - 'password' => 'sometimes|string|min:8', + 'password' => 'nullable|string|min:8', 'language' => 'sometimes|string|in:es,pt-BR,en', 'currency' => 'sometimes|string|size:3', 'user_type' => 'sometimes|string|in:free,pro,admin', + 'send_welcome_email' => 'sometimes|boolean', ]); - // Generate random password if not provided - $password = $validated['password'] ?? bin2hex(random_bytes(8)); + // Generate random password if not provided or empty + $password = !empty($validated['password']) ? $validated['password'] : bin2hex(random_bytes(8)); $userType = $validated['user_type'] ?? 'free'; + $sendWelcomeEmail = $validated['send_welcome_email'] ?? true; $user = User::create([ 'name' => $validated['name'], @@ -54,7 +58,7 @@ public function store(Request $request) 'plan_id' => $proPlan->id, 'status' => Subscription::STATUS_ACTIVE, 'current_period_start' => now(), - 'current_period_end' => now()->addYears(100), // "Lifetime" subscription + 'current_period_end' => Carbon::create(2037, 12, 31, 23, 59, 59), // "Lifetime" subscription (max timestamp) 'paypal_subscription_id' => 'ADMIN_GRANTED_' . strtoupper(bin2hex(random_bytes(8))), 'paypal_status' => 'ACTIVE', 'price_paid' => 0, @@ -69,6 +73,17 @@ public function store(Request $request) } } + // Send welcome email with temporary password + $emailSent = false; + if ($sendWelcomeEmail) { + try { + Mail::to($user->email)->send(new WelcomeNewUser($user, $password)); + $emailSent = true; + } catch (\Exception $e) { + \Log::error('Failed to send welcome email: ' . $e->getMessage()); + } + } + return response()->json([ 'success' => true, 'message' => 'Usuario creado correctamente', @@ -78,10 +93,12 @@ public function store(Request $request) 'name' => $user->name, 'email' => $user->email, 'is_admin' => $user->is_admin, + 'language' => $user->language, ], 'user_type' => $userType, 'subscription' => $subscriptionInfo, 'temporary_password' => isset($validated['password']) ? null : $password, + 'welcome_email_sent' => $emailSent, ], ], 201); } @@ -122,6 +139,9 @@ public function index(Request $request) 'id' => $user->id, 'name' => $user->name, 'email' => $user->email, + 'language' => $user->language, + 'currency' => $user->currency, + 'is_admin' => (bool) $user->is_admin, 'created_at' => $user->created_at, 'last_login_at' => $user->last_login_at, 'email_verified_at' => $user->email_verified_at, @@ -207,11 +227,20 @@ public function update(Request $request, $id) { $user = User::findOrFail($id); + // Don't allow changing main admin's admin status + if ($user->email === 'marco@cnxifly.com' && $request->has('is_admin') && !$request->is_admin) { + return response()->json([ + 'success' => false, + 'message' => 'No se puede remover permisos del administrador principal', + ], 403); + } + $validated = $request->validate([ 'name' => 'sometimes|string|max:255', 'email' => 'sometimes|email|unique:users,email,' . $id, 'language' => 'sometimes|string|in:es,pt-BR,en', 'currency' => 'sometimes|string|size:3', + 'is_admin' => 'sometimes|boolean', ]); $user->update($validated); @@ -219,7 +248,14 @@ public function update(Request $request, $id) return response()->json([ 'success' => true, 'message' => 'Usuario actualizado correctamente', - 'data' => $user, + 'data' => [ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + 'language' => $user->language, + 'currency' => $user->currency, + 'is_admin' => $user->is_admin, + ], ]); } @@ -268,6 +304,90 @@ public function destroy($id) ]); } + /** + * Change user subscription plan + */ + public function changePlan(Request $request, $id) + { + $user = User::findOrFail($id); + + $validated = $request->validate([ + 'plan' => 'required|string|in:free,pro', + ]); + + // If changing to free, cancel any existing subscription + if ($validated['plan'] === 'free') { + $subscription = $user->subscription; + if ($subscription) { + $subscription->update([ + 'status' => Subscription::STATUS_CANCELED, + 'canceled_at' => now(), + ]); + } + + return response()->json([ + 'success' => true, + 'message' => 'Usuario cambiado a plan Free', + 'data' => [ + 'plan' => 'free', + 'subscription' => null, + ], + ]); + } + + // If changing to pro, create or reactivate subscription + if ($validated['plan'] === 'pro') { + $proPlan = Plan::where('slug', 'pro-annual')->first(); + + if (!$proPlan) { + return response()->json([ + 'success' => false, + 'message' => 'Plan Pro no encontrado en el sistema', + ], 404); + } + + // Check if user has existing subscription + $subscription = $user->subscription; + + if ($subscription) { + // Reactivate existing subscription + $subscription->update([ + 'status' => Subscription::STATUS_ACTIVE, + 'canceled_at' => null, + 'current_period_start' => now(), + 'current_period_end' => Carbon::create(2037, 12, 31, 23, 59, 59), + ]); + } else { + // Create new subscription + $subscription = Subscription::create([ + 'user_id' => $user->id, + 'plan_id' => $proPlan->id, + 'status' => Subscription::STATUS_ACTIVE, + 'current_period_start' => now(), + 'current_period_end' => Carbon::create(2037, 12, 31, 23, 59, 59), + 'paypal_subscription_id' => 'ADMIN_GRANTED_' . strtoupper(bin2hex(random_bytes(8))), + 'paypal_status' => 'ACTIVE', + 'price_paid' => 0, + 'currency' => 'EUR', + ]); + } + + return response()->json([ + 'success' => true, + 'message' => 'Usuario cambiado a plan Pro', + 'data' => [ + 'plan' => 'pro', + 'subscription' => [ + 'id' => $subscription->id, + 'plan_name' => $proPlan->name, + 'status' => $subscription->status, + 'current_period_end' => $subscription->current_period_end, + ], + ], + ]); + } + } + /** * Get summary statistics */ diff --git a/backend/app/Mail/WelcomeNewUser.php b/backend/app/Mail/WelcomeNewUser.php new file mode 100644 index 0000000..dfc75d6 --- /dev/null +++ b/backend/app/Mail/WelcomeNewUser.php @@ -0,0 +1,77 @@ +user = $user; + $this->temporaryPassword = $temporaryPassword; + $this->language = $user->language ?? 'es'; + } + + /** + * Get the message headers. + */ + public function headers(): Headers + { + return new Headers( + text: [ + 'X-Priority' => '3', + 'X-Mailer' => 'WebMoney Mailer', + 'List-Unsubscribe' => '', + ], + ); + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + $subjects = [ + 'es' => 'WebMoney - Credenciales de acceso a tu cuenta', + 'pt-BR' => 'WebMoney - Credenciais de acesso à sua conta', + 'en' => 'WebMoney - Your account access credentials', + ]; + + return new Envelope( + subject: $subjects[$this->language] ?? $subjects['es'], + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + view: 'emails.welcome-new-user', + text: 'emails.welcome-new-user-text', + with: [ + 'user' => $this->user, + 'temporaryPassword' => $this->temporaryPassword, + 'language' => $this->language, + 'loginUrl' => config('app.frontend_url', 'https://webmoney.cnxifly.com') . '/login', + ], + ); + } +} diff --git a/backend/app/Models/SiteSetting.php b/backend/app/Models/SiteSetting.php new file mode 100644 index 0000000..39ff204 --- /dev/null +++ b/backend/app/Models/SiteSetting.php @@ -0,0 +1,52 @@ + 'array', + ]; + + /** + * Get a setting value by key + */ + public static function getValue(string $key, $default = null) + { + $setting = static::where('key', $key)->first(); + + if (!$setting) { + return $default; + } + + // If value is a simple string stored as JSON array, extract it + $value = $setting->value; + if (is_array($value) && isset($value['value'])) { + return $value['value']; + } + + return $value ?? $default; + } + + /** + * Set a setting value by key + */ + public static function setValue(string $key, $value, string $description = null): self + { + return static::updateOrCreate( + ['key' => $key], + [ + 'value' => is_array($value) ? $value : ['value' => $value], + 'description' => $description, + ] + ); + } +} diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php index 7c22982..5bd0152 100644 --- a/backend/app/Models/User.php +++ b/backend/app/Models/User.php @@ -31,8 +31,11 @@ class User extends Authenticatable 'country', 'timezone', 'locale', + 'language', + 'currency', 'password', 'is_admin', + 'email_verified_at', ]; /** diff --git a/backend/config/app.php b/backend/config/app.php index 423eed5..3afb892 100644 --- a/backend/config/app.php +++ b/backend/config/app.php @@ -54,6 +54,18 @@ 'url' => env('APP_URL', 'http://localhost'), + /* + |-------------------------------------------------------------------------- + | Frontend URL + |-------------------------------------------------------------------------- + | + | This URL is used for generating links to the frontend application, + | such as password reset links and email verification links. + | + */ + + 'frontend_url' => env('FRONTEND_URL', 'https://webmoney.cnxifly.com'), + /* |-------------------------------------------------------------------------- | Application Timezone diff --git a/backend/database/migrations/2025_01_15_000001_create_site_settings_table.php b/backend/database/migrations/2025_01_15_000001_create_site_settings_table.php new file mode 100644 index 0000000..13fe9e5 --- /dev/null +++ b/backend/database/migrations/2025_01_15_000001_create_site_settings_table.php @@ -0,0 +1,28 @@ +id(); + $table->string('key')->unique(); + $table->json('value')->nullable(); + $table->string('description')->nullable(); + $table->timestamps(); + }); + + // Insert default settings + \App\Models\SiteSetting::setValue('cnxifly_maintenance_mode', false, 'Modo de mantenimiento para cnxifly.com'); + \App\Models\SiteSetting::setValue('cnxifly_page_type', 'live', 'Tipo de página: live, maintenance, construction'); + } + + public function down(): void + { + Schema::dropIfExists('site_settings'); + } +}; diff --git a/backend/database/migrations/2025_12_17_155312_add_language_currency_to_users_table.php b/backend/database/migrations/2025_12_17_155312_add_language_currency_to_users_table.php new file mode 100644 index 0000000..6b4bb97 --- /dev/null +++ b/backend/database/migrations/2025_12_17_155312_add_language_currency_to_users_table.php @@ -0,0 +1,29 @@ +string('language', 10)->default('es')->after('locale'); + $table->string('currency', 3)->default('EUR')->after('language'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn(['language', 'currency']); + }); + } +}; diff --git a/backend/resources/views/emails/welcome-new-user-text.blade.php b/backend/resources/views/emails/welcome-new-user-text.blade.php new file mode 100644 index 0000000..652d58c --- /dev/null +++ b/backend/resources/views/emails/welcome-new-user-text.blade.php @@ -0,0 +1,66 @@ +@if($language === 'pt-BR') +{{-- PORTUGUÊS --}} +WebMoney - Credenciais de Acesso +================================ + +Olá, {{ $user->name }}! + +Sua conta WebMoney foi criada com sucesso. + +SUAS CREDENCIAIS DE ACESSO: +--------------------------- +Email: {{ $user->email }} +Senha: {{ $temporaryPassword }} + +IMPORTANTE: Por motivos de segurança, recomendamos que altere sua senha após o primeiro login. + +Acesse sua conta em: {{ $loginUrl }} + +--- +Equipe WebMoney +https://webmoney.cnxifly.com + +@elseif($language === 'en') +{{-- ENGLISH --}} +WebMoney - Access Credentials +============================= + +Hello, {{ $user->name }}! + +Your WebMoney account has been successfully created. + +YOUR ACCESS CREDENTIALS: +------------------------ +Email: {{ $user->email }} +Password: {{ $temporaryPassword }} + +IMPORTANT: For security reasons, we recommend changing your password after your first login. + +Access your account at: {{ $loginUrl }} + +--- +WebMoney Team +https://webmoney.cnxifly.com + +@else +{{-- ESPAÑOL --}} +WebMoney - Credenciales de Acceso +================================= + +Hola, {{ $user->name }}! + +Tu cuenta de WebMoney ha sido creada exitosamente. + +TUS CREDENCIALES DE ACCESO: +--------------------------- +Email: {{ $user->email }} +Contraseña: {{ $temporaryPassword }} + +IMPORTANTE: Por motivos de seguridad, te recomendamos cambiar tu contraseña después del primer inicio de sesión. + +Accede a tu cuenta en: {{ $loginUrl }} + +--- +Equipo WebMoney +https://webmoney.cnxifly.com +@endif diff --git a/backend/resources/views/emails/welcome-new-user.blade.php b/backend/resources/views/emails/welcome-new-user.blade.php new file mode 100644 index 0000000..ea5a855 --- /dev/null +++ b/backend/resources/views/emails/welcome-new-user.blade.php @@ -0,0 +1,294 @@ + + + + + + + @if($language === 'pt-BR') + Bem-vindo ao WebMoney + @elseif($language === 'en') + Welcome to WebMoney + @else + Bienvenido a WebMoney + @endif + + + + +
+
+ +
+ + @if($language === 'pt-BR') + {{-- PORTUGUÊS --}} +

Olá, {{ $user->name }}! 👋

+ +

Sua conta WebMoney foi criada com sucesso. Estamos muito felizes em tê-lo conosco!

+ +

Abaixo estão suas credenciais de acesso:

+ +
+

🔐 Suas Credenciais

+
+
Email
+
{{ $user->email }}
+
+
+
Senha Temporária
+
{{ $temporaryPassword }}
+
+
+ +
+

⚠️ Importante: Recomendamos que você altere sua senha após o primeiro login por motivos de segurança.

+
+ +
+ Acessar Minha Conta +
+ +
+

🚀 O que você pode fazer com o WebMoney:

+
    +
  • Gerenciar todas suas contas bancárias em um só lugar
  • +
  • Categorizar receitas e despesas automaticamente
  • +
  • Criar orçamentos e acompanhar seus gastos
  • +
  • Visualizar relatórios e gráficos detalhados
  • +
  • Definir metas financeiras e alcançá-las
  • +
+
+ +

Se você tiver alguma dúvida ou precisar de ajuda, não hesite em nos contatar.

+ +

Atenciosamente,
Equipe WebMoney

+ + @elseif($language === 'en') + {{-- ENGLISH --}} +

Hello, {{ $user->name }}! 👋

+ +

Your WebMoney account has been successfully created. We're thrilled to have you with us!

+ +

Below are your login credentials:

+ +
+

🔐 Your Credentials

+
+
Email
+
{{ $user->email }}
+
+
+
Temporary Password
+
{{ $temporaryPassword }}
+
+
+ +
+

⚠️ Important: We recommend changing your password after your first login for security purposes.

+
+ +
+ Access My Account +
+ +
+

🚀 What you can do with WebMoney:

+
    +
  • Manage all your bank accounts in one place
  • +
  • Automatically categorize income and expenses
  • +
  • Create budgets and track your spending
  • +
  • View detailed reports and charts
  • +
  • Set financial goals and achieve them
  • +
+
+ +

If you have any questions or need help, don't hesitate to contact us.

+ +

Best regards,
The WebMoney Team

+ + @else + {{-- ESPAÑOL (default) --}} +

¡Hola, {{ $user->name }}! 👋

+ +

Tu cuenta de WebMoney ha sido creada exitosamente. ¡Estamos muy contentos de tenerte con nosotros!

+ +

A continuación encontrarás tus credenciales de acceso:

+ +
+

🔐 Tus Credenciales

+
+
Email
+
{{ $user->email }}
+
+
+
Contraseña Temporal
+
{{ $temporaryPassword }}
+
+
+ +
+

⚠️ Importante: Te recomendamos cambiar tu contraseña después de tu primer inicio de sesión por motivos de seguridad.

+
+ +
+ Acceder a Mi Cuenta +
+ +
+

🚀 Lo que puedes hacer con WebMoney:

+
    +
  • Gestionar todas tus cuentas bancarias en un solo lugar
  • +
  • Categorizar ingresos y gastos automáticamente
  • +
  • Crear presupuestos y hacer seguimiento de tus gastos
  • +
  • Ver informes y gráficos detallados
  • +
  • Establecer metas financieras y alcanzarlas
  • +
+
+ +

Si tienes alguna pregunta o necesitas ayuda, no dudes en contactarnos.

+ +

Saludos cordiales,
El Equipo de WebMoney

+ @endif + + +
+ + diff --git a/backend/routes/api.php b/backend/routes/api.php index 98067f1..6abf342 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -25,6 +25,7 @@ use App\Http\Controllers\Api\PlanController; use App\Http\Controllers\Api\SubscriptionController; use App\Http\Controllers\Api\UserManagementController; +use App\Http\Controllers\Api\SiteSettingsController; // Public routes with rate limiting Route::post('/register', [AuthController::class, 'register'])->middleware('throttle:register'); @@ -236,6 +237,7 @@ Route::get('admin/users/{id}', [UserManagementController::class, 'show']); Route::put('admin/users/{id}', [UserManagementController::class, 'update']); Route::post('admin/users/{id}/reset-password', [UserManagementController::class, 'resetPassword']); + Route::post('admin/users/{id}/change-plan', [UserManagementController::class, 'changePlan']); Route::delete('admin/users/{id}', [UserManagementController::class, 'destroy']); // Configurações de Negócio (Markup) @@ -323,5 +325,17 @@ Route::get('preferences', [UserPreferenceController::class, 'index']); Route::put('preferences', [UserPreferenceController::class, 'update']); Route::post('preferences/test-notification', [UserPreferenceController::class, 'testNotification']); + + // ============================================ + // Site Settings (Admin only - Configurações do Site) + // ============================================ + Route::middleware('admin.only')->prefix('admin/site-settings')->group(function () { + Route::get('/', [SiteSettingsController::class, 'index']); + Route::get('/cnxifly/status', [SiteSettingsController::class, 'getCnxiflyStatus']); + Route::post('/cnxifly/toggle', [SiteSettingsController::class, 'toggleCnxiflyPage']); + Route::post('/cnxifly/deploy', [SiteSettingsController::class, 'deployCnxiflyPage']); + Route::get('/{key}', [SiteSettingsController::class, 'show']); + Route::put('/{key}', [SiteSettingsController::class, 'update']); + }); }); diff --git a/deploy-landing.sh b/deploy-landing.sh new file mode 100755 index 0000000..4c45bbc --- /dev/null +++ b/deploy-landing.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# Deploy landing page to cnxifly.com +# Usage: ./deploy-landing.sh [live|maintenance] + +MODE=${1:-live} +SERVER="root@213.165.93.60" +PASSWORD="Master9354" +DEST="/var/www/cnxifly" + +echo "🚀 Deploying cnxifly.com landing page in mode: $MODE" + +if [ "$MODE" = "live" ]; then + SOURCE="landing/index.html" +elif [ "$MODE" = "maintenance" ]; then + SOURCE="landing/maintenance.html" +else + echo "❌ Invalid mode. Use: live or maintenance" + exit 1 +fi + +# Check if source file exists +if [ ! -f "$SOURCE" ]; then + echo "❌ Source file not found: $SOURCE" + exit 1 +fi + +echo "📁 Copying $SOURCE to server..." + +# Deploy the file +sshpass -p "$PASSWORD" scp -o StrictHostKeyChecking=no "$SOURCE" "$SERVER:$DEST/index.html" + +if [ $? -eq 0 ]; then + echo "✅ Landing page deployed successfully!" + echo "🌐 Visit: https://cnxifly.com" +else + echo "❌ Deploy failed!" + exit 1 +fi diff --git a/frontend/public/sw.js b/frontend/public/sw.js index aaacfb6..8821fb5 100644 --- a/frontend/public/sw.js +++ b/frontend/public/sw.js @@ -1,5 +1,5 @@ // WebMoney Service Worker - PWA Support -const CACHE_VERSION = 'webmoney-v1.39.0'; +const CACHE_VERSION = 'webmoney-v1.40.0'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_DYNAMIC = `${CACHE_VERSION}-dynamic`; const CACHE_IMMUTABLE = `${CACHE_VERSION}-immutable`; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index c380ede..caa36cd 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -26,6 +26,7 @@ import Profile from './pages/Profile'; import Pricing from './pages/Pricing'; import Billing from './pages/Billing'; import Users from './pages/Users'; +import SiteSettings from './pages/SiteSettings'; function App() { return ( @@ -232,6 +233,16 @@ function App() { } /> + + + + + + } + /> } /> diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index fceebd4..fa9cfe3 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -120,8 +120,11 @@ const Layout = ({ children }) => { { path: '/profile', icon: 'bi-person-circle', label: t('nav.profile') }, { path: '/billing', icon: 'bi-credit-card', label: t('nav.billing') }, { path: '/pricing', icon: 'bi-tags-fill', label: t('nav.pricing') }, - // Admin only: User management - ...(isAdmin ? [{ path: '/users', icon: 'bi-people', label: t('nav.users') }] : []), + // Admin only: User management and Site Settings + ...(isAdmin ? [ + { path: '/users', icon: 'bi-people', label: t('nav.users') }, + { path: '/site-settings', icon: 'bi-globe', label: t('nav.siteSettings', 'Sitio Web') }, + ] : []), ] }, ]; diff --git a/frontend/src/components/Toast.jsx b/frontend/src/components/Toast.jsx index bc565e2..854c37f 100644 --- a/frontend/src/components/Toast.jsx +++ b/frontend/src/components/Toast.jsx @@ -15,7 +15,9 @@ export const ToastProvider = ({ children }) => { const addToast = useCallback((message, type = 'info', duration = 5000) => { const id = Date.now(); - const toast = { id, message, type, duration }; + // Map 'error' to 'danger' for backward compatibility + const mappedType = type === 'error' ? 'danger' : type; + const toast = { id, message, type: mappedType, duration }; setToasts((prev) => [...prev, toast]); @@ -48,8 +50,11 @@ export const ToastProvider = ({ children }) => { return addToast(message, 'info', duration); }, [addToast]); + // Alias for backward compatibility + const showToast = addToast; + return ( - + {children} diff --git a/frontend/src/pages/FinancialHealth.jsx b/frontend/src/pages/FinancialHealth.jsx index c41e321..c8f5022 100644 --- a/frontend/src/pages/FinancialHealth.jsx +++ b/frontend/src/pages/FinancialHealth.jsx @@ -228,6 +228,26 @@ const FinancialHealth = () => { }], } : null; + // Data sufficiency status + const dataStatus = data.data_status; + const showDataWarning = dataStatus && !dataStatus.has_sufficient_data; + + // Helper para obtener ícono y cor do alerta baseado no nível + const getDataWarningStyle = (level) => { + switch (level) { + case 'no_data': + return { icon: 'bi-database-x', color: 'danger', bg: 'rgba(239, 68, 68, 0.1)' }; + case 'insufficient': + return { icon: 'bi-exclamation-triangle', color: 'warning', bg: 'rgba(245, 158, 11, 0.1)' }; + case 'limited': + return { icon: 'bi-info-circle', color: 'info', bg: 'rgba(59, 130, 246, 0.1)' }; + case 'outdated': + return { icon: 'bi-clock-history', color: 'secondary', bg: 'rgba(100, 116, 139, 0.1)' }; + default: + return { icon: 'bi-info-circle', color: 'info', bg: 'rgba(59, 130, 246, 0.1)' }; + } + }; + return (
{/* Header */} @@ -250,6 +270,46 @@ const FinancialHealth = () => {
+ {/* Data Sufficiency Warning */} + {showDataWarning && ( +
+ +
+
+ {dataStatus.level === 'no_data' && t('financialHealth.dataWarning.noData', 'Datos Insuficientes')} + {dataStatus.level === 'insufficient' && t('financialHealth.dataWarning.insufficient', 'Análisis Limitado')} + {dataStatus.level === 'limited' && t('financialHealth.dataWarning.limited', 'Pocos Datos')} + {dataStatus.level === 'outdated' && t('financialHealth.dataWarning.outdated', 'Datos Desactualizados')} +
+

{dataStatus.message}

+
+ + + {dataStatus.counts?.accounts || 0} {t('financialHealth.dataWarning.accounts', 'cuentas')} + + + + {dataStatus.counts?.transactions || 0} {t('financialHealth.dataWarning.transactions', 'transacciones')} + + + + {dataStatus.counts?.recent_transactions || 0} {t('financialHealth.dataWarning.recentTransactions', 'últimos 30 días')} + +
+
+ + + {t('financialHealth.dataWarning.addData', 'Añadir Datos')} + +
+ )} + {/* Tabs */}
    {tabs.map(tab => ( diff --git a/frontend/src/pages/SiteSettings.jsx b/frontend/src/pages/SiteSettings.jsx new file mode 100644 index 0000000..05c0227 --- /dev/null +++ b/frontend/src/pages/SiteSettings.jsx @@ -0,0 +1,310 @@ +import { useState, useEffect } from 'react'; +import api from '../services/api'; +import { useToast } from '../components/Toast'; + +export default function SiteSettings() { + const toast = useToast(); + const [loading, setLoading] = useState(true); + const [deploying, setDeploying] = useState(false); + const [status, setStatus] = useState({ + mode: 'live', + maintenance_mode: false, + modes_available: { + live: 'Página institucional completa', + maintenance: 'Página de mantenimiento', + } + }); + + useEffect(() => { + fetchStatus(); + }, []); + + const fetchStatus = async () => { + try { + const response = await api.get('/admin/site-settings/cnxifly/status'); + setStatus(response.data); + } catch (error) { + console.error('Error fetching status:', error); + toast.error('Error al obtener el estado del sitio'); + } finally { + setLoading(false); + } + }; + + const handleToggleMode = async (newMode) => { + try { + setDeploying(true); + const response = await api.post('/admin/site-settings/cnxifly/toggle', { mode: newMode }); + + if (response.data.success) { + toast.success(response.data.message); + setStatus(prev => ({ + ...prev, + mode: newMode, + maintenance_mode: newMode !== 'live' + })); + } + } catch (error) { + console.error('Error toggling mode:', error); + toast.error('Error al cambiar el modo'); + } finally { + setDeploying(false); + } + }; + + const handleDeploy = async (mode) => { + if (!confirm(`¿Está seguro de desplegar la página en modo "${mode}"? Esto actualizará cnxifly.com`)) { + return; + } + + try { + setDeploying(true); + const response = await api.post('/admin/site-settings/cnxifly/deploy', { mode }); + + if (response.data.success) { + toast.success(`Configuración actualizada. Use el comando de deploy para aplicar los cambios.`); + + // Log deploy instructions to console for copying + console.log('Deploy command:', response.data.deploy_instructions?.command); + + setStatus(prev => ({ + ...prev, + mode: mode, + maintenance_mode: mode === 'maintenance' + })); + } + } catch (error) { + console.error('Error deploying:', error); + toast.error('Error al desplegar la página'); + } finally { + setDeploying(false); + } + }; + + const getModeColor = (mode, isActive) => { + if (!isActive) return 'bg-gray-700 border-gray-600 hover:border-gray-500'; + + switch (mode) { + case 'live': + return 'bg-green-900/30 border-green-500 ring-2 ring-green-500/30'; + case 'maintenance': + return 'bg-yellow-900/30 border-yellow-500 ring-2 ring-yellow-500/30'; + case 'construction': + return 'bg-orange-900/30 border-orange-500 ring-2 ring-orange-500/30'; + default: + return 'bg-gray-700 border-gray-600'; + } + }; + + const getModeTextColor = (mode) => { + switch (mode) { + case 'live': + return 'text-green-400'; + case 'maintenance': + return 'text-yellow-400'; + case 'construction': + return 'text-orange-400'; + default: + return 'text-gray-400'; + } + }; + + if (loading) { + return ( +
    + +
    + ); + } + + return ( +
    + {/* Header */} +
    +
    +

    + + Configuración del Sitio cnxifly.com +

    +

    + Controle el estado de la página institucional de ConneXiFly +

    +
    + +
    + + {/* Current Status */} +
    +
    +
    +

    Estado Actual

    +

    El modo seleccionado determina qué página se muestra

    +
    +
    + + + {status.mode} + +
    +
    + + {/* Mode Cards */} +
    + {/* Live Mode */} +
    !deploying && handleToggleMode('live')} + > +
    +
    +
    + +
    +
    +

    Página en Vivo

    +

    + Página institucional completa con todos los productos +

    +
    +
    + {status.mode === 'live' && ( + + )} +
    +
    + Muestra: WebMoney, EZPool, precios, registro y contacto +
    +
    + + {/* Maintenance Mode */} +
    !deploying && handleToggleMode('maintenance')} + > +
    +
    +
    + +
    +
    +

    En Mantenimiento

    +

    + Página simple informando mantenimiento +

    +
    +
    + {status.mode === 'maintenance' && ( + + )} +
    +
    + Muestra: Mensaje de mantenimiento con links a los productos +
    +
    +
    +
    + + {/* Deploy Section */} +
    +
    + +
    +

    Despliegue

    +

    Aplique los cambios al servidor de producción

    +
    +
    + +
    +
    + +
    +

    Importante

    +

    + Los cambios se guardan en la configuración, pero para aplicarlos al servidor debe ejecutar + el comando de deploy. La página actual en cnxifly.com no cambiará hasta que se despliegue. +

    +
    +
    +
    + +
    + + + +
    +
    + + {/* Preview Links */} + +
    + ); +} diff --git a/frontend/src/pages/Users.jsx b/frontend/src/pages/Users.jsx index fb0b99f..b7f7d4f 100644 --- a/frontend/src/pages/Users.jsx +++ b/frontend/src/pages/Users.jsx @@ -25,9 +25,24 @@ function Users() { // Create user modal states const [showCreateModal, setShowCreateModal] = useState(false); - const [createForm, setCreateForm] = useState({ name: '', email: '', password: '', user_type: 'free' }); + const [createForm, setCreateForm] = useState({ + name: '', + email: '', + password: '', + user_type: 'free', + language: 'es', + currency: 'EUR', + send_welcome_email: true + }); const [createdUser, setCreatedUser] = useState(null); const [creating, setCreating] = useState(false); + + // Edit user modal states + const [showEditModal, setShowEditModal] = useState(false); + const [editForm, setEditForm] = useState({ name: '', email: '', language: 'es', currency: 'EUR', is_admin: false }); + const [editing, setEditing] = useState(false); + const [editingUser, setEditingUser] = useState(null); + const [changingPlan, setChangingPlan] = useState(false); useEffect(() => { const handleResize = () => setIsMobile(window.innerWidth < 768); @@ -159,10 +174,97 @@ function Users() { const resetCreateModal = () => { setShowCreateModal(false); - setCreateForm({ name: '', email: '', password: '', user_type: 'free' }); + setCreateForm({ + name: '', + email: '', + password: '', + user_type: 'free', + language: 'es', + currency: 'EUR', + send_welcome_email: true + }); setCreatedUser(null); }; + const openEditModal = (user) => { + setEditingUser(user); + setEditForm({ + name: user.name || '', + email: user.email || '', + language: user.language || 'es', + currency: user.currency || 'EUR', + is_admin: user.is_admin || false, + }); + setShowEditModal(true); + }; + + const resetEditModal = () => { + setShowEditModal(false); + setEditForm({ name: '', email: '', language: 'es', currency: 'EUR', is_admin: false }); + setEditingUser(null); + }; + + const handleEditUser = async (e) => { + e.preventDefault(); + if (!editingUser) return; + + setEditing(true); + try { + const response = await api.put(`/admin/users/${editingUser.id}`, editForm); + if (response.data.success) { + showToast('Usuario actualizado correctamente', 'success'); + resetEditModal(); + fetchUsers(); + } + } catch (error) { + const message = error.response?.data?.message || 'Error al actualizar usuario'; + showToast(message, 'error'); + } finally { + setEditing(false); + } + }; + + const handleChangePlan = async (newPlan) => { + if (!editingUser) return; + + setChangingPlan(true); + try { + const response = await api.post(`/admin/users/${editingUser.id}/change-plan`, { plan: newPlan }); + if (response.data.success) { + showToast(response.data.message, 'success'); + // Update local state to reflect the change + setEditingUser(prev => ({ + ...prev, + subscription: response.data.data.subscription ? { + ...response.data.data.subscription, + plan_name: 'Pro Annual' + } : null + })); + fetchUsers(); + fetchSummary(); + } + } catch (error) { + const message = error.response?.data?.message || 'Error al cambiar plan'; + showToast(message, 'error'); + } finally { + setChangingPlan(false); + } + }; + + const handleToggleAdmin = async (user) => { + try { + const response = await api.put(`/admin/users/${user.id}`, { + is_admin: !user.is_admin, + }); + if (response.data.success) { + showToast(user.is_admin ? 'Permisos de admin removidos' : 'Permisos de admin concedidos', 'success'); + fetchUsers(); + } + } catch (error) { + showToast('Error al cambiar permisos', 'error'); + } + }; + const getUserTypeLabel = (type) => { const labels = { free: 'Free', @@ -390,6 +492,13 @@ function Users() { {formatDate(user.created_at)} + + + +
    +
    +
    +
    + + setEditForm(prev => ({ ...prev, name: e.target.value }))} + required + placeholder="Nombre completo" + /> +
    + +
    + + setEditForm(prev => ({ ...prev, email: e.target.value }))} + required + placeholder="email@ejemplo.com" + /> +
    +
    + +
    +
    + + +
    + +
    + + +
    +
    + + {/* User Info Card */} +
    +
    +
    +
    +
    + + Información +
    +

    + ID: + {editingUser.id} +

    +

    + Registrado: + {formatDate(editingUser.created_at)} +

    +

    + Plan actual: + {getStatusBadge(editingUser.subscription)} +

    +
    +
    +
    + + Uso +
    +

    + + {editingUser.usage?.accounts || 0} cuentas +

    +

    + + {editingUser.usage?.categories || 0} categorías +

    +

    + + {editingUser.usage?.transactions || 0} transacciones +

    +
    +
    +
    +
    + + {/* Change Plan Section */} +
    +
    +
    + + Cambiar Plan de Suscripción +
    +
    + + +
    +
    + {editingUser.subscription && editingUser.subscription.status === 'active' ? ( + <>Plan Pro activo hasta: {formatDate(editingUser.subscription.current_period_end)} + ) : ( + <>El usuario está en plan Free. Clic en "Pro" para activar suscripción Pro. + )} +
    +
    +
    + + {/* Admin Toggle - Only show if not the main admin */} + {editingUser.email !== 'marco@cnxifly.com' && ( +
    + setEditForm(prev => ({ ...prev, is_admin: e.target.checked }))} + /> + +
    + Los administradores tienen acceso completo al sistema y gestión de usuarios. +
    +
    + )} +
    + +
    + + +
    +
    + + + + )} ); } diff --git a/landing/index.html b/landing/index.html new file mode 100644 index 0000000..d7c780e --- /dev/null +++ b/landing/index.html @@ -0,0 +1,1027 @@ + + + + + + ConneXiFly - Soluciones Digitales para tu Negocio + + + + + + + + +
    + + Sitio en mantenimiento. Volveremos pronto. +
    + + + + + +
    +
    +
    +
    +

    + Soluciones Digitales para impulsar tu negocio +

    +

    + Desarrollamos plataformas digitales innovadoras para simplificar la gestión de tu negocio. Finanzas, piscinas, turismo y más. +

    + +
    +
    +
    +
    +
    +
    +
    + +
    WebMoney
    + Finanzas +
    +
    +
    +
    + +
    EZPool
    + Piscinas +
    +
    +
    +
    +
    + Usuarios activos + +24% +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +

    Nuestros Productos

    +

    Soluciones web diseñadas para cada sector

    +
    + +
    + +
    +
    +
    + +
    + + Disponible + +

    WebMoney

    +

    + Gestión financiera inteligente para personas y pequeños negocios. Controla tus cuentas, transacciones, presupuestos y metas financieras en un solo lugar. +

    +
      +
    • Múltiples cuentas y monedas
    • +
    • Categorización automática
    • +
    • Presupuestos y metas
    • +
    • Análisis de salud financiera
    • +
    • Informes detallados
    • +
    +
    + €49/año +
    +

    Plan Free disponible con funciones limitadas

    + + Comenzar Gratis + +
    +
    + + +
    +
    +
    + +
    + + Próximamente + +

    EZPool

    +

    + Software de mantenimiento de piscinas para expertos modernos. Todo lo que necesitas para administrar tu negocio de servicios de piscinas en una sola aplicación. +

    +
      +
    • Gestión de clientes y piscinas
    • +
    • Agenda de servicios
    • +
    • Control de productos químicos
    • +
    • Rutas optimizadas
    • +
    • Facturación integrada
    • +
    +
    + €79/año +
    +

    Precio de lanzamiento - 30% de descuento

    + +
    +
    +
    +
    +
    + + +
    +
    +
    +

    ¿Por qué ConneXiFly?

    +

    Características que nos diferencian

    +
    + +
    +
    +
    +
    + +
    +

    Seguridad Avanzada

    +

    Tus datos están protegidos con encriptación de nivel bancario y cumplimiento GDPR.

    +
    +
    +
    +
    +
    + +
    +

    Multiplataforma

    +

    Accede desde cualquier dispositivo: web, móvil o tablet, con sincronización en tiempo real.

    +
    +
    +
    +
    +
    + +
    +

    Soporte Premium

    +

    Equipo de soporte dedicado disponible para ayudarte en cada paso del camino.

    +
    +
    +
    +
    +
    + +
    +

    Rendimiento Óptimo

    +

    Aplicaciones rápidas y eficientes, optimizadas para la mejor experiencia de usuario.

    +
    +
    +
    +
    +
    + +
    +

    Análisis Inteligente

    +

    Insights y reportes automáticos para tomar mejores decisiones de negocio.

    +
    +
    +
    +
    +
    + +
    +

    Actualizaciones Continuas

    +

    Nuevas funcionalidades y mejoras constantemente, sin costo adicional.

    +
    +
    +
    +
    +
    + + +
    +
    +

    ¿Listo para empezar?

    +

    Únete a miles de usuarios que ya confían en ConneXiFly

    + +
    +
    + + + + + + + + + + + diff --git a/landing/maintenance.html b/landing/maintenance.html new file mode 100644 index 0000000..c606b46 --- /dev/null +++ b/landing/maintenance.html @@ -0,0 +1,190 @@ + + + + + + ConneXiFly - En Mantenimiento + + + + + +
    +
    + +
    + +
    + +
    + +

    Estamos en Mantenimiento

    +

    + Estamos trabajando para traerte una mejor experiencia. + Volveremos muy pronto con novedades increíbles. +

    + + + +
    +

    ¿Necesitas ayuda? admin@cnxifly.com

    +
    +
    + +