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'));
    }
}