diff --git a/.env.example b/.env.example index 1c7216d..79ed088 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -APP_NAME=Ominiboard +APP_NAME=Omniboard APP_ENV=local APP_KEY= APP_DEBUG=true @@ -20,10 +20,10 @@ LOG_STACK=single LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug -DB_CONNECTION=mysqli +DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 -DB_DATABASE=laravel +DB_DATABASE=omniboard DB_USERNAME=root DB_PASSWORD= diff --git a/app/Http/Controllers/Api/AmiEventController.php b/app/Http/Controllers/Api/AmiEventController.php new file mode 100644 index 0000000..6bbba44 --- /dev/null +++ b/app/Http/Controllers/Api/AmiEventController.php @@ -0,0 +1,148 @@ +header('X-Tenant-Token'); + $tenant = Tenant::where('api_key', $token)->first(); + + if (!$tenant) { + return response()->json(['error' => 'Unauthorized'], 401); + } + + // 2. Dados do Evento + $data = $request->all(); + $eventName = $data['Event'] ?? null; + $queueName = $data['Queue'] ?? null; + + if (!$eventName || !$queueName) { + return response()->json(['status' => 'ignored']); + } + + // DEPOIS (Correto) + $queue = Queue::where('tenant_id', $tenant->id) + ->where('source_id', $queueName) // Procura "08000" na coluna source_id + ->first();; + + // Se a fila não existe no banco, ignoramos ou criamos automaticamente (opcional) + if (!$queue) { + return response()->json(['error' => 'Fila não encontrada'], 404); + } + + // 4. Processar Lógica de Negócio + switch ($eventName) { + case 'QueueCallerJoin': + $this->handleJoin($queue, $data); + break; + case 'AgentConnect': + $this->handleConnect($queue, $data); + break; + case 'AgentComplete': // Ou QueueCallerLeave com TalkTime + $this->handleComplete($queue, $data); + break; + case 'QueueCallerAbandon': + $this->handleAbandon($queue, $data); + break; + } + + return response()->json(['status' => 'success']); + } + + // LÓGICA DE NEGÓCIO (A mesma do script anterior, mas agora com Eloquent) + + private function handleJoin($queue, $data) + { + // Adiciona na Lista de Espera COM TENANT_ID + WaitingList::create([ + 'tenant_id' => $queue->tenant_id, // <--- ADICIONE ESTA LINHA + 'queue_id' => $queue->id, + 'caller_number' => $data['CallerIDNum'], + 'caller_name' => $data['CallerIDName'] ?? 'Desconhecido', + 'entered_at' => now(), + ]); + + // Incrementa Total do Dia + $this->updateMetric($queue, 'received_count', 1); + } + + private function handleConnect($queue, $data) + { + // Remove da Lista de Espera + WaitingList::where('queue_id', $queue->id) + ->where('caller_number', $data['CallerIDNum']) + ->delete(); + + // Atualiza TME (Média Ponderada) + // Nota: Isso é simplificado. Em produção real, calcular média ponderada em SQL é melhor. + $metric = $this->getTodayMetric($queue); + $holdTime = intval($data['HoldTime'] ?? 0); + + // Novo TME = ((Atual * Qtd) + Novo) / (Qtd + 1) + $newAvg = (($metric->avg_wait_time * $metric->answered_count) + $holdTime) / ($metric->answered_count + 1); + + $metric->avg_wait_time = $newAvg; + $metric->answered_count += 1; + $metric->save(); + } + + private function handleComplete($queue, $data) + { + $talkTime = intval($data['TalkTime'] ?? 0); + $metric = $this->getTodayMetric($queue); + + // Evita divisão por zero se o connect falhou + if ($metric->answered_count > 0) { + // Novo TMA = ((Atual * (Qtd-1)) + Novo) / Qtd + // Nota: Usamos Qtd (answered_count) como divisor pois a chamada já foi contada no Connect + $newAvg = (($metric->avg_talk_time * ($metric->answered_count - 1)) + $talkTime) / $metric->answered_count; + $metric->avg_talk_time = $newAvg; + $metric->save(); + } + } + + private function handleAbandon($queue, $data) + { + // Remove da Espera + WaitingList::where('queue_id', $queue->id) + ->where('caller_number', $data['CallerIDNum']) + ->delete(); + + // Incrementa Abandonos + $this->updateMetric($queue, 'abandoned_count', 1); + } + + // Helpers + private function getTodayMetric($queue) + { + return DailyMetric::firstOrCreate( + [ + 'queue_id' => $queue->id, + 'date' => Carbon::today() + ], + [ + 'tenant_id' => $queue->tenant_id, // <--- O PULO DO GATO: Herda o ID da Fila + 'received_count' => 0, + 'answered_count' => 0, + 'abandoned_count' => 0 + ] + ); + } + + private function updateMetric($queue, $field, $value) + { + $metric = $this->getTodayMetric($queue); + $metric->increment($field, $value); + } +} diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php new file mode 100644 index 0000000..557e174 --- /dev/null +++ b/app/Http/Controllers/DashboardController.php @@ -0,0 +1,39 @@ + function ($query) { + // Filtra para pegar apenas os dados de HOJE + $query->whereDate('date', Carbon::today()); + }, + + // 2. Relacionamento com Lista de Espera (Ao Vivo) + 'waitingList' => function ($query) { + // Ordena: Quem chegou primeiro aparece no topo da lista interna (se formos exibir detalhes) + $query->orderBy('entered_at', 'asc'); + } + ]) + // Ordena as filas por nome para ficarem sempre na mesma posição no Dashboard + ->orderBy('name', 'asc') + ->get(); + + return Inertia::render('Dashboard', [ + 'queues' => $queues, + ]); + } +} diff --git a/app/Models/Agent.php b/app/Models/Agent.php new file mode 100644 index 0000000..bf4b90b --- /dev/null +++ b/app/Models/Agent.php @@ -0,0 +1,29 @@ + 'datetime', + ]; + + public function tenant() + { + return $this->belongsTo(Tenant::class); + } + + public function calls() + { + return $this->hasMany(Call::class); + } +} \ No newline at end of file diff --git a/app/Models/Call.php b/app/Models/Call.php new file mode 100644 index 0000000..5ef51af --- /dev/null +++ b/app/Models/Call.php @@ -0,0 +1,37 @@ + 'datetime', + 'finished_at' => 'datetime', + ]; + + public function queue() + { + return $this->belongsTo(Queue::class); + } + + public function agent() + { + return $this->belongsTo(Agent::class); + } +} \ No newline at end of file diff --git a/app/Models/DailyMetric.php b/app/Models/DailyMetric.php new file mode 100644 index 0000000..d18178a --- /dev/null +++ b/app/Models/DailyMetric.php @@ -0,0 +1,30 @@ + 'date', + 'sla_percentage' => 'float', + ]; + + public function queue() + { + return $this->belongsTo(Queue::class); + } +} \ No newline at end of file diff --git a/app/Models/Queue.php b/app/Models/Queue.php new file mode 100644 index 0000000..6413546 --- /dev/null +++ b/app/Models/Queue.php @@ -0,0 +1,37 @@ +belongsTo(Tenant::class); + } + + // Uma fila tem muitas chamadas (histórico) + public function calls() + { + return $this->hasMany(Call::class); + } + + // Uma fila tem muitas métricas diárias + public function dailyMetrics() + { + return $this->hasMany(DailyMetric::class); + } + + // Uma fila tem itens na lista de espera (ao vivo) + public function waitingList() + { + return $this->hasMany(WaitingList::class); + } +} \ No newline at end of file diff --git a/app/Models/Tenant.php b/app/Models/Tenant.php new file mode 100644 index 0000000..74d76d6 --- /dev/null +++ b/app/Models/Tenant.php @@ -0,0 +1,31 @@ +hasMany(Queue::class); + } + + // Um Cliente tem muitos Agentes + public function agents() + { + return $this->hasMany(Agent::class); + } + + public function users() + { + return $this->hasMany(User::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 749c7b7..fd52678 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -21,6 +21,7 @@ class User extends Authenticatable 'name', 'email', 'password', + 'tenant_id', ]; /** @@ -45,4 +46,9 @@ protected function casts(): array 'password' => 'hashed', ]; } + + public function tenant() + { + return $this->belongsTo(Tenant::class); + } } diff --git a/app/Models/WaitingList.php b/app/Models/WaitingList.php new file mode 100644 index 0000000..bba9c64 --- /dev/null +++ b/app/Models/WaitingList.php @@ -0,0 +1,33 @@ + 'datetime', + ]; + + public function queue() + { + return $this->belongsTo(Queue::class); + } +} diff --git a/app/Traits/BelongsToTenant.php b/app/Traits/BelongsToTenant.php new file mode 100644 index 0000000..d537390 --- /dev/null +++ b/app/Traits/BelongsToTenant.php @@ -0,0 +1,33 @@ +tenant_id) { + + // Adiciona um filtro GLOBAL em todas as consultas + static::addGlobalScope('tenant', function (Builder $builder) { + $builder->where('tenant_id', Auth::user()->tenant_id); + }); + + // Preenche automaticamente o tenant_id ao CRIAR registros + static::creating(function ($model) { + $model->tenant_id = Auth::user()->tenant_id; + }); + } + } + + public function tenant() + { + return $this->belongsTo(Tenant::class); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 5c02a59..0e21cf2 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -7,6 +7,7 @@ return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) diff --git a/config/sanctum.php b/config/sanctum.php new file mode 100644 index 0000000..44527d6 --- /dev/null +++ b/config/sanctum.php @@ -0,0 +1,84 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + Sanctum::currentApplicationUrlWithPort(), + // Sanctum::currentRequestHost(), + ))), + + /* + |-------------------------------------------------------------------------- + | Sanctum Guards + |-------------------------------------------------------------------------- + | + | This array contains the authentication guards that will be checked when + | Sanctum is trying to authenticate a request. If none of these guards + | are able to authenticate the request, Sanctum will use the bearer + | token that's present on an incoming request for authentication. + | + */ + + 'guard' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. This will override any values set in the token's + | "expires_at" attribute, but first-party sessions are not affected. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Token Prefix + |-------------------------------------------------------------------------- + | + | Sanctum can prefix new tokens in order to take advantage of numerous + | security scanning initiatives maintained by open source platforms + | that notify developers if they commit tokens into repositories. + | + | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning + | + */ + + 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, + 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, + 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + ], + +]; diff --git a/database/migrations/2025_12_15_134836_create_omniboard_schema.php b/database/migrations/2025_12_15_134836_create_omniboard_schema.php new file mode 100644 index 0000000..cbcfee4 --- /dev/null +++ b/database/migrations/2025_12_15_134836_create_omniboard_schema.php @@ -0,0 +1,100 @@ +id(); + $table->string('name'); + $table->string('api_key')->unique(); // Token para o Agente Local + $table->timestamps(); + }); + + // 2. Queues (Filas de Voz ou Chat) + Schema::create('queues', function (Blueprint $table) { + $table->id(); + $table->foreignId('tenant_id')->constrained()->cascadeOnDelete(); + $table->string('name'); + $table->string('type')->default('voice'); // voice, whatsapp + $table->string('source_id')->nullable(); // ID no Asterisk/Helena + $table->integer('sla_threshold')->default(20); // Meta de SLA em segundos + $table->timestamps(); + }); + + // 3. Agents (Atendentes) + Schema::create('agents', function (Blueprint $table) { + $table->id(); + $table->foreignId('tenant_id')->constrained()->cascadeOnDelete(); + $table->string('name'); + $table->string('extension')->nullable(); + $table->enum('status', ['available', 'paused', 'talking', 'offline'])->default('offline'); + $table->timestamp('last_status_change')->useCurrent(); + $table->timestamps(); + }); + + // 4. Calls (Histórico detalhado) + Schema::create('calls', function (Blueprint $table) { + $table->uuid('id')->primary(); + $table->foreignId('tenant_id')->constrained()->cascadeOnDelete(); + $table->foreignId('queue_id')->constrained()->cascadeOnDelete(); + $table->foreignId('agent_id')->nullable()->constrained('agents'); + $table->string('caller_id'); // Telefone ou Nome + $table->enum('status', ['waiting', 'answered', 'abandoned']); + $table->integer('wait_time')->default(0); // Segundos na fila + $table->integer('talk_time')->default(0); // Segundos falando + $table->timestamp('entered_at'); + $table->timestamp('finished_at')->nullable(); + $table->timestamps(); + }); + + // 5. Daily Metrics (Snapshot consolidado para o Dashboard carregar rápido) + Schema::create('daily_metrics', function (Blueprint $table) { + $table->id(); + $table->foreignId('queue_id')->constrained()->cascadeOnDelete(); + $table->date('date'); + + // Métricas visíveis na imagem + $table->integer('received_count')->default(0); + $table->integer('answered_count')->default(0); + $table->integer('abandoned_count')->default(0); + $table->decimal('sla_percentage', 5, 2)->default(0); // Ex: 96.50 + $table->integer('avg_wait_time')->default(0); // Em segundos + $table->integer('avg_talk_time')->default(0); // Em segundos + $table->integer('max_wait_time')->default(0); // Em segundos + $table->integer('transferred_count')->default(0); + + $table->timestamps(); + + // Garante uma métrica por fila por dia + $table->unique(['queue_id', 'date']); + }); + + // 6. Waiting List (Barra lateral "Próximos Atendimentos") + Schema::create('waiting_list', function (Blueprint $table) { + $table->id(); + $table->foreignId('queue_id')->constrained()->cascadeOnDelete(); + $table->string('caller_name')->nullable(); + $table->string('caller_number'); + $table->timestamp('entered_at'); + $table->integer('attempt_count')->default(1); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('waiting_list'); + Schema::dropIfExists('daily_metrics'); + Schema::dropIfExists('calls'); + Schema::dropIfExists('agents'); + Schema::dropIfExists('queues'); + Schema::dropIfExists('tenants'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_12_15_160047_create_personal_access_tokens_table.php b/database/migrations/2025_12_15_160047_create_personal_access_tokens_table.php new file mode 100644 index 0000000..40ff706 --- /dev/null +++ b/database/migrations/2025_12_15_160047_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->morphs('tokenable'); + $table->text('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable()->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/database/migrations/2025_12_15_180352_add_tenant_id_to_users_table.php b/database/migrations/2025_12_15_180352_add_tenant_id_to_users_table.php new file mode 100644 index 0000000..88fc1fc --- /dev/null +++ b/database/migrations/2025_12_15_180352_add_tenant_id_to_users_table.php @@ -0,0 +1,28 @@ +foreignId('tenant_id')->nullable()->after('id')->constrained('tenants')->nullOnDelete(); + }); + } + + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropForeign(['tenant_id']); + $table->dropColumn('tenant_id'); + }); + } +}; diff --git a/database/migrations/2025_12_15_181659_add_tenant_id_to_daily_metrics.php b/database/migrations/2025_12_15_181659_add_tenant_id_to_daily_metrics.php new file mode 100644 index 0000000..39a7faf --- /dev/null +++ b/database/migrations/2025_12_15_181659_add_tenant_id_to_daily_metrics.php @@ -0,0 +1,38 @@ +foreignId('tenant_id')->nullable()->after('id')->constrained()->cascadeOnDelete(); + }); + + // SCRIPT DE CORREÇÃO: Preenche o tenant_id baseado na fila a que a métrica pertence + // Isso evita que dados antigos fiquem "órfãos" + DB::statement(" + UPDATE daily_metrics dm + JOIN queues q ON dm.queue_id = q.id + SET dm.tenant_id = q.tenant_id + "); + + // Depois de preencher, podemos (opcionalmente) tornar obrigatório, + // mas vamos deixar nullable por segurança agora. + } + + public function down() + { + Schema::table('daily_metrics', function (Blueprint $table) { + $table->dropForeign(['tenant_id']); + $table->dropColumn('tenant_id'); + }); + } +}; diff --git a/database/migrations/2025_12_15_182047_add_tenant_id_to_waiting_list.php b/database/migrations/2025_12_15_182047_add_tenant_id_to_waiting_list.php new file mode 100644 index 0000000..c8fe369 --- /dev/null +++ b/database/migrations/2025_12_15_182047_add_tenant_id_to_waiting_list.php @@ -0,0 +1,35 @@ +foreignId('tenant_id')->nullable()->after('id')->constrained()->cascadeOnDelete(); + }); + + // Popula dados existentes baseado na fila + DB::statement(" + UPDATE waiting_list wl + JOIN queues q ON wl.queue_id = q.id + SET wl.tenant_id = q.tenant_id + "); + } + + public function down() + { + Schema::table('waiting_list', function (Blueprint $table) { + $table->dropForeign(['tenant_id']); + $table->dropColumn('tenant_id'); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 6b901f8..8de12ae 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -17,9 +17,17 @@ public function run(): void { // User::factory(10)->create(); + // Cria seu usuário de acesso (Admin) + User::factory()->create([ + 'name' => 'Admin OmniBoard', + 'email' => 'admin@omniboard.com', + 'password' => bcrypt('password'), // A senha será 'password' + ]); + User::factory()->create([ 'name' => 'Test User', 'email' => 'test@example.com', ]); + $this->call([OmniBoardSeeder::class]); } } diff --git a/database/seeders/OmniBoardSeeder.php b/database/seeders/OmniBoardSeeder.php new file mode 100644 index 0000000..90c1d62 --- /dev/null +++ b/database/seeders/OmniBoardSeeder.php @@ -0,0 +1,91 @@ +insertGetId([ + 'name' => 'LinePBX Demo', + 'api_key' => Str::random(32), + 'created_at' => now(), + 'updated_at' => now(), + ]); + + // 2. Criar as Filas (Baseado na sua imagem) + $queues = [ + ['name' => 'Central de Atendimento Voz New', 'type' => 'voice'], + ['name' => 'Unific Central de Atendimento WEB', 'type' => 'whatsapp'], + ['name' => 'Unific Coleta Domiciliar WEB', 'type' => 'whatsapp'], + ['name' => 'Unific Autorizacao WEB', 'type' => 'whatsapp'], + ['name' => 'Imagem', 'type' => 'voice'], + ]; + + foreach ($queues as $q) { + $queueId = DB::table('queues')->insertGetId([ + 'tenant_id' => $tenantId, + 'name' => $q['name'], + 'type' => $q['type'], + 'created_at' => now(), + 'updated_at' => now(), + ]); + + // 3. Simular Métricas do Dia (Para preencher os cards) + // Vou gerar números aleatórios mas realistas para cada fila + $received = rand(50, 600); + $answered = intval($received * rand(60, 95) / 100); + $abandoned = $received - $answered; + $sla = rand(20, 98); // SLA variado para testar cores (verde/vermelho) + + DB::table('daily_metrics')->insert([ + 'queue_id' => $queueId, + 'date' => Carbon::today(), + 'received_count' => $received, + 'answered_count' => $answered, + 'abandoned_count' => $abandoned, + 'sla_percentage' => $sla, + 'avg_wait_time' => rand(60, 300), // 1 a 5 min + 'avg_talk_time' => rand(180, 600), // 3 a 10 min + 'max_wait_time' => rand(600, 3000), // Picos altos + 'transferred_count' => rand(0, 10), + 'created_at' => now(), + 'updated_at' => now(), + ]); + + // 4. Povoar a Lista de Espera (Sidebar "Próximos Atendimentos") + // Adiciona 3 a 5 pessoas esperando em cada fila + for ($i = 0; $i < rand(3, 5); $i++) { + DB::table('waiting_list')->insert([ + 'queue_id' => $queueId, + 'caller_name' => fake()->firstName() . ' ' . fake()->lastName(), + 'caller_number' => fake()->phoneNumber(), + 'entered_at' => Carbon::now()->subMinutes(rand(1, 45)), // Entrou entre 1 e 45 min atrás + 'attempt_count' => rand(1, 3), + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + } + + // 5. Criar Agentes + $statuses = ['available', 'talking', 'paused', 'offline']; + for ($i=0; $i < 10; $i++) { + DB::table('agents')->insert([ + 'tenant_id' => $tenantId, + 'name' => fake()->name(), + 'extension' => rand(1000, 9999), + 'status' => $statuses[array_rand($statuses)], + 'last_status_change' => now()->subMinutes(rand(5, 120)), + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + } +} \ No newline at end of file diff --git a/resources/js/Layouts/AuthenticatedLayout.vue b/resources/js/Layouts/AuthenticatedLayout.vue index 154305f..48ad96d 100644 --- a/resources/js/Layouts/AuthenticatedLayout.vue +++ b/resources/js/Layouts/AuthenticatedLayout.vue @@ -3,196 +3,133 @@ import { ref } from 'vue'; import ApplicationLogo from '@/Components/ApplicationLogo.vue'; import Dropdown from '@/Components/Dropdown.vue'; import DropdownLink from '@/Components/DropdownLink.vue'; -import NavLink from '@/Components/NavLink.vue'; -import ResponsiveNavLink from '@/Components/ResponsiveNavLink.vue'; import { Link } from '@inertiajs/vue3'; -const showingNavigationDropdown = ref(false); +const showingSidebar = ref(false); + +const menuItems = [ + { name: 'Dashboard', route: 'dashboard', icon: 'M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z' }, + { name: 'Monitoramento', route: '#', icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z' }, + { name: 'Relatórios', route: '#', icon: 'M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z' }, + { name: 'Filas', route: '#', icon: 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10' }, + { name: 'Agentes', route: '#', icon: 'M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z' }, + { name: 'Configurações', route: '#', icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z' }, +]; + \ No newline at end of file diff --git a/resources/js/Layouts/GuestLayout.vue b/resources/js/Layouts/GuestLayout.vue index 1b209cb..460d9d3 100644 --- a/resources/js/Layouts/GuestLayout.vue +++ b/resources/js/Layouts/GuestLayout.vue @@ -1,22 +1,25 @@ - - \ No newline at end of file diff --git a/resources/js/Pages/Auth/ForgotPassword.vue b/resources/js/Pages/Auth/ForgotPassword.vue index fe5a196..5d3e937 100644 --- a/resources/js/Pages/Auth/ForgotPassword.vue +++ b/resources/js/Pages/Auth/ForgotPassword.vue @@ -26,9 +26,8 @@ const submit = () => {
- Forgot your password? No problem. Just let us know your email - address and we will email you a password reset link that will allow - you to choose a new one. + Esqueceu sua senha? Sem problemas! Apenas nos deixe saber seu endereço de e-mail + e nós enviaremos um link de redefinição para sua senha!
{ :class="{ 'opacity-25': form.processing }" :disabled="form.processing" > - Email Password Reset Link + Enviar e-mail de redefinição de senha!
diff --git a/resources/js/Pages/Auth/Login.vue b/resources/js/Pages/Auth/Login.vue index 8e906a9..7244b58 100644 --- a/resources/js/Pages/Auth/Login.vue +++ b/resources/js/Pages/Auth/Login.vue @@ -1,100 +1,111 @@ - - \ No newline at end of file diff --git a/resources/js/Pages/Auth/Register.vue b/resources/js/Pages/Auth/Register.vue index de4a5bf..a41416a 100644 --- a/resources/js/Pages/Auth/Register.vue +++ b/resources/js/Pages/Auth/Register.vue @@ -97,7 +97,7 @@ const submit = () => { :href="route('login')" class="rounded-md text-sm text-gray-600 underline hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" > - Already registered? + Já possui uma conta? { :class="{ 'opacity-25': form.processing }" :disabled="form.processing" > - Register + Registrar diff --git a/resources/js/Pages/Auth/ResetPassword.vue b/resources/js/Pages/Auth/ResetPassword.vue index e795844..78e9017 100644 --- a/resources/js/Pages/Auth/ResetPassword.vue +++ b/resources/js/Pages/Auth/ResetPassword.vue @@ -93,7 +93,7 @@ const submit = () => { :class="{ 'opacity-25': form.processing }" :disabled="form.processing" > - Reset Password + Redefinir senha diff --git a/resources/js/Pages/Dashboard.vue b/resources/js/Pages/Dashboard.vue index f22f58e..ec8d50f 100644 --- a/resources/js/Pages/Dashboard.vue +++ b/resources/js/Pages/Dashboard.vue @@ -1,30 +1,155 @@ + \ No newline at end of file diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..99dbd45 --- /dev/null +++ b/routes/api.php @@ -0,0 +1,11 @@ +user(); +})->middleware('auth:sanctum'); + +Route::post('/v1/ami-informations', [AmiEventController::class, 'handle']); diff --git a/routes/web.php b/routes/web.php index 067c4f5..96604b2 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,5 +1,6 @@ middleware(['auth', 'verified'])->name('dashboard'); +Route::get('/dashboard', [DashboardController::class, 'index'])->middleware(['auth', 'verified'])->name('dashboard'); Route::middleware('auth')->group(function () { Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); @@ -24,4 +23,4 @@ Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); }); -require __DIR__.'/auth.php'; +require __DIR__ . '/auth.php'; diff --git a/tailwind.config.js b/tailwind.config.js index d7c4a93..b44ce68 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -15,8 +15,18 @@ export default { fontFamily: { sans: ['Figtree', ...defaultTheme.fontFamily.sans], }, + // ADICIONE AQUI AS CORES DA INGLINE + colors: { + ingline: { + 900: '#0f172a', // Fundo Sidebar (Quase preto) + 800: '#1e293b', // "Ing" (Azul Petróleo) + 500: '#3b82f6', // "line" (Azul Vibrante) + 400: '#60a5fa', // Azul claro (Hover) + purple: '#9333ea', // O Roxo da logo + } + } }, }, plugins: [forms], -}; +}; \ No newline at end of file