<?php
namespace App\Controller\Api\v3;
use App\Controller\Api\Service\CustomerService;
use App\Controller\Api\Service\ProjectService;
use App\Entity\Projects;
use App\Entity\User;
use App\Enum\SeverityInterface;
use App\Service\SerializeService\BranchSerialize;
use App\Service\SerializeService\CustomerSerialize;
use App\Service\SerializeService\ProjectSerialize;
use App\Service\SerializeService\ReferenceSerialize;
use App\Service\StreamingService;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @Route("/api/v3/project")
*/
class ProjectController extends AbstractController
{
/**
* ✅
* @Route("/streamed-fetch/{project_id}", name="api_v3_project_fetch_streamed", defaults={"project_id": null})
* @ParamConverter("project", options={"mapping":{"project_id": "id"}})
*/
public function streamedFetch(?Projects $project, TranslatorInterface $translator, ProjectService $projectService, ProjectSerialize $projectSerialize, BranchSerialize $branchSerialize, CustomerSerialize $customerSerialize, ReferenceSerialize $referenceSerialize): StreamedResponse
{
$streaming = new StreamingService([
"localText" => ["completed" => $translator->trans('Completed')]
]);
$streaming->appendStream(
"branches",
$translator->trans('Loading project data'),
// 🔥Branches fetch $ serialize
fn() => $branchSerialize->branches()
);
$streaming->appendStream(
"customers",
$translator->trans('Loading customers data'),
// 🔥Customer fetch $ serialize
fn() => $customerSerialize->serializeCore()
);
$streaming->appendStream(
"countries",
$translator->trans('Loading countries data'),
// 🔥 Countries fetch $ serialize
fn() => $referenceSerialize->serializedCountries()
);
$streaming->appendStream(
"certificates",
$translator->trans('Loading certificates data'),
// 🔥 Certificates fetch $ serialize
fn() => $referenceSerialize->serializedCertificates()
);
$streaming->appendStream(
"qualifications",
$translator->trans('Loading Qualifications data'),
// 🔥 Qualifications fetch $ serialize
fn() => $referenceSerialize->serializedQualifications()
);
$streaming->appendStream(
"invoice_types",
$translator->trans('Loading Invoice type'),
// 🔥 InvoiceTypes fetch $ serialize
fn() => $referenceSerialize->serializedInvoiceTypes()
);
if($project){
$streaming->appendStream(
"project",
$translator->trans('Loading Project'),
fn() => $projectSerialize->setEntity($project)->serializeFull()
);
}
return $streaming->flush();
}
/**
* ✅
* @Route("/projects", name="api_v3_projects")
* @param EntityManagerInterface $manager
* @param SerializerInterface $serializer
* @param ProjectSerialize $projectSerialize
* @return Response
* Partner Analyse for projects
*/
public function allProjects( EntityManagerInterface $manager, SerializerInterface $serializer, ProjectSerialize $projectSerialize ): Response
{
$projects = new ArrayCollection($manager->getRepository(Projects::class)->findAll());
$projects = $projectSerialize->setEntity($projects)->serializeWithRequirements();
return $this->json([
"projects" => $projects
], Response::HTTP_OK, [], []);
}
/**
* ✅
* @Route("/projects/{classify}", name="api_v3_projects_classify")
* @param ProjectService $projectService
* @param ProjectSerialize $projectSerialize
* @param TranslatorInterface $translator
* @param Request $request
* @param string $classify
* @return Response
* Partner Analyse for projects
* @throws \Exception
*/
public function fetchClassifyProjects(ProjectService $projectService, ProjectSerialize $projectSerialize, TranslatorInterface $translator, Request $request, string $classify = "active" ): Response
{
$projects = $projectService->fetchProjects($classify);
if($projectService->getException()){
return $this->json([
"message" => $projectService->getException()->getMessage(),
"severity" => SeverityInterface::WARNING
], $projectService->getException()->getCode());
}
$serializedProjects = $projectSerialize->setEntity($projects)->serializeForMainList();
return $this->json([
"projects" => $serializedProjects,
$classify => $serializedProjects,
"message" => $translator->trans('Projects fetched successfully'),
"severity" => SeverityInterface::SUCCESS
], Response::HTTP_OK);
}
/**
*
* @Route("/fetch/{project_id}", name="api_v3_project_fetch", methods={"POST"}, requirements={"project_id": "\d+"})
* @ParamConverter("project", options={"mapping":{"project_id": "id"}})
* @param int|null $project_id
* @throws \Exception
*/
public function fetch( ?Projects $project, ProjectSerialize $projectSerialize,
CustomerSerialize $customerSerialize,
ReferenceSerialize $referenceSerialize,
CustomerService $customerService, TranslatorInterface $translator, int $project_id ): Response
{
if( $project_id && !$project ){
return $this->json([
"message" => $translator->trans('Project not found'),
"severity" => SeverityInterface::ERROR
], Response::HTTP_NOT_FOUND);
}
$serializedProject = $projectSerialize->setEntity($project)->serializeFull();
return $this->json([
"message" => $translator->trans('Loaded project successfully'),
"severity" => SeverityInterface::SUCCESS,
"project" => $serializedProject,
], Response::HTTP_OK );
}
/**
* ✅✅
* @Route("/assign-branches/{project_id}", name="api_v3_project_assign_branches_to_project", defaults={"project_id": null})
* @ParamConverter("project", options={"mapping":{"project_id": "id"}})
* @param int|null $project_id
* @return Response
* Partner Analyse for projects
* @throws \Exception
*/
public function assignBranches( ?Projects $project, TranslatorInterface $translator, ProjectService $projectService, Request $request, ProjectSerialize $projectSerialize, int $project_id = null ): Response
{
if($project_id && !$project){
return $this->json([
"message" => $translator->trans('Project not found'),
"severity" => SeverityInterface::ERROR
]);
}
$params = $request->request;
#dd($params);
$project = $projectService->setEntity($project, $project_id)->setParams($params)->joinBranches();
if($projectService->getException()){
return $this->json([
"message" => $projectService->getException()->getMessage(),
"severity" => SeverityInterface::ERROR
]);
}
$serializedProject = $projectSerialize->setEntity($project)->serializeFull();
return $this->json([
"message" => $translator->trans('Joined branches to project successfully'),
"severity" => SeverityInterface::SUCCESS,
"project" => $serializedProject
], Response::HTTP_OK, [], []);
}
/**
* ✅
* @Route("/upsert/{project_id}", name="api_v3_project_upsert", defaults={"project_id": null})
* @ParamConverter("project", options={"mapping":{"project_id": "id"}})
* @param int|null $project_id
* @return Response
* Partner Analyse for projects
* @throws \Exception
*/
public function upsert( ?Projects $project, TranslatorInterface $translator, ProjectService $projectService, Request $request, ProjectSerialize $projectSerialize, int $project_id = null ): Response
{
if($project_id && !$project){
return $this->json([
"message" => $translator->trans('Project not found'),
"severity" => SeverityInterface::ERROR
]);
}
$project = $projectService->setEntity($project, $project_id)->setParams($request->request)->upsert();
if($projectService->getException()){
return $this->json([
"message" => $projectService->getException()->getMessage(),
"severity" => SeverityInterface::WARNING
]);
}
$serializedProject = $projectSerialize->setEntity($project)->serializeFull();
#dd($serializedProject);
return $this->json([
"message" => $translator->trans('Project upsert successfully'),
"severity" => SeverityInterface::SUCCESS,
"project" => $serializedProject
], Response::HTTP_OK, [], []);
}
/**
* TODO README !!!
* TODO Sync (Progress) Su an icin (ProjectOrderService) icinde bagimsiz yonetiliyor!, onysaki projece calculator Servise var onda Isleri parcalara böl ve o servisi kullan !!!
* ✅
* @Route("/sync-progress/{project_id}", name="api_project_sync_progress", methods={"POST"})
* @ParamConverter("project", options={"mapping":{"project_id":"id"}})
*/
public function syncProgress(?Projects $project, ProjectService $projectService, ProjectSerialize $projectSerialize, TranslatorInterface $translator, ?int $project_id = null ): Response
{
try{
$project = $projectService->setEntity($project, $project_id)->syncMetricProgress();
$serializedProject = $projectSerialize->setEntity($project)->serializeFull();
return $this->json([
'project' => $serializedProject,
"message"=>$translator->trans('Progress sync successfully'),
"severity" => SeverityInterface::SUCCESS
], Response::HTTP_OK );
} catch (\Exception $exception){
return $this->json([
'message'=>$exception->getMessage(),
"severity" => SeverityInterface::ERROR
], Response::HTTP_UNAUTHORIZED );
}
}
/**
*
* @Route("/overview/{project_id}", name="api_v3_project_overview", methods={"POST"}, requirements={"project_id": "\d+"})
* @ParamConverter("project", options={"mapping":{"project_id": "id"}})
* @param int|null $project_id
* @throws \Exception
*/
public function overview( ?Projects $project, ProjectSerialize $projectSerialize,
TranslatorInterface $translator, int $project_id ): Response
{
if( $project_id && !$project ){
return $this->json([
"message" => $translator->trans('Project not found'),
"severity" => SeverityInterface::ERROR
], Response::HTTP_NOT_FOUND);
}
$serializedProject = $projectSerialize->setEntity($project)->serializeFull();
$serializedMetrics = $projectSerialize->setEntity($project)->serializeOverview();
return $this->json([
"message" => $translator->trans('Loaded project successfully'),
"severity" => SeverityInterface::SUCCESS,
"project" => $serializedProject,
"metrics" => $serializedMetrics
], Response::HTTP_OK );
}
/**
* @Route("/re-classify/{classify}", name="app_api_project_reclassify", methods={"POST"})
* @ParamConverter("project", options={"mapping": {"project_id": "id"}})
* @throws \Exception
*/
public function reClassify ( ProjectService $projectService, ProjectSerialize $projectSerialize, Request $request, TranslatorInterface $translator, string $classify = "all"): Response {
$projectService
->setParams($request->request)
->reClassify($classify);
if(!is_null($projectService->getException())){
return $this->json([
"message" => $projectService->getException()->getMessage(),
"severity" => SeverityInterface::ERROR
], Response::HTTP_NOT_ACCEPTABLE);
}
$sourceClassify = $request->request->get('source');
$targetClassify = $classify;
$projectsTarget = $projectService->fetchProjects($targetClassify);
$projectsCurrent = $projectService->fetchProjects($sourceClassify);
if($projectService->getException()){
return $this->json([
"message" => $projectService->getException()->getMessage(),
"severity" => SeverityInterface::WARNING
], $projectService->getException()->getCode());
}
// Moved To
$serTarget = $projectSerialize->setEntity($projectsTarget)->serializeForMainList();
// Leaved from
$serCurr = $projectSerialize->setEntity($projectsCurrent)->serializeForMainList();
return $this->json([
// Current Classify
"source" => ["key" => $sourceClassify, "value" => $serCurr],
"target" => ["key" => $classify, "value" => $serTarget],
"message" => $translator->trans('Projects classified successfully'),
"severity" => SeverityInterface::SUCCESS
], Response::HTTP_OK);
}
/**
* @Route("/labor-details/{project_id}", name="api_v3_project_labor_details", methods={"POST"}, requirements={"project_id": "\d+"})
* @ParamConverter("project", options={"mapping": {"project_id": "id"}})
*/
public function laborDetails(?Projects $project, ProjectService $projectService, Request $request): Response
{
$laborDetails = $projectService
->setEntity($project)
->setParams($request->request)
->laborDetails();
if ($projectService->getException()) {
return $this->json([
'message' => $projectService->getException()->getMessage(),
'severity' => SeverityInterface::WARNING,
], Response::HTTP_BAD_REQUEST);
}
return $this->json($laborDetails, Response::HTTP_OK);
}
/**
* Tüm projelerin metriklerini SSE (Server-Sent Events) stream ile hesaplar.
* Her proje işlendikçe progress event'i gönderilir.
* Abort veya hata durumunda Doctrine transaction rollback yapılır (DB'ye yazılmaz).
*
* Nginx: proxy_buffering off veya X-Accel-Buffering: no header gereklidir.
*
* @Route("/sync-all-metrics", name="api_v3_project_sync_all_metrics", methods={"POST"})
*/
public function syncAllMetrics(ProjectService $projectService, Request $request): StreamedResponse
{
$syncToken = (string) $request->request->get('syncToken', uniqid('sync_', true));
return new StreamedResponse(function() use ($projectService, $syncToken) {
ignore_user_abort(false);
$emit = function(array $event) {
echo 'data: ' . json_encode($event) . "\n\n";
if (ob_get_level() > 0) {
ob_flush();
}
flush();
};
$projectService->syncAllMetricsStreamed($syncToken, $emit);
}, Response::HTTP_OK, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache, no-store',
'X-Accel-Buffering' => 'no',
'Connection' => 'keep-alive',
]);
}
/**
* Tek bir projenin metriklerini SSE stream ile hesaplar.
* syncAllMetricsStreamed'in aynı altyapısını kullanır, sadece o projeye filtreli.
*
* @Route("/sync-project-metrics/{project_id}", name="api_v3_project_sync_project_metrics", methods={"POST"}, requirements={"project_id": "\d+"})
* @ParamConverter("project", options={"mapping":{"project_id": "id"}})
*/
public function syncProjectMetrics(?Projects $project, ProjectService $projectService, Request $request, int $project_id): StreamedResponse
{
if (!$project) {
return new StreamedResponse(function() use ($project_id) {
echo 'data: ' . json_encode(['type' => 'error', 'message' => "Project #{$project_id} not found"]) . "\n\n";
flush();
}, Response::HTTP_OK, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache, no-store',
'X-Accel-Buffering' => 'no',
]);
}
$syncToken = (string) $request->request->get('syncToken', uniqid('sync_', true));
return new StreamedResponse(function() use ($projectService, $syncToken, $project) {
ignore_user_abort(false);
$emit = function(array $event) {
echo 'data: ' . json_encode($event) . "\n\n";
if (ob_get_level() > 0) {
ob_flush();
}
flush();
};
$projectService->syncAllMetricsStreamed($syncToken, $emit, $project->getId());
}, Response::HTTP_OK, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache, no-store',
'X-Accel-Buffering' => 'no',
'Connection' => 'keep-alive',
]);
}
/**
* FE'nin abort sinyali gönderdiği endpoint.
* Temp dosyaya flag yazar; syncAllMetricsStreamed her iterasyonda bunu kontrol eder.
*
* @Route("/abort-sync", name="api_v3_project_abort_sync", methods={"POST"})
*/
public function abortSync(ProjectService $projectService, Request $request): Response
{
$syncToken = (string) $request->request->get('syncToken', '');
if (empty($syncToken)) {
return $this->json(['message' => 'syncToken required'], Response::HTTP_BAD_REQUEST);
}
$projectService->setAbortFlag($syncToken);
return $this->json(['message' => 'Abort signal received'], Response::HTTP_OK);
}
/**
* Bos projeyi siler (order / stakeholder / timesheet payment yoksa).
* Bos degilse bloklar.
*
* @Route("/delete/{project_id}", name="api_v3_project_delete", methods={"POST"}, requirements={"project_id": "\d+"})
* @ParamConverter("project", options={"mapping":{"project_id": "id"}})
*/
public function delete(?Projects $project, ProjectService $projectService, TranslatorInterface $translator, Request $request, ?int $project_id = null): Response
{
if ($project_id && !$project) {
return $this->json([
"message" => $translator->trans("Project not found!"),
"severity" => SeverityInterface::ERROR
], Response::HTTP_NOT_FOUND);
}
// Stakeholder/payment blogunu asmak icin force + override sifresi
$force = filter_var($request->request->get('force'), FILTER_VALIDATE_BOOLEAN);
$password = $request->request->get('password');
$removedId = $projectService->setEntity($project)->cascadeDelete($force, $password);
if ($projectService->getException()) {
$exception = $projectService->getException();
return $this->json([
"message" => $exception->getMessage(),
"severity" => SeverityInterface::WARNING
], $exception->getCode() === Response::HTTP_UNAUTHORIZED ? Response::HTTP_UNAUTHORIZED : Response::HTTP_BAD_REQUEST);
}
return $this->json([
"message" => $translator->trans('Project deleted successfully'),
"severity" => SeverityInterface::SUCCESS,
"id" => $removedId,
"mode" => "delete"
], Response::HTTP_OK);
}
/**
* Force-delete onizlemesi: projenin tum bagimlilik ozeti (aggregate).
*
* @Route("/dependency-summary/{project_id}", name="api_v3_project_dependency_summary", methods={"POST"}, requirements={"project_id": "\d+"})
* @ParamConverter("project", options={"mapping":{"project_id": "id"}})
*/
public function dependencySummary(?Projects $project, ProjectService $projectService, TranslatorInterface $translator): Response
{
if (!$project) {
return $this->json([
"message" => $translator->trans("Project not found!"),
"severity" => SeverityInterface::ERROR
], Response::HTTP_NOT_FOUND);
}
return $this->json([
"summary" => $projectService->setEntity($project)->dependencySummary()
], Response::HTTP_OK);
}
/**
* Projeyi + TUM bagimliliklarini streamed (adim adim) force-delete eder. GERI DONUS YOK.
* EventSource yalnizca GET -> methods YAZILMAZ. Sifre query param (basit desen).
*
* @Route("/force-delete-streamed/{project_id}", name="api_v3_project_force_delete_streamed", requirements={"project_id": "\d+"})
* @ParamConverter("project", options={"mapping":{"project_id": "id"}})
*/
public function forceDeleteStreamed(?Projects $project, ProjectService $projectService, Request $request): StreamedResponse
{
$password = $request->query->get('password');
return $projectService->setEntity($project)->forceDeleteStreamed($password);
}
}