atualizacao

This commit is contained in:
2026-04-03 17:00:07 +00:00
parent 730313470b
commit e336913d4d
26 changed files with 5124 additions and 395 deletions

View File

@@ -0,0 +1,149 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class BackfillConditionalAccessDerivedData extends Command
{
protected $signature = 'conditional-access:backfill-derived {--chunk=1000} {--only-empty} {--no-policies}';
protected $description = 'Preenche colunas derivadas e sincroniza políticas da tabela acesso_condicionals';
public function handle(): int
{
$chunkSize = max((int) $this->option('chunk'), 100);
$onlyEmpty = (bool) $this->option('only-empty');
$syncPolicies = ! (bool) $this->option('no-policies');
$query = DB::table('acesso_condicionals')
->select([
'id',
'location_json',
'device_detail_json',
'status_json',
'applied_policies_json',
'location_key',
'device_operating_system',
'status_error_code',
])
->orderBy('id');
if ($onlyEmpty) {
$query->where(function ($subQuery) {
$subQuery->whereNull('location_key')
->orWhereNull('device_operating_system')
->orWhereNull('status_error_code');
});
}
$bar = $this->output->createProgressBar((clone $query)->count());
$bar->start();
$query->chunkById($chunkSize, function ($rows) use ($syncPolicies, $bar) {
$policyRowsToInsert = [];
$ids = [];
foreach ($rows as $row) {
$ids[] = $row->id;
$location = $this->decodeJson($row->location_json ?? null);
$device = $this->decodeJson($row->device_detail_json ?? null);
$status = $this->decodeJson($row->status_json ?? null);
$policies = $this->decodeJson($row->applied_policies_json ?? null);
$city = trim((string) ($location['city'] ?? ''));
$state = trim((string) ($location['state'] ?? ''));
$country = trim((string) ($location['countryOrRegion'] ?? ''));
$locationKey = mb_strtolower($city . '|' . $state . '|' . $country);
$locationKey = $locationKey === '||' ? null : $locationKey;
DB::table('acesso_condicionals')
->where('id', $row->id)
->update([
'location_city' => $city !== '' ? $city : null,
'location_state' => $state !== '' ? $state : null,
'location_country' => $country !== '' ? $country : null,
'location_key' => $locationKey,
'device_display_name' => $this->nullableString($device['displayName'] ?? $device['deviceId'] ?? null),
'device_operating_system' => $this->nullableString($device['operatingSystem'] ?? null),
'device_browser' => $this->nullableString($device['browser'] ?? null),
'device_is_compliant' => array_key_exists('isCompliant', (array) $device) ? (int) ((bool) $device['isCompliant']) : null,
'device_is_managed' => array_key_exists('isManaged', (array) $device) ? (int) ((bool) $device['isManaged']) : null,
'status_error_code' => $this->nullableString(isset($status['errorCode']) ? (string) $status['errorCode'] : null),
'status_failure_reason' => $this->nullableString($status['failureReason'] ?? null),
]);
if ($syncPolicies && is_array($policies)) {
foreach ($policies as $policy) {
if (! is_array($policy)) {
continue;
}
$policyRowsToInsert[] = [
'acesso_conditional_id' => $row->id,
'policy_id' => $this->nullableString($policy['id'] ?? null),
'policy_name' => trim((string) ($policy['displayName'] ?? 'Sem nome')) ?: 'Sem nome',
'policy_result' => $this->nullableString($policy['result'] ?? null),
'grant_controls_json' => $this->jsonOrNull($policy['enforcedGrantControls'] ?? null),
'session_controls_json' => $this->jsonOrNull($policy['enforcedSessionControls'] ?? null),
'created_at' => now(),
'updated_at' => now(),
];
}
}
$bar->advance();
}
if ($syncPolicies && $ids !== []) {
DB::table('acesso_conditional_policies')->whereIn('acesso_conditional_id', $ids)->delete();
foreach (array_chunk($policyRowsToInsert, 1000) as $insertChunk) {
if ($insertChunk !== []) {
DB::table('acesso_conditional_policies')->insert($insertChunk);
}
}
}
}, 'id');
$bar->finish();
$this->newLine(2);
$this->info('Backfill concluído com sucesso.');
return self::SUCCESS;
}
private function decodeJson(mixed $value): mixed
{
if (is_array($value)) {
return $value;
}
if (! is_string($value) || trim($value) === '') {
return null;
}
$decoded = json_decode($value, true);
return json_last_error() === JSON_ERROR_NONE ? $decoded : null;
}
private function nullableString(mixed $value): ?string
{
if ($value === null) {
return null;
}
$value = trim((string) $value);
return $value !== '' ? $value : null;
}
private function jsonOrNull(mixed $value): ?string
{
if ($value === null) {
return null;
}
return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
}

View File

@@ -0,0 +1,453 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class ProjectConditionalAccessShadowTables extends Command
{
protected $signature = 'conditional-access:project-shadow
{--chunk=1000 : Quantidade de registros por lote}
{--from-id= : Processa apenas a partir deste id de origem}
{--only-missing : Processa apenas eventos ainda não projetados}
{--rebuild-policies : Recria as políticas dos eventos processados}';
protected $description = 'Projeta a tabela fonte acesso_condicionals em tabelas shadow indexadas, sem alterar a tabela original, com cliente_id e tenant_id.';
public function handle(): int
{
if (! Schema::hasTable('acesso_condicionals')) {
$this->error('Tabela fonte acesso_condicionals não encontrada.');
return self::FAILURE;
}
if (! Schema::hasTable('m365_creds')) {
$this->error('Tabela m365_creds não encontrada. Ela é necessária para resolver cliente_id a partir do tenant_id.');
return self::FAILURE;
}
if (! Schema::hasColumn('acesso_condicionals', 'tenant_id')) {
$this->error('A coluna tenant_id não foi encontrada em acesso_condicionals.');
return self::FAILURE;
}
$hasClientesTable = Schema::hasTable('clientes');
$chunk = max((int) $this->option('chunk'), 100);
$fromId = $this->option('from-id') !== null ? max((int) $this->option('from-id'), 1) : null;
$onlyMissing = (bool) $this->option('only-missing');
$rebuildPolicies = (bool) $this->option('rebuild-policies');
$query = DB::table('acesso_condicionals')
->select([
'id',
'tenant_id',
'created_date_time',
'user_display_name',
'user_principal_name',
'app_display_name',
'ip_address',
'conditional_access_status',
'location_json',
'device_detail_json',
'status_json',
'applied_policies_json',
])
->orderBy('id');
if ($fromId !== null) {
$query->where('id', '>=', $fromId);
}
if ($onlyMissing) {
$query->whereNotExists(function ($subQuery) {
$subQuery->selectRaw('1')
->from('acesso_conditional_event_index as i')
->whereColumn('i.source_event_id', 'acesso_condicionals.id');
});
}
$total = (clone $query)->count('id');
$this->info('Eventos elegíveis para projeção: ' . number_format($total, 0, ',', '.'));
if ($total === 0) {
$this->info('Nada a processar.');
return self::SUCCESS;
}
$progress = $this->output->createProgressBar($total);
$progress->start();
$query->chunkById($chunk, function ($rows) use ($progress, $rebuildPolicies, $hasClientesTable) {
$tenantIds = $rows->pluck('tenant_id')
->map(fn ($value) => $this->nullableString($value))
->filter()
->unique()
->values()
->all();
$credsByTenant = [];
if ($tenantIds !== []) {
$credsByTenant = DB::table('m365_creds')
->whereIn('id_tenant', $tenantIds)
->orderBy('id')
->get(['id', 'id_tenant', 'id_cliente'])
->groupBy('id_tenant')
->map(fn ($group) => $group->first())
->all();
}
$clienteIds = [];
foreach ($credsByTenant as $cred) {
$clienteId = $this->nullableUnsignedBigInt($cred->id_cliente ?? null);
if ($clienteId !== null) {
$clienteIds[] = $clienteId;
}
}
$clienteIds = array_values(array_unique($clienteIds));
$clientesById = [];
if ($hasClientesTable && $clienteIds !== []) {
$clientesById = DB::table('clientes')
->whereIn('id', $clienteIds)
->pluck('name', 'id')
->all();
}
$eventRows = [];
$policiesPerEvent = [];
$sourceIds = [];
$now = now();
foreach ($rows as $row) {
$sourceIds[] = (int) $row->id;
$tenantId = $this->nullableString($row->tenant_id ?? null);
$cred = $tenantId !== null ? ($credsByTenant[$tenantId] ?? null) : null;
$m365CredId = $cred?->id !== null ? (int) $cred->id : null;
$clienteId = $this->nullableUnsignedBigInt($cred->id_cliente ?? null);
$clienteName = $clienteId !== null ? $this->nullableString($clientesById[$clienteId] ?? null) : null;
$location = $this->decodeJson($row->location_json ?? null);
$device = $this->decodeJson($row->device_detail_json ?? null);
$status = $this->decodeJson($row->status_json ?? null);
$policies = $this->decodeJson($row->applied_policies_json ?? null);
$policyList = is_array($policies) ? array_values(array_filter($policies, 'is_array')) : [];
$city = $this->nullableString($location['city'] ?? null);
$state = $this->nullableString($location['state'] ?? null);
$country = $this->nullableString($location['countryOrRegion'] ?? null);
$locationKey = $this->buildLocationKey($city, $state, $country);
[$executiveOutcome, $policyCount] = $this->deriveEventOutcome($policyList);
$eventRows[] = [
'source_event_id' => (int) $row->id,
'cliente_id' => $clienteId,
'cliente_name' => $clienteName,
'm365_cred_id' => $m365CredId,
'tenant_id' => $tenantId,
'source_payload_hash' => hash('sha256', json_encode([
'tenant_id' => $tenantId,
'created_date_time' => $row->created_date_time,
'user_display_name' => $row->user_display_name,
'user_principal_name' => $row->user_principal_name,
'app_display_name' => $row->app_display_name,
'ip_address' => $row->ip_address,
'conditional_access_status' => $row->conditional_access_status,
'location_json' => $row->location_json,
'device_detail_json' => $row->device_detail_json,
'status_json' => $row->status_json,
'applied_policies_json' => $row->applied_policies_json,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)),
'created_date_time' => $row->created_date_time,
'user_display_name' => $this->nullableString($row->user_display_name),
'user_principal_name' => $this->nullableString($row->user_principal_name),
'app_display_name' => $this->nullableString($row->app_display_name),
'ip_address' => $this->nullableString($row->ip_address),
'conditional_access_status' => $this->nullableString($row->conditional_access_status),
'conditional_access_status_class' => $this->classifyAccessStatus((string) ($row->conditional_access_status ?? '')),
'executive_outcome_class' => $executiveOutcome,
'location_city' => $city,
'location_state' => $state,
'location_country' => $country,
'location_key' => $locationKey,
'device_display_name' => $this->nullableString($device['displayName'] ?? $device['deviceId'] ?? null),
'device_operating_system' => $this->nullableString($device['operatingSystem'] ?? null),
'device_browser' => $this->nullableString($device['browser'] ?? null),
'device_is_compliant' => array_key_exists('isCompliant', (array) $device) ? (int) ((bool) $device['isCompliant']) : null,
'device_is_managed' => array_key_exists('isManaged', (array) $device) ? (int) ((bool) $device['isManaged']) : null,
'status_error_code' => $this->nullableString(isset($status['errorCode']) ? (string) $status['errorCode'] : null),
'status_failure_reason' => $this->nullableString($status['failureReason'] ?? null),
'applied_policy_count' => $policyCount,
'source_synced_at' => $now,
'created_at' => $now,
'updated_at' => $now,
];
$policiesPerEvent[(int) $row->id] = [
'cliente_id' => $clienteId,
'm365_cred_id' => $m365CredId,
'tenant_id' => $tenantId,
'items' => $policyList,
];
$progress->advance();
}
DB::table('acesso_conditional_event_index')->upsert(
$eventRows,
['source_event_id'],
[
'cliente_id',
'cliente_name',
'm365_cred_id',
'tenant_id',
'source_payload_hash',
'created_date_time',
'user_display_name',
'user_principal_name',
'app_display_name',
'ip_address',
'conditional_access_status',
'conditional_access_status_class',
'executive_outcome_class',
'location_city',
'location_state',
'location_country',
'location_key',
'device_display_name',
'device_operating_system',
'device_browser',
'device_is_compliant',
'device_is_managed',
'status_error_code',
'status_failure_reason',
'applied_policy_count',
'source_synced_at',
'updated_at',
]
);
if (! $rebuildPolicies || $sourceIds === []) {
return;
}
$eventIndexMap = DB::table('acesso_conditional_event_index')
->whereIn('source_event_id', $sourceIds)
->pluck('id', 'source_event_id')
->map(fn ($value) => (int) $value)
->all();
$eventIndexIds = array_values($eventIndexMap);
if ($eventIndexIds !== []) {
DB::table('acesso_conditional_policy_index')->whereIn('event_index_id', $eventIndexIds)->delete();
}
$policyRows = [];
foreach ($policiesPerEvent as $sourceEventId => $payload) {
$eventIndexId = $eventIndexMap[$sourceEventId] ?? null;
if (! $eventIndexId) {
continue;
}
foreach (($payload['items'] ?? []) as $policy) {
$derived = $this->derivePolicyFlags((string) ($policy['result'] ?? ''));
$policyRows[] = [
'event_index_id' => $eventIndexId,
'source_event_id' => $sourceEventId,
'cliente_id' => $payload['cliente_id'] ?? null,
'm365_cred_id' => $payload['m365_cred_id'] ?? null,
'tenant_id' => $payload['tenant_id'] ?? null,
'policy_id' => $this->nullableString($policy['id'] ?? null),
'policy_name' => trim((string) ($policy['displayName'] ?? 'Sem nome')) ?: 'Sem nome',
'policy_result' => $this->nullableString($policy['result'] ?? null),
'policy_result_class' => $derived['class'],
'is_report_only' => $derived['is_report_only'] ? 1 : 0,
'grant_controls_json' => $this->jsonOrNull($policy['enforcedGrantControls'] ?? null),
'session_controls_json' => $this->jsonOrNull($policy['enforcedSessionControls'] ?? null),
'created_at' => $now,
'updated_at' => $now,
];
}
}
foreach (array_chunk($policyRows, 1000) as $chunkRows) {
if ($chunkRows !== []) {
DB::table('acesso_conditional_policy_index')->insert($chunkRows);
}
}
}, 'id', 'id');
$progress->finish();
$this->newLine(2);
$this->info('Projeção concluída com sucesso.');
return self::SUCCESS;
}
private function decodeJson(mixed $value): mixed
{
if (is_array($value)) {
return $value;
}
if (! is_string($value) || trim($value) === '') {
return null;
}
$decoded = json_decode($value, true);
return json_last_error() === JSON_ERROR_NONE ? $decoded : null;
}
private function nullableString(mixed $value): ?string
{
if ($value === null) {
return null;
}
$value = trim((string) $value);
return $value !== '' ? $value : null;
}
private function nullableUnsignedBigInt(mixed $value): ?int
{
if ($value === null) {
return null;
}
$value = trim((string) $value);
if ($value === '' || ! preg_match('/^\d+$/', $value)) {
return null;
}
$parsed = (int) $value;
return $parsed > 0 ? $parsed : null;
}
private function jsonOrNull(mixed $value): ?string
{
if ($value === null) {
return null;
}
return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
private function buildLocationKey(?string $city, ?string $state, ?string $country): ?string
{
$parts = [mb_strtolower((string) $city), mb_strtolower((string) $state), mb_strtolower((string) $country)];
$joined = trim(implode('|', $parts), '|');
return $joined !== '' ? $joined : null;
}
private function classifyAccessStatus(string $value): string
{
$flat = $this->normalizeFlat($value);
if ($flat === '') {
return 'unknown';
}
if (str_contains($flat, 'reportonly')) {
return 'report-only';
}
if ($this->containsAny($flat, ['failure', 'falha', 'error', 'erro', 'block', 'blocked', 'bloque'])) {
return 'failure';
}
if ($this->containsAny($flat, ['success', 'sucesso'])) {
return 'success';
}
if ($this->containsAny($flat, ['notapplied', 'naoaplicado', 'nãoaplicado'])) {
return 'not-applied';
}
if ($this->containsAny($flat, ['interrupt', 'interromp'])) {
return 'interrupted';
}
return 'other';
}
private function derivePolicyFlags(string $value): array
{
$flat = $this->normalizeFlat($value);
if ($flat === '') {
return ['class' => 'unknown', 'is_report_only' => false];
}
$isReportOnly = str_contains($flat, 'reportonly');
$class = 'other';
if ($this->containsAny($flat, ['failure', 'falha', 'error', 'erro', 'block', 'blocked', 'bloque'])) {
$class = 'failure';
} elseif ($this->containsAny($flat, ['success', 'sucesso'])) {
$class = 'success';
} elseif ($this->containsAny($flat, ['notapplied', 'naoaplicado', 'nãoaplicado'])) {
$class = 'not-applied';
} elseif ($this->containsAny($flat, ['interrupt', 'interromp'])) {
$class = 'interrupted';
} elseif ($isReportOnly) {
$class = 'report-only';
}
return ['class' => $class, 'is_report_only' => $isReportOnly];
}
private function deriveEventOutcome(array $policies): array
{
$hasFailure = false;
$hasWouldBlock = false;
$hasSuccess = false;
$hasNotApplied = false;
$hasInterrupted = false;
foreach ($policies as $policy) {
$derived = $this->derivePolicyFlags((string) ($policy['result'] ?? ''));
if ($derived['class'] === 'failure' && $derived['is_report_only'] === false) {
$hasFailure = true;
}
if ($derived['class'] === 'failure' && $derived['is_report_only'] === true) {
$hasWouldBlock = true;
}
if ($derived['class'] === 'success') {
$hasSuccess = true;
}
if ($derived['class'] === 'not-applied') {
$hasNotApplied = true;
}
if ($derived['class'] === 'interrupted') {
$hasInterrupted = true;
}
}
$outcome = 'other';
if ($hasFailure) {
$outcome = 'blocked';
} elseif ($hasWouldBlock) {
$outcome = 'would-block';
} elseif ($hasSuccess) {
$outcome = 'allowed';
} elseif ($hasNotApplied) {
$outcome = 'not-applied';
} elseif ($hasInterrupted) {
$outcome = 'interrupted';
}
return [$outcome, count($policies)];
}
private function normalizeFlat(string $value): string
{
$value = mb_strtolower(trim($value), 'UTF-8');
$value = str_replace([' ', '-', '_'], '', $value);
return iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value) ?: $value;
}
private function containsAny(string $haystack, array $needles): bool
{
foreach ($needles as $needle) {
if ($needle !== '' && str_contains($haystack, $needle)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class AcessoCondicionalController extends Controller
{
//
}

View File

@@ -10,7 +10,7 @@ class ClientesRelatorioController extends Controller
{
public function index()
{
$relatorios = clientes_relatorio::all();
$relatorios = clientes_relatorio::paginate(10);
return view("relatorios.index", compact('relatorios'));
}
@@ -32,30 +32,39 @@ class ClientesRelatorioController extends Controller
$ultimoRelatorio = clientes_relatorio::where('id_cliente', $clienteId)
->latest('id')
->first();
try {
if ($ultimoRelatorio) {
$dadosNovoRelatorio = $ultimoRelatorio->toArray();
if ($ultimoRelatorio) {
$dadosNovoRelatorio = $ultimoRelatorio->toArray();
unset(
$dadosNovoRelatorio['id'],
$dadosNovoRelatorio['created_at'],
$dadosNovoRelatorio['updated_at']
);
unset(
$dadosNovoRelatorio['id'],
$dadosNovoRelatorio['created_at'],
$dadosNovoRelatorio['updated_at']
);
// campos que você NÃO quer copiar
unset(
$dadosNovoRelatorio['status'],
$dadosNovoRelatorio['data_envio'],
$dadosNovoRelatorio['observacao_final']
);
// campos que você NÃO quer copiar
unset(
$dadosNovoRelatorio['status'],
$dadosNovoRelatorio['data_envio'],
$dadosNovoRelatorio['observacao_final']
);
$novoRelatorio = clientes_relatorio::create($dadosNovoRelatorio);
} else {
$novoRelatorio = clientes_relatorio::create($dadosNovoRelatorio);
} else {
$novoRelatorio = clientes_relatorio::create([
'cliente_id' => $clienteId,
]);
'id_cliente' => $clienteId,
'title' => 'Novo Relatório',
'horas_utilizadas' => 0
]);
}
return redirect()->back()->with('success', 'Relatório criado com sucesso.' . $ultimoRelatorio);
} catch (\Throwable $th) {
return redirect()->back()->with('error', 'Relatório não criado.' . $th->getMessage());
}
return redirect()->back()->with('success', 'Relatório criado com sucesso.');
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class acesso_condicional extends Model
{
//
}

View File

@@ -45,4 +45,14 @@ class clientes extends Model
{
return $this->hasMany(clientes_contratos::class, 'cliente_id');
}
public function chamados()
{
return $this->hasMany(clientes_chamado::class, 'id_cliente');
}
public function relatorios()
{
return $this->hasMany(clientes_relatorio::class, 'id_cliente');
}
}

View File

@@ -6,5 +6,19 @@ use Illuminate\Database\Eloquent\Model;
class clientes_relatorio extends Model
{
//
protected $fillable = [
'status',
'title',
'id_cliente',
'horas_utilizadas',
'chamados',
'melhorias',
'nota_consultor',
'observacao_consultor'
];
public function cliente()
{
return $this->hasOne(clientes::class,'id','id_cliente');
}
}

View File

@@ -63,6 +63,7 @@ return [
]) : [],
],
'mariadb' => [
'driver' => 'mariadb',
'url' => env('DB_URL'),
@@ -148,7 +149,7 @@ return [
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')) . '-database-'),
'persistent' => env('REDIS_PERSISTENT', false),
],

View File

@@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('acesso_condicionals', function (Blueprint $table) {
$table->id();
$table->string('nome_cliente');
$table->string('tenant_id');
$table->dateTime('chunk_start');
$table->dateTime('chunk_end');
$table->string('graph_id');
$table->dateTime('created_date_time');
$table->string('user_display_name');
$table->string('user_principal_name');
$table->string('app_display_name');
$table->string('ip_address');
$table->string('conditional_access_status');
$table->json('location_json');
$table->json('device_detail_json');
$table->json('applied_policies_json');
$table->json('status_json');
$table->json('raw_json');
$table->dateTime('imported_at');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('acesso_condicionals');
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('acesso_condicionals', function (Blueprint $table) {
$table->string('chunk_label')->after('tenant_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('acesso_condicionals', function (Blueprint $table) {
//
});
}
};

View File

@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('acesso_condicionals', function (Blueprint $table) {
$table->index('created_date_time', 'idx_acesso_condicionals_created_date_time');
});
}
public function down(): void
{
Schema::table('acesso_condicionals', function (Blueprint $table) {
$table->dropIndex('idx_acesso_condicionals_created_date_time');
});
}
};

View File

@@ -0,0 +1,95 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::table('acesso_condicionals', function (Blueprint $table) {
if (! Schema::hasColumn('acesso_condicionals', 'location_city')) {
$table->string('location_city', 120)->nullable()->after('location_json');
}
if (! Schema::hasColumn('acesso_condicionals', 'location_state')) {
$table->string('location_state', 120)->nullable()->after('location_city');
}
if (! Schema::hasColumn('acesso_condicionals', 'location_country')) {
$table->string('location_country', 120)->nullable()->after('location_state');
}
if (! Schema::hasColumn('acesso_condicionals', 'location_key')) {
$table->string('location_key', 255)->nullable()->after('location_country');
}
if (! Schema::hasColumn('acesso_condicionals', 'device_display_name')) {
$table->string('device_display_name', 255)->nullable()->after('device_detail_json');
}
if (! Schema::hasColumn('acesso_condicionals', 'device_operating_system')) {
$table->string('device_operating_system', 120)->nullable()->after('device_display_name');
}
if (! Schema::hasColumn('acesso_condicionals', 'device_browser')) {
$table->string('device_browser', 120)->nullable()->after('device_operating_system');
}
if (! Schema::hasColumn('acesso_condicionals', 'device_is_compliant')) {
$table->boolean('device_is_compliant')->nullable()->after('device_browser');
}
if (! Schema::hasColumn('acesso_condicionals', 'device_is_managed')) {
$table->boolean('device_is_managed')->nullable()->after('device_is_compliant');
}
if (! Schema::hasColumn('acesso_condicionals', 'status_error_code')) {
$table->string('status_error_code', 40)->nullable()->after('status_json');
}
if (! Schema::hasColumn('acesso_condicionals', 'status_failure_reason')) {
$table->text('status_failure_reason')->nullable()->after('status_error_code');
}
});
Schema::table('acesso_condicionals', function (Blueprint $table) {
$table->index('created_date_time', 'idx_ac_created_date_time');
$table->index('conditional_access_status', 'idx_ac_status');
$table->index('app_display_name', 'idx_ac_app');
$table->index('user_principal_name', 'idx_ac_user_principal');
$table->index('location_key', 'idx_ac_location_key');
$table->index('device_operating_system', 'idx_ac_device_os');
$table->index('device_browser', 'idx_ac_device_browser');
$table->index('status_error_code', 'idx_ac_status_error_code');
});
}
public function down(): void
{
Schema::table('acesso_condicionals', function (Blueprint $table) {
$table->dropIndex('idx_ac_created_date_time');
$table->dropIndex('idx_ac_status');
$table->dropIndex('idx_ac_app');
$table->dropIndex('idx_ac_user_principal');
$table->dropIndex('idx_ac_location_key');
$table->dropIndex('idx_ac_device_os');
$table->dropIndex('idx_ac_device_browser');
$table->dropIndex('idx_ac_status_error_code');
$table->dropColumn([
'location_city',
'location_state',
'location_country',
'location_key',
'device_display_name',
'device_operating_system',
'device_browser',
'device_is_compliant',
'device_is_managed',
'status_error_code',
'status_failure_reason',
]);
});
}
};

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('acesso_conditional_policies', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('acesso_conditional_id');
$table->string('policy_id', 120)->nullable();
$table->string('policy_name', 255);
$table->string('policy_result', 120)->nullable();
$table->json('grant_controls_json')->nullable();
$table->json('session_controls_json')->nullable();
$table->timestamps();
$table->foreign('acesso_conditional_id', 'fk_ac_policy_parent')
->references('id')
->on('acesso_condicionals')
->cascadeOnDelete();
$table->index('acesso_conditional_id', 'idx_ac_policy_parent');
$table->index('policy_name', 'idx_ac_policy_name');
$table->index('policy_result', 'idx_ac_policy_result');
$table->index(['policy_name', 'policy_result'], 'idx_ac_policy_name_result');
});
}
public function down(): void
{
Schema::dropIfExists('acesso_conditional_policies');
}
};

