@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Services\AdministrativeGeography;
|
||||
use App\Services\ConflictGuard;
|
||||
use App\Services\OrganizationAccess;
|
||||
use App\Services\OrganizationConfiguration;
|
||||
use App\Services\RequisitionWorkflow;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class AdministrativeGeographyController extends Controller
|
||||
{
|
||||
/** @return array<string,mixed> */
|
||||
private function reviewFilters(Request $request, AdministrativeGeography $geography): array
|
||||
{
|
||||
$actor = $request->user('web');
|
||||
abort_unless($actor instanceof User, 403);
|
||||
app(OrganizationConfiguration::class)->administrator($actor);
|
||||
abort_unless($geography->enabled(), 503);
|
||||
$filters = $request->validate(['search' => 'nullable|string|max:120', 'issue' => ['nullable', Rule::in(array_keys(AdministrativeGeography::ISSUES))], 'page' => 'sometimes|integer|min:1|max:10000']);
|
||||
|
||||
return ['search' => trim($filters['search'] ?? ''), 'issue' => $filters['issue'] ?? '', 'page' => (int) ($filters['page'] ?? 1)];
|
||||
}
|
||||
|
||||
public function review(Request $request, AdministrativeGeography $geography): Response
|
||||
{
|
||||
$filters = $this->reviewFilters($request, $geography);
|
||||
$rows = $geography->inconsistencies($filters);
|
||||
Inertia::encryptHistory();
|
||||
$response = Inertia::render('company/geography-review', ['filters' => $filters, 'records' => array_slice($rows, ($filters['page'] - 1) * 50, 50), 'total' => count($rows), 'manifest' => $geography->catalogue()['manifest'], 'issues' => AdministrativeGeography::ISSUES])->toResponse($request);
|
||||
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function exportReview(Request $request, AdministrativeGeography $geography): StreamedResponse
|
||||
{
|
||||
$filters = $this->reviewFilters($request, $geography);
|
||||
$rows = $geography->inconsistencies($filters);
|
||||
|
||||
return response()->streamDownload(function () use ($rows) {
|
||||
$output = fopen('php://output', 'w');
|
||||
if ($output === false) {
|
||||
throw new \RuntimeException('Não foi possível preparar a exportação.');
|
||||
}
|
||||
$safe = fn ($value) => preg_match('/^[\s]*[=+@-]|^[\t\r\n]/u', $value) ? "'".$value : $value;
|
||||
fputcsv($output, ['Linha', 'ID', 'Provincia', 'Distrito', 'Posto_Administrativo', 'Localidade', 'Bairro_Urbano', 'Aldeia_Quarteirao', 'Designacao', 'Diagnosticos'], ',', '"', '');
|
||||
foreach ($rows as $record) {
|
||||
$raw = $record['raw'];
|
||||
fputcsv($output, array_map($safe, [(string) $record['source_row'], $record['id'], ...array_values(array_diff_key($raw, ['ID' => true])), implode('; ', array_map(fn ($issue) => AdministrativeGeography::ISSUES[$issue], $record['issues']))]), ',', '"', '');
|
||||
}
|
||||
fclose($output);
|
||||
}, 'sgc-cartografia-pendencias.csv', ['Content-Type' => 'text/csv; charset=UTF-8', 'Cache-Control' => 'private, no-store, max-age=0', 'X-Content-Type-Options' => 'nosniff']);
|
||||
}
|
||||
|
||||
public function index(Request $request, AdministrativeGeography $geography): JsonResponse
|
||||
{
|
||||
abort_unless($request->user('web') instanceof User && $request->user('web')->tenant_id !== null, 403);
|
||||
abort_unless($geography->enabled(), 503);
|
||||
$data = $request->validate(['parent' => 'nullable|string|regex:/^[0-9]+$/D|max:20', 'page' => 'sometimes|integer|min:1|max:10000']);
|
||||
|
||||
return response()->json($geography->children($data['parent'] ?? null, (int) ($data['page'] ?? 1)));
|
||||
}
|
||||
|
||||
public function company(Request $request, AdministrativeGeography $geography): RedirectResponse
|
||||
{
|
||||
abort_unless($geography->enabled(), 503);
|
||||
$data = $request->validate(['geography_reference' => 'present|nullable|array:country,catalogue,record_id', 'geography_reference.catalogue' => 'required_with:geography_reference|string|size:64', 'geography_reference.record_id' => 'required_with:geography_reference|string|regex:/^[0-9]+$/D|max:20', 'expected_reference' => 'present|nullable|array:country,catalogue,record_id']);
|
||||
$actor = $request->user('web');
|
||||
abort_unless($actor instanceof User, 403);
|
||||
DB::transaction(function () use ($actor, $data, $geography) {
|
||||
app(ConflictGuard::class)->lockCompany($actor);
|
||||
$actor = app(OrganizationConfiguration::class)->administrator($actor);
|
||||
abort_if(app(OrganizationAccess::class)->maintenanceOnly($actor), 403);
|
||||
$company = Tenant::query()->lockForUpdate()->findOrFail($actor->tenant_id);
|
||||
if ($company->geography_reference != $data['expected_reference']) {
|
||||
throw ValidationException::withMessages(['geography_reference' => 'A localização foi alterada. Reabra a parametrização.']);
|
||||
}
|
||||
$before = $company->geography_reference;
|
||||
$after = $geography->canonical($data['geography_reference']);
|
||||
if ($before != $after) {
|
||||
$company->update(['geography_reference' => $after]);
|
||||
app(RequisitionWorkflow::class)->audit($actor, 'company.geography_updated', null, ['geography_reference' => $before], ['geography_reference' => $after]);
|
||||
}
|
||||
});
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\OrganizationAccess;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Inertia\Inertia;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AuditMetadataController extends Controller
|
||||
{
|
||||
private function actor(Request $request): User
|
||||
{
|
||||
$user = $request->user('web');
|
||||
abort_unless($user instanceof User && $user->tenant_id !== null, 403);
|
||||
$user = User::query()->where('tenant_id', $user->tenant_id)->findOrFail($user->id);
|
||||
$access = app(OrganizationAccess::class);
|
||||
abort_unless(! $access->maintenanceOnly($user) && in_array('audit_metadata_read', $access->permissions($user), true), 403);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private function filters(Request $request): array
|
||||
{
|
||||
return $request->validate(['action' => 'nullable|string|max:100', 'actor_id' => 'nullable|integer|min:1',
|
||||
'from' => 'nullable|date_format:Y-m-d', 'to' => ['nullable', 'date_format:Y-m-d', Rule::when($request->filled('from'), 'after_or_equal:from')], 'page' => 'nullable|integer|min:1|max:1000000']);
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $filters */
|
||||
private function query(User $actor, array $filters): Builder
|
||||
{
|
||||
return DB::table('audit_logs')->where('tenant_id', $actor->tenant_id)
|
||||
->when(! empty($filters['action']), fn ($q) => $q->where('action', $filters['action']))
|
||||
->when(! empty($filters['actor_id']), fn ($q) => $q->where('user_id', $filters['actor_id']))
|
||||
->when(! empty($filters['from']), fn ($q) => $q->whereDate('created_at', '>=', $filters['from']))
|
||||
->when(! empty($filters['to']), fn ($q) => $q->whereDate('created_at', '<=', $filters['to']))
|
||||
->select(['id', 'created_at', 'action', 'user_id']);
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$this->actor($request);
|
||||
$filters = $this->filters($request);
|
||||
|
||||
return DB::transaction(function () use ($request, $filters) {
|
||||
$this->readOnly();
|
||||
$actor = $this->actor($request);
|
||||
Inertia::encryptHistory();
|
||||
|
||||
$response = Inertia::render('audit/metadata', ['filters' => $filters,
|
||||
'events' => $this->query($actor, $filters)->orderBy('id')->paginate(25)->withQueryString()])->toResponse($request);
|
||||
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
|
||||
|
||||
return $response;
|
||||
});
|
||||
}
|
||||
|
||||
public function csv(Request $request): Response
|
||||
{
|
||||
$this->actor($request);
|
||||
$filters = $this->filters($request);
|
||||
|
||||
return response()->streamDownload(function () use ($request, $filters) {
|
||||
DB::transaction(function () use ($request, $filters) {
|
||||
$this->readOnly();
|
||||
$actor = $this->actor($request);
|
||||
$out = fopen('php://output', 'w');
|
||||
if ($out === false) {
|
||||
throw new \RuntimeException('Não foi possível preparar a exportação.');
|
||||
}
|
||||
$text = static function (mixed $value): string {
|
||||
$value = (string) ($value ?? '');
|
||||
|
||||
return preg_match('/^[\s\x00-\x1F]*[=+\-@]/u', $value) ? "'".$value : $value;
|
||||
};
|
||||
try {
|
||||
fwrite($out, "\xEF\xBB\xBF");
|
||||
fputcsv($out, ['Metadados de eventos da empresa', 'Consulta', now()->toIso8601String()], ';', '"', '');
|
||||
fputcsv($out, ['Registo', 'Data', 'Acção', 'Utilizador (id)'], ';', '"', '');
|
||||
foreach ($this->query($actor, $filters)->lazyById(100) as $event) {
|
||||
fputcsv($out, array_map($text, [$event->id, $event->created_at, $event->action, $event->user_id]), ';', '"', '');
|
||||
}
|
||||
} finally {
|
||||
fclose($out);
|
||||
}
|
||||
});
|
||||
}, 'eventos-metadados-'.now()->format('Ymd-His').'.csv', ['Content-Type' => 'text/csv; charset=UTF-8', 'Cache-Control' => 'private, no-store, max-age=0']);
|
||||
}
|
||||
|
||||
private function readOnly(): void
|
||||
{
|
||||
if (DB::getDriverName() === 'pgsql' && DB::transactionLevel() === 1) {
|
||||
DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,12 @@ use App\Models\CandidateAccount;
|
||||
use App\Models\PeopleProcess;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Services\AdministrativeGeography;
|
||||
use App\Services\CandidateAccess;
|
||||
use App\Services\CandidateDocuments;
|
||||
use App\Services\CandidateProfiles;
|
||||
use App\Services\CandidateSubmissions;
|
||||
use App\Services\CandidateTerritory;
|
||||
use App\Services\CandidateTriage;
|
||||
use App\Services\ConflictGuard;
|
||||
use App\Services\CurrentActor;
|
||||
@@ -17,6 +19,7 @@ use App\Services\OrganizationConfiguration;
|
||||
use App\Services\RequisitionWorkflow;
|
||||
use App\Services\SubmissionForms;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -117,12 +120,25 @@ class CandidatePortalController extends Controller
|
||||
$scopedSelection = is_array($selection) && ($selection['company'] ?? null) === $company && ($selection['account'] ?? null) === $account->id && is_int($selection['id'] ?? null) && $selection['id'] > 0;
|
||||
$selected = $this->rememberOpportunity($request, $account, $scopedSelection ? (int) $selection['id'] : null);
|
||||
|
||||
return $this->page($request, 'candidate/profile', ['companyId' => $company, 'email' => $account->email, 'profile' => $profiles->profile($account),
|
||||
$profile = $profiles->profile($account);
|
||||
|
||||
return $this->page($request, 'candidate/profile', ['companyId' => $company, 'email' => $account->email, 'profile' => $profile,
|
||||
'geographyEnabled' => app(AdministrativeGeography::class)->enabled(),
|
||||
'availabilityGeography' => app(AdministrativeGeography::class)->describe($profile['data']['availability_reference'] ?? null),
|
||||
'emailVerified' => $account->email_verified_at !== null, 'accessStatus' => $scopedSelection && $selected === null ? 'opportunity_unavailable' : $request->session()->get('candidate_access_status'), 'emailDeliveryAvailable' => app(CandidateAccess::class)->deliveryAvailable(),
|
||||
'opportunities' => $opportunities->items(), 'opportunityPagination' => ['prev' => $opportunities->previousPageUrl(), 'next' => $opportunities->nextPageUrl()], 'selectedOpportunity' => $selected,
|
||||
'submissions' => $receipts->items(), 'submissionPagination' => ['prev' => $receipts->previousPageUrl(), 'next' => $receipts->nextPageUrl(), 'total' => $receipts->total()]]);
|
||||
}
|
||||
|
||||
public function geography(Request $request, int $company, AdministrativeGeography $geography): JsonResponse
|
||||
{
|
||||
abort_unless($this->account($request, $company) !== null, 401);
|
||||
abort_unless($geography->enabled(), 503);
|
||||
$data = $request->validate(['parent' => 'nullable|string|regex:/^[0-9]+$/D|max:20', 'page' => 'sometimes|integer|min:1|max:10000']);
|
||||
|
||||
return response()->json($geography->children($data['parent'] ?? null, (int) ($data['page'] ?? 1)), 200, ['Cache-Control' => 'private, no-store, max-age=0']);
|
||||
}
|
||||
|
||||
public function save(Request $request, int $company, CandidateProfiles $profiles): Response
|
||||
{
|
||||
$account = $this->account($request, $company);
|
||||
@@ -293,21 +309,42 @@ class CandidatePortalController extends Controller
|
||||
$record = PeopleProcess::query()->where('tenant_id', $actor->tenant_id)->where('id', $process)->firstOrFail();
|
||||
$triage = app(CandidateTriage::class);
|
||||
abort_unless($triage->canRead($actor, $record), 403);
|
||||
$filter = $request->validate(['submission' => 'nullable|integer|min:1']);
|
||||
$rows = DB::table('candidate_submissions')->where('tenant_id', $actor->tenant_id)->where('people_process_id', $process)->orderBy('id')->paginate(20);
|
||||
$filter = $request->validate(['submission' => 'nullable|integer|min:1', 'location' => 'nullable|string|regex:/^[0-9]+$/D|max:20', 'page' => 'sometimes|integer|min:1|max:1000000']);
|
||||
$query = DB::table('candidate_submissions')->where('tenant_id', $actor->tenant_id)->where('people_process_id', $process);
|
||||
if (isset($filter['submission'])) {
|
||||
$rows = DB::table('candidate_submissions')->where('tenant_id', $actor->tenant_id)->where('people_process_id', $process)->where('id', $filter['submission'])->paginate(20)->withQueryString();
|
||||
$query->where('id', $filter['submission']);
|
||||
}
|
||||
if (! empty($filter['location'])) {
|
||||
app(CandidateTerritory::class)->apply($query, $filter['location']);
|
||||
}
|
||||
$rows = $query->orderBy('id')->paginate(20)->withQueryString();
|
||||
$decisions = Schema::hasTable('candidate_triage') ? DB::table('candidate_triage')->where('tenant_id', $actor->tenant_id)->where('people_process_id', $process)->whereIn('candidate_submission_id', $rows->pluck('id'))->get(['candidate_submission_id', 'outcome', 'reason', 'public_message', 'created_at', 'people_application_id'])->keyBy('candidate_submission_id') : collect();
|
||||
$rows->through(fn ($row) => ['id' => $row->id, 'reference' => $row->reference, 'received_at' => $row->created_at, 'snapshot' => json_decode($row->snapshot, true, 32, JSON_THROW_ON_ERROR), 'decision' => $decisions->get($row->id)]);
|
||||
app(RequisitionWorkflow::class)->audit($actor, 'candidate_submissions.consulted', null, null, ['process_id' => $process]);
|
||||
|
||||
return $this->page($request, 'people/submissions', ['process' => ['id' => $record->id, 'title' => $record->title, 'version' => $record->version], 'publication' => $submissions->publication($record), 'submissions' => $rows,
|
||||
'filters' => $filter, 'geographyEnabled' => app(AdministrativeGeography::class)->enabled() && in_array(DB::getDriverName(), ['sqlite', 'pgsql'], true),
|
||||
'geographyFilter' => empty($filter['location']) ? null : app(AdministrativeGeography::class)->describe(['country' => 'MZ', 'catalogue' => config('geography.source_sha256'), 'record_id' => $filter['location']]),
|
||||
'canPrepare' => $triage->canPrepare($actor, $record), 'canTriage' => Schema::hasTable('candidate_triage') && $triage->canPrepare($actor, $record) && in_array($record->status, ['open', 'assessment'], true),
|
||||
'publicUrl' => '/candidaturas/'.$actor->tenant_id.'/oportunidades/'.$record->id]);
|
||||
});
|
||||
}
|
||||
|
||||
public function processGeography(Request $request, int $process, AdministrativeGeography $geography): JsonResponse
|
||||
{
|
||||
return DB::transaction(function () use ($request, $process, $geography) {
|
||||
$actor = $this->staff($request);
|
||||
app(ConflictGuard::class)->lockCompany($actor);
|
||||
$actor = app(CurrentActor::class)->lock($actor);
|
||||
$record = PeopleProcess::query()->where('tenant_id', $actor->tenant_id)->where('id', $process)->firstOrFail();
|
||||
abort_unless(app(CandidateTriage::class)->canRead($actor, $record), 403);
|
||||
abort_unless($geography->enabled(), 503);
|
||||
$data = $request->validate(['parent' => 'nullable|string|regex:/^[0-9]+$/D|max:20', 'page' => 'sometimes|integer|min:1|max:10000']);
|
||||
|
||||
return response()->json($geography->children($data['parent'] ?? null, (int) ($data['page'] ?? 1)), 200, ['Cache-Control' => 'private, no-store, max-age=0']);
|
||||
});
|
||||
}
|
||||
|
||||
public function publish(Request $request, int $process, CandidateSubmissions $submissions): Response
|
||||
{
|
||||
$submissions->publish($this->staff($request), $process, $request->all());
|
||||
|
||||
@@ -7,11 +7,17 @@ use App\Models\CriticalityLevel;
|
||||
use App\Models\Department;
|
||||
use App\Models\Equipment;
|
||||
use App\Models\EquipmentSparePart;
|
||||
use App\Models\Requisition;
|
||||
use App\Models\SparePart;
|
||||
use App\Models\StockItem;
|
||||
use App\Models\StockMovement;
|
||||
use App\Models\StockOperation;
|
||||
use App\Models\User;
|
||||
use App\Services\AdministrativeGeography;
|
||||
use App\Services\ConflictGuard;
|
||||
use App\Services\CurrentActor;
|
||||
use App\Services\EquipmentWarehouseRequests;
|
||||
use App\Services\OrganizationAccess;
|
||||
use App\Services\ReceiptWorkflow;
|
||||
use App\Services\RequisitionWorkflow;
|
||||
use Brick\Math\BigDecimal;
|
||||
@@ -26,17 +32,66 @@ use Inertia\Response;
|
||||
|
||||
class EquipmentController extends Controller
|
||||
{
|
||||
private function actor(Request $request): User
|
||||
{
|
||||
$actor = $request->user('web');
|
||||
abort_unless($actor instanceof User, 403);
|
||||
|
||||
return $actor;
|
||||
}
|
||||
|
||||
public function show(Request $request, int $equipment): \Symfony\Component\HttpFoundation\Response
|
||||
{
|
||||
$actor = $request->user('web');
|
||||
abort_unless($actor instanceof User, 403);
|
||||
$actor = User::query()->where('tenant_id', $actor->tenant_id)->findOrFail($actor->id);
|
||||
abort_unless($actor->hasFunctions([PurchasingRole::Admin, PurchasingRole::Warehouse, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
|
||||
$request->validate(['purchases_page' => 'nullable|integer|min:1|max:1000000', 'movements_page' => 'nullable|integer|min:1|max:1000000', 'requests_page' => 'nullable|integer|min:1|max:1000000']);
|
||||
|
||||
return DB::transaction(function () use ($request, $actor, $equipment) {
|
||||
if (DB::getDriverName() === 'pgsql' && DB::transactionLevel() === 1) {
|
||||
DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
||||
}
|
||||
$asset = Equipment::query()->when($actor->departmentScoped(), fn ($q) => $q->where('department_id', $actor->department_id))->findOrFail($equipment);
|
||||
$purchases = Requisition::query()->visibleTo($actor)->whereHas('items', fn ($q) => $q->where('equipment_id', $asset->id))
|
||||
->with(['items' => fn ($q) => $q->where('equipment_id', $asset->id)->select(['id', 'requisition_id', 'description', 'quantity', 'unit'])])
|
||||
->orderBy('id')->paginate(15, ['id', 'number', 'status', 'needed_by'], 'purchases_page')->withQueryString();
|
||||
$movements = StockOperation::query()->where('equipment_id', $asset->id)->orderBy('id')
|
||||
->paginate(15, ['id', 'kind', 'reference', 'quantity_milli', 'recorded_at', 'snapshot'], 'movements_page')->withQueryString()
|
||||
->through(fn (StockOperation $operation) => [
|
||||
'id' => $operation->id, 'kind' => $operation->kind, 'reference' => $operation->reference,
|
||||
'quantity_milli' => $operation->quantity_milli, 'recorded_at' => $operation->recorded_at,
|
||||
'item' => $operation->snapshot['stock_item'] ?? null, 'warehouse' => $operation->snapshot['warehouse'] ?? null,
|
||||
]);
|
||||
Inertia::encryptHistory();
|
||||
|
||||
$response = Inertia::render('equipment/show', ['equipment' => $asset->only(['id', 'code', 'description', 'area', 'status']),
|
||||
'geography' => app(AdministrativeGeography::class)->describe($asset->geography_reference),
|
||||
'warehouseRequests' => app(EquipmentWarehouseRequests::class)->page($actor, $asset),
|
||||
'purchases' => $purchases, 'movements' => $movements])->toResponse($request);
|
||||
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
|
||||
|
||||
return $response;
|
||||
});
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
abort_unless($request->user()->hasFunctions([PurchasingRole::Admin, PurchasingRole::Warehouse, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
|
||||
abort_unless($this->actor($request)->hasFunctions([PurchasingRole::Admin, PurchasingRole::Warehouse, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
|
||||
$filters = $request->validate(['location' => 'nullable|string|regex:/^[0-9]+$/D|max:20']);
|
||||
$geography = app(AdministrativeGeography::class);
|
||||
$assets = Equipment::query();
|
||||
if ($request->user()->departmentScoped()) {
|
||||
$assets->where('department_id', $request->user()->department_id);
|
||||
if (! empty($filters['location'])) {
|
||||
abort_unless($geography->availableFor('equipment'), 503);
|
||||
$assets->where('geography_reference->catalogue', config('geography.source_sha256'))->whereIn('geography_reference->record_id', $geography->descendants($filters['location']));
|
||||
}
|
||||
if ($this->actor($request)->departmentScoped()) {
|
||||
$assets->where('department_id', $this->actor($request)->department_id);
|
||||
}
|
||||
$equipment = $assets->orderBy('code')->get();
|
||||
$links = EquipmentSparePart::query()->whereIn('equipment_id', $equipment->pluck('id'))->get();
|
||||
$parts = SparePart::query();
|
||||
if ($request->user()->departmentScoped()) {
|
||||
if ($this->actor($request)->departmentScoped() || ! empty($filters['location'])) {
|
||||
$parts->whereIn('id', $links->pluck('spare_part_id'));
|
||||
}
|
||||
$parts = $parts->get()->map(function (SparePart $part) {
|
||||
@@ -47,24 +102,28 @@ class EquipmentController extends Controller
|
||||
});
|
||||
$items = StockItem::query();
|
||||
$departments = Department::query();
|
||||
if ($request->user()->departmentScoped()) {
|
||||
if ($this->actor($request)->departmentScoped() || ! empty($filters['location'])) {
|
||||
$items->whereIn('id', $parts->pluck('stock_item_id')->merge($equipment->pluck('stock_item_id')->filter()));
|
||||
$departments->whereKey($request->user()->department_id);
|
||||
if ($this->actor($request)->departmentScoped()) {
|
||||
$departments->whereKey($this->actor($request)->department_id);
|
||||
}
|
||||
}
|
||||
|
||||
return Inertia::render('equipment/index', [
|
||||
'geographyEnabled' => $geography->availableFor('equipment'), 'equipmentGeography' => $equipment->mapWithKeys(fn (Equipment $asset) => [$asset->id => $geography->describe($asset->geography_reference)])->all(),
|
||||
'filters' => $filters, 'geographyFilter' => empty($filters['location']) ? null : $geography->describe(['catalogue' => config('geography.source_sha256'), 'record_id' => $filters['location'], 'country' => 'MZ']),
|
||||
'equipment' => $equipment, 'parts' => $parts, 'links' => $links, 'levels' => CriticalityLevel::query()->orderBy('code')->get(),
|
||||
'items' => $items->orderBy('code')->get(['id', 'code', 'name', 'unit']),
|
||||
'departments' => $departments->orderBy('name')->get(['id', 'name']),
|
||||
'history' => StockOperation::query()->whereNotNull('equipment_id')->whereIn('equipment_id', $equipment->pluck('id'))->latest('id')->paginate(15),
|
||||
'canManage' => $request->user()->hasFunctions([PurchasingRole::Admin, PurchasingRole::Warehouse, PurchasingRole::Procurement]),
|
||||
'canConfigure' => $request->user()->hasFunctions([PurchasingRole::Admin]),
|
||||
'canManage' => $this->actor($request)->hasFunctions([PurchasingRole::Admin, PurchasingRole::Warehouse, PurchasingRole::Procurement]),
|
||||
'canConfigure' => $this->actor($request)->hasFunctions([PurchasingRole::Admin]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request, string $kind): RedirectResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
$user = $this->actor($request);
|
||||
abort_unless(in_array($kind, ['categorias', 'activos', 'pecas', 'associacoes']), 404);
|
||||
abort_unless($kind === 'categorias' ? $user->hasFunctions([PurchasingRole::Admin]) : $user->hasFunctions([PurchasingRole::Admin, PurchasingRole::Warehouse, PurchasingRole::Procurement]), 403);
|
||||
$tenant = $user->tenant_id;
|
||||
@@ -76,9 +135,17 @@ class EquipmentController extends Controller
|
||||
'pecas' => ['id' => ['nullable', 'integer', $exists('spare_parts')], 'version' => 'nullable|required_with:id|integer|min:1', 'stock_item_id' => ['required', 'integer', $exists('stock_items'), Rule::unique('spare_parts')->ignore($request->input('id'))], 'criticality_level_id' => ['required', 'integer', $exists('criticality_levels')], 'minimum_quantity' => ['required', 'regex:/^\d{1,9}(\.\d{1,3})?$/']],
|
||||
'associacoes' => ['equipment_id' => ['required', 'integer', $exists('equipment')], 'spare_part_id' => ['required', 'integer', $exists('spare_parts')]],
|
||||
};
|
||||
if ($kind === 'activos') {
|
||||
$rules += app(AdministrativeGeography::class)->referenceRules('equipment');
|
||||
}
|
||||
$data = $request->validate($rules);
|
||||
DB::transaction(function () use ($data, $kind, $user) {
|
||||
app(ConflictGuard::class)->lockCompany($user);
|
||||
$user = app(CurrentActor::class)->lock($user, $kind === 'categorias' ? [PurchasingRole::Admin] : [PurchasingRole::Admin, PurchasingRole::Warehouse, PurchasingRole::Procurement]);
|
||||
abort_if(app(OrganizationAccess::class)->maintenanceOnly($user), 403);
|
||||
if ($kind === 'activos' && array_key_exists('geography_reference', $data)) {
|
||||
$data['geography_reference'] = app(AdministrativeGeography::class)->canonicalFor('equipment', $data['geography_reference']);
|
||||
}
|
||||
$model = match ($kind) {
|
||||
'categorias' => new CriticalityLevel,'activos' => new Equipment,'pecas' => new SparePart,'associacoes' => new EquipmentSparePart
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ class GoodsReceiptController extends Controller
|
||||
private function visibleOrders(Request $request): Builder
|
||||
{
|
||||
Gate::authorize('viewAny', GoodsReceipt::class);
|
||||
$orders = PurchaseOrder::query();
|
||||
$orders = PurchaseOrder::query()->operational();
|
||||
if ($this->actor($request)->departmentScoped()) {
|
||||
$orders->whereIn('requisition_id', Requisition::query()->visibleTo($this->actor($request))->select('id'));
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class InvoiceController extends Controller
|
||||
$response = Inertia::render('invoices/index', ['focusInvoiceId' => $focus, 'focusOrderId' => $focusOrder, 'stockReversals' => app(InventoryReversals::class)->financialNotices(), 'stockReturns' => app(InventoryDispositions::class)->financialNotices(),
|
||||
'invoices' => Invoice::query()->whereIn('purchase_order_id', PurchaseOrder::query()->visibleTo($actor)->select('id'))->when($focus !== null, fn ($q) => $q->whereKey($focus))->when($focusOrder !== null, fn ($q) => $q->where('purchase_order_id', $focusOrder))->with('items')->latest('id')->paginate(15)->withQueryString()
|
||||
->through(fn (Invoice $invoice) => [...$invoice->toArray(), 'canValidate' => app(InvoiceWorkflow::class)->reviewerEligible($actor, $invoice)]),
|
||||
'orders' => PurchaseOrder::query()->visibleTo($actor)->with('items')->latest('id')->get()->map(fn ($order) => ['id' => $order->id, 'number' => $order->number, 'supplier' => $order->snapshot['supplier']['name'], 'items' => $order->items]),
|
||||
'orders' => PurchaseOrder::query()->visibleTo($actor)->operational()->with('items')->latest('id')->get()->map(fn ($order) => ['id' => $order->id, 'number' => $order->number, 'supplier' => $order->snapshot['supplier']['name'], 'items' => $order->items]),
|
||||
'payablesAvailable' => SupplierPayables::enabled() && app(SupplierPayables::class)->can($actor, 'view'), 'canManage' => $actor->hasFunctions([PurchasingRole::Finance]) && ! app(OrganizationAccess::class)->maintenanceOnly($actor)])->toResponse($request);
|
||||
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\OrderCancellation;
|
||||
use App\Models\OrderCancellationPolicy;
|
||||
use App\Models\PurchaseOrder;
|
||||
use App\Models\Requisition;
|
||||
use App\Models\User;
|
||||
use App\Services\OrderCancellationPolicyWorkflow;
|
||||
use App\Services\OrderCancellationWorkflow;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Inertia\Inertia;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class OrderCancellationController extends Controller
|
||||
{
|
||||
private function actor(Request $request): User
|
||||
{
|
||||
$actor = $request->user('web');
|
||||
abort_unless($actor instanceof User, 403);
|
||||
abort_unless(config('order_cancellation.enabled') && Schema::hasTable('order_cancellations'), 503, 'Cancelamento de ordens não activado.');
|
||||
|
||||
return $actor;
|
||||
}
|
||||
|
||||
public function index(Request $request, OrderCancellationWorkflow $workflow): Response
|
||||
{
|
||||
$actor = $this->actor($request);
|
||||
$focus = $request->validate(['cancellation_id' => 'nullable|integer|min:1'])['cancellation_id'] ?? null;
|
||||
abort_unless($actor->hasFunctions(['admin', 'procurement', 'finance', 'requester', 'department_head']), 403);
|
||||
|
||||
return DB::transaction(function () use ($request, $actor, $workflow, $focus) {
|
||||
if (DB::getDriverName() === 'pgsql' && DB::transactionLevel() === 1) {
|
||||
DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
||||
}
|
||||
Inertia::encryptHistory();
|
||||
$records = OrderCancellation::query()->whereIn('purchase_order_id', PurchaseOrder::query()->visibleTo($actor)->select('id'))
|
||||
->when($focus !== null, fn ($q) => $q->whereKey($focus))
|
||||
->orderByDesc('id')->paginate(15)->withQueryString()->through(function ($record) use ($actor, $workflow) {
|
||||
$order = PurchaseOrder::query()->findOrFail($record->purchase_order_id);
|
||||
$source = Requisition::query()->findOrFail($order->requisition_id);
|
||||
|
||||
return [...$record->only(['id', 'purchase_order_id', 'created_by', 'reason', 'evidence', 'status', 'review_reason', 'reviewed_at', 'revised_requisition_id']),
|
||||
'order_number' => $order->number, 'canDecide' => $workflow->canDecide($actor, $record),
|
||||
'source_href' => $actor->hasFunctions(['admin', 'procurement', 'finance', 'department_head']) ? '/ordens/'.$order->id : '/requisicoes/'.$source->id.'/processo',
|
||||
'canRevise' => $workflow->canRevise($actor, $record)];
|
||||
});
|
||||
$response = Inertia::render('purchase-controls/orders', ['records' => $records,
|
||||
'policies' => $actor->hasFunctions(['admin']) ? OrderCancellationPolicy::query()->latest('revision')->get()->map(fn ($p) => [...$p->toArray(),
|
||||
'canReview' => app(OrderCancellationPolicyWorkflow::class)->canReview($actor, $p)]) : [],
|
||||
'canAdminister' => $actor->hasFunctions(['admin']), 'canDecide' => $actor->hasFunctions(['finance'])])->toResponse($request);
|
||||
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
|
||||
|
||||
return $response;
|
||||
});
|
||||
}
|
||||
|
||||
public function request(Request $request, PurchaseOrder $order, OrderCancellationWorkflow $workflow): Response
|
||||
{
|
||||
$workflow->request($this->actor($request), $order, $request->all());
|
||||
|
||||
return redirect('/ordens/cancelamentos');
|
||||
}
|
||||
|
||||
public function decide(Request $request, int $cancellation, OrderCancellationWorkflow $workflow): Response
|
||||
{
|
||||
$actor = $this->actor($request);
|
||||
$data = $request->validate(['decision' => 'required|in:approved,rejected', 'reason' => 'required|string|min:10|max:4000']);
|
||||
$workflow->decide($actor, OrderCancellation::query()->findOrFail($cancellation), $data['decision'], $data['reason']);
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function revise(Request $request, int $cancellation, OrderCancellationWorkflow $workflow): Response
|
||||
{
|
||||
$actor = $this->actor($request);
|
||||
$data = $request->validate(['needed_by' => 'required|date_format:Y-m-d|after_or_equal:today', 'reason' => 'required|string|min:10|max:4000']);
|
||||
$record = $workflow->revise($actor, OrderCancellation::query()->findOrFail($cancellation), $data['needed_by'], $data['reason']);
|
||||
|
||||
return redirect('/requisicoes/'.$record->id);
|
||||
}
|
||||
|
||||
public function policy(Request $request, OrderCancellationPolicyWorkflow $workflow): Response
|
||||
{
|
||||
$workflow->propose($this->actor($request), $request->all());
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function policyReview(Request $request, int $policy, OrderCancellationPolicyWorkflow $workflow): Response
|
||||
{
|
||||
$actor = $this->actor($request);
|
||||
$data = $request->validate(['decision' => 'required|in:approved,rejected', 'reason' => 'required|string|min:10|max:4000']);
|
||||
$workflow->review($actor, OrderCancellationPolicy::query()->findOrFail($policy), $data['decision'], $data['reason']);
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Services\AdministrativeGeography;
|
||||
use App\Services\AiPreparationPlan;
|
||||
use App\Services\ComparisonPolicy;
|
||||
use App\Services\IntelligencePolicy;
|
||||
@@ -37,6 +38,8 @@ class OrganizationConfigurationController extends Controller
|
||||
$response = Inertia::render('company/parameters', ['ready' => $ready, 'active' => (bool) ($state->active ?? false), 'version' => $state->version ?? 1,
|
||||
'workflowTasks' => app(WorkflowTasks::class)->catalogue(),
|
||||
'activationPreview' => app(OrganizationActivationPreview::class)->build($actor),
|
||||
'geographyEnabled' => app(AdministrativeGeography::class)->enabled(),
|
||||
'geography' => app(AdministrativeGeography::class)->describe(Tenant::query()->findOrFail($actor->tenant_id)->geography_reference),
|
||||
'companyRules' => $this->companyRules((int) $actor->tenant_id),
|
||||
'comparisonPolicy' => app(ComparisonPolicy::class)->company(Tenant::query()->findOrFail($actor->tenant_id)),
|
||||
'intelligencePolicy' => app(IntelligencePolicy::class)->company(Tenant::query()->findOrFail($actor->tenant_id)),
|
||||
|
||||
@@ -15,9 +15,11 @@ use App\Services\PeopleEvidenceBrief;
|
||||
use App\Services\PeopleForms;
|
||||
use App\Services\PeopleHiring;
|
||||
use App\Services\PeopleOperationalInsights;
|
||||
use App\Services\PeopleProcessProjection;
|
||||
use App\Services\PeopleRegistry;
|
||||
use App\Services\PeopleWorkdesk;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Validation\Rule;
|
||||
@@ -89,21 +91,9 @@ class PeopleHiringController extends Controller
|
||||
$access = app(PeopleAccess::class);
|
||||
abort_unless($access->can($this->actor($request), $record, 'view'), 403);
|
||||
$registrySearch = $request->validate(['cadastro' => 'nullable|string|max:100'])['cadastro'] ?? '';
|
||||
$snapshot = $workflow->snapshot($record);
|
||||
$rows = $workflow->matrix($record);
|
||||
if (! $access->can($this->actor($request), $record, 'financial')) {
|
||||
unset($snapshot['rules']['annex']['fields']['financial']);
|
||||
unset($snapshot['selection']['rules']['annex']['fields']['financial']);
|
||||
}
|
||||
if (! $access->can($this->actor($request), $record, 'interviews')) {
|
||||
$rows = array_map(fn ($row) => [...$row, 'reviews' => []], $rows);
|
||||
if (isset($snapshot['selection']['rows'])) {
|
||||
$snapshot['selection']['rows'] = array_map(fn ($row) => [...$row, 'reviews' => []], $snapshot['selection']['rows']);
|
||||
}
|
||||
}
|
||||
if (! $access->can($this->actor($request), $record, 'contract') && ! $access->can($this->actor($request), $record, 'contract_view')) {
|
||||
$snapshot['contract'] = null;
|
||||
}
|
||||
$projection = app(PeopleProcessProjection::class)->read($this->actor($request), $record);
|
||||
$snapshot = $projection['process'];
|
||||
$rows = $projection['candidates'];
|
||||
$clarifications = app(PeopleClarifications::class)->page($this->actor($request), $record);
|
||||
$company = Tenant::query()->findOrFail($this->actor($request)->tenant_id);
|
||||
$policy = app(IntelligencePolicy::class)->company($company);
|
||||
@@ -127,6 +117,24 @@ class PeopleHiringController extends Controller
|
||||
});
|
||||
}
|
||||
|
||||
public function dossier(Request $request, int $process, PeopleProcessProjection $projection): Response
|
||||
{
|
||||
return DB::transaction(function () use ($request, $process, $projection) {
|
||||
if (DB::getDriverName() === 'pgsql' && DB::transactionLevel() === 1) {
|
||||
DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
||||
}
|
||||
$record = PeopleProcess::query()->findOrFail($process);
|
||||
$data = $projection->read($this->actor($request), $record);
|
||||
|
||||
return response()->json(['schema' => 'sgc.people.process-dossier.v1', 'exported_at' => now()->toIso8601String(),
|
||||
'effect' => 'preparation_only',
|
||||
'process' => Arr::only($data['process'], ['id', 'kind', 'title', 'purpose', 'closes_on', 'status', 'version', 'selected_application_id', 'rules', 'selection', 'contract']),
|
||||
'candidates' => $data['candidates']])
|
||||
->header('Content-Disposition', 'attachment; filename="PES-'.$record->id.'-dossier-v'.$record->version.'.json"')
|
||||
->header('Cache-Control', 'private, no-store, max-age=0')->header('X-Content-Type-Options', 'nosniff');
|
||||
});
|
||||
}
|
||||
|
||||
public function access(Request $request, int $process, PeopleAccess $access): Response
|
||||
{
|
||||
$record = PeopleProcess::query()->findOrFail($process);
|
||||
|
||||
@@ -11,12 +11,14 @@ use App\Models\RecommendationAttempt;
|
||||
use App\Models\Requisition;
|
||||
use App\Models\User;
|
||||
use App\Services\FormalRecommendation;
|
||||
use App\Services\OrderCancellationWorkflow;
|
||||
use App\Services\OrganizationAccess;
|
||||
use App\Services\ProposalComparison;
|
||||
use App\Services\PurchaseWorkflow;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
@@ -117,6 +119,9 @@ class PurchaseController extends Controller
|
||||
|
||||
return Inertia::render('purchases/show', [
|
||||
'order' => $order->load('items'),
|
||||
'orderCancellation' => ['enabled' => (bool) config('order_cancellation.enabled') && Schema::hasTable('order_cancellations'),
|
||||
'state' => app(OrderCancellationWorkflow::class)->state($order),
|
||||
'canRequest' => app(OrderCancellationWorkflow::class)->canRequest($actor, $order)],
|
||||
'history' => AuditLog::query()->where('requisition_id', $order->requisition_id)->whereIn('action', ['award.created', 'award.approved', 'award.rejected', 'purchase_order.issued'])->orderBy('id')->get(['id', 'user_id', 'action', 'created_at']),
|
||||
]);
|
||||
}
|
||||
@@ -126,6 +131,7 @@ class PurchaseController extends Controller
|
||||
$actor = $this->access($request);
|
||||
PurchaseOrder::query()->visibleTo($actor)->findOrFail($order->id);
|
||||
|
||||
return view('purchases.print', ['order' => $order->load('items'), 'snapshot' => $order->snapshot]);
|
||||
return view('purchases.print', ['order' => $order->load('items'), 'snapshot' => $order->snapshot,
|
||||
'operationalState' => app(OrderCancellationWorkflow::class)->state($order)]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,24 +5,39 @@ namespace App\Http\Controllers;
|
||||
use App\Enums\PurchasingRole;
|
||||
use App\Models\StockItem;
|
||||
use App\Models\StockMovement;
|
||||
use App\Models\User;
|
||||
use App\Models\Warehouse;
|
||||
use App\Services\AdministrativeGeography;
|
||||
use App\Services\ConflictGuard;
|
||||
use App\Services\CurrentActor;
|
||||
use App\Services\InventoryLots;
|
||||
use App\Services\OrganizationAccess;
|
||||
use App\Services\RequisitionWorkflow;
|
||||
use Brick\Math\BigDecimal;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class StockController extends Controller
|
||||
{
|
||||
private function actor(Request $request): User
|
||||
{
|
||||
$actor = $request->user('web');
|
||||
abort_unless($actor instanceof User, 403);
|
||||
|
||||
return $actor;
|
||||
}
|
||||
|
||||
private function access(Request $request, bool $manage = false): void
|
||||
{
|
||||
$roles = $manage ? [PurchasingRole::Admin, PurchasingRole::Warehouse] : [PurchasingRole::Admin, PurchasingRole::Warehouse, PurchasingRole::Procurement, PurchasingRole::Finance];
|
||||
abort_unless($request->user()->hasFunctions($roles), 403);
|
||||
abort_unless($this->actor($request)->hasFunctions($roles), 403);
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
@@ -30,15 +45,30 @@ class StockController extends Controller
|
||||
$this->access($request);
|
||||
$filters = $request->validate([
|
||||
'search' => 'nullable|string|max:100',
|
||||
'warehouse_id' => ['nullable', 'integer', Rule::exists('warehouses', 'id')->where('tenant_id', $request->user()->tenant_id)],
|
||||
'location' => 'nullable|string|regex:/^[0-9]+$/D|max:20',
|
||||
'warehouse_id' => ['nullable', 'integer', Rule::exists('warehouses', 'id')->where('tenant_id', $this->actor($request)->tenant_id)],
|
||||
]);
|
||||
$items = StockItem::query()->orderBy('code')->get(['id', 'code', 'name', 'unit']);
|
||||
$warehouses = Warehouse::query()->orderBy('code')->get(['id', 'code', 'name']);
|
||||
$geography = app(AdministrativeGeography::class);
|
||||
$warehouseQuery = Warehouse::query();
|
||||
if (! empty($filters['location'])) {
|
||||
abort_unless($geography->availableFor('warehouses'), 503);
|
||||
$warehouseQuery->where('geography_reference->catalogue', config('geography.source_sha256'))->whereIn('geography_reference->record_id', $geography->descendants($filters['location']));
|
||||
}
|
||||
$columns = ['id', 'code', 'name'];
|
||||
if (Schema::hasColumn('warehouses', 'geography_reference')) {
|
||||
$columns[] = 'geography_reference';
|
||||
}
|
||||
$warehouses = $warehouseQuery->orderBy('code')->get($columns);
|
||||
$warehouseGeography = $warehouses->mapWithKeys(fn (Warehouse $warehouse) => [$warehouse->id => $geography->describe($warehouse->geography_reference)])->all();
|
||||
$selected = StockItem::query();
|
||||
if (! empty($filters['search'])) {
|
||||
$selected->where(fn ($q) => $q->where('code', 'like', '%'.$filters['search'].'%')->orWhere('name', 'like', '%'.$filters['search'].'%'));
|
||||
}
|
||||
$movements = StockMovement::query()->whereIn('stock_item_id', $selected->select('id'));
|
||||
if (! empty($filters['location'])) {
|
||||
$movements->whereIn('warehouse_id', $warehouses->pluck('id'));
|
||||
}
|
||||
if (! empty($filters['warehouse_id'])) {
|
||||
$movements->where('warehouse_id', $filters['warehouse_id']);
|
||||
}
|
||||
@@ -50,8 +80,10 @@ class StockController extends Controller
|
||||
|
||||
return Inertia::render('stock/index', [
|
||||
'items' => $items, 'warehouses' => $warehouses, 'balances' => $balances,
|
||||
'geographyEnabled' => $geography->availableFor('warehouses'), 'warehouseGeography' => $warehouseGeography,
|
||||
'geographyFilter' => empty($filters['location']) ? null : $geography->describe(['catalogue' => config('geography.source_sha256'), 'record_id' => $filters['location'], 'country' => 'MZ']),
|
||||
'movements' => $movements->latest('id')->paginate(20)->withQueryString(), 'filters' => $filters,
|
||||
'canManage' => $request->user()->hasFunctions([PurchasingRole::Admin, PurchasingRole::Warehouse]),
|
||||
'canManage' => $this->actor($request)->hasFunctions([PurchasingRole::Admin, PurchasingRole::Warehouse]),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -60,12 +92,12 @@ class StockController extends Controller
|
||||
$this->access($request, true);
|
||||
$request->merge(['code' => Str::upper(trim($request->string('code')->toString())), 'unit' => Str::lower(trim($request->string('unit')->toString()))]);
|
||||
$data = $request->validate([
|
||||
'code' => ['required', 'regex:/^[A-Z0-9][A-Z0-9._-]{0,29}$/', Rule::unique('stock_items')->where('tenant_id', $request->user()->tenant_id)],
|
||||
'code' => ['required', 'regex:/^[A-Z0-9][A-Z0-9._-]{0,29}$/', Rule::unique('stock_items')->where('tenant_id', $this->actor($request)->tenant_id)],
|
||||
'name' => 'required|string|max:200', 'unit' => 'required|string|max:30',
|
||||
]);
|
||||
DB::transaction(function () use ($request, $audit, $data) {
|
||||
$item = StockItem::query()->create([...$data, 'tenant_id' => $request->user()->tenant_id, 'created_by' => $request->user()->id]);
|
||||
$audit->audit($request->user(), 'stock.item_created', null, null, $item->toArray());
|
||||
$item = StockItem::query()->create([...$data, 'tenant_id' => $this->actor($request)->tenant_id, 'created_by' => $this->actor($request)->id]);
|
||||
$audit->audit($this->actor($request), 'stock.item_created', null, null, $item->toArray());
|
||||
});
|
||||
|
||||
return back();
|
||||
@@ -76,12 +108,44 @@ class StockController extends Controller
|
||||
$this->access($request, true);
|
||||
$request->merge(['code' => Str::upper(trim($request->string('code')->toString()))]);
|
||||
$data = $request->validate([
|
||||
'code' => ['required', 'regex:/^[A-Z0-9][A-Z0-9._-]{0,29}$/', Rule::unique('warehouses')->where('tenant_id', $request->user()->tenant_id)],
|
||||
'code' => ['required', 'regex:/^[A-Z0-9][A-Z0-9._-]{0,29}$/', Rule::unique('warehouses')->where('tenant_id', $this->actor($request)->tenant_id)],
|
||||
'name' => 'required|string|max:200',
|
||||
...app(AdministrativeGeography::class)->referenceRules('warehouses'),
|
||||
]);
|
||||
DB::transaction(function () use ($request, $audit, $data) {
|
||||
$warehouse = Warehouse::query()->create([...$data, 'tenant_id' => $request->user()->tenant_id, 'created_by' => $request->user()->id]);
|
||||
$audit->audit($request->user(), 'stock.warehouse_created', null, null, $warehouse->toArray());
|
||||
app(ConflictGuard::class)->lockCompany($this->actor($request));
|
||||
$actor = app(CurrentActor::class)->lock($this->actor($request), [PurchasingRole::Admin, PurchasingRole::Warehouse]);
|
||||
abort_if(app(OrganizationAccess::class)->maintenanceOnly($actor), 403);
|
||||
if (array_key_exists('geography_reference', $data)) {
|
||||
$data['geography_reference'] = app(AdministrativeGeography::class)->canonicalFor('warehouses', $data['geography_reference']);
|
||||
}
|
||||
$warehouse = Warehouse::query()->create([...$data, 'tenant_id' => $this->actor($request)->tenant_id, 'created_by' => $this->actor($request)->id]);
|
||||
$audit->audit($this->actor($request), 'stock.warehouse_created', null, null, $warehouse->toArray());
|
||||
});
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function warehouseGeography(Request $request, int $warehouse, RequisitionWorkflow $audit): RedirectResponse
|
||||
{
|
||||
$this->access($request, true);
|
||||
$geography = app(AdministrativeGeography::class);
|
||||
abort_unless($geography->availableFor('warehouses'), 503);
|
||||
$data = $request->validate([...$geography->referenceRules('warehouses'), 'geography_reference' => 'present|nullable|array:country,catalogue,record_id', 'expected_reference' => 'present|nullable|array:country,catalogue,record_id']);
|
||||
DB::transaction(function () use ($request, $warehouse, $audit, $data, $geography) {
|
||||
app(ConflictGuard::class)->lockCompany($this->actor($request));
|
||||
$actor = app(CurrentActor::class)->lock($this->actor($request), [PurchasingRole::Admin, PurchasingRole::Warehouse]);
|
||||
abort_if(app(OrganizationAccess::class)->maintenanceOnly($actor), 403);
|
||||
$record = Warehouse::query()->lockForUpdate()->findOrFail($warehouse);
|
||||
if ($record->geography_reference != $data['expected_reference']) {
|
||||
throw ValidationException::withMessages(['geography_reference' => 'A localização mudou. Actualize antes de guardar.']);
|
||||
}
|
||||
$before = $record->toArray();
|
||||
$after = $geography->canonicalFor('warehouses', $data['geography_reference']);
|
||||
if ($record->geography_reference != $after) {
|
||||
$record->update(['geography_reference' => $after]);
|
||||
$audit->audit($actor, 'stock.warehouse_geography_updated', null, $before, $record->toArray());
|
||||
}
|
||||
});
|
||||
|
||||
return back();
|
||||
|
||||
@@ -6,9 +6,11 @@ use App\Enums\PurchasingRole;
|
||||
use App\Http\Requests\SaveSupplierRequest;
|
||||
use App\Models\Supplier;
|
||||
use App\Models\User;
|
||||
use App\Services\AdministrativeGeography;
|
||||
use App\Services\ConflictGuard;
|
||||
use App\Services\CurrentActor;
|
||||
use App\Services\RequisitionWorkflow;
|
||||
use App\Services\SupplierGeography;
|
||||
use App\Services\SupplierParticipation;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -41,13 +43,16 @@ class SupplierController extends Controller
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
abort_unless($this->actor($request)->hasFunctions([PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
|
||||
$filters = $request->validate(['search' => 'nullable|string|max:100']);
|
||||
$filters = $request->validate(['search' => 'nullable|string|max:100', 'location' => 'nullable|string|regex:/^[0-9]+$/D|max:20']);
|
||||
$query = Supplier::query();
|
||||
if (! empty($filters['location'])) {
|
||||
$query = app(SupplierGeography::class)->matching($filters['location']);
|
||||
}
|
||||
if (! empty($filters['search'])) {
|
||||
$query->where(fn ($q) => $q->where('name', 'like', '%'.$filters['search'].'%')->orWhere('nuit', 'like', '%'.$filters['search'].'%'));
|
||||
}
|
||||
|
||||
return Inertia::render('suppliers/index', ['suppliers' => $query->orderBy('name')->paginate(15)->withQueryString(), 'filters' => $filters, 'canCreate' => app(SupplierParticipation::class)->canPrepare($this->actor($request)), 'canReview' => app(SupplierParticipation::class)->canReviewRole($this->actor($request))]);
|
||||
return Inertia::render('suppliers/index', ['suppliers' => $query->orderBy('name')->paginate(15)->withQueryString(), 'filters' => $filters, 'geographyFilter' => empty($filters['location']) ? null : app(AdministrativeGeography::class)->describe(['catalogue' => config('geography.source_sha256'), 'record_id' => $filters['location'], 'country' => 'MZ']), 'geographyEnabled' => app(AdministrativeGeography::class)->enabled(), 'canCreate' => app(SupplierParticipation::class)->canPrepare($this->actor($request)), 'canReview' => app(SupplierParticipation::class)->canReviewRole($this->actor($request))]);
|
||||
}
|
||||
|
||||
public function store(SaveSupplierRequest $request, RequisitionWorkflow $workflow): RedirectResponse
|
||||
@@ -56,6 +61,9 @@ class SupplierController extends Controller
|
||||
DB::transaction(function () use ($request, $data, $workflow) {
|
||||
$actor = $this->currentActor($request, PurchasingRole::Procurement);
|
||||
abort_unless(app(SupplierParticipation::class)->canPrepare($actor), 403);
|
||||
if (array_key_exists('geography_reference', $data)) {
|
||||
$data['geography_reference'] = app(AdministrativeGeography::class)->canonical($data['geography_reference']);
|
||||
}
|
||||
$supplier = Supplier::query()->create([...$data, 'tenant_id' => $actor->tenant_id, 'created_by' => $actor->id, 'last_changed_by' => $actor->id, 'status' => 'pending']);
|
||||
$workflow->audit($actor, 'supplier.created', null, null, $supplier->toArray());
|
||||
});
|
||||
@@ -71,6 +79,7 @@ class SupplierController extends Controller
|
||||
Inertia::encryptHistory();
|
||||
$response = Inertia::render('suppliers/show', [
|
||||
'supplier' => $supplier,
|
||||
'geography' => app(AdministrativeGeography::class)->describe($supplier->geography_reference),
|
||||
'documents' => $canDocuments ? $supplier->documents()->latest('id')->get() : [],
|
||||
'canEdit' => $this->actor($request)->can('update', $supplier),
|
||||
'canDocuments' => $canDocuments,
|
||||
@@ -85,7 +94,7 @@ class SupplierController extends Controller
|
||||
{
|
||||
Gate::authorize('update', $supplier);
|
||||
|
||||
return Inertia::render('suppliers/edit', ['supplier' => $supplier]);
|
||||
return Inertia::render('suppliers/edit', ['supplier' => $supplier, 'geographyEnabled' => app(AdministrativeGeography::class)->enabled(), 'geography' => app(AdministrativeGeography::class)->describe($supplier->geography_reference)]);
|
||||
}
|
||||
|
||||
public function update(SaveSupplierRequest $request, Supplier $supplier, RequisitionWorkflow $workflow): RedirectResponse
|
||||
@@ -95,6 +104,9 @@ class SupplierController extends Controller
|
||||
DB::transaction(function () use ($request, $supplier, $workflow, $data) {
|
||||
$actor = $this->currentActor($request, PurchasingRole::Procurement);
|
||||
abort_unless(app(SupplierParticipation::class)->canPrepare($actor), 403);
|
||||
if (array_key_exists('geography_reference', $data)) {
|
||||
$data['geography_reference'] = app(AdministrativeGeography::class)->canonical($data['geography_reference']);
|
||||
}
|
||||
$supplier = Supplier::query()->lockForUpdate()->findOrFail($supplier->id);
|
||||
if ($supplier->version !== (int) $data['version']) {
|
||||
throw ValidationException::withMessages(['version' => 'A ficha foi alterada por outro utilizador. Reabra a edição para carregar os dados actuais.']);
|
||||
|
||||
@@ -5,7 +5,9 @@ namespace App\Http\Controllers;
|
||||
use App\Models\SupplierEvaluation;
|
||||
use App\Models\SupplierImprovement;
|
||||
use App\Models\User;
|
||||
use App\Services\AdministrativeGeography;
|
||||
use App\Services\SupplierEvaluationWorkflow;
|
||||
use App\Services\SupplierGeography;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -16,6 +18,22 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class SupplierImprovementReportController extends Controller
|
||||
{
|
||||
private function actor(Request $request): User
|
||||
{
|
||||
$actor = $request->user('web');
|
||||
abort_unless($actor instanceof User, 403);
|
||||
|
||||
return $actor;
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $filters
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
private function territory(array $filters): ?array
|
||||
{
|
||||
return empty($filters['location']) ? null : app(AdministrativeGeography::class)->describe(['country' => 'MZ', 'catalogue' => config('geography.source_sha256'), 'record_id' => $filters['location']]);
|
||||
}
|
||||
|
||||
private function snapshot(\Closure $read): mixed
|
||||
{
|
||||
if (DB::getDriverName() === 'pgsql' && DB::transactionLevel() !== 0) {
|
||||
@@ -34,12 +52,12 @@ class SupplierImprovementReportController extends Controller
|
||||
/** @return array<string,mixed> */
|
||||
private function filters(Request $request): array
|
||||
{
|
||||
$actor = app(SupplierEvaluationWorkflow::class)->reader($request->user());
|
||||
$actor = app(SupplierEvaluationWorkflow::class)->reader($this->actor($request));
|
||||
$request->setUserResolver(fn () => $actor);
|
||||
abort_unless(Schema::hasTable('supplier_improvements'), 503, 'A instalação das medidas de melhoria ainda não está concluída.');
|
||||
$filters = $request->validate(['supplier_id' => 'nullable|integer|min:1', 'responsible_id' => 'nullable|integer|min:1',
|
||||
'status' => 'nullable|in:open,review,returned,closed', 'from' => 'nullable|date_format:Y-m-d', 'to' => 'nullable|date_format:Y-m-d',
|
||||
'overdue' => 'nullable|in:1', 'search' => 'nullable|string|max:200']);
|
||||
'overdue' => 'nullable|in:1', 'search' => 'nullable|string|max:200', 'location' => 'nullable|string|regex:/^[0-9]+$/D|max:20']);
|
||||
if (! empty($filters['from']) && ! empty($filters['to']) && $filters['to'] < $filters['from']) {
|
||||
throw ValidationException::withMessages(['to' => 'O fim do intervalo deve ser igual ou posterior ao início.']);
|
||||
}
|
||||
@@ -52,7 +70,11 @@ class SupplierImprovementReportController extends Controller
|
||||
*/
|
||||
private function query(Request $request, array $filters, string $today): Builder
|
||||
{
|
||||
$query = SupplierImprovement::query()->visibleTo($request->user());
|
||||
$query = SupplierImprovement::query()->visibleTo($this->actor($request));
|
||||
if (! empty($filters['location'])) {
|
||||
$suppliers = app(SupplierGeography::class)->matching($filters['location'])->select('id');
|
||||
$query->whereHas('evaluation', fn ($evaluation) => $evaluation->whereIn('supplier_id', $suppliers));
|
||||
}
|
||||
if (! empty($filters['supplier_id'])) {
|
||||
$query->whereHas('evaluation', fn ($q) => $q->where('supplier_id', $filters['supplier_id']));
|
||||
}
|
||||
@@ -81,7 +103,7 @@ class SupplierImprovementReportController extends Controller
|
||||
*/
|
||||
private function records(Builder $query, Request $request): Builder
|
||||
{
|
||||
return $query->with(['evaluation:id,supplier_id,supplier_snapshot,revision', 'responsible' => fn ($q) => $q->where('tenant_id', $request->user()->tenant_id)->select(['id', 'name'])]);
|
||||
return $query->with(['evaluation:id,supplier_id,supplier_snapshot,revision', 'responsible' => fn ($q) => $q->where('tenant_id', $this->actor($request)->tenant_id)->select(['id', 'name'])]);
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
@@ -99,17 +121,17 @@ class SupplierImprovementReportController extends Controller
|
||||
$props = $this->snapshot(function () use ($request): array {
|
||||
$filters = $this->filters($request);
|
||||
$today = today()->toDateString();
|
||||
$all = SupplierImprovement::query()->visibleTo($request->user());
|
||||
$all = SupplierImprovement::query()->visibleTo($this->actor($request));
|
||||
$query = $this->query($request, $filters, $today);
|
||||
$counts = (clone $query)->selectRaw('status, COUNT(*) AS count')->groupBy('status')->pluck('count', 'status')->map(fn ($n) => (int) $n);
|
||||
$overdue = (clone $query)->where('status', '!=', 'closed')->where('due_on', '<', $today)->count();
|
||||
// Choices derive only from visible measures, including prior revisions and former assignees.
|
||||
$evaluations = SupplierEvaluation::query()->visibleTo($request->user())->whereIn('id', (clone $all)->select('supplier_evaluation_id'))->orderByDesc('id')->get(['supplier_id', 'supplier_snapshot']);
|
||||
$evaluations = SupplierEvaluation::query()->visibleTo($this->actor($request))->whereIn('id', (clone $all)->select('supplier_evaluation_id'))->orderByDesc('id')->get(['supplier_id', 'supplier_snapshot']);
|
||||
|
||||
return ['filters' => $filters, 'today' => $today, 'generatedAt' => now()->toIso8601String(), 'counts' => $counts, 'overdue' => $overdue,
|
||||
return ['filters' => $filters, 'geographyEnabled' => app(AdministrativeGeography::class)->enabled(), 'geographyFilter' => $this->territory($filters), 'today' => $today, 'generatedAt' => now()->toIso8601String(), 'counts' => $counts, 'overdue' => $overdue,
|
||||
'records' => $this->records($query, $request)->orderBy('due_on')->orderBy('id')->paginate(25)->withQueryString()->through(fn (SupplierImprovement $m) => $this->row($m, $today)),
|
||||
'suppliers' => $evaluations->map(fn (SupplierEvaluation $e) => ['id' => $e->supplier_id, 'name' => $e->supplier_snapshot['name']])->unique('id')->values(),
|
||||
'responsibles' => User::query()->where('tenant_id', $request->user()->tenant_id)->whereIn('id', (clone $all)->select('responsible_id'))->orderBy('name')->get(['id', 'name'])];
|
||||
'responsibles' => User::query()->where('tenant_id', $this->actor($request)->tenant_id)->whereIn('id', (clone $all)->select('responsible_id'))->orderBy('name')->get(['id', 'name'])];
|
||||
});
|
||||
Inertia::encryptHistory();
|
||||
$response = Inertia::render('reports/supplier-improvements', $props)->toResponse($request);
|
||||
@@ -121,6 +143,9 @@ class SupplierImprovementReportController extends Controller
|
||||
public function csv(Request $request): Response
|
||||
{
|
||||
$filters = $this->filters($request);
|
||||
if (! empty($filters['location'])) {
|
||||
app(SupplierGeography::class)->matching($filters['location']);
|
||||
}
|
||||
$text = fn (?string $value) => preg_match('/^[\s]*[=+\-@]|^[\t\r\n]/u', $value ?? '') ? "'".$value : ($value ?? '');
|
||||
|
||||
return response()->streamDownload(fn () => $this->snapshot(function () use ($request, $filters, $text) {
|
||||
@@ -133,7 +158,12 @@ class SupplierImprovementReportController extends Controller
|
||||
}
|
||||
fwrite($out, "\xEF\xBB\xBF");
|
||||
fputcsv($out, ['Medidas de melhoria de fornecedores', 'Consulta', now()->toIso8601String(), 'Data de referência', $today], ';', '"', '');
|
||||
fputcsv($out, ['Filtros', $text(json_encode($filters, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR))], ';', '"', '');
|
||||
$exportFilters = $filters;
|
||||
if (! empty($filters['location'])) {
|
||||
$territory = $this->territory($filters);
|
||||
$exportFilters['localizacao_actual_da_ficha'] = implode(' / ', array_column($territory['path'] ?? [], 'name'));
|
||||
}
|
||||
fputcsv($out, ['Filtros', $text(json_encode($exportFilters, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR))], ';', '"', '');
|
||||
fputcsv($out, ['Medida', 'Fornecedor preservado', 'Avaliação', 'Revisão', 'Título', 'Objectivo', 'Responsável', 'Prazo', 'Estado', 'Prazo ultrapassado', 'Versão', 'Resultado', 'Evidência', 'Última conferência', 'Conferida em', 'Avaliação e acompanhamento', 'Relatório individual'], ';', '"', '');
|
||||
foreach ($this->records($this->query($request, $filters, $today), $request)->lazyById(200) as $m) {
|
||||
$row = $this->row($m, $today);
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Models\StockItem;
|
||||
use App\Models\User;
|
||||
use App\Models\Warehouse;
|
||||
use App\Models\WarehouseRequest;
|
||||
use App\Models\WarehouseRequestEvent;
|
||||
use App\Services\InternalSupplyTrace;
|
||||
use App\Services\OrganizationAccess;
|
||||
use App\Services\WarehouseFulfilment;
|
||||
@@ -22,6 +23,12 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class WarehouseRequestController extends Controller
|
||||
{
|
||||
/** @return array<string,mixed> */
|
||||
private function filters(Request $request, User $actor): array
|
||||
{
|
||||
return $request->validate(['view' => 'nullable|in:all,review,reserve,delivery,receipt', 'state' => 'nullable|in:all,draft,submitted,authorized,partial,delivered,closed,rejected,cancelled', 'search' => 'nullable|string|max:100', 'warehouse_id' => ['nullable', 'integer', Rule::exists('warehouses', 'id')->where('tenant_id', $actor->tenant_id)], 'page' => 'nullable|integer|min:1|max:1000000']);
|
||||
}
|
||||
|
||||
public function index(Request $request, WarehouseWorkdesk $desk): Response
|
||||
{
|
||||
$actor = $request->user('web');
|
||||
@@ -31,21 +38,10 @@ class WarehouseRequestController extends Controller
|
||||
if (DB::getDriverName() === 'pgsql' && DB::transactionLevel() === 1) {
|
||||
DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
||||
}
|
||||
$filters = $request->validate(['view' => 'nullable|in:all,review,reserve,delivery,receipt', 'state' => 'nullable|in:all,draft,submitted,authorized,partial,delivered,closed,rejected,cancelled', 'search' => 'nullable|string|max:100', 'warehouse_id' => ['nullable', 'integer', Rule::exists('warehouses', 'id')->where('tenant_id', $actor->tenant_id)], 'page' => 'nullable|integer|min:1|max:1000000']);
|
||||
$filters = $this->filters($request, $actor);
|
||||
$prepare = $actor->hasFunctions(['warehouse_request_prepare']) && ! app(OrganizationAccess::class)->maintenanceOnly($actor);
|
||||
$ready = Schema::hasTable('warehouse_requests');
|
||||
$base = $desk->visible($actor);
|
||||
if (! empty($filters['warehouse_id'])) {
|
||||
$base->where('warehouse_id', $filters['warehouse_id']);
|
||||
}
|
||||
if (! empty($filters['search'])) {
|
||||
$search = trim($filters['search']);
|
||||
if (preg_match('/^(?:RA-)?([1-9][0-9]{0,9})$/iD', $search, $match)) {
|
||||
$base->whereKey($match[1]);
|
||||
} else {
|
||||
$base->where(fn ($q) => $q->whereLike('snapshot->requester->name', '%'.$search.'%')->orWhereLike('snapshot->destination', '%'.$search.'%'));
|
||||
}
|
||||
}
|
||||
$base = $desk->filteredBase($actor, $filters);
|
||||
$views = $ready ? $desk->views(clone $base, $actor) : [];
|
||||
$query = $desk->forView(clone $base, $actor, $filters['view'] ?? 'all');
|
||||
$states = $ready ? DB::query()->fromSub((clone $query)->selectRaw($desk->stateExpression().' as projected_state')->toBase(), 'request_states')->selectRaw('projected_state, COUNT(*) as total')->groupBy('projected_state')->pluck('total', 'projected_state') : [];
|
||||
@@ -70,6 +66,49 @@ class WarehouseRequestController extends Controller
|
||||
});
|
||||
}
|
||||
|
||||
public function csv(Request $request, WarehouseWorkdesk $desk): Response
|
||||
{
|
||||
$actor = $request->user('web');
|
||||
abort_unless($actor instanceof User && $actor->hasFunctions(['admin', 'warehouse_request_read', 'warehouse_request_prepare', 'warehouse_request_review', 'warehouse_request_deliver']), 403);
|
||||
$filters = $this->filters($request, $actor);
|
||||
abort_unless(Schema::hasTable('warehouse_requests'), 503);
|
||||
|
||||
return response()->streamDownload(function () use ($actor, $filters, $desk) {
|
||||
DB::transaction(function () use ($actor, $filters, $desk) {
|
||||
if (DB::getDriverName() === 'pgsql' && DB::transactionLevel() === 1) {
|
||||
DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
||||
}
|
||||
$actor = User::query()->findOrFail($actor->id);
|
||||
abort_unless($actor->hasFunctions(['admin', 'warehouse_request_read', 'warehouse_request_prepare', 'warehouse_request_review', 'warehouse_request_deliver']), 403);
|
||||
$query = $desk->forView($desk->filteredBase($actor, $filters), $actor, $filters['view'] ?? 'all');
|
||||
if (($filters['state'] ?? 'all') !== 'all') {
|
||||
$query->whereRaw($desk->stateExpression().' = ?', [$filters['state']]);
|
||||
}
|
||||
$out = fopen('php://output', 'w');
|
||||
if ($out === false) {
|
||||
throw new \RuntimeException('Não foi possível gerar o ficheiro.');
|
||||
}
|
||||
$safe = fn (string $value): string => preg_match('/^[\s]*[=+\-@]|^[\t\r\n]/u', $value) ? "'".$value : $value;
|
||||
try {
|
||||
fwrite($out, "\xEF\xBB\xBF");
|
||||
fputcsv($out, ['Requisições internas', now()->toIso8601String(), 'Sem factura; entregas acumuladas não descontam devoluções ou correcções.'], ';', '"', '');
|
||||
fputcsv($out, ['Filtros', json_encode(array_diff_key($filters, ['page' => true]), JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)], ';', '"', '');
|
||||
fputcsv($out, ['Requisição', 'Estado', 'Versão', 'Requisitante', 'Armazém', 'Centro de custo', 'Destino', 'Finalidade', 'Linha', 'Artigo', 'Descrição', 'Unidade', 'Utilização', 'Equipamento', 'Pedida', 'Reservada agora', 'Entregue acumulada', 'Por entregar', 'Cancelada', 'Reservas disponíveis', 'Consulta'], ';', '"', '');
|
||||
foreach ($query->lazyById(100) as $record) {
|
||||
$p = app(WarehouseFulfilment::class)->progress($record);
|
||||
$s = $record->snapshot;
|
||||
foreach ($s['lines'] as $index => $line) {
|
||||
$row = ['RA-'.$record->id, $p['state'], (string) $p['version'], $s['requester']['name'], $s['warehouse']['code'], $s['cost_center']['code'], $s['destination'], $s['reason'], (string) ($index + 1), $line['item']['code'], $line['item']['name'], $line['item']['unit'], $line['destination'], $line['equipment']['code'] ?? '', ...array_map(fn ($quantity) => number_format($quantity / 1000, 3, '.', ''), [$line['quantity_milli'], $p['reserved'][$index], $p['delivered'][$index], $p['remaining'][$index], $p['cancelled'][$index]]), $p['reservation_ready'] ? 'Sim' : 'Não', url('/stock/requisicoes/'.$record->id)];
|
||||
fputcsv($out, array_map($safe, $row), ';', '"', '');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
fclose($out);
|
||||
}
|
||||
});
|
||||
}, 'requisicoes-internas-'.now()->format('Ymd-His').'.csv', ['Content-Type' => 'text/csv; charset=UTF-8', 'Cache-Control' => 'private, no-store, max-age=0']);
|
||||
}
|
||||
|
||||
public function store(Request $request, WarehouseRequests $service): Response
|
||||
{
|
||||
$actor = $request->user('web');
|
||||
@@ -94,7 +133,7 @@ class WarehouseRequestController extends Controller
|
||||
$p = $service->progress($r);
|
||||
Inertia::encryptHistory();
|
||||
$response = Inertia::render('stock/request', ['record' => $r->only(['id', 'snapshot', 'created_at']), 'progress' => $p, 'ready' => Schema::hasTable('warehouse_request_events'),
|
||||
'canViewReturns' => config('stock_returns.enabled') && $actor->hasFunctions(['warehouse_return_read']) && !app(OrganizationAccess::class)->maintenanceOnly($actor),
|
||||
'canViewReturns' => config('stock_returns.enabled') && $actor->hasFunctions(['warehouse_return_read']) && ! app(OrganizationAccess::class)->maintenanceOnly($actor),
|
||||
'health' => $desk->health($r, $p), 'people' => $desk->people($r, $p),
|
||||
'supplyTrace' => app(InternalSupplyTrace::class)->forRequest($actor, $r, $p), ...$desk->actions($actor, $r, $p)])->toResponse($request);
|
||||
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
|
||||
@@ -111,4 +150,44 @@ class WarehouseRequestController extends Controller
|
||||
|
||||
return redirect('/stock/requisicoes/'.$id);
|
||||
}
|
||||
|
||||
public function printRequest(Request $request, int $id, WarehouseFulfilment $service): Response
|
||||
{
|
||||
$actor = $request->user('web');
|
||||
abort_unless($actor instanceof User, 403);
|
||||
|
||||
return DB::transaction(function () use ($actor, $id, $service) {
|
||||
if (DB::getDriverName() === 'pgsql' && DB::transactionLevel() === 1) {
|
||||
DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
||||
}
|
||||
$record = WarehouseRequest::query()->findOrFail($id);
|
||||
abort_unless(app(WarehouseWorkdesk::class)->canView($actor, $record), 403);
|
||||
|
||||
return response()->view('stock.request-print', ['record' => $record, 'progress' => $service->progress($record), 'generatedAt' => now()])
|
||||
->header('Cache-Control', 'private, no-store, max-age=0')->header('X-Content-Type-Options', 'nosniff');
|
||||
});
|
||||
}
|
||||
|
||||
public function printDelivery(Request $request, int $id, int $delivery): Response
|
||||
{
|
||||
$actor = $request->user('web');
|
||||
abort_unless($actor instanceof User, 403);
|
||||
|
||||
return DB::transaction(function () use ($actor, $id, $delivery) {
|
||||
if (DB::getDriverName() === 'pgsql' && DB::transactionLevel() === 1) {
|
||||
DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
||||
}
|
||||
$record = WarehouseRequest::query()->findOrFail($id);
|
||||
abort_unless(app(WarehouseWorkdesk::class)->canView($actor, $record), 403);
|
||||
abort_unless(Schema::hasTable('warehouse_request_events'), 503, 'Registo de entregas indisponível.');
|
||||
$event = WarehouseRequestEvent::query()->where('warehouse_request_id', $record->id)->where('action', 'deliver')->findOrFail($delivery);
|
||||
$confirmation = WarehouseRequestEvent::query()->where('warehouse_request_id', $record->id)->where('action', 'receive')
|
||||
->where('data->delivery_event_id', $event->id)->orderBy('version')->first();
|
||||
$authorization = WarehouseRequestEvent::query()->where('warehouse_request_id', $record->id)->where('action', 'authorize')
|
||||
->where('version', '<', $event->version)->orderByDesc('version')->first();
|
||||
|
||||
return response()->view('stock.delivery-print', ['record' => $record, 'delivery' => $event, 'confirmation' => $confirmation, 'authorization' => $authorization])
|
||||
->header('Cache-Control', 'private, no-store, max-age=0')->header('X-Content-Type-Options', 'nosniff');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user