feat(projects): Plan 5 Task 2 — index expanded (filters/search/pagination/ids) + show

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-11 18:08:01 +03:00
parent 622773f929
commit 35310b5517
6 changed files with 275 additions and 25 deletions
@@ -5,48 +5,79 @@ declare(strict_types=1);
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\ProjectResource;
use App\Models\Project;
use App\Models\Tenant;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
/**
* Проекты tenant'а — для NewDealDialog dropdown'а и DealsView/Smart-filters.
* Проекты tenant'а расширенный API для ProjectsView + NewDealDialog.
*
* На MVP: tenant_id параметром. На prod: middleware('auth:sanctum')+'tenant'.
* index: фильтры по signal_type/status/search, пагинация, batch-fetch по ids.
* show: детальная карточка проекта с supplier_links.
*
* Auth: auth:sanctum + tenant middleware (устанавливает app.current_tenant_id для RLS).
* Task 2 Plan 5 заменяет MVP-версию (tenant_id параметром, без auth).
*/
class ProjectController extends Controller
{
/** GET /api/projects?tenant_id={id} */
/** GET /api/projects */
public function index(Request $request): JsonResponse
{
$tenantId = (int) $request->query('tenant_id', '0');
if ($tenantId < 1) {
return response()->json(['message' => 'Параметр tenant_id обязателен.'], 422);
$query = Project::query()->where('tenant_id', $request->user()->tenant_id);
// Batch-fetch по ids — возвращает без пагинации (для dropdown'ов и т.п.)
if ($ids = $request->query('ids')) {
$idArray = array_filter(array_map('intval', explode(',', (string) $ids)));
$items = $query->whereIn('id', $idArray)->get();
return response()->json(['data' => ProjectResource::collection($items)]);
}
$tenant = Tenant::find($tenantId);
if ($tenant === null) {
return response()->json(['message' => 'Тенант не найден.'], 404);
// Фильтр по типу сигнала
if ($type = $request->query('signal_type')) {
$query->where('signal_type', $type);
}
$projects = DB::transaction(function () use ($tenantId) {
DB::statement('SET LOCAL app.current_tenant_id = '.$tenantId);
// Фильтр по статусу жизненного цикла
$status = $request->query('status');
if ($status === 'archived') {
$query->archived();
} elseif ($status === 'active') {
$query->active()->where('is_active', true);
} elseif ($status === 'paused') {
$query->active()->where('is_active', false);
} else {
// По умолчанию: все не архивированные (active + paused)
$query->active();
}
return Project::query()
->where('is_active', true)
->orderBy('name')
->get(['id', 'name', 'tag', 'type']);
});
// Поиск по name и signal_identifier
if ($search = $request->query('search')) {
$query->where(function ($q) use ($search) {
$q->where('name', 'ilike', "%{$search}%")
->orWhere('signal_identifier', 'ilike', "%{$search}%");
});
}
$perPage = min((int) $request->query('per_page', '20'), 100);
$projects = $query->orderBy('created_at', 'desc')->paginate($perPage);
return response()->json([
'projects' => $projects->map(fn (Project $p) => [
'id' => $p->id,
'name' => $p->name,
'tag' => $p->tag,
'type' => $p->type,
]),
'data' => ProjectResource::collection($projects->items()),
'meta' => [
'current_page' => $projects->currentPage(),
'per_page' => $projects->perPage(),
'total' => $projects->total(),
],
]);
}
/** GET /api/projects/{id} */
public function show(Request $request, int $id): JsonResponse
{
$project = Project::where('tenant_id', $request->user()->tenant_id)->findOrFail($id);
return response()->json(['data' => new ProjectResource($project)]);
}
}