File manager - Edit - /var/www/html/portalHomolog/app/Http/Controllers/ProjetosProgramas/ProjetosProgramasController.php
Back
<?php namespace App\Http\Controllers\ProjetosProgramas; use App\Http\Controllers\Controller; use App\Http\Requests\ProjetosProgramas\ProjetosProgramasDestroyManyRequest; use App\Http\Requests\ProjetosProgramas\ProjetosProgramasHistoricoAndamentoStoreRequest; use App\Http\Requests\ProjetosProgramas\ProjetosProgramasPrioridadesAdicionarRequest; use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Storage; use Illuminate\Validation\Rule; use Illuminate\Support\Str; use Symfony\Component\HttpFoundation\Response; use App\Helpers\FileUploadHelper; use App\Http\Resources\ProjetosProgramas\ProjetoProgramaVisualizacaoResource; use App\Models\ProjetosProgramas\{ ProjetoPrograma, ProjetoProgramaHistorico, Programa, Objetivo, Tipo, Etapa, Prazo, Fonte, Eixo, Area, Ods, Anexo }; use App\Models\Structure; use App\Models\User; use Carbon\Carbon; class ProjetosProgramasController extends Controller { private string $permissao = 'GERENCIADOR_DE_PROJETOS_E_PROGRAMAS'; /** * Chaves de query aceitas para reaplicar os mesmos filtros da listagem (ex.: exclusão em massa). * * @var list<string> */ private const LISTAGEM_FILTROS_QUERY_KEYS = [ 'search', 'gestor', 'prioritario', 'andamento_cor', 'orcamento_status_cor', 'status_registro', 'bairro', 'bairo', 'tipo', 'status', 'etapa', 'classificacao', 'prazo', ]; private array $perPageOptions; private array $currencyFields = [ 'orcamento_previsto', 'orcamento_disponivel', ]; public function __construct() { $this->middleware('projetosProgramas')->except([ 'painelEstrategico', 'prioridadeToggleDashboard', 'prioridadesOrdenarDashboard', 'prioridadesCandidatosDashboard', 'prioridadesAdicionarDashboard', ]); $this->perPageOptions = config('projetos_programas.per_page_options', [10, 25, 50, 100]); } /** * Exibe a listagem de projetos e programas com filtros e paginação. * * Busca apenas opções de filtro que existem nos projetos cadastrados * para melhorar a experiência do usuário. * * @param Request $request Requisição HTTP com parâmetros de filtro e paginação * @return \Illuminate\View\View|\Illuminate\Http\RedirectResponse */ public function index(Request $request) { if ($response = $this->checkPermissao($this->permissao, 'visualizar')) { return $response; } if ($redirect = $this->redirectIfProgramaObjetivoFormTabMismatch($request, 'tab')) { return $redirect; } return view( 'admin.projetosProgramas.index', $this->buildProjetosProgramasIndexPayload($request, 'SUPOP') ); } /** * Retorna JSON com todos os dados do projeto para exibição em modal (somente leitura). */ public function visualizar(Request $request, ProjetoPrograma $projeto) { if ($response = $this->checkPermissao($this->permissao, 'visualizar')) { if ($request->wantsJson()) { return response()->json(['message' => __('Permissão negada.')], Response::HTTP_FORBIDDEN); } return $response; } $projeto->load([ 'gestor', 'tipo', 'programa', 'objetivo', 'classificacao', 'etapa', 'prazo', 'fonte', 'eixo', 'area', 'ods', 'orgaosEnvolvidos', 'historicos', 'anexos', 'prioridadeDefinidaPor', ]); $payload = (new ProjetoProgramaVisualizacaoResource( $projeto, $this->statusOptions(), $this->andamentoOrcamentoCoresOptions() ))->toArray(); return response()->json($payload); } /** * Lista o histórico de andamentos do projeto (JSON) para o modal da aba Projetos. */ public function historicosAndamentoList(Request $request, ProjetoPrograma $projeto): JsonResponse|RedirectResponse { if ($response = $this->checkPermissao($this->permissao, 'editar')) { if ($request->wantsJson()) { return response()->json(['message' => __('Permissão negada.')], Response::HTTP_FORBIDDEN); } return $response; } $historicos = $projeto->historicos() ->with('anexos') ->orderByDesc('data') ->orderByDesc('created_at') ->get() ->map(function (ProjetoProgramaHistorico $h) use ($projeto): array { return [ 'id' => $h->id, 'data' => $h->data?->format('Y-m-d'), 'data_formatada' => $h->data ? $h->data->format('d/m/Y') : '—', 'status' => $h->status, 'texto' => $h->texto, 'anexos' => $this->serializarAnexosHistorico($projeto, $h), ]; }); return response()->json(['historicos' => $historicos]); } /** * Registra um novo item no histórico de andamentos (e opcionalmente sincroniza o MAPÃO). */ public function storeHistoricoAndamento( ProjetosProgramasHistoricoAndamentoStoreRequest $request, ProjetoPrograma $projeto ): RedirectResponse { if ($response = $this->checkPermissao($this->permissao, 'editar')) { return $response; } $data = $request->validated(); $dataHistorico = $data['data'] ?? null; if ($dataHistorico === null || $dataHistorico === '') { $dataHistorico = now()->toDateString(); } DB::beginTransaction(); try { $historico = $projeto->historicos()->create([ 'data' => $dataHistorico, 'status' => $data['status'] ?? null, 'texto' => $data['texto'], ]); $arquivos = $request->file('anexos'); if ($arquivos !== null) { $listaArquivos = \is_array($arquivos) ? $arquivos : [$arquivos]; $this->persistirAnexosHistorico($projeto, $historico, $listaArquivos); } if ($request->boolean('sincronizar_andamento_mapao')) { $andamentoCor = $data['andamento_cor'] ?? null; $andamentoCor = $andamentoCor !== null && $andamentoCor !== '' ? (string) $andamentoCor : null; $projeto->update([ 'andamento' => $data['texto'], 'andamento_cor' => $andamentoCor, 'orcamento_status_cor' => $andamentoCor, ]); } DB::commit(); } catch (\Throwable $e) { DB::rollBack(); return redirect()->back() ->with('error', __('Não foi possível salvar o andamento. Tente novamente.')) ->withInput(); } return redirect()->back() ->with('success', __('* Andamento registrado com sucesso.')); } /** * Dados para embutir o módulo (abas resumidas + prioridades) na aba do Dashboard Estratégico. * * @return array<string, mixed> */ public function buildProjetosProgramasDashboardEmbedPayload(Request $request): array { /** @var User|null $user */ $user = Auth::user(); if ( !$user || !collect($user->listaPermissoes())->contains('GERENCIADOR_DO_DASHBOARD_ESTRATEGICO') ) { return []; } $prioridadeOrigem = $this->userIsPrefeitoAngra($user) ? 'PREFEITO' : 'SUPOP'; return array_merge( $this->buildProjetosProgramasIndexPayload($request, $prioridadeOrigem, 'pp_subtab', true), [ 'painelDashboard' => true, 'ppPainelEmbutidoDashboard' => true, 'ppModuleRoute' => 'admin.dashboardEstrategico.projetosProgramas', 'prioridadesOrdenarRoute' => 'admin.dashboardEstrategico.projetosProgramas.prioridades.ordenar', 'prioridadeToggleRoute' => 'admin.dashboardEstrategico.projetosProgramas.prioridade.toggle', 'prioridadesCandidatosRoute' => 'admin.dashboardEstrategico.projetosProgramas.prioridades.candidatos', 'prioridadesAdicionarRoute' => 'admin.dashboardEstrategico.projetosProgramas.prioridades.adicionar', ] ); } /** * Painel sincronizado com o módulo SUPOP (Dashboard Estratégico — permissão específica). */ public function painelEstrategico(Request $request) { /** @var User|null $user */ $user = Auth::user(); if ( !$user || !collect($user->listaPermissoes())->contains('GERENCIADOR_DO_DASHBOARD_ESTRATEGICO') ) { abort(403); } if ($redirect = $this->redirectIfProgramaObjetivoFormTabMismatch($request, 'tab', true)) { return $redirect; } $prioridadeOrigem = $this->userIsPrefeitoAngra($user) ? 'PREFEITO' : 'SUPOP'; return view( 'admin.projetosProgramas.index', array_merge($this->buildProjetosProgramasIndexPayload($request, $prioridadeOrigem, 'tab', true), [ 'painelDashboard' => true, 'prioridadesOrdenarRoute' => 'admin.dashboardEstrategico.projetosProgramas.prioridades.ordenar', 'prioridadeToggleRoute' => 'admin.dashboardEstrategico.projetosProgramas.prioridade.toggle', 'prioridadesCandidatosRoute' => 'admin.dashboardEstrategico.projetosProgramas.prioridades.candidatos', 'prioridadesAdicionarRoute' => 'admin.dashboardEstrategico.projetosProgramas.prioridades.adicionar', ]) ); } public function prioridadeToggleDashboard(Request $request, ProjetoPrograma $projeto) { return $this->prioridadeToggle($request, $projeto); } public function prioridadesOrdenarDashboard(Request $request) { return $this->prioridadesOrdenar($request); } public function prioridadesCandidatosDashboard(Request $request): JsonResponse { return $this->prioridadesCandidatos($request); } public function prioridadesAdicionarDashboard(ProjetosProgramasPrioridadesAdicionarRequest $request): JsonResponse { return $this->prioridadesAdicionar($request); } /** * Lista JSON de projetos para o modal de novas prioridades (cadastro completo / incompleto conforme contexto). */ public function prioridadesCandidatos(Request $request): JsonResponse { /** @var User|null $user */ $user = Auth::user(); if (!$this->userCanGerenciarPrioridadeProjeto($user)) { return response()->json(['message' => __('Permissão negada.')], Response::HTTP_FORBIDDEN); } $mapaoApenasCadastroCompleto = $this->dashboardEmbedListaSomenteCompleto($user) || $request->boolean('mapao_completo'); $listRequest = Request::create('/', 'GET', []); $rows = $this->projetosProgramasFilteredBaseQuery($listRequest, $mapaoApenasCadastroCompleto) ->with(['gestor']) ->orderBy('denominacao') ->get([ 'id', 'denominacao', 'codigo', 'prioritario', 'status_registro', 'gestor_structure_id', ]); $projects = $rows->map(function (ProjetoPrograma $p): array { $gestorLabel = null; if ($p->relationLoaded('gestor') && $p->gestor) { $gestorLabel = $p->gestor->abbreviation ? ($p->gestor->abbreviation . ' - ' . $p->gestor->name) : $p->gestor->name; } return [ 'id' => (string) $p->id, 'denominacao' => (string) ($p->denominacao ?? ''), 'codigo' => $p->codigo !== null && $p->codigo !== '' ? (string) $p->codigo : null, 'prioritario' => (bool) $p->prioritario, 'pode_priorizar' => $p->isCadastroCompleto(), 'gestor' => $gestorLabel, ]; }); return response()->json([ 'projects' => $projects, ]); } /** * Marca vários projetos como prioritários de uma vez (cadastro concluído, ainda não prioritários). */ public function prioridadesAdicionar(ProjetosProgramasPrioridadesAdicionarRequest $request): JsonResponse { /** @var User|null $user */ $user = Auth::user(); if (!$this->userCanGerenciarPrioridadeProjeto($user)) { return response()->json(['message' => __('Permissão negada.')], Response::HTTP_FORBIDDEN); } $data = $request->validated(); $origem = $this->resolvePrioridadeOrigemPersistencia($user, $data['origem'] ?? null); $ids = array_values(array_unique($data['project_ids'])); $query = ProjetoPrograma::query()->whereIn('id', $ids); if ($this->dashboardEmbedListaSomenteCompleto($user)) { $query->where('status_registro', 'COMPLETO'); } $projetos = $query->orderBy('denominacao')->get(); $added = 0; $skippedPrioritized = 0; $skippedIncomplete = 0; DB::beginTransaction(); try { $max = (int) ProjetoPrograma::where('prioritario', true)->max('ordem_prioridade'); $next = $max > 0 ? $max : 0; foreach ($projetos as $projeto) { if ($projeto->prioritario) { $skippedPrioritized++; continue; } if (!$projeto->isCadastroCompleto()) { $skippedIncomplete++; continue; } $next++; $projeto->update([ 'prioritario' => true, 'ordem_prioridade' => $next, 'prioridade_origem' => $origem, 'prioridade_definida_por_user_id' => $user?->id, 'acompanhamento_prioritario' => true, 'acompanhamento_governo' => true, ]); $added++; } DB::commit(); } catch (\Throwable $e) { DB::rollBack(); return response()->json([ 'message' => $e->getMessage(), ], Response::HTTP_INTERNAL_SERVER_ERROR); } if ($added === 0) { return response()->json([ 'message' => __('Nenhum projeto foi adicionado. Verifique se o cadastro está concluído, se o projeto já não é prioritário e se você tem permissão para este perfil.'), 'added' => 0, ], Response::HTTP_UNPROCESSABLE_ENTITY); } $ignored = $skippedPrioritized + $skippedIncomplete + (count($ids) - $projetos->count()); $message = $ignored > 0 ? __(':n projeto(s) priorizado(s). :i registro(s) ignorado(s) (já prioritários, cadastro em edição ou fora do escopo).', ['n' => $added, 'i' => $ignored]) : __(':n projeto(s) priorizado(s) com sucesso.', ['n' => $added]); return response()->json([ 'message' => $message, 'added' => $added, ]); } /** * Garante que formulários embutidos (pp_programa / pp_objetivo) só apareçam com a aba correspondente na URL. */ protected function redirectIfProgramaObjetivoFormTabMismatch( Request $request, string $tabQueryKey, bool $ocultarAbasProgramaObjetivoNoDashboardEstrategico = false ): ?RedirectResponse { if ($ocultarAbasProgramaObjetivoNoDashboardEstrategico) { if ($request->filled('pp_programa') || $request->filled('pp_objetivo')) { $next = array_merge($request->query(), [$tabQueryKey => 'projetos']); unset($next['pp_programa'], $next['pp_objetivo']); return redirect()->to($request->url() . (count($next) ? '?' . http_build_query($next) : '')); } $tab = (string) $request->get($tabQueryKey, ''); if (in_array($tab, ['programas', 'objetivos'], true)) { return redirect()->to($request->fullUrlWithQuery([$tabQueryKey => 'projetos'])); } return null; } if ($request->filled('pp_programa') && (string) $request->get($tabQueryKey) !== 'programas') { return redirect()->to($request->fullUrlWithQuery([$tabQueryKey => 'programas'])); } if ($request->filled('pp_objetivo') && (string) $request->get($tabQueryKey) !== 'objetivos') { return redirect()->to($request->fullUrlWithQuery([$tabQueryKey => 'objetivos'])); } return null; } /** * @param bool $mapaoApenasCadastroCompleto Quando true (Dashboard Estratégico), restringe projetos a status_registro COMPLETO. * @return array<string, mixed> */ protected function buildProjetosProgramasIndexPayload( Request $request, string $prioridadeOrigem, string $tabQueryKey = 'tab', bool $mapaoApenasCadastroCompleto = false ): array { $defaultInnerTab = 'projetos'; $tabAtual = (string) $request->get($tabQueryKey, $defaultInnerTab); if (! in_array($tabAtual, ['dashboard', 'projetos', 'programas', 'objetivos', 'prioridades', 'mapa'], true)) { $tabAtual = $defaultInnerTab; } if ($mapaoApenasCadastroCompleto && in_array($tabAtual, ['programas', 'objetivos'], true)) { $tabAtual = $defaultInnerTab; } $baseFiltered = $this->projetosProgramasFilteredBaseQuery($request, $mapaoApenasCadastroCompleto); $projetosProgramasResumo = $this->computeProjetosProgramasResumoKpis($baseFiltered); $ppDashboardCharts = $this->buildProjetosProgramasDashboardChartPayload($baseFiltered); $ppDashboardUltimosHistoricos = $this->buildProjetosProgramasUltimosHistoricosPayload($baseFiltered); $query = $this->filteredProjectsQuery($request, $mapaoApenasCadastroCompleto); $mapaoIdentificadores = $this->buildMapaoIdentificadorPorProjetoId( (clone $query)->with(['objetivo', 'programa'])->get() ); $perPage = $this->resolvePerPage((int) $request->get('per_page')); $projects = $query->paginate($perPage)->appends($request->query()); /** @var User|null $user */ $user = Auth::user(); $permissionDetails = $user?->getPermissaoDetalhes($this->permissao); $canDelete = (bool) ($permissionDetails['excluir'] ?? false); $canEditProjeto = (bool) ($permissionDetails['editar'] ?? false); $canManagePriorities = $this->userCanGerenciarPrioridadeProjeto($user); $filterParams = $request->except([ 'per_page', 'page_programas', 'per_page_programas', 'page_objetivos', 'per_page_objetivos', 'filtros_visiveis', 'pp_programa', 'pp_objetivo', ]); // Evita repassar parâmetros não escalares (ex.: columns[]) como hidden inputs na listagem. $filterParams = array_filter($filterParams, static function ($v) { return is_scalar($v) && $v !== '' && $v !== null; }); $prioritizedQuery = ProjetoPrograma::with(['gestor', 'prioridadeDefinidaPor']) ->where('prioritario', true); if ($mapaoApenasCadastroCompleto) { $prioritizedQuery->where('status_registro', 'COMPLETO'); } $prioritizedProjects = $prioritizedQuery ->orderBy('ordem_prioridade') ->orderByDesc('created_at') ->get(); $perPageProgramas = $this->resolvePerPage((int) $request->get('per_page_programas')); $currentPerPageProgramas = $perPageProgramas; $programasQuery = Programa::query(); if ($request->filled('search_programas')) { $searchProgramas = (string) $request->get('search_programas'); $programasQuery->where(function ($q) use ($searchProgramas): void { $q->where('nome', 'like', '%' . $searchProgramas . '%') ->orWhere('codigo', 'like', '%' . $searchProgramas . '%'); }); } $programas = $programasQuery ->orderByRaw('codigo IS NULL') ->orderBy('codigo') ->orderBy('nome') ->paginate($perPageProgramas, ['*'], 'page_programas') ->appends($request->query()); $perPageObjetivos = $this->resolvePerPage((int) $request->get('per_page_objetivos')); $currentPerPageObjetivos = $perPageObjetivos; $objetivosQuery = Objetivo::query(); if ($request->filled('search_objetivos')) { $searchObjetivos = (string) $request->get('search_objetivos'); $objetivosQuery->where('nome', 'like', '%' . $searchObjetivos . '%'); } $objetivos = $objetivosQuery ->orderBy('nome') ->paginate($perPageObjetivos, ['*'], 'page_objetivos') ->appends($request->query()); $ppProgramaForm = null; $ppObjetivoForm = null; if (! $mapaoApenasCadastroCompleto) { $permProgObj = $permissionDetails ?? []; $rawPrograma = $request->query('pp_programa'); if ($tabAtual === 'programas' && $rawPrograma !== null && $rawPrograma !== '') { if ($rawPrograma === 'novo' && ($permProgObj['adicionar'] ?? false)) { $ppProgramaForm = [ 'mode' => 'create', 'programa' => new Programa(), ]; } elseif (ctype_digit((string) $rawPrograma) && ($permProgObj['editar'] ?? false)) { $programaEdicao = Programa::query()->find((int) $rawPrograma); if ($programaEdicao) { $ppProgramaForm = [ 'mode' => 'edit', 'programa' => $programaEdicao, ]; } } } $rawObjetivo = $request->query('pp_objetivo'); if ($tabAtual === 'objetivos' && $rawObjetivo !== null && $rawObjetivo !== '') { if ($rawObjetivo === 'novo' && ($permProgObj['adicionar'] ?? false)) { $ppObjetivoForm = [ 'mode' => 'create', 'objetivo' => new Objetivo(), ]; } elseif (ctype_digit((string) $rawObjetivo) && ($permProgObj['editar'] ?? false)) { $objetivoEdicao = Objetivo::query()->find((int) $rawObjetivo); if ($objetivoEdicao) { $ppObjetivoForm = [ 'mode' => 'edit', 'objetivo' => $objetivoEdicao, ]; } } } } $gestoresIdsQuery = ProjetoPrograma::whereNotNull('gestor_structure_id'); if ($mapaoApenasCadastroCompleto) { $gestoresIdsQuery->where('status_registro', 'COMPLETO'); } $gestoresIds = $gestoresIdsQuery->distinct() ->pluck('gestor_structure_id'); $gestores = Structure::whereIn('id', $gestoresIds) ->orderBy('abbreviation') ->get(); $bairrosQuery = ProjetoPrograma::query()->whereNotNull('bairro')->where('bairro', '!=', ''); if ($mapaoApenasCadastroCompleto) { $bairrosQuery->where('status_registro', 'COMPLETO'); } $bairrosFiltroOpcoes = $bairrosQuery->distinct() ->orderBy('bairro') ->pluck('bairro') ->filter(static fn ($b) => $b !== null && trim((string) $b) !== '') ->map(static fn ($b) => (string) $b) ->values(); $mapaoCoresLegenda = $this->andamentoOrcamentoCoresOptions(); $statusRegistroFiltroOpcoes = [ 'EM_EDICAO' => __('Em edição'), 'COMPLETO' => __('Concluído'), ]; return [ 'projects' => $projects, 'mapaoIdentificadores' => $mapaoIdentificadores, 'gestores' => $gestores, 'mapaoCoresLegenda' => $mapaoCoresLegenda, 'bairrosFiltroOpcoes' => $bairrosFiltroOpcoes, 'bairroTodosRotulo' => ProjetoPrograma::bairroTodosRotulo(), 'statusRegistroFiltroOpcoes' => $statusRegistroFiltroOpcoes, 'perPageOptions' => $this->perPageOptions, 'currentPerPage' => $perPage, 'canDelete' => $canDelete, 'canEditProjeto' => $canEditProjeto, 'canManagePriorities' => $canManagePriorities, 'prioridadesCandidatosRoute' => 'admin.projetosProgramas.prioridades.candidatos', 'prioridadesAdicionarRoute' => 'admin.projetosProgramas.prioridades.adicionar', 'prioritizedProjects' => $prioritizedProjects, 'prioridadeOrigem' => $prioridadeOrigem, 'filterParams' => $filterParams, 'tabAtual' => $tabAtual, 'programas' => $programas, 'objetivos' => $objetivos, 'currentPerPageProgramas' => $currentPerPageProgramas, 'currentPerPageObjetivos' => $currentPerPageObjetivos, 'currentSort' => $request->get('sort_by', 'created_at'), 'currentSortDir' => $request->get('sort_dir', 'desc'), 'ppTabQueryKey' => $tabQueryKey, 'projetosProgramasResumo' => $projetosProgramasResumo, 'ppDashboardCharts' => $ppDashboardCharts, 'ppDashboardUltimosHistoricos' => $ppDashboardUltimosHistoricos, 'mapaoApenasCadastroCompleto' => $mapaoApenasCadastroCompleto, 'ppProgramaForm' => $ppProgramaForm, 'ppObjetivoForm' => $ppObjetivoForm, 'projetosListagemReturnQuery' => $this->projetosProgramasListagemRetornoFromRequest($request), ]; } public function create() { return view('admin.projetosProgramas.create', [ 'tipos' => Tipo::all(), 'gestores' => Structure::orderBy('abbreviation')->get(), 'objetivos' => Objetivo::orderBy('nome')->get(), 'programas' => Programa::query()->orderByRaw('codigo IS NULL')->orderBy('codigo')->orderBy('nome')->get(), 'programasPpa' => config('projetos_programas.programas_ppa', []), 'bairrosList' => config('projetos_programas.bairros', []), 'project' => new ProjetoPrograma(), 'projetosListagemReturnQuery' => [], ]); } public function identificacao(Request $request, ProjetoPrograma $projeto) { return view('admin.projetosProgramas.create', [ 'tipos' => Tipo::all(), 'gestores' => Structure::orderBy('abbreviation')->get(), 'objetivos' => Objetivo::orderBy('nome')->get(), 'programas' => Programa::query()->orderByRaw('codigo IS NULL')->orderBy('codigo')->orderBy('nome')->get(), 'programasPpa' => config('projetos_programas.programas_ppa', []), 'bairrosList' => config('projetos_programas.bairros', []), 'project' => $projeto, 'projetosListagemReturnQuery' => $this->projetosProgramasListagemRetornoFromRequest($request), ]); } public function storeIdentificacao(Request $request) { $programasPpa = array_keys(config('projetos_programas.programas_ppa', [])); $bairros = array_keys(config('projetos_programas.bairros', [])); // Normaliza consistente com a UI: quando o usuário marca "Não", a select pode vir vazia ('') // e isso pode falhar em validações numéricas; garantimos null antes de validar. if ($request->input('faz_parte_objetivo') === '0') { $request->merge(['project_objective_id' => null]); } if ($request->input('faz_parte_programa') === '0') { $request->merge(['project_program_id' => null]); } $data = $request->validate([ 'project_id' => ['nullable', 'uuid', 'exists:projects_programs,id'], 'denominacao' => ['required', 'string'], 'num_processo_adm' => ['nullable', 'string'], 'empresa_licitada' => ['nullable', 'string', 'max:255'], 'gestor_structure_id' => ['required', 'string', 'exists:structures,id'], 'tipo_id' => ['required', 'integer', 'exists:project_program_types,id'], 'numero_ordem' => ['nullable', 'string', 'max:255'], 'faz_parte_objetivo' => ['required', 'in:0,1'], 'project_objective_id' => [ Rule::requiredIf($request->input('faz_parte_objetivo') === '1'), 'nullable', 'integer', 'exists:project_objectives,id', ], 'faz_parte_programa' => ['required', 'in:0,1'], 'project_program_id' => [ Rule::requiredIf($request->input('faz_parte_programa') === '1'), 'nullable', 'integer', 'exists:project_programs,id', ], 'programa_ppa' => ['nullable', Rule::in($programasPpa)], 'cep' => [ 'nullable', 'string', 'max:12', function ($attribute, mixed $value, \Closure $fail): void { if ($value === null || $value === '') { return; } $digits = preg_replace('/\D/', '', (string) $value); if (strlen($digits) !== 8) { $fail(__('O CEP deve ter 8 dígitos.')); } }, ], 'endereco' => ['nullable', 'string'], 'latitude' => ['nullable', 'numeric', 'between:-90,90'], 'longitude' => ['nullable', 'numeric', 'between:-180,180'], 'bairro' => ['nullable', Rule::in($bairros)], ], $this->validationMessages(), $this->validationAttributes()); // Mantém consistência com a UI: se o usuário marcou "Não", // os relacionamentos devem ser gravados como null. if (($data['faz_parte_objetivo'] ?? '0') === '0') { $data['project_objective_id'] = null; } if (($data['faz_parte_programa'] ?? '0') === '0') { $data['project_program_id'] = null; } unset($data['faz_parte_objetivo'], $data['faz_parte_programa']); $cepRaw = $data['cep'] ?? null; if ($cepRaw !== null && $cepRaw !== '') { $d = preg_replace('/\D/', '', (string) $cepRaw); $data['cep'] = strlen($d) === 8 ? (substr($d, 0, 5) . '-' . substr($d, 5)) : null; } else { $data['cep'] = null; } $data['programa_ppa'] = $this->convertSelectableValue($data['programa_ppa'] ?? null, 'programas_ppa'); $data['bairro'] = $this->convertSelectableValue($data['bairro'] ?? null, 'bairros'); $data['identificacao_done'] = true; if (array_key_exists('empresa_licitada', $data) && $data['empresa_licitada'] === '') { $data['empresa_licitada'] = null; } if (!empty($data['project_id'])) { $project = ProjetoPrograma::findOrFail($data['project_id']); unset($data['project_id']); $project->update($data); } else { unset($data['project_id']); $project = ProjetoPrograma::create($data); } $listagem = $this->projetosProgramasListagemQueryParaRedirect($request->input('pp_listagem')); return redirect()->route('admin.projetosProgramas.dados', array_merge( ['projeto' => $project->id], $listagem ))->with('success', __("* Etapa de Identificação salva com sucesso!")); } public function cronograma(Request $request, ProjetoPrograma $projeto) { $projeto->load(['historicos' => function ($q) { $q->orderByDesc('data')->orderByDesc('created_at')->with('anexos'); }]); return view('admin.projetosProgramas.sections.cronograma', [ 'project' => $projeto, 'etapas' => Etapa::ordenadas()->get(), 'andamentoCores' => $this->andamentoOrcamentoCoresOptions(), 'projetosListagemReturnQuery' => $this->projetosProgramasListagemRetornoFromRequest($request), ]); } public function storeCronograma(Request $request, ProjetoPrograma $projeto) { if ($request->has('relacao_ppa') && is_string($request->input('relacao_ppa'))) { $request->merge(['relacao_ppa' => trim($request->input('relacao_ppa'))]); } $coresAndamento = array_keys($this->andamentoOrcamentoCoresOptions()); if ($request->input('andamento_cor') === '') { $request->merge(['andamento_cor' => null]); } $request->validate([ 'prazo_entrega_competencia' => ['nullable', 'regex:/^\d{4}-(0[1-9]|1[0-2])$/'], 'prazo_entrega_dias' => ['nullable', 'integer', 'min:0', 'max:36500'], 'acompanhamento_prioritario' => ['required', 'in:0,1'], 'etapa_id' => ['nullable', 'integer', 'exists:project_program_stages,id'], 'andamento' => ['nullable', 'string'], 'andamento_cor' => ['nullable', Rule::in($coresAndamento)], 'relacao_ppa_federal' => ['required', 'in:0,1'], 'relacao_ppa' => ['nullable', 'string', 'max:2000', 'required_if:relacao_ppa_federal,1'], 'novo_historico' => ['nullable', 'array'], 'novo_historico.*.data' => ['nullable', 'date'], 'novo_historico.*.status' => ['nullable', 'string', 'max:255'], 'novo_historico.*.texto' => ['nullable', 'string'], 'novo_historico.*.anexos' => ['nullable', 'array', 'max:10'], 'novo_historico.*.anexos.*' => ['file', 'max:51200'], 'historicos_remover' => ['nullable', 'array'], 'historicos_remover.*' => ['uuid'], ], $this->validationMessages(), $this->validationAttributes()); $prioritario = $request->input('acompanhamento_prioritario') === '1' || $request->input('acompanhamento_prioritario') === 1; /** @var User|null $user */ $user = Auth::user(); $podeGerenciarPrioridade = $this->userCanGerenciarPrioridadeProjeto($user); $prioridadeCampos = []; if ($prioritario) { if (!$podeGerenciarPrioridade) { return redirect()->back() ->with('error', __('Permissão negada! Entre em contato com o administrador do sistema.')) ->withInput(); } // Acompanhamento prioritário pode ser salvo antes do cadastro completo; entrada na fila do MAPÃO só após COMPLETO. if (!$projeto->prioritario && $projeto->isCadastroCompleto()) { $origem = $this->resolvePrioridadeOrigemPersistencia($user, null); $max = (int) ProjetoPrograma::where('prioritario', true)->max('ordem_prioridade'); $next = $max > 0 ? ($max + 1) : 1; $prioridadeCampos = [ 'prioritario' => true, 'ordem_prioridade' => $next, 'prioridade_origem' => $origem, 'prioridade_definida_por_user_id' => $user?->id, ]; } } elseif ($projeto->prioritario) { if (!$podeGerenciarPrioridade) { return redirect()->back() ->with('error', __('Permissão negada! Entre em contato com o administrador do sistema.')) ->withInput(); } $prioridadeCampos = [ 'prioritario' => false, 'ordem_prioridade' => null, 'prioridade_origem' => null, 'prioridade_definida_por_user_id' => null, ]; } $andamentoCor = $request->input('andamento_cor'); $andamentoCor = $andamentoCor !== null && $andamentoCor !== '' ? (string) $andamentoCor : null; $rawCompetencia = $request->input('prazo_entrega_competencia'); $competencia = \is_string($rawCompetencia) ? trim($rawCompetencia) : null; if ($competencia === '') { $competencia = null; } $prazoDias = null; $prazoCompetenciaGravar = null; if ($competencia !== null) { $prazoCompetenciaGravar = $competencia; $prazoDias = $this->diasPrazoAteFimMesCompetencia($competencia); } elseif ($request->input('prazo_entrega_dias') !== null && $request->input('prazo_entrega_dias') !== '') { $prazoDias = (int) $request->input('prazo_entrega_dias'); } $update = [ 'prazo_entrega_competencia' => $prazoCompetenciaGravar, 'prazo_entrega_dias' => $prazoDias, 'acompanhamento_prioritario' => $prioritario, 'acompanhamento_governo' => $prioritario, 'etapa_id' => $request->filled('etapa_id') ? (int) $request->input('etapa_id') : null, 'andamento' => $request->input('andamento'), 'andamento_cor' => $andamentoCor, 'orcamento_status_cor' => $andamentoCor, 'relacao_ppa' => (string) $request->input('relacao_ppa_federal') === '1' ? (trim((string) $request->input('relacao_ppa', '')) ?: null) : null, 'data_inicio' => null, 'data_termino' => null, 'prazo_id' => null, 'cronograma_done' => true, ]; $update = array_merge($update, $prioridadeCampos); DB::beginTransaction(); try { foreach ($request->input('historicos_remover', []) as $historicoId) { $this->removerHistoricoComAnexos((string) $projeto->id, (string) $historicoId); } foreach ($request->input('novo_historico', []) as $index => $row) { if (!is_array($row)) { continue; } $preenchido = array_filter([ $row['data'] ?? null, $row['status'] ?? null, $row['texto'] ?? null, ], static fn ($v) => $v !== null && $v !== ''); if ($preenchido === []) { continue; } $historico = $projeto->historicos()->create([ 'data' => $row['data'] ?? null, 'status' => $row['status'] ?? null, 'texto' => $row['texto'] ?? null, ]); $arquivos = $request->file("novo_historico.{$index}.anexos"); if ($arquivos !== null) { $listaArquivos = \is_array($arquivos) ? $arquivos : [$arquivos]; $this->persistirAnexosHistorico($projeto, $historico, $listaArquivos); } } $projeto->update($update); DB::commit(); } catch (\Throwable $e) { DB::rollBack(); return redirect()->back() ->with('error', $e->getMessage()) ->withInput(); } $listagem = $this->projetosProgramasListagemQueryParaRedirect($request->input('pp_listagem')); return redirect()->route('admin.projetosProgramas.areas', array_merge( ['projeto' => $projeto->id], $listagem ))->with('success', __("* Etapa de Cronograma salva com sucesso!")); } public function areas(Request $request, ProjetoPrograma $projeto) { return view('admin.projetosProgramas.sections.areas', [ 'project' => $projeto, 'eixos' => Eixo::with('areas')->get(), 'ods' => Ods::all(), 'projetosListagemReturnQuery' => $this->projetosProgramasListagemRetornoFromRequest($request), ]); } public function storeAreas(Request $request, ProjetoPrograma $projeto) { $data = $request->validate([ 'eixo_id' => ['required', 'integer', 'exists:project_program_eixos,id'], 'area_id' => ['required', 'integer', 'exists:project_program_areas,id'], 'ods' => ['nullable', 'array'], 'ods.*' => ['integer', 'exists:project_program_ods_types,id'], ], $this->validationMessages(), $this->validationAttributes()); $areaPertenceAoEixo = Area::where('id', $data['area_id'])->where('eixo_id', $data['eixo_id'])->exists(); if (!$areaPertenceAoEixo) { return redirect()->back() ->withErrors(__("A área selecionada não pertence ao eixo informado.")) ->withInput(); } $projeto->update([ 'eixo_id' => $data['eixo_id'], 'area_id' => $data['area_id'], 'areas_done' => true, ]); $projeto->ods()->sync($data['ods'] ?? []); $listagem = $this->projetosProgramasListagemQueryParaRedirect($request->input('pp_listagem')); return redirect()->route('admin.projetosProgramas.investimento', array_merge( ['projeto' => $projeto->id], $listagem ))->with('success', __("* Etapa de Projetos e Ações salva com sucesso!")); } public function contato(Request $request, ProjetoPrograma $projeto) { $listagem = $this->projetosProgramasListagemRetornoFromRequest($request); return redirect()->route('admin.projetosProgramas.dados', array_merge( ['projeto' => $projeto->id], $listagem )); } public function storeContato(Request $request, ProjetoPrograma $projeto) { $listagem = $this->projetosProgramasListagemQueryParaRedirect($request->input('pp_listagem')); return redirect()->route('admin.projetosProgramas.dados', array_merge( ['projeto' => $projeto->id], $listagem )); } public function dados(Request $request, ProjetoPrograma $projeto) { $projeto->load('orgaosEnvolvidos'); return view('admin.projetosProgramas.sections.dados', [ 'project' => $projeto, 'orgaos' => Structure::orderBy('abbreviation')->get(), 'projetosListagemReturnQuery' => $this->projetosProgramasListagemRetornoFromRequest($request), ]); } public function storeDados(Request $request, ProjetoPrograma $projeto) { $data = $request->validate([ 'descricao_objeto' => ['nullable', 'string'], 'gargalos' => ['nullable', 'string'], 'observacoes' => ['nullable', 'string'], 'orgaos' => ['nullable', 'array'], 'orgaos.*' => ['string', 'exists:structures,id'], ], $this->validationMessages(), $this->validationAttributes()); $data['show_metas_relatorio'] = true; $data['dados_done'] = true; $data['descricao_responsabilidades'] = null; $data['status_id'] = null; $data['dificuldade_id'] = null; $orgaos = $data['orgaos'] ?? []; unset($data['orgaos']); $projeto->update($data); $projeto->orgaosEnvolvidos()->sync($orgaos); $listagem = $this->projetosProgramasListagemQueryParaRedirect($request->input('pp_listagem')); return redirect()->route('admin.projetosProgramas.cronograma', array_merge( ['projeto' => $projeto->id], $listagem ))->with('success', __("* Etapa de Dados salva com sucesso!")); } public function investimento(Request $request, ProjetoPrograma $projeto) { return view('admin.projetosProgramas.sections.investimento', [ 'project' => $projeto, 'fontes' => Fonte::all(), 'projetosListagemReturnQuery' => $this->projetosProgramasListagemRetornoFromRequest($request), ]); } /** * Salva os dados de investimento do projeto/programa. * * Normaliza campos de moeda, valida os dados e garante que campos * numéricos não sejam null (convertendo para 0 quando vazios). * * @param Request $request Requisição HTTP com dados de investimento * @param ProjetoPrograma $projeto Instância do projeto/programa * @return \Illuminate\Http\RedirectResponse */ public function storeInvestimento(Request $request, ProjetoPrograma $projeto) { $this->normalizeCurrencyFields($request); $data = $request->validate([ 'fonte_id' => ['nullable', 'integer', 'exists:project_program_fontes,id'], 'fonte_outro' => ['nullable', 'string', 'max:255'], 'orcamento_previsto' => ['nullable', 'numeric'], 'orcamento_disponivel' => ['nullable', 'numeric'], 'conve_orgao' => ['nullable', 'string', 'max:255'], 'conve_orgao_outro' => ['nullable', 'string', 'max:255'], 'conve_proposta' => ['nullable', 'string', 'max:255'], 'conve_convenio' => ['nullable', 'string', 'max:255'], ], $this->validationMessages(), $this->validationAttributes()); foreach ($this->currencyFields as $field) { if (!isset($data[$field]) || $data[$field] === null || $data[$field] === '') { $data[$field] = 0; } } $nullableStringFields = ['fonte_outro', 'conve_orgao', 'conve_orgao_outro', 'conve_proposta', 'conve_convenio']; foreach ($nullableStringFields as $field) { if (isset($data[$field]) && $data[$field] === '') { $data[$field] = null; } } $data['investimento_done'] = true; $data['status_registro'] = 'COMPLETO'; $data['orcamento_status_cor'] = $projeto->andamento_cor; /** @var User|null $userPrior */ $userPrior = Auth::user(); if ( $this->userCanGerenciarPrioridadeProjeto($userPrior) && !$projeto->prioritario && ($projeto->acompanhamento_prioritario || $projeto->acompanhamento_governo) ) { $origem = $this->resolvePrioridadeOrigemPersistencia($userPrior, null); $max = (int) ProjetoPrograma::where('prioritario', true)->max('ordem_prioridade'); $next = $max > 0 ? ($max + 1) : 1; $data['prioritario'] = true; $data['ordem_prioridade'] = $next; $data['prioridade_origem'] = $origem; $data['prioridade_definida_por_user_id'] = $userPrior?->id; } $projeto->update($data); $listagem = $this->projetosProgramasListagemQueryParaRedirect($request->input('pp_listagem')); return redirect()->route('admin.projetosProgramas.index', array_merge( $listagem, ['tab' => 'projetos'] ))->with('success', __("* Etapa de Orçamento salva com sucesso! O cadastro foi finalizado.")); } public function anexosView(Request $request, ProjetoPrograma $projeto) { $listagem = $this->projetosProgramasListagemRetornoFromRequest($request); return redirect()->route('admin.projetosProgramas.investimento', array_merge( ['projeto' => $projeto->id], $listagem )); } public function storeAnexos(Request $request, ProjetoPrograma $projeto) { $listagem = $this->projetosProgramasListagemQueryParaRedirect($request->input('pp_listagem')); return redirect()->route('admin.projetosProgramas.index', array_merge( $listagem, ['tab' => 'projetos'] )); } public function edit(Request $request, ProjetoPrograma $projetosProgramas) { return redirect()->route('admin.projetosProgramas.identificacao', array_merge( ['projeto' => $projetosProgramas->id], $this->projetosProgramasListagemRetornoFromRequest($request) )); } public function show(ProjetoPrograma $projetosProgramas) { return view('admin.projetosProgramas.show', ['project' => $projetosProgramas]); } public function destroy(Request $request, ProjetoPrograma $projeto) { /** @var \App\Models\User|null $user */ $user = Auth::user(); if (!$user) { return redirect()->back()->with('error', __("Usuário não autenticado.")); } $permissionDetails = $user->getPermissaoDetalhes($this->permissao); if (!$permissionDetails || empty($permissionDetails['excluir'])) { return redirect()->back()->with('error', __("Permissão negada! Entre em contato com o administrador do sistema.")); } DB::beginTransaction(); try { $this->eliminarProjetoProgramaCompleto($projeto); DB::commit(); $listagem = $this->projetosProgramasListagemQueryParaRedirect($request->input('pp_listagem')); return redirect()->route('admin.projetosProgramas.index', array_merge( $listagem, ['tab' => 'projetos'] ))->with('success', __("* Registro excluído com sucesso!")); } catch (\Exception $e) { DB::rollBack(); return redirect()->back()->with('error', $e->getMessage()); } } /** * URL legada da página de exclusão em massa: redireciona para a listagem (fluxo via modal). */ public function exclusaoEmMassa(Request $request) { if ($response = $this->checkPermissao($this->permissao, 'excluir')) { return $response; } return redirect()->route('admin.projetosProgramas.index', $request->query()); } /** * Exclusão em massa: registros selecionados na página ou todos os resultados dos filtros atuais. */ public function destroyMany(ProjetosProgramasDestroyManyRequest $request) { if ($response = $this->checkPermissao($this->permissao, 'excluir')) { return $response; } $mapao = $request->boolean('mapao_apenas_cadastro_completo'); $filterRequest = $this->subRequestComFiltrosListagemProjetos($request); $bulkMode = (string) $request->validated('bulk_mode'); if ($bulkMode === 'selected_ids') { $requestedIds = collect($request->validated('project_ids'))->unique()->values(); $baseQuery = $this->projetosProgramasFilteredBaseQuery($filterRequest, $mapao); $allowedIds = $baseQuery->whereIn('id', $requestedIds)->pluck('id')->sort()->values(); if ($allowedIds->count() !== $requestedIds->count()) { return redirect() ->back() ->with('error', __('Não foi possível excluir: um ou mais registros não pertencem aos filtros atuais da listagem.')); } $idsToDelete = $allowedIds; } else { $idsToDelete = $this->projetosProgramasFilteredBaseQuery($filterRequest, $mapao)->pluck('id'); } if ($idsToDelete->isEmpty()) { return redirect() ->back() ->with('error', __('Nenhum registro elegível para exclusão.')); } $deleted = 0; DB::beginTransaction(); try { foreach ($idsToDelete->chunk(50) as $chunk) { $projetos = ProjetoPrograma::with('anexos')->whereIn('id', $chunk)->get(); foreach ($projetos as $projeto) { if (!$projeto instanceof ProjetoPrograma) { continue; } $this->eliminarProjetoProgramaCompleto($projeto); $deleted++; } } DB::commit(); } catch (\Throwable $e) { DB::rollBack(); return redirect() ->back() ->with('error', __('Erro ao excluir em massa: :msg', ['msg' => $e->getMessage()])); } $redirectQuery = $this->queryStringParaRetornoListagemProjetos($request); return redirect() ->route('admin.projetosProgramas.index', $redirectQuery) ->with( 'success', $deleted === 1 ? __('* :count registro excluído com sucesso.', ['count' => $deleted]) : __('* :count registros excluídos com sucesso.', ['count' => $deleted]) ); } /** * Remove projeto com pivôs, histórico e anexos (arquivos em disco). */ private function eliminarProjetoProgramaCompleto(ProjetoPrograma $projeto): void { $projeto->ods()->detach(); $projeto->orgaosEnvolvidos()->detach(); $projeto->historicos()->delete(); if (!$projeto->relationLoaded('todosAnexos')) { $projeto->load('todosAnexos'); } foreach ($projeto->todosAnexos as $anexo) { $this->deleteFile($anexo); $anexo->delete(); } $projeto->delete(); } /** * Request sintético só com parâmetros de filtro da listagem (segurança na exclusão em massa). */ private function subRequestComFiltrosListagemProjetos(Request $request): Request { $data = $request->only(self::LISTAGEM_FILTROS_QUERY_KEYS); return Request::create('/', 'GET', $data); } /** * Preserva aba, filtros e paginação ao retornar da exclusão em massa (sem repetir POST). * * @return array<string, mixed> */ private function queryStringParaRetornoListagemProjetos(Request $request): array { $keys = array_merge( ['tab', 'pp_subtab', 'page', 'per_page', 'sort_by', 'sort_dir', 'filtros_visiveis', 'search'], self::LISTAGEM_FILTROS_QUERY_KEYS ); $out = []; foreach ($keys as $key) { if (!$request->has($key)) { continue; } $value = $request->input($key); if (is_scalar($value) && $value !== '' && $value !== null) { $out[$key] = $value; } } if (!$request->filled('pp_subtab') && !$request->filled('tab')) { $out['tab'] = 'projetos'; } return $out; } /** * Chaves de query da listagem de projetos a preservar ao editar / voltar do assistente. * * @return list<string> */ private function projetosProgramasListagemContextoQueryKeys(): array { return array_values(array_unique(array_merge( ['page', 'per_page', 'sort_by', 'sort_dir', 'filtros_visiveis', 'search', 'tab', 'pp_subtab'], self::LISTAGEM_FILTROS_QUERY_KEYS ))); } /** * Extrai da query string atual os parâmetros a reapresentar na listagem (GET). * * @return array<string, scalar> */ private function projetosProgramasListagemRetornoFromRequest(Request $request): array { $out = []; foreach ($this->projetosProgramasListagemContextoQueryKeys() as $key) { if (!$request->query->has($key)) { continue; } $value = $request->query($key); if (!is_scalar($value) || $value === '' || $value === null) { continue; } $out[$key] = $value; } return $out; } /** * Sanitiza o array `pp_listagem` enviado em POST (hidden) para redirects do assistente. * * @param mixed $raw * @return array<string, scalar> */ private function projetosProgramasListagemQueryParaRedirect($raw): array { if (!is_array($raw)) { return []; } $allowed = array_flip($this->projetosProgramasListagemContextoQueryKeys()); $out = []; foreach ($raw as $key => $value) { $k = (string) $key; if (!isset($allowed[$k]) || !is_scalar($value) || $value === '' || $value === null) { continue; } $out[$k] = $value; } return $out; } /** * Toggle de prioridade (marcar/desmarcar). */ public function prioridadeToggle(Request $request, ProjetoPrograma $projeto) { /** @var User|null $user */ $user = Auth::user(); if (!$this->userCanGerenciarPrioridadeProjeto($user)) { if ($request->expectsJson()) { return response()->json(['message' => __('Permissão negada.')], Response::HTTP_FORBIDDEN); } return redirect()->back()->with('error', __('Permissão negada! Entre em contato com o administrador do sistema.')); } $data = $request->validate([ 'origem' => ['nullable', 'string', 'in:SUPOP,PREFEITO'], ]); $origem = $this->resolvePrioridadeOrigemPersistencia($user, $data['origem'] ?? null); if (!$projeto->prioritario && !$projeto->isCadastroCompleto()) { $msg = __('* Conclua o cadastro do projeto antes de marcar como prioritário.'); if ($request->expectsJson()) { return response()->json(['message' => $msg], Response::HTTP_UNPROCESSABLE_ENTITY); } return redirect()->back()->with('error', $msg); } DB::beginTransaction(); try { if ($projeto->prioritario) { $projeto->update([ 'prioritario' => false, 'ordem_prioridade' => null, 'prioridade_origem' => null, 'prioridade_definida_por_user_id' => null, 'acompanhamento_prioritario' => false, 'acompanhamento_governo' => false, ]); } else { $max = (int) ProjetoPrograma::where('prioritario', true)->max('ordem_prioridade'); $next = $max > 0 ? ($max + 1) : 1; $projeto->update([ 'prioritario' => true, 'ordem_prioridade' => $next, 'prioridade_origem' => $origem, 'prioridade_definida_por_user_id' => $user?->id, 'acompanhamento_prioritario' => true, 'acompanhamento_governo' => true, ]); } DB::commit(); } catch (\Throwable $e) { DB::rollBack(); return $request->expectsJson() ? response()->json(['message' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR) : redirect()->back()->with('error', $e->getMessage()); } $message = $projeto->prioritario ? __('* Projeto marcado como prioritário com sucesso!') : __('* Projeto removido dos prioritários com sucesso!'); if ($request->expectsJson()) { return response()->json([ 'message' => $message, 'prioritario' => $projeto->prioritario, ]); } return redirect()->back()->with('success', $message); } /** * Persiste a ordenação da lista de prioridades via drag and drop. */ public function prioridadesOrdenar(Request $request) { /** @var User|null $user */ $user = Auth::user(); if (!$this->userCanGerenciarPrioridadeProjeto($user)) { return response()->json([ 'message' => __('Permissão negada.'), ], Response::HTTP_FORBIDDEN); } $data = $request->validate([ 'order' => ['required', 'array', 'min:1'], 'order.*' => ['required', 'string', 'uuid'], ]); $ordered = array_values(array_unique($data['order'])); $prioritizedIds = ProjetoPrograma::where('prioritario', true)->pluck('id')->toArray(); $temPrioritarioIncompleto = ProjetoPrograma::query() ->whereIn('id', $prioritizedIds) ->where(function (Builder $q): void { $q->whereNull('status_registro') ->orWhere('status_registro', '!=', 'COMPLETO'); }) ->exists(); if ($temPrioritarioIncompleto) { return response()->json([ 'message' => __('Não é possível salvar a ordem: há projeto prioritário com cadastro em edição. Conclua os cadastros ou remova-os da lista.'), ], Response::HTTP_UNPROCESSABLE_ENTITY); } DB::beginTransaction(); try { $missingIds = array_values(array_diff($prioritizedIds, $ordered)); $finalOrder = array_values(array_merge($ordered, $missingIds)); foreach ($finalOrder as $index => $id) { $pos = $index + 1; $updated = ProjetoPrograma::query() ->where('id', $id) ->update([ 'prioritario' => true, 'ordem_prioridade' => $pos, 'updated_at' => now(), ]); if ($updated === 0) { throw new \RuntimeException(__('Registro prioritário inválido ou inexistente.')); } } DB::commit(); } catch (\Throwable $e) { DB::rollBack(); return response()->json([ 'message' => $e->getMessage(), ], Response::HTTP_INTERNAL_SERVER_ERROR); } return response()->json([ 'message' => __('* Ordem de prioridades atualizada com sucesso!'), ]); } public function upload(Request $request, ProjetoPrograma $projeto) { // Validação para múltiplos arquivos (quando name="arquivo[]") $rules = []; if ($request->hasFile('arquivo')) { $files = $request->file('arquivo'); // Se for array (múltiplos arquivos) if (is_array($files)) { $rules['arquivo'] = ['required', 'array']; $rules['arquivo.*'] = ['file', 'max:51200', 'mimes:pdf,doc,docx,xls,xlsx,jpg,jpeg,png']; } else { // Se for um único arquivo $rules['arquivo'] = ['required', 'file', 'max:51200', 'mimes:pdf,doc,docx,xls,xlsx,jpg,jpeg,png']; } } else { $rules['arquivo'] = ['required']; } $request->validate($rules, $this->validationMessages(), $this->validationAttributes()); $files = $request->file('arquivo'); $uploadedCount = 0; $directory = "projetos-programas/{$projeto->id}"; // Se for um único arquivo, converte para array if (!is_array($files)) { $files = [$files]; } foreach ($files as $file) { if (!$file || !$file->isValid()) { continue; } // Valida arquivo usando FileUploadHelper if (!FileUploadHelper::validateFile( $file, FileUploadHelper::ALLOWED_GENERAL_EXTENSIONS, FileUploadHelper::ALLOWED_GENERAL_MIME_TYPES )) { continue; // Pula arquivos inválidos } // Gera nome seguro $safeFileName = FileUploadHelper::generateSafeFileName( $file, FileUploadHelper::ALLOWED_GENERAL_EXTENSIONS ); $fileName = uniqid() . '_' . $safeFileName; // Salva no storage e retorna o caminho relativo (ex: projetos-programas/1/arquivo.pdf) $filePath = $file->storeAs($directory, $fileName, 'public'); // Sanitiza nome original para armazenar no banco usando helper seguro $originalName = FileUploadHelper::sanitizeOriginalFileName($file); $originalName = substr($originalName, 0, 255); // Limita tamanho $projeto->anexos()->create([ 'name' => $originalName, 'url' => str_replace('\\', '/', $filePath), 'mime' => $file->getMimeType(), 'size' => $file->getSize(), ]); $uploadedCount++; } if ($uploadedCount === 0) { $message = __("* Nenhum arquivo válido foi enviado."); if ($request->expectsJson()) { return response()->json(['message' => $message], Response::HTTP_BAD_REQUEST); } return redirect()->back()->with('error', $message); } $message = $uploadedCount === 1 ? __("* Arquivo anexado com sucesso!") : __("* {$uploadedCount} arquivos anexados com sucesso!"); if ($request->expectsJson()) { return response()->json(['message' => $message], Response::HTTP_CREATED); } return redirect()->back()->with('success', $message); } public function removeAnexo(Request $request, ProjetoPrograma $projeto, Anexo $anexo) { abort_unless($anexo->project_id === $projeto->id, 404); $this->deleteFile($anexo); $anexo->delete(); $message = __("* Anexo removido com sucesso!"); if ($request->expectsJson()) { return response()->json(['message' => $message]); } return redirect()->back()->with('success', $message); } public function downloadAnexo(ProjetoPrograma $projeto, Anexo $anexo) { abort_unless($anexo->project_id === $projeto->id, 404); if (!$anexo->url || !Storage::disk('public')->exists($anexo->url)) { return redirect()->back()->with('error', __("Arquivo não encontrado.")); } $filePath = Storage::disk('public')->path($anexo->url); return response()->download($filePath, $anexo->name); } public function anexos(ProjetoPrograma $projeto) { $projeto->load('anexos'); return response()->json([ 'project' => [ 'id' => $projeto->id, 'denominacao' => $projeto->denominacao, ], 'anexos' => $projeto->anexos->map(function ($anexo) use ($projeto) { return [ 'id' => $anexo->id, 'name' => $anexo->name, 'url' => $anexo->public_url, 'size' => $anexo->size_human, 'created_at' => optional($anexo->created_at)->format('d/m/Y H:i'), 'delete_url' => route('admin.projetosProgramas.anexos.remove', [$projeto->id, $anexo->id]), ]; }), ]); } public function imprimir(ProjetoPrograma $projeto) { $projeto->load(['objetivo', 'programa']); $mapaoGrupos = $this->buildMapaoGruposForPrint(collect([$projeto])); return view('admin.projetosProgramas.print-list', [ 'mapaoGrupos' => $mapaoGrupos, 'mapaoCoresLegenda' => $this->andamentoOrcamentoCoresOptions(), 'mapaoFiltrosResumo' => [], ]); } /** * Textos dos filtros ativos da listagem para exibição no MAPÃO impresso. * * @return list<array{label: string, value: string}> */ private function buildMapaoPrintFiltrosResumo(Request $request): array { $chaves = ['search', 'gestor', 'prioritario', 'andamento_cor', 'orcamento_status_cor', 'status_registro', 'bairro', 'bairo']; $algum = false; foreach ($chaves as $k) { if ($request->filled($k)) { $algum = true; break; } } if (!$algum) { return []; } $linhas = []; if ($request->filled('search')) { $linhas[] = [ 'label' => __('Buscar'), 'value' => (string) $request->input('search'), ]; } if ($request->filled('gestor')) { $sid = $request->input('gestor'); $st = is_scalar($sid) ? Structure::query()->find($sid) : null; $rotulo = $st ? ($st->abbreviation ? ((string) $st->abbreviation . ' - ' . (string) $st->name) : (string) $st->name) : (string) $sid; $linhas[] = [ 'label' => __('Secretaria'), 'value' => $rotulo, ]; } if ($request->filled('prioritario')) { $pv = (string) $request->input('prioritario'); $linhas[] = [ 'label' => __('Prioritário'), 'value' => $pv === '1' ? __('Sim') : ($pv === '0' ? __('Não') : $pv), ]; } $coresAnd = $this->andamentoOrcamentoCoresOptions(); if ($request->filled('andamento_cor')) { $ac = (string) $request->input('andamento_cor'); $txt = $ac === '_vazio' ? __('Sem legenda cadastrada') : ($coresAnd[$ac] ?? $ac); $linhas[] = [ 'label' => __('Andamento (cor)'), 'value' => $txt, ]; } $coresOrc = $this->andamentoOrcamentoCoresOptions(); if ($request->filled('orcamento_status_cor')) { $oc = (string) $request->input('orcamento_status_cor'); $txt = $oc === '_vazio' ? __('Sem situação cadastrada') : ($coresOrc[$oc] ?? $oc); $linhas[] = [ 'label' => __('Situação do orçamento'), 'value' => $txt, ]; } if ($request->filled('status_registro')) { $sr = (string) $request->input('status_registro'); $rotulo = $sr === 'COMPLETO' ? __('Concluído') : ($sr === 'EM_EDICAO' ? __('Em edição') : $sr); $linhas[] = [ 'label' => __('Situação do cadastro'), 'value' => $rotulo, ]; } $bNome = $request->input('bairro'); if (($bNome === null || $bNome === '') && $request->filled('bairo')) { $bNome = $request->input('bairo'); } if (is_string($bNome) && trim($bNome) !== '') { $linhas[] = [ 'label' => __('Bairro'), 'value' => trim($bNome), ]; } return $linhas; } public function imprimirLista(Request $request) { $projects = $this->filteredProjectsQuery($request) ->with(['objetivo', 'programa']) ->get(); $mapaoGrupos = $this->buildMapaoGruposForPrint($projects); return view('admin.projetosProgramas.print-list', [ 'mapaoGrupos' => $mapaoGrupos, 'mapaoCoresLegenda' => $this->andamentoOrcamentoCoresOptions(), 'mapaoFiltrosResumo' => $this->buildMapaoPrintFiltrosResumo($request), ]); } public function mapaDados(Request $request): JsonResponse { return $this->buildMapaDadosResponse($request, false); } public function mapaDadosDashboard(Request $request): JsonResponse { /** @var User|null $user */ $user = Auth::user(); if ( !$user || !collect($user->listaPermissoes())->contains('GERENCIADOR_DO_DASHBOARD_ESTRATEGICO') ) { return response()->json([ 'message' => __('Permissão negada.'), ], Response::HTTP_FORBIDDEN); } return $this->buildMapaDadosResponse($request, true); } private function buildMapaDadosResponse(Request $request, bool $mapaoApenasCadastroCompleto = false): JsonResponse { $projects = $this->projetosProgramasFilteredBaseQuery($request, $mapaoApenasCadastroCompleto, true) ->with([ 'gestor:id,abbreviation,name', 'programa:id,nome', 'objetivo:id,nome', ]) ->orderBy('denominacao') ->get([ 'id', 'denominacao', 'bairro', 'latitude', 'longitude', 'andamento', 'andamento_cor', 'orcamento_previsto', 'orcamento_disponivel', 'prazo_entrega_dias', 'prazo_entrega_competencia', 'prioritario', 'status_registro', 'gestor_structure_id', ]); $mapaoIdentificadores = $this->buildMapaoIdentificadorPorProjetoId($projects); $legendasAndamento = $this->andamentoOrcamentoCoresOptions(); $indiceReferenciaMunicipio = 0; $data = $projects ->map(function (ProjetoPrograma $project) use ( $mapaoIdentificadores, $legendasAndamento, &$indiceReferenciaMunicipio ): ?array { $coords = $project->isBairroTodos() ? $project->coordenadasMapa($indiceReferenciaMunicipio++) : $project->coordenadasMapa(); if ($coords === null) { return null; } $cor = (string) ($project->andamento_cor ?? ''); $andamentoLegenda = $cor !== '' && isset($legendasAndamento[$cor]) ? (string) $legendasAndamento[$cor] : ''; $gestor = ''; if ($project->gestor) { $gestor = $project->gestor->abbreviation ? ((string) $project->gestor->abbreviation . ' - ' . (string) $project->gestor->name) : (string) $project->gestor->name; } return [ 'id' => $project->id, 'numero_mapao' => $mapaoIdentificadores[$project->id] ?? '', 'denominacao' => (string) ($project->denominacao ?? ''), 'bairro' => (string) ($project->bairro ?? ''), 'bairro_todos' => $project->isBairroTodos(), 'bairro_exibicao' => $project->bairroRotuloExibicao(), 'coordenada_referencia_municipio' => (bool) ($coords['referencia_municipio'] ?? false), 'gestor' => $gestor, 'latitude' => $coords['latitude'], 'longitude' => $coords['longitude'], 'andamento_texto' => (string) ($project->andamento ?? ''), 'andamento_cor' => $cor, 'andamento_legenda' => $andamentoLegenda, 'orcamento_previsto' => (float) ($project->orcamento_previsto ?? 0), 'orcamento_disponivel' => (float) ($project->orcamento_disponivel ?? 0), 'prazo_entrega_dias' => $project->prazo_entrega_dias !== null ? (int) $project->prazo_entrega_dias : null, 'prazo_entrega_competencia' => $project->prazo_entrega_competencia !== null ? (string) $project->prazo_entrega_competencia : null, 'prioritario' => (bool) $project->prioritario, 'status_registro' => (string) ($project->status_registro ?? ''), 'visualizar_url' => route('admin.projetosProgramas.visualizar', $project->id), ]; }) ->filter() ->values(); return response()->json([ 'data' => $data, 'total' => $data->count(), ]); } public function relatorio(Request $request) { if ($response = $this->checkPermissao($this->permissao, 'visualizar')) { return $response; } return $this->renderRelatorio($request, false); } public function relatorioImprimir(Request $request) { if ($response = $this->checkPermissao($this->permissao, 'visualizar')) { return $response; } return $this->renderRelatorioPrint($request, false); } public function relatorioDashboard(Request $request) { /** @var User|null $user */ $user = Auth::user(); if ( !$user || !collect($user->listaPermissoes())->contains('GERENCIADOR_DO_DASHBOARD_ESTRATEGICO') ) { abort(403); } return $this->renderRelatorio($request, true); } public function relatorioDashboardImprimir(Request $request) { /** @var User|null $user */ $user = Auth::user(); if ( !$user || !collect($user->listaPermissoes())->contains('GERENCIADOR_DO_DASHBOARD_ESTRATEGICO') ) { abort(403); } return $this->renderRelatorioPrint($request, true); } private function renderRelatorio(Request $request, bool $mapaoApenasCadastroCompleto) { $allowedColumns = $this->relatorioAllowedColumns(); $allowedGroups = $this->relatorioAllowedGroups(); $selectedColumns = $request->input('columns', []); if (!is_array($selectedColumns)) { $selectedColumns = []; } $selectedColumns = array_values(array_filter($selectedColumns, static fn ($c) => is_string($c) && isset($allowedColumns[$c]))); // padrão: se o usuário não selecionar colunas, mantém uma visão mínima útil. if ($selectedColumns === []) { $selectedColumns = ['denominacao', 'secretaria', 'bairro']; } $groupKeys = $this->relatorioResolveGroupOrder( (string) $request->input('group1', ''), (string) $request->input('group2', ''), (string) $request->input('group3', ''), array_keys($allowedGroups) ); $reportBaseQuery = ProjetoPrograma::query() ->with(['gestor', 'tipo', 'classificacao', 'etapa', 'programa', 'objetivo']) ->orderByDesc('created_at'); if ($mapaoApenasCadastroCompleto) { $reportBaseQuery->where('status_registro', 'COMPLETO'); } $projects = $reportBaseQuery->get(); $reportRows = null; if ($projects->isNotEmpty()) { $reportRows = $this->buildRelatorioRows($projects, $groupKeys, $selectedColumns, $allowedGroups); } $relatorioBackQuery = $request->except(['columns', 'group1', 'group2', 'group3']); return view('admin.projetosProgramas.relatorio', [ 'titulo' => $mapaoApenasCadastroCompleto ? __('Relatório dinâmico — Projetos e Programas (Dashboard Estratégico)') : __('Relatório dinâmico — Projetos e Programas'), 'routeBack' => $mapaoApenasCadastroCompleto ? 'admin.dashboardEstrategico.index' : 'admin.projetosProgramas.index', 'routeBackQuery' => $mapaoApenasCadastroCompleto ? array_merge($relatorioBackQuery, ['pp_subtab' => '1']) : null, 'routeRelatorio' => $mapaoApenasCadastroCompleto ? 'admin.dashboardEstrategico.projetosProgramas.relatorio' : 'admin.projetosProgramas.relatorio', 'allowedColumns' => $allowedColumns, 'allowedGroups' => $allowedGroups, 'selectedColumns' => $selectedColumns, 'group1' => $groupKeys[0] ?? null, 'group2' => $groupKeys[1] ?? null, 'group3' => $groupKeys[2] ?? null, 'reportRows' => $reportRows, ]); } private function renderRelatorioPrint(Request $request, bool $mapaoApenasCadastroCompleto) { $allowedColumns = $this->relatorioAllowedColumns(); $allowedGroups = $this->relatorioAllowedGroups(); $selectedColumns = $request->input('columns', []); if (!is_array($selectedColumns)) { $selectedColumns = []; } $selectedColumns = array_values(array_filter($selectedColumns, static fn ($c) => is_string($c) && isset($allowedColumns[$c]))); if ($selectedColumns === []) { $selectedColumns = ['denominacao', 'secretaria', 'bairro']; } $groupKeys = $this->relatorioResolveGroupOrder( (string) $request->input('group1', ''), (string) $request->input('group2', ''), (string) $request->input('group3', ''), array_keys($allowedGroups) ); $reportBaseQueryPrint = ProjetoPrograma::query() ->with(['gestor', 'tipo', 'classificacao', 'etapa', 'programa', 'objetivo']) ->orderByDesc('created_at'); if ($mapaoApenasCadastroCompleto) { $reportBaseQueryPrint->where('status_registro', 'COMPLETO'); } $projects = $reportBaseQueryPrint->get(); $reportRows = $projects->isNotEmpty() ? $this->buildRelatorioRows($projects, $groupKeys, $selectedColumns, $allowedGroups) : []; return view('admin.projetosProgramas.relatorio-print', [ 'allowedColumns' => $allowedColumns, 'selectedColumns' => $selectedColumns, 'reportRows' => $reportRows, ]); } /** * @return array<string, string> */ private function relatorioAllowedColumns(): array { return [ 'denominacao' => __('Denominação'), 'secretaria' => __('Secretaria'), 'bairro' => __('Bairro'), 'tipo' => __('Tipo'), 'classificacao' => __('Classificação'), 'etapa' => __('Etapa'), 'situacao_cadastro' => __('Situação do cadastro'), 'andamento' => __('Andamento'), 'prazo' => __('Prazo de implantação'), 'orcamento_previsto' => __('Orçamento previsto'), 'orcamento_disponivel' => __('Orçamento disponível'), 'programa' => __('Programa'), 'objetivo' => __('Objetivo'), 'cep' => __('CEP'), 'endereco' => __('Rua'), ]; } /** * @return array<string, string> */ private function relatorioAllowedGroups(): array { return [ 'secretaria' => __('Secretaria'), 'bairro' => __('Bairro'), 'tipo' => __('Tipo'), 'classificacao' => __('Classificação'), 'etapa' => __('Etapa'), 'situacao_cadastro' => __('Situação do cadastro'), 'programa' => __('Programa'), 'objetivo' => __('Objetivo'), ]; } /** * @param array<int, string> $allowedGroupKeys * @return array<int, string> */ private function relatorioResolveGroupOrder(string $g1, string $g2, string $g3, array $allowedGroupKeys): array { $keys = array_filter([$g1, $g2, $g3], static fn ($k) => $k !== ''); $keys = array_values(array_unique($keys)); $keys = array_values(array_filter($keys, static fn ($k) => in_array($k, $allowedGroupKeys, true))); return $keys; } /** * @param Collection<int, ProjetoPrograma> $projects * @param array<int, string> $groupKeys * @param array<int, string> $selectedColumns * @param array<string, string> $allowedGroups * @return array<int, array<string, mixed>> */ private function buildRelatorioRows(Collection $projects, array $groupKeys, array $selectedColumns, array $allowedGroups): array { $rows = []; $emitGroup = function (Collection $items, int $depth, array $remainingGroups) use (&$emitGroup, &$rows, $selectedColumns, $allowedGroups): void { $totals = $this->relatorioTotals($items); if ($remainingGroups === []) { foreach ($items as $p) { $rows[] = [ 'type' => 'data', 'depth' => $depth, 'label' => (string) ($p->denominacao ?? ''), 'cols' => $this->relatorioRowColumns($p, $selectedColumns), 'totals' => $this->relatorioTotals(collect([$p])), ]; } // Subtotal só tem valor quando há mais de 1 item no grupo; // com 1 item é idêntico à própria linha — puro ruído visual. if ($depth > 0 && $totals['count'] > 1) { $rows[] = [ 'type' => 'subtotal', 'depth' => max(0, $depth - 1), 'label' => '', 'cols' => [], 'totals' => $totals, ]; } return; } $groupKey = array_shift($remainingGroups); $label = $allowedGroups[$groupKey] ?? Str::headline($groupKey); $grouped = $items->groupBy(function (ProjetoPrograma $p) use ($groupKey): string { return $this->relatorioGroupValue($p, $groupKey); }); foreach ($grouped as $value => $subItems) { $rows[] = [ 'type' => 'group_header', 'depth' => $depth, 'label' => $label . ': ' . ($value !== '' ? $value : '—'), 'cols' => [], 'totals' => $this->relatorioTotals($subItems), ]; $emitGroup($subItems, $depth + 1, $remainingGroups); } // Subtotal intermediário: apenas grupos aninhados com mais de 1 item. if ($depth > 0 && $totals['count'] > 1) { $rows[] = [ 'type' => 'subtotal', 'depth' => max(0, $depth - 1), 'label' => '', 'cols' => [], 'totals' => $totals, ]; } }; if ($groupKeys === []) { foreach ($projects as $p) { $rows[] = [ 'type' => 'data', 'depth' => 0, 'label' => (string) ($p->denominacao ?? ''), 'cols' => $this->relatorioRowColumns($p, $selectedColumns), 'totals' => $this->relatorioTotals(collect([$p])), ]; } } else { $emitGroup($projects, 0, $groupKeys); } $rows[] = [ 'type' => 'grand_total', 'depth' => 0, 'label' => '', 'cols' => [], 'totals' => $this->relatorioTotals($projects), ]; return $rows; } /** * @return array{count:int, orcamento_previsto:float, orcamento_disponivel:float} */ private function relatorioTotals(Collection $items): array { return [ 'count' => $items->count(), 'orcamento_previsto' => (float) $items->sum(static fn (ProjetoPrograma $p) => (float) ($p->orcamento_previsto ?? 0)), 'orcamento_disponivel' => (float) $items->sum(static fn (ProjetoPrograma $p) => (float) ($p->orcamento_disponivel ?? 0)), ]; } private function relatorioGroupValue(ProjetoPrograma $p, string $groupKey): string { return match ($groupKey) { 'secretaria' => (string) ($p->gestor?->abbreviation ? ($p->gestor->abbreviation . ' - ' . $p->gestor->name) : ($p->gestor?->name ?? '')), 'bairro' => (string) ($p->bairro ?? ''), 'tipo' => (string) (optional($p->tipo)->nome ?? ''), 'classificacao' => (string) (optional($p->classificacao)->nome ?? ''), 'etapa' => (string) (optional($p->etapa)->nome ?? ''), 'situacao_cadastro' => (string) ($p->status_registro_label ?? ''), 'programa' => (string) (optional($p->programa)->nome ?? ''), 'objetivo' => (string) (optional($p->objetivo)->nome ?? ''), default => '', }; } /** * @param array<int, string> $selectedColumns * @return array<string, string> */ private function relatorioRowColumns(ProjetoPrograma $p, array $selectedColumns): array { $out = []; foreach ($selectedColumns as $col) { $out[$col] = match ($col) { 'denominacao' => (string) ($p->denominacao ?? ''), 'secretaria' => (string) ($p->gestor?->abbreviation ? ($p->gestor->abbreviation . ' - ' . $p->gestor->name) : ($p->gestor?->name ?? '')), 'bairro' => (string) ($p->bairro ?? ''), 'tipo' => (string) (optional($p->tipo)->nome ?? ''), 'classificacao' => (string) (optional($p->classificacao)->nome ?? ''), 'etapa' => (string) (optional($p->etapa)->nome ?? ''), 'situacao_cadastro' => (string) ($p->status_registro_label ?? ''), 'andamento' => (string) ($p->andamento ?? ''), 'prazo' => $p->textoPrazoImplantacaoRelatorio(), 'orcamento_previsto' => 'R$ ' . number_format((float) ($p->orcamento_previsto ?? 0), 2, ',', '.'), 'orcamento_disponivel' => 'R$ ' . number_format((float) ($p->orcamento_disponivel ?? 0), 2, ',', '.'), 'programa' => (string) (optional($p->programa)->nome ?? ''), 'objetivo' => (string) (optional($p->objetivo)->nome ?? ''), 'cep' => (string) ($p->cep ?? ''), 'endereco' => (string) ($p->endereco ?? ''), default => '', }; } return $out; } /** * Agrupa para o MAPÃO: prioriza programa; se não houver, agrupa por objetivo; senão um único grupo. * Cada grupo gera uma linha-pai (Nº inteiro + título em célula mesclada) e linhas-filhas (Nº 1.1, 1.2…). * * @param Collection<int, ProjetoPrograma> $projects * @return Collection<int, array{titulo_banner: string, projetos: Collection<int, ProjetoPrograma>}> */ private function buildMapaoGruposForPrint(Collection $projects): Collection { if ($projects->isEmpty()) { return collect(); } $grouped = $projects->groupBy(static function (ProjetoPrograma $p): string { if ($p->project_program_id) { return 'p:' . $p->project_program_id; } if ($p->project_objective_id) { return 'o:' . $p->project_objective_id; } return 'x:'; }); return $grouped ->map(function (Collection $projetos): array { /** @var ProjetoPrograma $first */ $first = $projetos->first(); if ($first->programa) { $titulo = (string) $first->programa->nome; } elseif ($first->objetivo) { $titulo = (string) $first->objetivo->nome; } else { $titulo = '—'; } $sorted = $projetos ->sortBy(static function (ProjetoPrograma $p): array { $ord = $p->numero_ordem; $ordEmpty = ($ord === null || $ord === ''); return [$ordEmpty ? 1 : 0, (string) ($ord ?? ''), mb_strtolower($p->denominacao ?? '')]; }) ->values(); return [ 'titulo_banner' => $titulo, 'projetos' => $sorted, ]; }) ->sortBy(static fn (array $g): string => mb_strtolower($g['titulo_banner'])) ->values(); } /** * Numeração hierárquica idêntica ao MAPÃO impresso (ex.: 1.1, 2.3), por id do projeto. * * @param Collection<int, ProjetoPrograma> $projects * @return array<string, string> */ private function buildMapaoIdentificadorPorProjetoId(Collection $projects): array { $grupos = $this->buildMapaoGruposForPrint($projects); $map = []; foreach ($grupos as $grupoIndex => $grupo) { $numPai = $grupoIndex + 1; foreach ($grupo['projetos'] as $childIndex => $proj) { $map[$proj->id] = $numPai . '.' . ($childIndex + 1); } } return $map; } /** * Opções de cor para andamento e situação orçamentária (legenda institucional). * * @return array<string, string> */ private function andamentoOrcamentoCoresOptions(): array { return [ 'verde' => __('Projeto em andamento'), 'vermelho' => __('Projeto sem valor definido'), 'amarelo' => __('Projeto com valor definido, mas sem recurso ou pendente de licitação'), ]; } private function userIsPrefeitoAngra(?User $user): bool { if (!$user) { return false; } $user->loadMissing('pertence'); return $user->pertence?->slug === 'prefeito-de-angra-dos-reis'; } private function userCanGerenciarPrioridadeProjeto(?User $user): bool { if (!$user) { return false; } $detalhes = $user->getPermissaoDetalhes($this->permissao); if (!empty($detalhes['editar'])) { return true; } return collect($user->listaPermissoes())->contains('GERENCIADOR_DO_DASHBOARD_ESTRATEGICO') && $this->userIsPrefeitoAngra($user); } /** * No painel do prefeito (sem edição SUPOP), a lista MAPÃO considera apenas cadastro completo. */ private function dashboardEmbedListaSomenteCompleto(?User $user): bool { if (!$user) { return false; } $hasSupopEdit = !empty($user->getPermissaoDetalhes($this->permissao)['editar']); $dashboard = collect($user->listaPermissoes())->contains('GERENCIADOR_DO_DASHBOARD_ESTRATEGICO'); return $dashboard && !$hasSupopEdit; } /** * Define SUPOP ou PREFEITO conforme o perfil (evita origem incorreta vinda do cliente). */ private function resolvePrioridadeOrigemPersistencia(?User $user, ?string $requested): string { $requested = $requested === 'PREFEITO' ? 'PREFEITO' : 'SUPOP'; if (!$user) { return 'SUPOP'; } $isPrefeitoDashboard = $this->userIsPrefeitoAngra($user) && collect($user->listaPermissoes())->contains('GERENCIADOR_DO_DASHBOARD_ESTRATEGICO'); $hasSupopEdit = !empty($user->getPermissaoDetalhes($this->permissao)['editar']); if ($hasSupopEdit && !$isPrefeitoDashboard) { return 'SUPOP'; } if ($isPrefeitoDashboard && !$hasSupopEdit) { return 'PREFEITO'; } return $requested === 'PREFEITO' ? 'PREFEITO' : 'SUPOP'; } /** * Remove um registro de histórico e seus anexos em disco. */ private function removerHistoricoComAnexos(string $projectId, string $historicoId): void { $historico = ProjetoProgramaHistorico::query() ->where('project_id', $projectId) ->where('id', $historicoId) ->with('anexos') ->first(); if ($historico === null) { return; } foreach ($historico->anexos as $anexo) { $this->deleteFile($anexo); $anexo->delete(); } $historico->delete(); } /** * Persiste anexos vinculados a um item do histórico de andamento. * * @param list<\Illuminate\Http\UploadedFile>|null $files */ private function persistirAnexosHistorico( ProjetoPrograma $projeto, ProjetoProgramaHistorico $historico, ?array $files ): int { if ($files === null || $files === []) { return 0; } $uploadedCount = 0; $directory = "projetos-programas/{$projeto->id}/historico/{$historico->id}"; foreach ($files as $file) { if (!$file || !$file->isValid()) { continue; } if (!FileUploadHelper::validateFile( $file, FileUploadHelper::ALLOWED_GENERAL_EXTENSIONS, FileUploadHelper::ALLOWED_GENERAL_MIME_TYPES )) { continue; } $safeFileName = FileUploadHelper::generateSafeFileName( $file, FileUploadHelper::ALLOWED_GENERAL_EXTENSIONS ); $fileName = uniqid() . '_' . $safeFileName; $filePath = $file->storeAs($directory, $fileName, 'public'); $originalName = FileUploadHelper::sanitizeOriginalFileName($file); $originalName = substr($originalName, 0, 255); $historico->anexos()->create([ 'project_id' => $projeto->id, 'name' => $originalName, 'url' => str_replace('\\', '/', $filePath), 'mime' => $file->getMimeType(), 'size' => $file->getSize(), ]); $uploadedCount++; } return $uploadedCount; } /** * @return list<array{id: string, name: string, view_url: string, download_url: string}> */ private function serializarAnexosHistorico(ProjetoPrograma $projeto, ProjetoProgramaHistorico $historico): array { if (!$historico->relationLoaded('anexos')) { $historico->load('anexos'); } return $historico->anexos->map(static function (Anexo $anexo) use ($projeto): array { return [ 'id' => $anexo->id, 'name' => $anexo->name, 'view_url' => $anexo->public_url, 'download_url' => route('admin.projetosProgramas.anexos.download', [$projeto->id, $anexo->id]), ]; })->values()->all(); } private function deleteFile(Anexo $anexo): void { if ($anexo->url === '' || $anexo->url === null) { return; } // Remove o arquivo do storage público if (Storage::disk('public')->exists($anexo->url)) { Storage::disk('public')->delete($anexo->url); return; } // Fallback para caminhos antigos (compatibilidade) $normalized = str_replace('\\', '/', $anexo->url); if (str_starts_with($normalized, 'storage/')) { $storagePath = str_replace('storage/', '', $normalized); if (Storage::disk('public')->exists($storagePath)) { Storage::disk('public')->delete($storagePath); return; } } $publicPath = public_path($normalized); if (file_exists($publicPath)) { @unlink($publicPath); return; } $legacyPath = storage_path(str_replace(['pmar/assets/files', 'pmar\\assets\\files'], ['app/public', 'app/public'], $normalized)); if (file_exists($legacyPath)) { @unlink($legacyPath); } } /** * Query base de projetos com os mesmos filtros da listagem (sem eager load nem ordenação com join). */ private function projetosProgramasFilteredBaseQuery( Request $request, bool $mapaoApenasCadastroCompleto = false, bool $mapaIncluirProjetosBairroTodos = false ): Builder { $query = ProjetoPrograma::query(); $this->applyProjetosProgramasListFilters($request, $query, $mapaIncluirProjetosBairroTodos); if ($mapaoApenasCadastroCompleto) { $query->where('status_registro', 'COMPLETO'); } return $query; } /** * Aplica busca textual e filtros da listagem de projetos. */ private function applyProjetosProgramasListFilters( Request $request, Builder $query, bool $mapaIncluirProjetosBairroTodos = false ): void { if ($request->filled('search')) { $search = $request->search; $query->where(function ($q) use ($search) { $q->where('denominacao', 'like', "%{$search}%") ->orWhere('codigo', 'like', "%{$search}%") ->orWhere('descricao_objeto', 'like', "%{$search}%") ->orWhere('bairro', 'like', "%{$search}%") ->orWhere('endereco', 'like', "%{$search}%") ->orWhere('cep', 'like', "%{$search}%"); }); } if ($request->filled('gestor')) { $query->where('gestor_structure_id', $request->gestor); } if ($request->filled('prioritario')) { $pv = (string) $request->prioritario; if ($pv === '1') { $query->where('prioritario', true); } elseif ($pv === '0') { $query->where('prioritario', false); } } if ($request->filled('andamento_cor')) { $ac = (string) $request->andamento_cor; if ($ac === '_vazio') { $query->where(function (Builder $q): void { $q->whereNull('andamento_cor')->orWhere('andamento_cor', ''); }); } elseif (in_array($ac, ['verde', 'amarelo', 'vermelho'], true)) { $query->where('andamento_cor', $ac); } } if ($request->filled('orcamento_status_cor')) { $oc = (string) $request->orcamento_status_cor; if ($oc === '_vazio') { $query->where(function (Builder $q): void { $q->whereNull('orcamento_status_cor')->orWhere('orcamento_status_cor', ''); }); } elseif (in_array($oc, ['verde', 'amarelo', 'vermelho'], true)) { $query->where('orcamento_status_cor', $oc); } } if ($request->filled('status_registro')) { $sr = (string) $request->status_registro; if (in_array($sr, ['EM_EDICAO', 'COMPLETO'], true)) { $query->where('status_registro', $sr); } } $bairroFiltro = $request->input('bairro'); if (($bairroFiltro === null || $bairroFiltro === '') && $request->filled('bairo')) { $bairroFiltro = $request->input('bairo'); } if (is_string($bairroFiltro)) { $bairroFiltro = trim($bairroFiltro); } if ($bairroFiltro !== null && $bairroFiltro !== '') { $bairroTodosRotulo = ProjetoPrograma::bairroTodosRotulo(); if ($mapaIncluirProjetosBairroTodos && $bairroFiltro !== $bairroTodosRotulo) { $query->where(function (Builder $q) use ($bairroFiltro, $bairroTodosRotulo): void { $q->where('bairro', $bairroFiltro) ->orWhere('bairro', $bairroTodosRotulo); }); } else { $query->where('bairro', $bairroFiltro); } } // Parâmetros legados (links antigos / favoritos) if ($request->filled('tipo')) { $query->where('tipo_id', $request->tipo); } if ($request->filled('status')) { $query->where('status_id', $request->status); } if ($request->filled('etapa')) { $query->where('etapa_id', $request->etapa); } if ($request->filled('classificacao')) { $query->where('classificacao_id', $request->classificacao); } if ($request->filled('prazo')) { $query->where('prazo_id', $request->prazo); } } /** * Totais e contagens alinhados à situação do orçamento (cores: verde / vermelho / amarelo). * * @return array{ * orcamento_total_previsto: float, * orcamento_total_disponivel: float, * qtd_em_andamento: int, * qtd_sem_valor_definido: int, * qtd_sem_recurso_ou_licitacao: int * } */ private function computeProjetosProgramasResumoKpis(Builder $query): array { $row = (clone $query)->toBase() ->selectRaw( 'COALESCE(SUM(orcamento_previsto), 0) as total_previsto, ' . 'COALESCE(SUM(orcamento_disponivel), 0) as total_disponivel, ' . 'SUM(CASE WHEN orcamento_status_cor = ? THEN 1 ELSE 0 END) as qtd_verde, ' . 'SUM(CASE WHEN orcamento_status_cor = ? THEN 1 ELSE 0 END) as qtd_vermelho, ' . 'SUM(CASE WHEN orcamento_status_cor = ? THEN 1 ELSE 0 END) as qtd_amarelo', ['verde', 'vermelho', 'amarelo'] ) ->first(); return [ 'orcamento_total_previsto' => (float) ($row->total_previsto ?? 0), 'orcamento_total_disponivel' => (float) ($row->total_disponivel ?? 0), 'qtd_em_andamento' => (int) ($row->qtd_verde ?? 0), 'qtd_sem_valor_definido' => (int) ($row->qtd_vermelho ?? 0), 'qtd_sem_recurso_ou_licitacao' => (int) ($row->qtd_amarelo ?? 0), ]; } /** * Agregações para gráficos da aba Dados (mesmo conjunto filtrado de projetos). * * @return array<string, mixed> */ private function buildProjetosProgramasDashboardChartPayload(Builder $baseFiltered): array { $rowOrc = (clone $baseFiltered)->toBase()->selectRaw( 'SUM(CASE WHEN orcamento_status_cor = ? THEN 1 ELSE 0 END) as verde, ' . 'SUM(CASE WHEN orcamento_status_cor = ? THEN 1 ELSE 0 END) as amarelo, ' . 'SUM(CASE WHEN orcamento_status_cor = ? THEN 1 ELSE 0 END) as vermelho, ' . 'SUM(CASE WHEN orcamento_status_cor IS NULL OR TRIM(COALESCE(orcamento_status_cor, "")) = "" THEN 1 ELSE 0 END) as sem', ['verde', 'amarelo', 'vermelho'] )->first(); $rowPrazo = (clone $baseFiltered)->toBase()->selectRaw( 'SUM(CASE WHEN prazo_entrega_dias IS NULL THEN 1 ELSE 0 END) as sem_prazo, ' . 'SUM(CASE WHEN prazo_entrega_dias IS NOT NULL AND prazo_entrega_dias >= 0 AND prazo_entrega_dias <= 90 THEN 1 ELSE 0 END) as ate90, ' . 'SUM(CASE WHEN prazo_entrega_dias > 90 AND prazo_entrega_dias <= 180 THEN 1 ELSE 0 END) as ate180, ' . 'SUM(CASE WHEN prazo_entrega_dias > 180 THEN 1 ELSE 0 END) as acima180' )->first(); $porGestorRows = (clone $baseFiltered)->reorder() ->selectRaw('gestor_structure_id, COUNT(*) as cnt') ->groupBy('gestor_structure_id') ->orderByDesc('cnt') ->limit(12) ->get(); $gestorIds = $porGestorRows->pluck('gestor_structure_id')->filter()->unique()->values(); $structures = $gestorIds->isNotEmpty() ? Structure::query()->whereIn('id', $gestorIds)->get()->keyBy('id') : collect(); $labelsGestor = []; $dataGestor = []; foreach ($porGestorRows as $gr) { $cnt = (int) ($gr->cnt ?? 0); $sid = $gr->gestor_structure_id; if ($sid === null) { $labelsGestor[] = __('Sem secretaria'); } else { $st = $structures->get($sid); $labelsGestor[] = $st ? (string) ($st->abbreviation !== null && $st->abbreviation !== '' ? $st->abbreviation : $st->name) : __('Secretaria (id: :id)', ['id' => (string) $sid]); } $dataGestor[] = $cnt; } $paletteGestor = ['#0d6efd', '#6610f2', '#6f42c1', '#d63384', '#dc3545', '#fd7e14', '#ffc107', '#198754', '#20c997', '#0dcaf0', '#6c757d', '#212529']; $monthsOrdered = []; for ($i = 11; $i >= 0; $i--) { $monthsOrdered[] = Carbon::now()->copy()->subMonths($i)->startOfMonth()->format('Y-m'); } $histCounts = collect(); $histConnection = (new ProjetoProgramaHistorico())->getConnectionName(); if (DB::connection($histConnection)->getDriverName() === 'mysql') { $histCounts = ProjetoProgramaHistorico::query() ->whereIn('project_id', (clone $baseFiltered)->select('id')) ->where('data', '>=', Carbon::now()->copy()->subMonths(11)->startOfMonth()->startOfDay()) ->selectRaw('DATE_FORMAT(`data`, "%Y-%m") as ym, COUNT(*) as c') ->groupBy('ym') ->orderBy('ym') ->pluck('c', 'ym'); } $labelsHistorico = []; $dataHistorico = []; foreach ($monthsOrdered as $ym) { $labelsHistorico[] = Carbon::createFromFormat('Y-m', $ym)->format('m/Y'); $dataHistorico[] = (int) ($histCounts->get($ym, 0)); } return [ 'orcamento' => [ 'labels' => [ __('Em andamento (MAPÃO)'), __('Sem valor definido (MAPÃO)'), __('Com valor definido, sem recurso ou pendente de licitação (MAPÃO)'), __('Sem situação cadastrada (MAPÃO)'), ], 'data' => [ (int) ($rowOrc->verde ?? 0), (int) ($rowOrc->vermelho ?? 0), (int) ($rowOrc->amarelo ?? 0), (int) ($rowOrc->sem ?? 0), ], 'colors' => ['#198754', '#dc3545', '#ffc107', '#6c757d'], ], 'prazo_implantacao' => [ 'labels' => [ __('Sem prazo em dias informado'), __('Até 90 dias'), __('91 a 180 dias'), __('Acima de 180 dias'), ], 'data' => [ (int) ($rowPrazo->sem_prazo ?? 0), (int) ($rowPrazo->ate90 ?? 0), (int) ($rowPrazo->ate180 ?? 0), (int) ($rowPrazo->acima180 ?? 0), ], 'colors' => ['#6c757d', '#198754', '#ffc107', '#dc3545'], ], 'por_secretaria' => [ 'labels' => $labelsGestor, 'data' => $dataGestor, 'colors' => array_slice($paletteGestor, 0, max(count($dataGestor), 1)), ], 'historico_volume_mes' => [ 'labels' => $labelsHistorico, 'data' => $dataHistorico, ], ]; } /** * Últimos registros de histórico de andamento entre os projetos do conjunto filtrado. * * @return Collection<int, array<string, string>> */ private function buildProjetosProgramasUltimosHistoricosPayload(Builder $baseFiltered): Collection { return ProjetoProgramaHistorico::query() ->whereIn('project_id', (clone $baseFiltered)->select('id')) ->with(['projeto:id,denominacao']) ->orderByDesc('data') ->orderByDesc('created_at') ->limit(25) ->get() ->map(static function (ProjetoProgramaHistorico $h): array { $texto = trim((string) ($h->texto ?? '')); return [ 'data' => $h->data !== null ? $h->data->format('d/m/Y') : '', 'status' => (string) ($h->status ?? ''), 'texto' => Str::limit($texto !== '' ? $texto : '—', 200), 'projeto' => (string) ($h->projeto?->denominacao ?? ''), 'projeto_id' => (string) $h->project_id, ]; }); } private function filteredProjectsQuery(Request $request, bool $mapaoApenasCadastroCompleto = false): Builder { $query = $this->projetosProgramasFilteredBaseQuery($request, $mapaoApenasCadastroCompleto) ->with(['gestor', 'tipo', 'classificacao', 'etapa']); $sortBy = (string) $request->get('sort_by', ''); $sortDirRaw = strtolower((string) $request->get('sort_dir', 'desc')); $sortDir = in_array($sortDirRaw, ['asc', 'desc'], true) ? $sortDirRaw : 'desc'; switch ($sortBy) { case 'mapao_num': $query->leftJoin('project_programs as pp_sort_prog', 'projects_programs.project_program_id', '=', 'pp_sort_prog.id') ->leftJoin('project_objectives as pp_sort_obj', 'projects_programs.project_objective_id', '=', 'pp_sort_obj.id') ->select('projects_programs.*') ->orderByRaw("LOWER(TRIM(COALESCE(pp_sort_prog.nome, pp_sort_obj.nome, ''))) {$sortDir}") ->orderBy('projects_programs.numero_ordem', $sortDir) ->orderBy('projects_programs.denominacao', $sortDir); break; case 'denominacao': case 'bairro': case 'orcamento_previsto': case 'orcamento_disponivel': case 'tipo_id': case 'classificacao_id': case 'etapa_id': case 'andamento_cor': case 'status_registro': case 'created_at': $query->orderBy('projects_programs.' . $sortBy, $sortDir); break; case 'prazo_implantacao': $query->orderByRaw('(projects_programs.prazo_entrega_competencia IS NULL) ASC') ->orderBy('projects_programs.prazo_entrega_competencia', $sortDir) ->orderBy('projects_programs.prazo_entrega_dias', $sortDir); break; case 'gestor_structure_id': $query->join('structures', 'projects_programs.gestor_structure_id', '=', 'structures.id') ->select('projects_programs.*') ->orderBy('structures.abbreviation', $sortDir); break; default: $query->orderByDesc('projects_programs.created_at'); break; } return $query; } private function resolvePerPage(?int $perPage): int { if ($perPage && in_array($perPage, $this->perPageOptions, true)) { return $perPage; } return $this->perPageOptions[0] ?? 10; } private function convertSelectableValue(?string $key, string $listKey): ?string { if ($key === null || $key === '') { return null; } $list = config("projetos_programas.{$listKey}", []); return $list[$key] ?? null; } /** * Normaliza campos de moeda do request, convertendo formatos brasileiros para decimal. * * Processa todos os campos de moeda definidos em $currencyFields, * convertendo valores vazios para 0 e formatos brasileiros (1.234,56) para float. * * @param Request $request Requisição HTTP * @return void */ private function normalizeCurrencyFields(Request $request): void { $payload = []; foreach ($this->currencyFields as $field) { // Processa todos os campos, mesmo que não estejam no request $value = $request->input($field); $payload[$field] = $this->toDecimal($value); } if (!empty($payload)) { $request->merge($payload); } } /** * Converte um valor para decimal, tratando formatos brasileiros e valores vazios. * * Converte strings vazias, null ou false para 0.0. * Suporta formatos brasileiros (1.234,56) e numéricos simples. * * @param mixed $value Valor a ser convertido * @return float Valor decimal (0.0 se inválido) */ private function toDecimal($value): float { if ($value === null || $value === '' || $value === false) { return 0.0; } if (is_numeric($value)) { return (float) $value; } $normalized = str_replace(['.', ' '], '', (string) $value); $normalized = str_replace(',', '.', $normalized); return is_numeric($normalized) ? (float) $normalized : 0.0; } private function validationMessages(): array { return [ 'required' => 'O campo :attribute é obrigatório.', 'date' => 'Informe uma data válida para :attribute.', 'numeric' => 'Informe um valor numérico em :attribute.', 'integer' => 'Selecione uma opção válida em :attribute.', 'exists' => 'O item selecionado em :attribute é inválido.', 'array' => 'O campo :attribute precisa ser uma lista válida.', 'email' => 'Informe um e-mail válido em :attribute.', 'file' => 'Selecione um arquivo válido para :attribute.', 'max' => 'O campo :attribute deve possuir no máximo :max caracteres.', 'in' => 'O valor selecionado em :attribute é inválido.', 'uuid' => 'O identificador informado em :attribute é inválido.', 'boolean' => 'Selecione uma opção válida em :attribute.', ]; } private function validationAttributes(): array { return [ 'project_id' => 'Identificador do projeto', 'denominacao' => 'Denominação', 'num_processo_adm' => 'Nº do processo administrativo', 'empresa_licitada' => 'Empresa licitada', 'gestor_structure_id' => 'Órgão gestor', 'tipo_id' => 'Tipo', 'programa_ppa' => 'Programa no PPA', 'numero_ordem' => 'Nº (ordem hierárquica)', 'faz_parte_objetivo' => 'Faz parte de um Objetivo', 'project_objective_id' => 'Objetivo', 'faz_parte_programa' => 'Faz parte de um Programa', 'project_program_id' => 'Programa', 'cep' => 'CEP', 'endereco' => 'Rua', 'latitude' => 'Latitude', 'longitude' => 'Longitude', 'bairro' => 'Bairro', 'data_inicio' => 'Data de início', 'data_termino' => 'Data de término', 'prazo_id' => 'Prazo de entrega', 'prazo_entrega_competencia' => 'Prazo de entrega (competência mês/an)', 'prazo_entrega_dias' => 'Prazo de entrega (dias)', 'acompanhamento_prioritario' => 'Acompanhamento prioritário', 'acompanhamento_governo' => 'Acompanhamento intensivo pelo Governo', 'classificacao_id' => 'Classificação', 'etapa_id' => 'Etapa', 'relacao_ppa_federal' => 'Relação com PPA do Governo Federal', 'relacao_ppa' => 'Descrição da relação com o PPA federal', 'eixo_id' => 'Eixo', 'area_id' => 'Área', 'ods' => 'ODS', 'contato_nome' => 'Nome do contato', 'contato_matricula' => 'Matrícula do contato', 'contato_ramal' => 'Ramal', 'contato_telefone' => 'Telefone', 'contato_email' => 'E-mail', 'descricao_objeto' => 'Descrição do objeto', 'descricao_responsabilidades' => 'Descrição das responsabilidades', 'status_id' => 'Status', 'dificuldade_id' => 'Principais dificuldades', 'andamento' => 'Descrição em texto do andamento', 'andamento_cor' => 'Cor do andamento (MAPÃO)', 'gargalos' => 'Gargalos (dificuldades)', 'observacoes' => 'Observação', 'orgaos' => 'Órgãos envolvidos', 'fonte_id' => 'Fonte', 'fonte_outro' => 'Outra fonte', 'custo_total' => 'Custo total', 'custeio_anual' => 'Custeio anual', 'orcamento_previsto' => 'Orçamento previsto', 'orcamento_disponivel' => 'Orçamento disponível', 'ppa_2022' => 'PPA 2022', 'ppa_2023' => 'PPA 2023', 'ppa_2024' => 'PPA 2024', 'ppa_2025' => 'PPA 2025', 'loa_2022' => 'LOA 2022', 'loa_2023' => 'LOA 2023', 'loa_2024' => 'LOA 2024', 'loa_2025' => 'LOA 2025', 'manutencao_2022' => 'Manutenção 2022', 'manutencao_2023' => 'Manutenção 2023', 'manutencao_2024' => 'Manutenção 2024', 'manutencao_2025' => 'Manutenção 2025', 'conve_orgao' => 'Órgão (Convênio)', 'conve_orgao_outro' => 'Outro órgão (Convênio)', 'conve_proposta' => 'Nº da proposta', 'conve_convenio' => 'Nº do convênio', 'arquivo' => 'Arquivo', ]; } private function statusOptions(): array { return [ 1 => 'Sem proposta', 2 => 'Projeto Básico', 3 => 'Projeto Executivo', 4 => 'Licitando', 5 => 'Tramitando para Licitar', 6 => 'Outro', ]; } /** * Dias da data de hoje até o último dia do mês escolhido (inclusivo). * * @param non-empty-string $yyyymm formato YYYY-MM validado antes da chamada. */ private function diasPrazoAteFimMesCompetencia(string $yyyymm): int { $tz = (string) config('app.timezone'); $inicio = Carbon::now($tz)->startOfDay(); $fimMes = Carbon::parse($yyyymm . '-01', $tz)->endOfMonth()->startOfDay(); if ($inicio->gt($fimMes)) { return 0; } return (int) $inicio->diffInDays($fimMes) + 1; } }
| ver. 1.4 |
Github
|
.
| PHP 8.3.28 | Generation time: 0.02 |
proxy
|
phpinfo
|
Settings