View File

@@ -0,0 +1,84 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('acesso_conditional_event_index', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('source_event_id')->unique();
$table->unsignedBigInteger('cliente_id')->nullable();
$table->string('cliente_name', 255)->nullable();
$table->unsignedBigInteger('m365_cred_id')->nullable();
$table->string('tenant_id', 255)->nullable();
$table->char('source_payload_hash', 64)->nullable();
$table->dateTime('created_date_time')->index('idx_acei_created_date_time');
$table->string('user_display_name', 255)->nullable();
$table->string('user_principal_name', 255)->nullable();
$table->string('app_display_name', 255)->nullable();
$table->string('ip_address', 64)->nullable();
$table->string('conditional_access_status', 120)->nullable();
$table->string('conditional_access_status_class', 40)->nullable();
$table->string('executive_outcome_class', 40)->nullable();
$table->string('location_city', 120)->nullable();
$table->string('location_state', 120)->nullable();
$table->string('location_country', 120)->nullable();
$table->string('location_key', 255)->nullable();
$table->string('device_display_name', 255)->nullable();
$table->string('device_operating_system', 120)->nullable();
$table->string('device_browser', 120)->nullable();
$table->boolean('device_is_compliant')->nullable();
$table->boolean('device_is_managed')->nullable();
$table->string('status_error_code', 40)->nullable();
$table->text('status_failure_reason')->nullable();
$table->unsignedSmallInteger('applied_policy_count')->default(0);
$table->timestamp('source_synced_at')->nullable();
$table->timestamps();
$table->index('cliente_id', 'idx_acei_cliente_id');
$table->index('tenant_id', 'idx_acei_tenant_id');
$table->index('m365_cred_id', 'idx_acei_m365_cred_id');
$table->index(['cliente_id', 'tenant_id'], 'idx_acei_cliente_tenant');
$table->index(['cliente_id', 'created_date_time'], 'idx_acei_cliente_created');
$table->index(['tenant_id', 'created_date_time'], 'idx_acei_tenant_created');
$table->index(['cliente_id', 'tenant_id', 'created_date_time'], 'idx_acei_cliente_tenant_created');
$table->index('conditional_access_status', 'idx_acei_status');
$table->index('conditional_access_status_class', 'idx_acei_status_class');
$table->index('executive_outcome_class', 'idx_acei_exec_outcome');
$table->index('app_display_name', 'idx_acei_app');
$table->index('user_principal_name', 'idx_acei_user_principal');
$table->index('location_key', 'idx_acei_location_key');
$table->index('device_operating_system', 'idx_acei_device_os');
$table->index('device_browser', 'idx_acei_device_browser');
$table->index('device_is_compliant', 'idx_acei_device_compliant');
$table->index('device_is_managed', 'idx_acei_device_managed');
$table->index('status_error_code', 'idx_acei_status_error_code');
$table->index(['created_date_time', 'conditional_access_status_class'], 'idx_acei_created_status_class');
$table->index(['created_date_time', 'executive_outcome_class'], 'idx_acei_created_exec_outcome');
$table->index(['created_date_time', 'app_display_name'], 'idx_acei_created_app');
$table->index(['created_date_time', 'device_operating_system'], 'idx_acei_created_os');
$table->index(['created_date_time', 'device_browser'], 'idx_acei_created_browser');
$table->index(['cliente_id', 'created_date_time', 'app_display_name'], 'idx_acei_cliente_created_app');
$table->index(['cliente_id', 'created_date_time', 'device_operating_system'], 'idx_acei_cliente_created_os');
$table->index(['tenant_id', 'created_date_time', 'conditional_access_status_class'], 'idx_acei_tenant_created_status');
});
}
public function down(): void
{
Schema::dropIfExists('acesso_conditional_event_index');
}
};

