feat: relation revendeur sur Customer/Website + WebsiteConfiguration

Customer :
- Ajout revendeurCode (VARCHAR 10, nullable) : stocke le code du revendeur
  apporteur d'affaire (pas de FK, suppression revendeur sans impact)
- Select revendeur dans le formulaire de création client
- Champ revendeur dans la fiche client (info + section système)

Website :
- Ajout revendeurCode (VARCHAR 10, nullable) : même logique que Customer

WebsiteConfiguration (nouvelle entité) :
- website (ManyToOne CASCADE) : site parent
- type (VARCHAR 25) : clé de configuration
- value (TEXT) : valeur
- Contrainte unique (website_id, type)

Formulaire création client :
- Select "Revendeur (apporteur d'affaire)" avec liste des revendeurs actifs

Fiche client :
- Onglet Info : champ code revendeur éditable
- Section système : affiche le code revendeur

Migrations : ALTER TABLE customer/website ADD revendeur_code,
CREATE TABLE website_configuration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-04 21:39:26 +02:00
parent c849a31ea1
commit e03233d922
9 changed files with 193 additions and 4 deletions

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260404193257 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE customer ADD revendeur_code VARCHAR(10) DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE customer DROP revendeur_code');
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260404193605 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE website_configuration (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, type VARCHAR(25) NOT NULL, value TEXT NOT NULL, website_id INT NOT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_8BC287E818F45C82 ON website_configuration (website_id)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_8BC287E818F45C828CDE5729 ON website_configuration (website_id, type)');
$this->addSql('ALTER TABLE website_configuration ADD CONSTRAINT FK_8BC287E818F45C82 FOREIGN KEY (website_id) REFERENCES website (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE website ADD revendeur_code VARCHAR(10) DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE website_configuration DROP CONSTRAINT FK_8BC287E818F45C82');
$this->addSql('DROP TABLE website_configuration');
$this->addSql('ALTER TABLE website DROP revendeur_code');
}
}

View File

@@ -13,6 +13,7 @@ use App\Service\MailerService;
use App\Service\MeilisearchService; use App\Service\MeilisearchService;
use App\Service\OvhService; use App\Service\OvhService;
use App\Service\UserManagementService; use App\Service\UserManagementService;
use App\Repository\RevendeurRepository;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
@@ -47,6 +48,7 @@ class ClientsController extends AbstractController
public function create( public function create(
Request $request, Request $request,
CustomerRepository $customerRepository, CustomerRepository $customerRepository,
RevendeurRepository $revendeurRepository,
EntityManagerInterface $em, EntityManagerInterface $em,
MeilisearchService $meilisearch, MeilisearchService $meilisearch,
UserManagementService $userService, UserManagementService $userService,
@@ -57,7 +59,9 @@ class ClientsController extends AbstractController
#[Autowire(env: 'STRIPE_SK')] string $stripeSecretKey, #[Autowire(env: 'STRIPE_SK')] string $stripeSecretKey,
): Response { ): Response {
if ('POST' !== $request->getMethod()) { if ('POST' !== $request->getMethod()) {
return $this->render('admin/clients/create.html.twig'); return $this->render('admin/clients/create.html.twig', [
'revendeurs' => $revendeurRepository->findBy(['isActive' => true], ['codeRevendeur' => 'ASC']),
]);
} }
try { try {
@@ -223,6 +227,7 @@ class ClientsController extends AbstractController
$customer->setZipCode(trim($request->request->getString('zipCode')) ?: null); $customer->setZipCode(trim($request->request->getString('zipCode')) ?: null);
$customer->setCity(trim($request->request->getString('city')) ?: null); $customer->setCity(trim($request->request->getString('city')) ?: null);
$customer->setTypeCompany(trim($request->request->getString('typeCompany')) ?: null); $customer->setTypeCompany(trim($request->request->getString('typeCompany')) ?: null);
$customer->setRevendeurCode(trim($request->request->getString('revendeurCode')) ?: null);
$customer->setGeoLat(trim($request->request->getString('geoLat')) ?: null); $customer->setGeoLat(trim($request->request->getString('geoLat')) ?: null);
$customer->setGeoLong(trim($request->request->getString('geoLong')) ?: null); $customer->setGeoLong(trim($request->request->getString('geoLong')) ?: null);
} }

View File

@@ -112,6 +112,9 @@ class Customer
#[ORM\Column(length: 50, unique: true, nullable: true)] #[ORM\Column(length: 50, unique: true, nullable: true)]
private ?string $codeComptable = null; private ?string $codeComptable = null;
#[ORM\Column(length: 10, nullable: true)]
private ?string $revendeurCode = null;
#[ORM\Column(nullable: true)] #[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $updatedAt = null; private ?\DateTimeImmutable $updatedAt = null;
@@ -133,6 +136,18 @@ class Customer
return $this; return $this;
} }
public function getRevendeurCode(): ?string
{
return $this->revendeurCode;
}
public function setRevendeurCode(?string $revendeurCode): static
{
$this->revendeurCode = $revendeurCode;
return $this;
}
public function generateCodeComptable(): string public function generateCodeComptable(): string
{ {
$prefix = '411'; $prefix = '411';

View File

@@ -38,6 +38,9 @@ class Website
#[ORM\Column(length: 20)] #[ORM\Column(length: 20)]
private string $state = self::STATE_CREATED; private string $state = self::STATE_CREATED;
#[ORM\Column(length: 10, nullable: true)]
private ?string $revendeurCode = null;
#[ORM\Column] #[ORM\Column]
private \DateTimeImmutable $createdAt; private \DateTimeImmutable $createdAt;
@@ -110,6 +113,18 @@ class Website
return self::STATE_OPEN === $this->state; return self::STATE_OPEN === $this->state;
} }
public function getRevendeurCode(): ?string
{
return $this->revendeurCode;
}
public function setRevendeurCode(?string $revendeurCode): static
{
$this->revendeurCode = $revendeurCode;
return $this;
}
public function getCreatedAt(): \DateTimeImmutable public function getCreatedAt(): \DateTimeImmutable
{ {
return $this->createdAt; return $this->createdAt;

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\UniqueConstraint(columns: ['website_id', 'type'])]
class WebsiteConfiguration
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: Website::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private Website $website;
#[ORM\Column(length: 25)]
private string $type;
#[ORM\Column(type: 'text')]
private string $value;
public function __construct(Website $website, string $type, string $value)
{
$this->website = $website;
$this->type = $type;
$this->value = $value;
}
public function getId(): ?int
{
return $this->id;
}
public function getWebsite(): Website
{
return $this->website;
}
public function getType(): string
{
return $this->type;
}
public function setType(string $type): static
{
$this->type = $type;
return $this;
}
public function getValue(): string
{
return $this->value;
}
public function setValue(string $value): static
{
$this->value = $value;
return $this;
}
}

