File "ProjectController-20260128183637.php"
Full Path: /var/www/html/back/app/Http/Controllers/Api/V1/ProjectController-20260128183637.php
File size: 19.06 KB
MIME-type: text/x-php
Charset: utf-8
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\V1;
use App\Attributes\OpenApiResponse;
use App\Domain\Payment\Enums\PaymentStatusEnum;
use App\Domain\Payment\Enums\PaymentTypeEnum;
use App\Domain\Project\Enums\ProjectStatusEnum;
use App\Domain\Project\Requests\CreateProjectRequest;
use App\Domain\Project\Requests\UpdateProjectRequest;
use App\Domain\Project\Services\ProjectService;
use App\Exports\PaymentsExport;
use App\Http\Controllers\Api\ApiController;
use App\Http\Resources\ProjectGroupResource;
use App\Http\Resources\ProjectResource;
use App\Http\Resources\ProjectWithoutGroupsResource;
use App\Http\Resources\ShowProjectResource;
use App\Http\Resources\StatsProjectResource;
use App\Imports\ProjectsImport;
use App\Models\Payment;
use App\Models\PaymentDistribution;
use App\Models\Project;
use App\Responses\ResponseDto;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Knuckles\Scribe\Attributes\Group;
use Maatwebsite\Excel\Facades\Excel;
/**
* Контроллер для управления проектами.
*/
class ProjectController extends ApiController
{
protected ProjectService $projectService;
public function __construct(ProjectService $projectService)
{
$this->projectService = $projectService;
}
/**
* Получить список всех проектов с платежами.
*
* @queryParam search string Поисковый запрос для фильтрации проектов. Пример: "Laravel"
* @queryParam project_group_id int ID категории для фильтрации проектов. Пример: 3
* @queryParam payment_id int ID платежа. Пример: 3
*/
#[Group('projects')]
#[OpenApiResponse(Project::class)]
public function projectsWithPayments(int $modelID, Request $request): ResponseDto
{
$projects = $this->projectService->getAll($modelID, $request, true)
->with(['paymentDistributions', 'paymentDistributions.payment.contragent'])
->get();
$totalProjects = 0;
$totalIncome = 0;
$totalOutcome = 0;
$result = [];
foreach ($projects as $i => $project) {
$income = $outcome = $paymentsNum = $process = 0;
$payments = [];
/** @var Project $project */
foreach ($project->paymentDistributions as $pd) {
$totalProjects++;
if ($pd->payment->payment_type == PaymentTypeEnum::PAYMENT_TYPE_RECEPTION->value) {
$totalIncome += $pd->payment->amount;
$income += $pd->payment->amount;
} elseif ($pd->payment->payment_type == PaymentTypeEnum::PAYMENT_TYPE_PAYMENT->value) {
$outcome += $pd->payment->amount;
$totalOutcome += $pd->payment->amount;
}
if (!in_array($pd->payment->status, [
PaymentStatusEnum::STATUS_PAID->value,
PaymentStatusEnum::STATUS_RECEIVED->value,
], true)) {
$paymentsNum++;
$process += $pd->payment->amount;
}
$payments[] = [
'name' => $pd->payment->name,
'article' => $pd->payment->articleTitle,
'amount' => $pd->payment->amount,
'contragent' => $pd->payment->contragent,
];
}
$project->forceFill([
'total_income' => $income,
'total_outcome' => $outcome,
'in_progress' => $process,
'payments' => $payments,
'payments_num' => $paymentsNum,
]);
$project = $project->toArray();
unset($project['payment_distributions']);
$result[] = $project;
}
return new ResponseDto(
data: [
'projects' => $result,
'overall' => [
'income' => $totalIncome,
'outcome' => $totalOutcome,
'projects_num' => $totalProjects,
]
],
status: true
);
}
/**
* Получить список всех проектов.
*
* @queryParam search string Поисковый запрос для фильтрации проектов. Пример: "Laravel"
* @queryParam project_group_id int ID категории для фильтрации проектов. Пример: 3
* @queryParam payment_id int ID платежа. Пример: 3
*/
#[Group('projects')]
#[OpenApiResponse(Project::class)]
public function index(int $modelID, Request $request): ResponseDto
{
return new ResponseDto(
data: ProjectResource::collection($this->projectService->getAll($modelID, $request)->paginate(4000)),
status: true
);
}
public function listExceptDelete(int $modelId, Request $request)
{
$projectId = $request->query('projectId');
return new ResponseDto(
data: ProjectResource::collection($this->projectService->getListExceptDelete($modelId, $projectId)),
status: true
);
}
/**
* Экспортировать платежи в Excel, результат = url для скачивания
*
* @queryParam ids array - список ID проектов для экспорта
*/
#[Group('projects')]
#[OpenApiResponse(Project::class)]
public function excelExport(int $modelID, Request $request): ResponseDto
{
$ids = $request->get('ids', []);
$export = new PaymentsExport(Payment::whereHas('distributions', function ($query) use ($ids): void {
$query->whereIn('project_id', $ids);
}), $ids);
$filename = 'payments-' . date('Y-m-d-H-i-s') . '.xlsx';
Excel::store($export, $filename, 'public');
return new ResponseDto(
data: ['url' => asset("storage/{$filename}")],
status: true
);
}
/**
* Получить список всех проектов с группировкой по группам и еще список проектов вне групп (через generic группу)
*
* @queryParam search string Поисковый запрос для фильтрации проектов. Пример: "Laravel"
* @queryParam sorting array сортировка, пример sort=article&sort_direction=ASC
*/
#[Group('projects')]
#[OpenApiResponse(Project::class)]
public function groupedList(int $modelID, Request $request): ResponseDto
{
$projectsGroups = $this->projectService->getProjectGroupList($modelID, $request);
$projectsWithoutGroups = $this->projectService->getProjectsWithoutGroupsList($modelID, $request);
$stats = [
'in_work' => 0,
'income' => 0,
'outcome' => 0,
];
$sort = $request->get('sort');
$direction = $request->get('sort_direction', 'ASC');
if ($sort) {
if ($sort == 'name') {
$projectsGroups = $projectsGroups->map(function ($group) use ($direction) {
if (!$group->projects->isEmpty()) {
$group->projects = collect($group->projects)->sort(function ($a, $b) use ($direction) {
$getFirstRussianLetters = fn($s) => preg_match('/[А-Яа-яЁё]/u', $s, $m) ? mb_strtolower($m[0], 'UTF-8') : 'я';
$getNum = fn($s) => preg_match('/\d+/', $s, $m) ? (int)$m[0] : 999999;
$letters1 = $getFirstRussianLetters($a->offer_number);
$letters2 = $getFirstRussianLetters($b->offer_number);
$n1 = $getNum($a->offer_number);
$n2 = $getNum($b->offer_number);
if ($letters1 !== $letters2) {
return $direction === 'DESC' ? strcmp($letters2, $letters1) : strcmp($letters1, $letters2);
}
if ($n1 !== $n2) {
return $direction === 'DESC' ? ($n2 <=> $n1) : ($n1 <=> $n2);
}
return $direction === 'DESC' ? strcmp($b->offer_number, $a->offer_number) : strcmp($a->offer_number, $b->offer_number);
})->values();
}
return $group;
});
$projectsGroups = $projectsGroups->sortBy(function ($group) {
if (!$group->projects->isEmpty()) {
$project = $group->projects->first();
return $project->offer_number;
}
return '';
}, SORT_NATURAL, $direction == 'DESC');
$projectsWithoutGroups = collect($projectsWithoutGroups)->sort(function ($a, $b) use ($direction) {
$getFirstRussianLetters = fn($s) => preg_match('/[А-Яа-яЁё]/u', $s, $m) ? mb_strtolower($m[0], 'UTF-8') : 'я';
$getNum = fn($s) => preg_match('/\d+/', $s, $m) ? (int)$m[0] : 999999;
$letters1 = $getFirstRussianLetters($a->offer_number);
$letters2 = $getFirstRussianLetters($b->offer_number);
$n1 = $getNum($a->offer_number);
$n2 = $getNum($b->offer_number);
if ($letters1 !== $letters2) {
return $direction === 'DESC' ? strcmp($letters2, $letters1) : strcmp($letters1, $letters2);
}
if ($n1 !== $n2) {
return $direction === 'DESC' ? ($n2 <=> $n1) : ($n1 <=> $n2);
}
return $direction === 'DESC' ? strcmp($b->offer_number, $a->offer_number) : strcmp($a->offer_number, $b->offer_number);
});
}
if ($sort == 'created_at') {
$projectsGroups = $projectsGroups->map(function ($group) use ($direction) {
if ($group->projects->isNotEmpty()) {
$sortedProjects = $group->projects->sortBy(
fn($project) => $project->created_at?->getTimestamp() ?? PHP_INT_MIN,
SORT_NUMERIC,
$direction == 'DESC'
)->values();
$group->setRelation('projects', $sortedProjects);
}
return $group;
});
$projectsGroups = $projectsGroups->sortBy(
fn($group) => $group->projects->first()?->created_at?->getTimestamp() ?? PHP_INT_MIN,
SORT_NUMERIC,
$direction == 'DESC'
)->values();
$projectsWithoutGroups = $projectsWithoutGroups->sortBy('created_at', SORT_NATURAL, $direction === 'DESC');
}
if ($sort == 'last_update') {
$projectsGroups = $projectsGroups->map(function ($group) use ($direction) {
if ($group && $group->projects && !$group->projects->isEmpty()) {
$group->projects = $group->projects->sortBy(function ($project) use ($direction) {
if ($project && !$project->paymentDistributions->isEmpty()) {
return $project->paymentDistributions->sortBy(function ($distribution) use ($direction) {
return $distribution->payment->payment_date;
}, SORT_REGULAR, $direction == 'DESC')->values();
}
return '0000-00-00';
}, SORT_REGULAR, $direction === 'DESC')->values();
}
return $group;
});
$projectsGroups = $projectsGroups->sortBy(function ($group) use ($direction) {
if ($group && $group->projects && !$group->projects->isEmpty()) {
$maxPaymentDate = $group->projects->map(function ($project) {
if ($project && !$project->paymentDistributions->isEmpty()) {
return $project->paymentDistributions->max(function ($distribution) {
return $distribution->payment->payment_date;
});
}
return null;
})->filter()->max();
return $maxPaymentDate;
}
return '0000-00-00';
}, SORT_REGULAR, $direction === 'DESC')->values();
$projectsWithoutGroups = $projectsWithoutGroups->sortBy(function ($project) use ($direction) {
if ($project && !$project->paymentDistributions->isEmpty()) {
return $project->paymentDistributions->max(function ($distribution) {
return $distribution->payment->payment_date;
});
}
return null;
}, SORT_REGULAR, $direction === 'DESC')->values();
}
}
$projectsWithoutGroups->each(function ($projectsWithoutGroup) use (&$stats) {
$income = $outcome = $in_work = 0;
if ($projectsWithoutGroup->status == ProjectStatusEnum::PROJECT_STATUS_ACTIVE->value) {
$in_work += 1;
}
if (!empty($projectsWithoutGroup->paymentDistributions)) {
$projectsWithoutGroup->paymentDistributions->each(function (PaymentDistribution $paymentDistribution) use (&$income, &$outcome) {
if (($paymentDistribution->payment->payment_type == PaymentTypeEnum::PAYMENT_TYPE_RECEPTION->value) ||
($paymentDistribution->payment->payment_type == PaymentTypeEnum::PAYMENT_TYPE_RECEPTION_FROM_1C->value)) {
$income += $paymentDistribution->amount;
} else {
$outcome += $paymentDistribution->amount;
}
});
}
$stats['in_work'] += $in_work;
$stats['income'] += $income;
$stats['outcome'] += $outcome;
});
$projectsGroups->each(function ($projectGroup) use (&$stats) {
$income = $outcome = $in_work = 0;
$projectGroup['projects']->each(function ($project) use (&$income, &$outcome, &$in_work) {
if ($project->status == ProjectStatusEnum::PROJECT_STATUS_ACTIVE->value) {
$in_work += 1;
}
if ($project->paymentDistributions) {
$project->paymentDistributions->each(function (PaymentDistribution $paymentDistribution) use (&$income, &$outcome) {
if (($paymentDistribution->payment->payment_type == PaymentTypeEnum::PAYMENT_TYPE_RECEPTION->value) ||
($paymentDistribution->payment->payment_type == PaymentTypeEnum::PAYMENT_TYPE_RECEPTION_FROM_1C->value)) {
$income += $paymentDistribution->amount;
} else {
$outcome += $paymentDistribution->amount;
}
});
}
});
$stats['in_work'] += $in_work;
$stats['income'] += $income;
$stats['outcome'] += $outcome;
});
return new ResponseDto(
data: [
'header' => new StatsProjectResource($stats),
'projects_groups' => ProjectGroupResource::collection($projectsGroups),
'projects_without_groups' => ProjectWithoutGroupsResource::collection($projectsWithoutGroups),
],
status: true
);
}
/**
* Получить проект по ID.
*/
#[
Group('projects')]
#[OpenApiResponse(Project::class)]
public function show(int $modelID, int $id): ResponseDto
{
$data = $this->projectService->findById($id);
return new ResponseDto(
data: new ShowProjectResource($data),
status: true
);
}
/**
* Сумма платежей по проекту
*/
#[Group('projects')]
#[OpenApiResponse(Project::class)]
public function payments(int $modelID, int $id): ResponseDto
{
return new ResponseDto(
data: $this->projectService->payments($id),
status: true
);
}
/**
* Создать новый проект.
*/
#[Group('projects')]
#[OpenApiResponse(Project::class)]
public function store(int $modelID, CreateProjectRequest $request): ResponseDto
{
return new ResponseDto(
data: $this->projectService->create($modelID, $request),
status: true
);
}
public function updatePaymentArticle(int $modelId, $projectId, Request $request)
{
$amountLimit = $request->query('amount_limit');
return new ResponseDto(
status: $this->projectService->updatePaymentArticle($projectId, $amountLimit),
);
}
public function updateLimitsProject(int $modelId, $id, Request $request)
{
$limit = $request->input('limit');
$operationType = $request->query('operation_type');
$this->projectService->updateLimitsProject($id, $operationType, $limit);
}
/**
* Обновить существующий проект.
*/
#[Group('projects')]
#[OpenApiResponse(Project::class)]
public function update(int $modelID, int $id, UpdateProjectRequest $request): ResponseDto
{
return new ResponseDto(
status: (bool)$this->projectService->update($id, $request)
);
}
/**
* Удалить проект.
*
* @queryParam new_project_id int - ID проекта для переноса платежей (опционально)
*/
#[Group('projects')]
#[OpenApiResponse(ResponseDto::class)]
public function destroy(int $modelID, int $id, Request $request): ResponseDto
{
return new ResponseDto(
status: (bool)$this->projectService->delete($id, $request->get('new_project_id', null))
);
}
public function addArticleToProject(int $modelId, Request $request)
{
return new ResponseDto(
status: (bool)$this->projectService->addArticleToProject($request->query('projectId'), $request->query('articleId'))
);
}
public function deletePaymentArticle($projectId, Request $request)
{
$articleId = $request->query('articleId');
$newArticleId = $request->query('newArticleId');
return new ResponseDto(
status: $this->projectService->deletePaymentArticle($projectId, $articleId, $newArticleId)
);
}
public function import(Request $request)
{
Excel::import(new ProjectsImport(), $request->file('file'));
}
}