View File

@@ -0,0 +1,59 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('acesso_conditional_policy_index', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('event_index_id');
$table->unsignedBigInteger('source_event_id');
$table->unsignedBigInteger('cliente_id')->nullable();
$table->unsignedBigInteger('m365_cred_id')->nullable();
$table->string('tenant_id', 255)->nullable();
$table->string('policy_id', 120)->nullable();
$table->string('policy_name', 255);
$table->string('policy_result', 120)->nullable();
$table->string('policy_result_class', 40)->nullable();
$table->boolean('is_report_only')->default(false);
$table->json('grant_controls_json')->nullable();
$table->json('session_controls_json')->nullable();
$table->timestamps();
$table->foreign('event_index_id', 'fk_acpi_event_index')
->references('id')
->on('acesso_conditional_event_index')
->cascadeOnDelete();
$table->index('event_index_id', 'idx_acpi_event_index');
$table->index('source_event_id', 'idx_acpi_source_event');
$table->index('cliente_id', 'idx_acpi_cliente_id');
$table->index('tenant_id', 'idx_acpi_tenant_id');
$table->index('m365_cred_id', 'idx_acpi_m365_cred_id');
$table->index(['cliente_id', 'tenant_id'], 'idx_acpi_cliente_tenant');
$table->index(['cliente_id', 'event_index_id'], 'idx_acpi_cliente_event');
$table->index(['tenant_id', 'event_index_id'], 'idx_acpi_tenant_event');
$table->index('policy_name', 'idx_acpi_policy_name');
$table->index('policy_result', 'idx_acpi_policy_result');
$table->index('policy_result_class', 'idx_acpi_result_class');
$table->index('is_report_only', 'idx_acpi_report_only');
$table->index(['policy_name', 'policy_result'], 'idx_acpi_policy_name_result');
$table->index(['policy_name', 'policy_result_class'], 'idx_acpi_policy_name_result_class');
$table->index(['event_index_id', 'policy_name'], 'idx_acpi_event_policy_name');
$table->index(['cliente_id', 'policy_name'], 'idx_acpi_cliente_policy_name');
$table->index(['cliente_id', 'policy_result_class'], 'idx_acpi_cliente_result_class');
$table->index(['tenant_id', 'policy_name'], 'idx_acpi_tenant_policy_name');
$table->index(['tenant_id', 'policy_result_class'], 'idx_acpi_tenant_result_class');
});
}
public function down(): void
{
Schema::dropIfExists('acesso_conditional_policy_index');
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('clientes_relatorios', function (Blueprint $table) {
$table->string('status')->default('Novo')->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('clientes_relatorios', function (Blueprint $table) {
//
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('clientes_relatorios', function (Blueprint $table) {
$table->string('chamados')->nullable()->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('clientes_relatorios', function (Blueprint $table) {
//
});
}
};

BIN
dump.rdb

Binary file not shown.

View File

@@ -123,7 +123,8 @@
<div class="card shadow-sm border-0">
<div class="card-header bg-white border-0 pb-0">
<h5 class="mb-0">Utilização dos Contratos</h5>
<small class="text-muted">Acompanhe o consumo de horas de cada contrato</small>
<small class="text-muted">Acompanhe o consumo de horas de cada contrato
</small>
</div>
<div class="card-body">
@@ -138,7 +139,7 @@
<tbody>
@foreach($cliente->contratos as $contrato)
@php
$horas_utilizadas = 140; // valor fixo por enquanto
$horas_utilizadas = $cliente->chamados->sum('minutos_utilizados') / 60;
$horas_contratadas = $contrato->horas_contratadas ?? 0;
$porcentagem_utilizada = $horas_contratadas > 0

View File

@@ -1,331 +1,396 @@
@extends('theme.default')
@section('page.title', 'Clientes')
@section('content')
@include('theme.alertas')
<!-- End - Page Title & Breadcrumb -->
@include('theme.alertas')
<div class="container-fluid">
<div class="row">
<div class="col-xl-12 bst-seller">
<div class="d-flex align-items-center justify-content-between mb-4">
<h4 class="mb-0">Lista de CLientes</h4>
<div class="d-flex align-items-center">
<a class="btn btn-primary btn-sm ms-2" data-bs-toggle="offcanvas" href="#addTaskmodal" role="button"
aria-controls="addTaskmodal">+ Cliente</a>
</div>
</div>
<div class="card h-auto">
<div class="card-header border-0 align-items-center">
<div id="tableCustomerFilter"></div>
<div id="tableCustomerExcelBTN"></div>
</div>
<div class="card-body table-card-body px-0 pt-0 pb-2">
<div class="table-responsive check-wrapper">
<table id="customerTable" class="table">
<thead class="table-light">
<tr>
<th class="mw-100">ID</th>
<th class="mw-200">Nome</th>
<th class="mw-100">CNPJ</th>
<th class="mw-100"></th>
</tr>
</thead>
<tbody>
@foreach ($clientes as $cliente)
<tr>
<td><span>{{ $cliente->id }}</span></td>
<td>
<div class="d-flex">
<img src="{{ $cliente->logotipo }}" class="avatar avatar-sm me-2" alt="">
<div class="clearfix">
<h6 class="mb-0"><a
href="{{ route('cliente', $cliente->id) }}">{{ $cliente->name }}</a>
</h6>
<small>{{ $cliente->endereco['logradouro'] }},
{{ $cliente->endereco['numero'] }}
@if (isset($cliente->endereco['complemento']))
({{ $cliente->endereco['complemento'] }}) -
@else
-
@endif
{{ $cliente->endereco['cep'] }}</small>
</div>
</div>
</td>
<td><a class="text-primary">{{ $cliente->cnpj }}</a></td>
<td class="text-end">
<div class="dropdown">
<button type="button" class="btn btn-sm btn-primary light btn-square"
data-bs-toggle="dropdown">
<i class="fa-solid fa-ellipsis"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" data-bs-toggle="offcanvas"
href="#editar-cliente-{{ $cliente->id }}"
aria-controls="editar-cliente-{{ $cliente->id }}">Editar</a>
</li>
<li><a class="dropdown-item text-danger" data-bs-toggle="offcanvas"
href="#deletar-cliente-{{ $cliente->id }}"
aria-controls="deletar-cliente-{{ $cliente->id }}">Deletar</a>
</li>
</ul>
</div>
</td>
</tr>
<!-- MODAL DE EDIÇÃO -->
<div class="offcanvas offcanvas-end custom-offcanvas" tabindex="-1"
id="editar-cliente-{{ $cliente->id }}">
<div class="offcanvas-header pb-0">
<h2 class="modal-title fs-5" id="#gridSystemModal1">Editar {{ $cliente->name }}
</h2>
<button type="button" class="btn btn-square btn-danger light btn-sm ms-auto"
data-bs-dismiss="offcanvas" aria-label="Close">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="offcanvas-body">
<form class="row" action="{{ route('clientes.edit', $cliente->id) }}"
method="POST">
@csrf
<div class="col-xl-12 mb-3">
<label for="exampleFormControlInputfirst" class="form-label">Nome do
Cliente</label>
<input type="text" class="form-control" id="name" name="name"
value="{{ $cliente->name }}" required>
</div>
<div class="col-xl-12 mb-3">
<label for="exampleFormControlInputfirst"
class="form-label">CNPJ</label>
<input type="text" class="form-control" id="cnpj_editar" name="cnpj"
value="{{ $cliente->cnpj }}" required maxlength="18">
</div>
<div class="col-xl-8 mb-3">
<label for="exampleFormControlInputfirst"
class="form-label">Endereço</label>
<input type="text" class="form-control" id="logradouro"
name="logradouro" value="{{ $cliente->endereco['logradouro'] }}"
required>
</div>
<div class="col-xl-4 mb-3">
<label for="exampleFormControlInputfirst"
class="form-label">Número</label>
<input type="text" class="form-control" id="logradouro_numero"
name="logradouro_numero" value="{{ $cliente->endereco['numero'] }}"
required>
</div>
<div class="col-xl-8 mb-3">
<label for="exampleFormControlInputfirst"
class="form-label">Complemento</label>
<input type="text" class="form-control" id="logradouro_complemento"
name="logradouro_complemento"
value="{{ $cliente->endereco['complemento'] }}">
</div>
<div class="col-xl-4 mb-3">
<label for="exampleFormControlInputfirst" class="form-label">CEP</label>
<input type="text" class="form-control" id="logradouro_cep_editar"
name="logradouro_cep" value="{{ $cliente->endereco['cep'] }}"
required maxlength="9">
</div>
<div class="col-xl-12">
<button class="btn btn-danger light">Cancel</button>
<button type="submit" class="btn btn-warning ms-2">Atualizar</button>
</div>
</form>
</div>
</div>
<div class="container-fluid d-flex flex-column min-vh-100">
<div class="row flex-grow-1">
<div class="col-12 d-flex flex-column bst-seller">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 mb-4">
<div>
<h4 class="mb-1">Clientes</h4>
<p class="mb-0 text-muted">Gerencie os clientes cadastrados</p>
</div>
<!-- -->
<div class="offcanvas offcanvas-end custom-offcanvas" tabindex="-1"
id="deletar-cliente-{{ $cliente->id }}">
<div class="offcanvas-header pb-0">
<h2 class="modal-title fs-5" id="#gridSystemModal1">Editar {{ $cliente->name }}
</h2>
<button type="button" class="btn btn-square btn-danger light btn-sm ms-auto"
data-bs-dismiss="offcanvas" aria-label="Close">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="offcanvas-body">
<form class="row" action="{{ route('clientes.delete', $cliente->id) }}"
method="POST">
@csrf
<div class="col-xl-12 mb-3">
<label for="exampleFormControlInputfirst" class="form-label">Você tem
certeza que quer deletar ?</label>
</div>
<div class="col-xl-12">
<button class="btn btn-danger light">Cancelar</button>
<button type="submit" class="btn btn-danger ms-2">Deletar</button>
</div>
</form>
</div>
</div>
@endforeach
<div class="d-flex align-items-center">
<a class="btn btn-primary btn-sm ms-2" data-bs-toggle="offcanvas" href="#addTaskmodal"
role="button" aria-controls="addTaskmodal">
+ Cliente
</a>
</div>
</div>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card flex-grow-1 overflow-visible d-flex flex-column">
<div class="card-header border-0 d-flex flex-wrap align-items-center justify-content-between gap-3">
<div>
<h5 class="mb-0">Lista de clientes</h5>
</div>
<!-- modal -->
<div class="offcanvas offcanvas-end custom-offcanvas" tabindex="-1" id="addTaskmodal">
<div class="offcanvas-header pb-0">
<h2 class="modal-title fs-5" id="#gridSystemModal1">Novo Cliente</h2>
<button type="button" class="btn btn-square btn-danger light btn-sm ms-auto" data-bs-dismiss="offcanvas"
aria-label="Close">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="offcanvas-body">
<form class="row" action="{{ route('clientes.add') }}" method="POST">
@csrf
<div class="col-xl-12 mb-3">
<label for="exampleFormControlInputfirst" class="form-label">Nome do Cliente</label>
<input type="text" class="form-control" id="name" name="name" placeholder="Nome do Cliente" required>
</div>
<div class="col-xl-12 mb-3">
<label for="exampleFormControlInputfirst" class="form-label">CNPJ</label>
<input type="text" class="form-control" id="cnpj_novo" name="cnpj" placeholder="CNPJ" required
maxlength="18">
</div>
<div class="col-xl-8 mb-3">
<label for="exampleFormControlInputfirst" class="form-label">Endereço</label>
<input type="text" class="form-control" id="logradouro" name="logradouro" placeholder="Endereço"
required>
</div>
<div class="col-xl-4 mb-3">
<label for="exampleFormControlInputfirst" class="form-label">Número</label>
<input type="text" class="form-control" id="logradouro_numero" name="logradouro_numero"
placeholder="Número" required>
</div>
<div class="col-xl-8 mb-3">
<label for="exampleFormControlInputfirst" class="form-label">Complemento</label>
<input type="text" class="form-control" id="logradouro_complemento" name="logradouro_complemento"
placeholder="Complemento">
</div>
<div class="col-xl-4 mb-3">
<label for="exampleFormControlInputfirst" class="form-label">CEP</label>
<input type="text" class="form-control" id="logradouro_cep_novo" name="logradouro_cep" placeholder="CEP"
required maxlength="9">
</div>
<div class="col-xl-12">
<button data-bs-dismiss="offcanvas" class="btn btn-danger light">Cancelar</button>
<button type="submit" class="btn btn-primary ms-2">Cadastrar</button>
</div>
</form>
</div>
</div>
<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center">
<div class="d-flex flex-wrap align-items-center gap-2">
<div id="tableCustomerFilter"></div>
<div id="tableCustomerExcelBTN"></div>
</div>
</div>
{{-- Previous --}}
<li class="page-item {{ $clientes->onFirstPage() ? 'disabled' : '' }}">
<a class="page-link" href="{{ $clientes->previousPageUrl() ?? '#' }}">Anterior</a>
</li>
<div class="card-body table-card-body px-0 pt-0 pb-2 overflow-visible d-flex flex-column">
<div class="table-responsive check-wrapper table-responsive-dropdown flex-grow-1">
<table id="customerTable" class="table align-middle mb-0">
<thead class="table-light">
<tr>
<th class="mw-100">ID</th>
<th class="mw-200">Nome</th>
<th class="mw-100">CNPJ</th>
<th class="mw-100 text-end pe-4">Opções</th>
</tr>
</thead>
@php
$current = $clientes->currentPage();
$last = $clientes->lastPage();
$start = max(1, $current - 2);
$end = min($last, $current + 2);
<tbody>
@forelse ($clientes as $cliente)
<tr>
<td>
<span>{{ $cliente->id }}</span>
</td>
// Ajustar quando estiver no começo
if ($current <= 3) {
$start = 1;
$end = min(5, $last);
}
<td>
<div class="d-flex align-items-center">
<img src="{{ $cliente->logotipo }}" class="avatar avatar-sm me-3 rounded"
alt="Logo do cliente">
// Ajustar quando estiver no final
if ($current > $last - 3) {
$start = max(1, $last - 4);
$end = $last;
}
@endphp
<div class="clearfix">
<h6 class="mb-1">
<a href="{{ route('cliente', $cliente->id) }}">
{{ $cliente->name }}
</a>
</h6>
{{-- "..." antes --}}
@if ($start > 1)
<li class="page-item">
<a class="page-link" href="{{ $clientes->url(1) }}">1</a>
</li>
<li class="page-item disabled">
<a class="page-link">...</a>
</li>
@endif
<small class="text-muted">
{{ $cliente->endereco['logradouro'] ?? '' }},
{{ $cliente->endereco['numero'] ?? '' }}
@if (isset($cliente->endereco['complemento']) && !empty($cliente->endereco['complemento']))
({{ $cliente->endereco['complemento'] }}) -
@else
-
@endif
{{ $cliente->endereco['cep'] ?? '' }}
</small>
</div>
</div>
</td>
{{-- Laço das páginas --}}
@for ($i = $start; $i <= $end; $i++)
<li class="page-item {{ $current == $i ? 'active' : '' }}">
<a class="page-link" href="{{ $clientes->url($i) }}">{{ $i }}</a>
</li>
@endfor
<td>
<a class="text-primary">{{ $cliente->cnpj }}</a>
</td>
{{-- "..." depois --}}
@if ($end < $last)
<li class="page-item disabled">
<a class="page-link">...</a>
</li>
<li class="page-item">
<a class="page-link" href="{{ $clientes->url($last) }}">{{ $last }}</a>
</li>
@endif
<td class="text-end pe-4 td-options">
<div class="dropdown">
<button type="button"
class="btn btn-sm btn-primary light btn-square"
data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-ellipsis"></i>
</button>
{{-- Next --}}
<li class="page-item {{ $clientes->hasMorePages() ? '' : 'disabled' }}">
<a class="page-link" href="{{ $clientes->nextPageUrl() ?? '#' }}">Próximo</a>
</li>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a class="dropdown-item" data-bs-toggle="offcanvas"
href="#editar-cliente-{{ $cliente->id }}"
aria-controls="editar-cliente-{{ $cliente->id }}">
Editar
</a>
</li>
<li>
<a class="dropdown-item text-danger"
data-bs-toggle="offcanvas"
href="#deletar-cliente-{{ $cliente->id }}"
aria-controls="deletar-cliente-{{ $cliente->id }}">
Deletar
</a>
</li>
</ul>
</div>
</td>
</tr>
</ul>
</nav>
<div class="offcanvas offcanvas-end custom-offcanvas" tabindex="-1"
id="editar-cliente-{{ $cliente->id }}">
<div class="offcanvas-header pb-0">
<h2 class="modal-title fs-5">Editar {{ $cliente->name }}</h2>
<button type="button"
class="btn btn-square btn-danger light btn-sm ms-auto"
data-bs-dismiss="offcanvas" aria-label="Close">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="offcanvas-body">
<form class="row" action="{{ route('clientes.edit', $cliente->id) }}"
method="POST">
@csrf
<div class="col-xl-12 mb-3">
<label class="form-label">Nome do Cliente</label>
<input type="text" class="form-control" name="name"
value="{{ $cliente->name }}" required>
</div>
<div class="col-xl-12 mb-3">
<label class="form-label">CNPJ</label>
<input type="text" class="form-control cnpj-editar" name="cnpj"
value="{{ $cliente->cnpj }}" required maxlength="18">
</div>
<div class="col-xl-8 mb-3">
<label class="form-label">Endereço</label>
<input type="text" class="form-control" name="logradouro"
value="{{ $cliente->endereco['logradouro'] }}" required>
</div>
<div class="col-xl-4 mb-3">
<label class="form-label">Número</label>
<input type="text" class="form-control"
name="logradouro_numero"
value="{{ $cliente->endereco['numero'] }}" required>
</div>
<div class="col-xl-8 mb-3">
<label class="form-label">Complemento</label>
<input type="text" class="form-control"
name="logradouro_complemento"
value="{{ $cliente->endereco['complemento'] }}">
</div>
<div class="col-xl-4 mb-3">
<label class="form-label">CEP</label>
<input type="text" class="form-control cep-editar"
name="logradouro_cep"
value="{{ $cliente->endereco['cep'] }}" required
maxlength="9">
</div>
<div class="col-xl-12">
<button type="button" class="btn btn-danger light"
data-bs-dismiss="offcanvas">
Cancelar
</button>
<button type="submit" class="btn btn-warning ms-2">
Atualizar
</button>
</div>
</form>
</div>
</div>
<div class="offcanvas offcanvas-end custom-offcanvas" tabindex="-1"
id="deletar-cliente-{{ $cliente->id }}">
<div class="offcanvas-header pb-0">
<h2 class="modal-title fs-5">Deletar {{ $cliente->name }}</h2>
<button type="button"
class="btn btn-square btn-danger light btn-sm ms-auto"
data-bs-dismiss="offcanvas" aria-label="Close">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="offcanvas-body">
<form class="row" action="{{ route('clientes.delete', $cliente->id) }}"
method="POST">
@csrf
<div class="col-xl-12 mb-3">
<label class="form-label">
Você tem certeza que quer deletar?
</label>
</div>
<div class="col-xl-12">
<button type="button" class="btn btn-danger light"
data-bs-dismiss="offcanvas">
Cancelar
</button>
<button type="submit" class="btn btn-danger ms-2">
Deletar
</button>
</div>
</form>
</div>
</div>
@empty
<tr>
<td colspan="4" class="text-center py-5 text-muted">
Nenhum cliente encontrado.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
<div class="mt-4">
<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mb-0">
<li class="page-item {{ $clientes->onFirstPage() ? 'disabled' : '' }}">
<a class="page-link" href="{{ $clientes->previousPageUrl() ?? '#' }}">Anterior</a>
</li>
@php
$current = $clientes->currentPage();
$last = $clientes->lastPage();
$start = max(1, $current - 2);
$end = min($last, $current + 2);
if ($current <= 3) {
$start = 1;
$end = min(5, $last);
}
if ($current > $last - 3) {
$start = max(1, $last - 4);
$end = $last;
}
@endphp
@if ($start > 1)
<li class="page-item">
<a class="page-link" href="{{ $clientes->url(1) }}">1</a>
</li>
<li class="page-item disabled">
<a class="page-link">...</a>
</li>
@endif
@for ($i = $start; $i <= $end; $i++)
<li class="page-item {{ $current == $i ? 'active' : '' }}">
<a class="page-link" href="{{ $clientes->url($i) }}">{{ $i }}</a>
</li>
@endfor
@if ($end < $last)
<li class="page-item disabled">
<a class="page-link">...</a>
</li>
<li class="page-item">
<a class="page-link" href="{{ $clientes->url($last) }}">{{ $last }}</a>
</li>
@endif
<li class="page-item {{ $clientes->hasMorePages() ? '' : 'disabled' }}">
<a class="page-link" href="{{ $clientes->nextPageUrl() ?? '#' }}">Próximo</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
</div>
<div class="offcanvas offcanvas-end custom-offcanvas" tabindex="-1" id="addTaskmodal">
<div class="offcanvas-header pb-0">
<h2 class="modal-title fs-5">Novo Cliente</h2>
<button type="button" class="btn btn-square btn-danger light btn-sm ms-auto"
data-bs-dismiss="offcanvas" aria-label="Close">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="offcanvas-body">
<form class="row" action="{{ route('clientes.add') }}" method="POST">
@csrf
<div class="col-xl-12 mb-3">
<label class="form-label">Nome do Cliente</label>
<input type="text" class="form-control" name="name" placeholder="Nome do Cliente" required>
</div>
<div class="col-xl-12 mb-3">
<label class="form-label">CNPJ</label>
<input type="text" class="form-control" id="cnpj_novo" name="cnpj" placeholder="CNPJ" required
maxlength="18">
</div>
<div class="col-xl-8 mb-3">
<label class="form-label">Endereço</label>
<input type="text" class="form-control" name="logradouro" placeholder="Endereço" required>
</div>
<div class="col-xl-4 mb-3">
<label class="form-label">Número</label>
<input type="text" class="form-control" name="logradouro_numero" placeholder="Número" required>
</div>
<div class="col-xl-8 mb-3">
<label class="form-label">Complemento</label>
<input type="text" class="form-control" name="logradouro_complemento"
placeholder="Complemento">
</div>
<div class="col-xl-4 mb-3">
<label class="form-label">CEP</label>
<input type="text" class="form-control" id="logradouro_cep_novo" name="logradouro_cep"
placeholder="CEP" required maxlength="9">
</div>
<div class="col-xl-12">
<button type="button" data-bs-dismiss="offcanvas" class="btn btn-danger light">
Cancelar
</button>
<button type="submit" class="btn btn-primary ms-2">
Cadastrar
</button>
</div>
</form>
</div>
</div>
@endsection
@section('styles')
<style>
.table-responsive-dropdown {
overflow-x: auto;
overflow-y: visible !important;
}
.card.overflow-visible,
.card-body.overflow-visible {
overflow: visible !important;
}
.td-options {
position: relative;
overflow: visible !important;
}
.td-options .dropdown-menu {
z-index: 9999;
}
</style>
@endsection
@section('scripts')
<script>
document.getElementById('cnpj_novo').addEventListener('input', function () {
let v = this.value.replace(/\D/g, '');
<script>
function aplicarMascaraCnpj(valor) {
let v = valor.replace(/\D/g, '');
v = v.replace(/^(\d{2})(\d)/, '$1.$2');
v = v.replace(/^(\d{2})\.(\d{3})(\d)/, '$1.$2.$3');
v = v.replace(/\.(\d{3})(\d)/, '.$1/$2');
v = v.replace(/(\d{4})(\d)/, '$1-$2');
return v;
}
// Monta a máscara
v = v.replace(/^(\d{2})(\d)/, "$1.$2");
v = v.replace(/^(\d{2})\.(\d{3})(\d)/, "$1.$2.$3");
v = v.replace(/\.(\d{3})(\d)/, ".$1/$2");
v = v.replace(/(\d{4})(\d)/, "$1-$2");
function aplicarMascaraCep(valor) {
let v = valor.replace(/\D/g, '');
v = v.replace(/^(\d{5})(\d)/, '$1-$2');
return v;
}
this.value = v;
});
</script>
<script>
document.getElementById('cnpj_editar').addEventListener('input', function () {
let v = this.value.replace(/\D/g, '');
// Monta a máscara
v = v.replace(/^(\d{2})(\d)/, "$1.$2");
v = v.replace(/^(\d{2})\.(\d{3})(\d)/, "$1.$2.$3");
v = v.replace(/\.(\d{3})(\d)/, ".$1/$2");
v = v.replace(/(\d{4})(\d)/, "$1-$2");
this.value = v;
});
</script>
<script>
document.getElementById('logradouro_cep_novo').addEventListener('input', function () {
let v = this.value.replace(/\D/g, '');
v = v.replace(/^(\d{5})(\d)/, "$1-$2");
this.value = v;
});
</script>
<script>
document.getElementById('logradouro_cep_editar').addEventListener('input', function () {
let v = this.value.replace(/\D/g, '');
v = v.replace(/^(\d{5})(\d)/, "$1-$2");
this.value = v;
});
</script>
document.addEventListener('input', function(e) {
if (e.target.id === 'cnpj_novo' || e.target.classList.contains('cnpj-editar')) {
e.target.value = aplicarMascaraCnpj(e.target.value);
}
if (e.target.id === 'logradouro_cep_novo' || e.target.classList.contains('cep-editar')) {
e.target.value = aplicarMascaraCep(e.target.value);
}
});
</script>
@endsection

View File

@@ -1,102 +1,219 @@
@extends('theme.default')
@section('page.title', 'Relatórios')
@section('content')
@include('theme.alertas')
<div class="container-fluid">
<div class="row">
<div class="col-xl-12 bst-seller">
<div class="d-flex align-items-center justify-content-between mb-4">
<div class="container-fluid d-flex flex-column min-vh-100">
<div class="row flex-grow-1">
<div class="col-12 d-flex flex-column bst-seller">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3 mb-4">
<div>
<h4 class="mb-1">Relatórios</h4>
<p class="mb-0 text-muted">Gerencie os relatórios cadastrados</p>
</div>
<div class="d-flex align-items-center">
<!-- Button trigger modal -->
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModal">
<button type="button" class="btn btn-primary" data-bs-toggle="modal"
data-bs-target="#exampleModal">
+ Add Relatório
</button>
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form action="{{ route('relatorios.store') }}" method="POST">
@csrf
<!-- Modal -->
<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form action="{{ route('relatorios.store') }}" method="POST">
@csrf
<div class="modal-header">
<h1 class="modal-title fs-5" id="exampleModalLabel">Novo Relatório</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="cliente_id" class="form-label">Cliente</label>
<select name="cliente_id" id="cliente_id" class="form-select" required>
<option value="">Carregando clientes...</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary"
data-bs-dismiss="modal">Fechar</button>
<button type="submit" class="btn btn-primary">Criar relatório</button>
</div>
</form>
<div class="modal-header">
<h1 class="modal-title fs-5" id="exampleModalLabel">Novo Relatório</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="cliente_id" class="form-label">Cliente</label>
<select name="cliente_id" id="cliente_id" class="form-select" required>
<option value="">Carregando clientes...</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary"
data-bs-dismiss="modal">Fechar</button>
<button type="submit" class="btn btn-primary">Criar relatório</button>
</div>
</form>
</div>
</div>
</div>
<div class="card h-auto">
<div class="card-header border-0 align-items-center">
<div id="tableCustomerFilter"></div>
<div id="tableCustomerExcelBTN"></div>
<div class="card flex-grow-1 overflow-visible d-flex flex-column">
<div class="card-header border-0 d-flex flex-wrap align-items-center justify-content-between gap-3">
<div>
<h5 class="mb-0">Lista de relatórios</h5>
</div>
<div class="d-flex flex-wrap align-items-center gap-2">
<div id="tableCustomerFilter"></div>
<div id="tableCustomerExcelBTN"></div>
</div>
</div>
<div class="card-body table-card-body px-0 pt-0 pb-2">
<div class="table-responsive check-wrapper">
<table id="customerTable" class="table">
<div class="card-body table-card-body px-0 pt-0 pb-2 overflow-visible d-flex flex-column">
<div class="table-responsive check-wrapper table-responsive-dropdown flex-grow-1">
<table id="customerTable" class="table align-middle mb-0">
<thead class="table-light">
<tr>
<th class="mw-100">ID</th>
<th class="mw-200">Título</th>
<th class="mw-100">Status</th>
<th class="mw-100">Opções</th>
<th class="mw-100 text-end pe-4">Opções</th>
</tr>
</thead>
<tbody>
@foreach ($relatorios as $relatorio)
@forelse ($relatorios as $relatorio)
<tr>
<td><span>{{$relatorio->id}}</span></td>
<td>
<div class="d-flex">
<img src="assets/images/avatar/small/avatar1.webp"
class="avatar avatar-sm me-2" alt="">
<div class="d-flex align-items-center">
<img src="@if (isset($relatorio->cliente['logotipo']) && !empty($relatorio->cliente['logotipo']))
{{ $relatorio->cliente['logotipo'] }}
@else
https://cloudessential.tech/assets/logo-cloud-essential-D_wtjG9m.png
@endif"
class="avatar avatar-sm me-3 rounded" alt="Logo do cliente">
<div class="clearfix">
<h6 class="mb-0">{{$relatorio->titulo}}</h6>
<h6 class="mb-1">{{ $relatorio->title }}</h6>
<span class="text-muted">
@if (isset($relatorio->cliente['name']) && !empty($relatorio->cliente['name']))
{{ $relatorio->cliente['name'] }}
@else
Sem cliente associado
@endif
</span>
</div>
</div>
</td>
<td><span class="badge badge-success light">{{$relatorio->status}}</span></td>
<td><span></span></td>
</tr>
@endforeach
<td>
<span class="badge badge-success light">
{{ $relatorio->status }}
</span>
</td>
<td class="td-options text-end pe-4">
<div class="dropdown">
<button class="btn btn-primary light sharp" type="button"
data-bs-toggle="dropdown" aria-expanded="false">
Ações
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a class="dropdown-item" href="javascript:void(0);">
Editar Relatório
</a>
</li>
<li>
<a class="dropdown-item text-danger"
href="javascript:void(0);">
Apagar Relatório
</a>
</li>
</ul>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="3" class="text-center py-5 text-muted">
Nenhum relatório encontrado.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
<div class="mt-4">
<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mb-0">
<li class="page-item {{ $relatorios->onFirstPage() ? 'disabled' : '' }}">
<a class="page-link" href="{{ $relatorios->previousPageUrl() ?? '#' }}">Anterior</a>
</li>
@php
$current = $relatorios->currentPage();
$last = $relatorios->lastPage();
$start = max(1, $current - 2);
$end = min($last, $current + 2);
if ($current <= 3) {
$start = 1;
$end = min(5, $last);
}
if ($current > $last - 3) {
$start = max(1, $last - 4);
$end = $last;
}
@endphp
@if ($start > 1)
<li class="page-item">
<a class="page-link" href="{{ $relatorios->url(1) }}">1</a>
</li>
<li class="page-item disabled">
<a class="page-link">...</a>
</li>
@endif
@for ($i = $start; $i <= $end; $i++)
<li class="page-item {{ $current == $i ? 'active' : '' }}">
<a class="page-link" href="{{ $relatorios->url($i) }}">{{ $i }}</a>
</li>
@endfor
@if ($end < $last)
<li class="page-item disabled">
<a class="page-link">...</a>
</li>
<li class="page-item">
<a class="page-link" href="{{ $relatorios->url($last) }}">{{ $last }}</a>
</li>
@endif
<li class="page-item {{ $relatorios->hasMorePages() ? '' : 'disabled' }}">
<a class="page-link" href="{{ $relatorios->nextPageUrl() ?? '#' }}">Próximo</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
</div>
@endsection
@section('scripts')
<script>
document.addEventListener('DOMContentLoaded', function () {
document.addEventListener('DOMContentLoaded', function() {
const modal = document.getElementById('exampleModal');
const select = document.getElementById('cliente_id');
modal.addEventListener('show.bs.modal', function () {
modal.addEventListener('show.bs.modal', function() {
select.innerHTML = '<option value="">Carregando clientes...</option>';
fetch("{{ route('clientes.lista') }}")
@@ -118,4 +235,26 @@
});
});
</script>
@endsection
@section('styles')
<style>
.table-responsive-dropdown {
overflow-x: auto;
overflow-y: visible !important;
}
.card.overflow-visible,
.card-body.overflow-visible {
overflow: visible !important;
}
.td-options {
position: relative;
overflow: visible !important;
}
.td-options .dropdown-menu {
z-index: 9999;
}
</style>
@endsection

File diff suppressed because it is too large Load Diff

View File

@@ -10,11 +10,11 @@
<link href="{{ asset('assets/css/plugins.css') }}" rel="stylesheet">
<link href="{{ asset('assets/css/style.css') }}" rel="stylesheet">
<!-- Plugins Stylesheet -->
<link href="/assets/vendor/metismenu/dist/metisMenu.min.css" rel="stylesheet">
<link href="/assets/vendor/bootstrap-select/dist/css/bootstrap-select.min.css" rel="stylesheet">
<link class="main-switcher" href="/assets/css/switcher.css" rel="stylesheet">
<link href="{{ asset('assets/vendor/metismenu/dist/metisMenu.min.css') }}" rel="stylesheet">
<link href="{{ asset('assets/vendor/bootstrap-select/dist/css/bootstrap-select.min.css') }}" rel="stylesheet">
<link class="main-switcher" href="{{ asset('assets/css/switcher.css') }}" rel="stylesheet">
<link href="assets/vendor/dropzone/dropzone.min.css" rel="stylesheet">
<link href="{{ asset('assets/vendor/dropzone/dropzone.min.css') }}" rel="stylesheet">
@yield('styles')
</head>

View File

@@ -9,4 +9,5 @@ Artisan::command('inspire', function () {
})->purpose('Display an inspiring quote');
Schedule::command('sincronismo-clientes:all')->dailyAt('10:00');
Schedule::command('sincronismo-clientes:all')->dailyAt('18:00');
Schedule::command('sincronismo-clientes:all')->dailyAt('18:00');
Schedule::command('conditional-access:project-shadow --chunk=1000')->hourly();

View File

@@ -5,6 +5,7 @@ use App\Http\Controllers\M365CredController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\ClientesRelatorioController;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ConditionalAccessReportController;
@@ -20,7 +21,8 @@ Route::middleware('auth')->group(function () {
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
Route::get('/dashboard', function () {
return view('dashboard'); })->name('dashboard');
return view('dashboard');
})->name('dashboard');
## Clientes
Route::get('/clientes', [ClientesController::class, 'index'])->name('clientes');
@@ -43,6 +45,17 @@ Route::middleware('auth')->group(function () {
## Relatórios
Route::get('/relatorios', [ClientesRelatorioController::class, 'index'])->name('relatorios');
Route::post('/relatorios/store', [ClientesRelatorioController::class, 'store'])->name('relatorios.store');
Route::get('/relatorios/acesso-condicional', [ConditionalAccessReportController::class, 'index'])
->name('reports.conditional-access');
Route::get('/relatorios/acesso-condicional/summary', [ConditionalAccessReportController::class, 'summary'])
->name('reports.conditional-access.summary');
Route::get('/relatorios/acesso-condicional/data', [ConditionalAccessReportController::class, 'data'])
->name('reports.conditional-access.data');
Route::get('/relatorios/acesso-condicional/export', [ConditionalAccessReportController::class, 'export'])
->name('reports.conditional-access.export');
});
require __DIR__ . '/auth.php';