View File

@@ -68,6 +68,15 @@
</select> </select>
</div> </div>
</div> </div>
<div class="mt-4">
<label for="revendeurCode" class="block text-xs font-bold uppercase tracking-wider mb-2">Revendeur (apporteur d'affaire)</label>
<select id="revendeurCode" name="revendeurCode" class="w-full px-4 py-3 glass text-sm font-bold">
<option value="">— Aucun —</option>
{% for rev in revendeurs %}
<option value="{{ rev.codeRevendeur }}">{{ rev.codeRevendeur }}{{ rev.raisonSociale ?? rev.user.fullName }}</option>
{% endfor %}
</select>
</div>
</section> </section>
<section class="glass p-6"> <section class="glass p-6">

View File

@@ -120,6 +120,10 @@
<label for="rna" class="block text-xs font-bold uppercase tracking-wider mb-2">RNA</label> <label for="rna" class="block text-xs font-bold uppercase tracking-wider mb-2">RNA</label>
<input type="text" id="rna" name="rna" value="{{ customer.rna }}" maxlength="20" class="w-full px-4 py-3 input-glass text-sm font-medium"> <input type="text" id="rna" name="rna" value="{{ customer.rna }}" maxlength="20" class="w-full px-4 py-3 input-glass text-sm font-medium">
</div> </div>
<div>
<label for="revendeurCode" class="block text-xs font-bold uppercase tracking-wider mb-2">Code revendeur</label>
<input type="text" id="revendeurCode" name="revendeurCode" value="{{ customer.revendeurCode }}" maxlength="10" placeholder="EC-XXXX" class="w-full px-4 py-3 input-glass text-sm font-medium font-mono">
</div>
</div> </div>
</section> </section>
@@ -168,6 +172,10 @@
<span class="text-gray-400 font-bold uppercase text-[9px] block">Modifie le</span> <span class="text-gray-400 font-bold uppercase text-[9px] block">Modifie le</span>
<span class="font-bold">{{ customer.updatedAt ? customer.updatedAt|date('d/m/Y H:i') : '—' }}</span> <span class="font-bold">{{ customer.updatedAt ? customer.updatedAt|date('d/m/Y H:i') : '—' }}</span>
</div> </div>
<div>
<span class="text-gray-400 font-bold uppercase text-[9px] block">Revendeur</span>
<span class="font-mono font-bold">{{ customer.revendeurCode ?? '—' }}</span>
</div>
</div> </div>
{% if customer.user.hasTempPassword %} {% if customer.user.hasTempPassword %}

View File

@@ -78,7 +78,7 @@ class ClientsControllerTest extends TestCase
$userService = $this->createStub(UserManagementService::class); $userService = $this->createStub(UserManagementService::class);
$logger = $this->createStub(LoggerInterface::class); $logger = $this->createStub(LoggerInterface::class);
$response = $controller->create($request, $repo, $em, $meilisearch, $userService, $logger, $this->createStub(HttpClientInterface::class), $this->createStub(MailerService::class), $this->createStub(Environment::class), 'sk_test_***'); $response = $controller->create($request, $repo, $this->createStub(\App\Repository\RevendeurRepository::class), $em, $meilisearch, $userService, $logger, $this->createStub(HttpClientInterface::class), $this->createStub(MailerService::class), $this->createStub(Environment::class), 'sk_test_***');
$this->assertInstanceOf(Response::class, $response); $this->assertInstanceOf(Response::class, $response);
} }
@@ -98,7 +98,7 @@ class ClientsControllerTest extends TestCase
$userService->method('createBaseUser')->willThrowException(new \InvalidArgumentException('Champs requis')); $userService->method('createBaseUser')->willThrowException(new \InvalidArgumentException('Champs requis'));
$logger = $this->createStub(LoggerInterface::class); $logger = $this->createStub(LoggerInterface::class);
$response = $controller->create($request, $repo, $em, $meilisearch, $userService, $logger, $this->createStub(HttpClientInterface::class), $this->createStub(MailerService::class), $this->createStub(Environment::class), 'sk_test_***'); $response = $controller->create($request, $repo, $this->createStub(\App\Repository\RevendeurRepository::class), $em, $meilisearch, $userService, $logger, $this->createStub(HttpClientInterface::class), $this->createStub(MailerService::class), $this->createStub(Environment::class), 'sk_test_***');
$this->assertInstanceOf(Response::class, $response); $this->assertInstanceOf(Response::class, $response);
} }
@@ -118,7 +118,7 @@ class ClientsControllerTest extends TestCase
$userService->method('createBaseUser')->willThrowException(new \RuntimeException('DB error')); $userService->method('createBaseUser')->willThrowException(new \RuntimeException('DB error'));
$logger = $this->createStub(LoggerInterface::class); $logger = $this->createStub(LoggerInterface::class);
$response = $controller->create($request, $repo, $em, $meilisearch, $userService, $logger, $this->createStub(HttpClientInterface::class), $this->createStub(MailerService::class), $this->createStub(Environment::class), 'sk_test_***'); $response = $controller->create($request, $repo, $this->createStub(\App\Repository\RevendeurRepository::class), $em, $meilisearch, $userService, $logger, $this->createStub(HttpClientInterface::class), $this->createStub(MailerService::class), $this->createStub(Environment::class), 'sk_test_***');
$this->assertInstanceOf(Response::class, $response); $this->assertInstanceOf(Response::class, $response);
} }
@@ -286,6 +286,7 @@ class ClientsControllerTest extends TestCase
$response = $controller->create( $response = $controller->create(
$request, $request,
$repo, $repo,
$this->createStub(\App\Repository\RevendeurRepository::class),
$this->createStub(EntityManagerInterface::class), $this->createStub(EntityManagerInterface::class),
$this->createStub(MeilisearchService::class), $this->createStub(MeilisearchService::class),
$userService, $userService,
@@ -326,6 +327,7 @@ class ClientsControllerTest extends TestCase
$response = $controller->create( $response = $controller->create(
$request, $request,
$repo, $repo,
$this->createStub(\App\Repository\RevendeurRepository::class),
$this->createStub(EntityManagerInterface::class), $this->createStub(EntityManagerInterface::class),
$this->createStub(MeilisearchService::class), $this->createStub(MeilisearchService::class),
$userService, $userService,
@@ -369,6 +371,7 @@ class ClientsControllerTest extends TestCase
$response = $controller->create( $response = $controller->create(
$request, $request,
$repo, $repo,
$this->createStub(\App\Repository\RevendeurRepository::class),
$this->createStub(EntityManagerInterface::class), $this->createStub(EntityManagerInterface::class),
$meilisearch, $meilisearch,
$userService, $userService,