validate([ 'q' => ['nullable', 'string', 'max:150'], 'status' => ['nullable', Rule::in(['submitted', 'reviewed', 'rejected', 'expired'])], 'geo_location_id' => ['nullable', 'uuid', Rule::exists('geo_locations', 'id')->whereNull('deleted_at')], 'demand_page' => ['nullable', 'integer', 'min:1'], 'capacity_page' => ['nullable', 'integer', 'min:1'], ]); $demands = LabourDemand::query(); $capacities = TrainingCapacity::query(); foreach ([$demands, $capacities] as $query) { if ($term = trim($filters['q'] ?? '')) { $query->whereLike('qualification', '%'.addcslashes($term, '%_\\').'%'); } if ($territory = $filters['geo_location_id'] ?? null) { $query->where('geo_location_id', $territory); } if (($filters['status'] ?? null) === 'expired') { $query->where('valid_until', '<', now()->toDateString()); } elseif ($status = $filters['status'] ?? null) { $query->where('status', $status)->where('valid_until', '>=', now()->toDateString()); } } return Inertia::render('education/index', [ 'demands' => $demands->latest()->orderBy('id')->paginate(20, ['*'], 'demand_page')->withQueryString(), 'capacities' => $capacities->latest()->orderBy('id')->paginate(20, ['*'], 'capacity_page')->withQueryString(), 'filters' => $filters, 'indicators' => [ 'demand_records' => LabourDemand::count(), 'capacity_records' => TrainingCapacity::count(), 'expired_demands' => LabourDemand::where('valid_until', '<', now()->toDateString())->count(), 'pending_demands' => LabourDemand::where('status', 'submitted')->where('valid_until', '>=', now()->toDateString())->count(), ], 'skills' => Skill::orderBy('name')->get(['id', 'code', 'name']), 'occupations' => Occupation::orderBy('name')->get(['id', 'code', 'name']), 'providers' => TrainingProvider::where('status', '!=', 'inactive')->orderBy('name')->get(['id', 'name']), 'courses' => Course::where('status', '!=', 'retired')->whereIn('provider_id', TrainingProvider::where('status', '!=', 'inactive')->select('id'))->orderBy('title')->get(['id', 'title', 'provider_id']), 'projects' => Project::orderBy('name')->get(['id', 'code', 'name']), 'territories' => DB::table('geo_locations')->whereNull('deleted_at') ->whereIn('location_type', ['province', 'district', 'admin_post', 'locality']) ->orderBy('name')->get(['id', 'name', 'parent_id', 'location_type']), 'today' => now()->toDateString(), ]); } public function catalog(Request $request, string $kind): RedirectResponse { $model = match ($kind) { 'skills' => Skill::class, 'occupations' => Occupation::class, 'providers' => TrainingProvider::class, 'courses' => Course::class, default => abort(404), }; $record = new $model; $rules = [$kind === 'courses' ? 'title' : 'name' => ['required', 'string', 'max:200']]; if ($kind !== 'providers') { $rules['code'] = ['required', 'string', 'max:80', Rule::unique($record->getTable(), 'code')->whereNull('deleted_at')]; } if ($kind === 'courses') { $rules['provider_id'] = ['required', 'uuid', Rule::exists('training_providers', 'id')->whereNull('deleted_at')->whereNot('status', 'inactive')]; } $data = $request->validate($rules); PortalAudit::save($record, [...$data, ...match ($kind) { 'providers' => ['status' => 'candidate'], 'courses' => ['status' => 'draft'], default => [], }], $request->user()->id); return back()->with('success', 'Cadastro interno criado. Não constitui acreditação ou certificação.'); } public function store(Request $request, string $kind): RedirectResponse { $demand = $kind === 'demands'; $data = $request->validate([ 'occupation_id' => ['required', 'uuid', Rule::exists('occupations', 'id')->whereNull('deleted_at')], 'skill_id' => ['required', 'uuid', Rule::exists('skills', 'id')->whereNull('deleted_at')], 'geo_location_id' => ['required', 'uuid', Rule::exists('geo_locations', 'id')->whereNull('deleted_at')], $demand ? 'project_id' : 'course_id' => ['required', 'uuid', $demand ? Rule::exists('projects', 'id')->whereNull('deleted_at') : Rule::exists('courses', 'id')->whereNull('deleted_at')->whereNot('status', 'retired') ->where(fn ($query) => $query->whereIn('provider_id', TrainingProvider::where('status', '!=', 'inactive')->select('id')))], 'employer' => [$demand ? 'required' : 'exclude', 'string', 'max:200'], 'qualification' => ['required', 'string', 'max:150'], 'quantity' => ['required', 'integer', 'min:1', 'max:10000000'], 'period_start' => ['required', 'date_format:Y-m-d'], 'period_end' => ['required', 'date_format:Y-m-d', 'after_or_equal:period_start'], 'source' => ['required', 'string', 'min:10', 'max:5000'], 'reference_date' => ['required', 'date_format:Y-m-d', 'before_or_equal:today'], 'valid_until' => ['required', 'date_format:Y-m-d', 'after_or_equal:reference_date'], ]); PortalAudit::save($demand ? new LabourDemand : new TrainingCapacity, [...$data, 'submitted_by' => $request->user()->id, 'status' => 'submitted'], $request->user()->id); return back()->with('success', 'Declaração registada para revisão independente.'); } public function review(Request $request, string $kind, string $id): RedirectResponse { $data = $request->validate(['version_no' => ['required', 'integer', 'min:1'], 'status' => ['required', 'in:reviewed,rejected'], 'review_reason' => ['required', 'string', 'min:15', 'max:3000']]); $model = $kind === 'demands' ? LabourDemand::class : TrainingCapacity::class; DB::transaction(function () use ($model, $id, $data, $request) { $record = $model::whereKey($id)->lockForUpdate()->firstOrFail(); abort_if((int) $record->getAttribute('submitted_by') === $request->user()->id, 403, 'Outro gestor deve efectuar a revisão.'); abort_if((int) $record->getAttribute('version_no') !== $data['version_no'], 409, 'A versão foi alterada. Recarregue antes de decidir.'); abort_if($record->getAttribute('valid_until') < now()->toDateString(), 422, 'A fonte expirou.'); PortalAudit::save($record, [...$data, 'version_no' => $data['version_no'] + 1, 'reviewed_by' => $request->user()->id], $request->user()->id); }); return back()->with('success', 'Revisão interna registada; não substitui decisão institucional.'); } public function analysis(string $id, EducationGapCalculator $calculator): Response { $demand = LabourDemand::findOrFail($id); $current = $demand->getAttribute('valid_until') >= now()->toDateString(); $eligible = $current && $demand->getAttribute('status') === 'reviewed'; $candidates = $eligible ? TrainingCapacity::where('status', 'reviewed')->where('valid_until', '>=', now()->toDateString()) ->whereIn('course_id', Course::where('status', '!=', 'retired') ->whereIn('provider_id', TrainingProvider::where('status', '!=', 'inactive')->select('id'))->select('id')) ->where('occupation_id', $demand->getAttribute('occupation_id'))->where('skill_id', $demand->getAttribute('skill_id')) ->where('geo_location_id', $demand->getAttribute('geo_location_id'))->where('qualification', $demand->getAttribute('qualification')) ->where('period_end', '<=', $demand->getAttribute('period_start'))->orderBy('period_end')->get() : collect(); return Inertia::render('education/analysis', ['demand' => $demand, 'candidates' => $candidates, 'proposal' => EducationInvestmentProposal::where('labour_demand_id', $id)->first(), 'sectors' => Sector::orderBy('name')->get(['id', 'name']), 'territoryPath' => app(InvestmentGeography::class)->path($demand->getAttribute('geo_location_id'), true), 'eligible' => $eligible, 'gap' => $calculator->observed((int) $demand->getAttribute('quantity'), null, $eligible, $current)]); } }