diff --git a/assets/js/components/CrudDataTable.vue b/assets/js/components/CrudDataTable.vue index 52cbf3f..22d6749 100644 --- a/assets/js/components/CrudDataTable.vue +++ b/assets/js/components/CrudDataTable.vue @@ -193,6 +193,10 @@ const props = defineProps({ type: String, default: '' }, + entityNameArticle: { + type: String, + default: '' + }, columns: { type: Array, required: true @@ -308,6 +312,11 @@ const exportItems = computed(() => [ // Computed create button label const createLabel = computed(() => { if (props.entityName) { + // If custom article is provided, use it + if (props.entityNameArticle) { + return `Neu${props.entityNameArticle === 'ein' ? 'es' : 'er'} ${props.entityName}` + } + // Fallback: Simple heuristic return `Neue${props.entityName.endsWith('e') ? 'r' : 'n'} ${props.entityName}` } return 'Neu' diff --git a/assets/js/layout/AppMenu.vue b/assets/js/layout/AppMenu.vue index 540bc10..8f4e812 100644 --- a/assets/js/layout/AppMenu.vue +++ b/assets/js/layout/AppMenu.vue @@ -13,13 +13,15 @@ const model = ref([ { label: 'CRM', items: [ - { label: 'Kontakte', icon: 'pi pi-fw pi-users', to: '/contacts' } + { label: 'Kontakte', icon: 'pi pi-fw pi-users', to: '/contacts' }, + { label: 'Projekte', icon: 'pi pi-fw pi-briefcase', to: '/projects' } ] }, { label: 'Administration', visible: () => authStore.isAdmin, items: [ + { label: 'Projekt-Status', icon: 'pi pi-fw pi-tag', to: '/project-statuses' }, { label: 'Benutzerverwaltung', icon: 'pi pi-fw pi-user-edit', to: '/users' }, { label: 'Rollenverwaltung', icon: 'pi pi-fw pi-shield', to: '/roles' }, { label: 'Einstellungen', icon: 'pi pi-fw pi-cog', to: '/settings' } diff --git a/assets/js/router.js b/assets/js/router.js index 8702bf4..b23b23f 100644 --- a/assets/js/router.js +++ b/assets/js/router.js @@ -1,6 +1,8 @@ import { createRouter, createWebHistory } from 'vue-router'; import Dashboard from './views/Dashboard.vue'; import ContactManagement from './views/ContactManagement.vue'; +import ProjectManagement from './views/ProjectManagement.vue'; +import ProjectStatusManagement from './views/ProjectStatusManagement.vue'; import UserManagement from './views/UserManagement.vue'; import RoleManagement from './views/RoleManagement.vue'; import SettingsManagement from './views/SettingsManagement.vue'; @@ -8,6 +10,8 @@ import SettingsManagement from './views/SettingsManagement.vue'; const routes = [ { path: '/', name: 'dashboard', component: Dashboard }, { path: '/contacts', name: 'contacts', component: ContactManagement }, + { path: '/projects', name: 'projects', component: ProjectManagement }, + { path: '/project-statuses', name: 'project-statuses', component: ProjectStatusManagement, meta: { requiresAdmin: true } }, { path: '/users', name: 'users', component: UserManagement, meta: { requiresAdmin: true } }, { path: '/roles', name: 'roles', component: RoleManagement, meta: { requiresAdmin: true } }, { path: '/settings', name: 'settings', component: SettingsManagement, meta: { requiresAdmin: true } }, diff --git a/assets/js/views/ContactManagement.vue b/assets/js/views/ContactManagement.vue index 2c1f278..89a33c8 100644 --- a/assets/js/views/ContactManagement.vue +++ b/assets/js/views/ContactManagement.vue @@ -4,6 +4,7 @@ ref="tableRef" title="Kontakte" entity-name="Kontakt" + entity-name-article="einen" :columns="contactColumns" data-source="/api/contacts" storage-key="contactTableColumns" diff --git a/assets/js/views/ProjectManagement.vue b/assets/js/views/ProjectManagement.vue new file mode 100644 index 0000000..1f7d3c0 --- /dev/null +++ b/assets/js/views/ProjectManagement.vue @@ -0,0 +1,710 @@ + + + + + + + + + + + + + + + {{ data.name }} + + + + {{ data.projectNumber }} + + + + {{ data.customer.companyName }} + Kein Kunde zugewiesen + + + + + Kein Status + + + + {{ data.orderNumber }} + + + + {{ formatDate(data.orderDate) }} + + + + {{ formatDate(data.startDate) }} + + + + {{ formatDate(data.endDate) }} + + + + {{ formatCurrency(data.budget) }} + + + + {{ data.hourContingent }} h + + + + + + + + {{ formatDate(data.createdAt) }} + + + + {{ formatDate(data.updatedAt) }} + + + + + + + + Grunddaten + + + Projektname * + + Projektname ist erforderlich + + + + Beschreibung + + + + + Kunde + + + + + Status + + + + + {{ slotProps.option.name }} + + + + + + + Typ + + + + Beruflich + + + + Privat + + + + + + + Nummern & Daten + + + Projektnummer + + + + + Bestellnummer + + + + + Bestelldatum + + + + + Startdatum + + + + + Enddatum + + + + + + Budget & Kontingent + + + Budget (€) + + + + + Stundenkontingent + + + + + + + + + + + + + + + + Möchten Sie das Projekt {{ projectToDelete?.name }} wirklich löschen? + + + + + + + + + + + + Grunddaten + + + Projektname + {{ viewingProject.name || '-' }} + + + + Projektnummer + {{ viewingProject.projectNumber || '-' }} + + + + Beschreibung + {{ viewingProject.description || '-' }} + + + + Kunde + {{ viewingProject.customer?.companyName || '-' }} + + + + Status + + - + + + + Typ + + + + + + Nummern & Daten + + + Bestellnummer + {{ viewingProject.orderNumber || '-' }} + + + + Bestelldatum + {{ formatDate(viewingProject.orderDate) || '-' }} + + + + Startdatum + {{ formatDate(viewingProject.startDate) || '-' }} + + + + Enddatum + {{ formatDate(viewingProject.endDate) || '-' }} + + + + + Budget & Kontingent + + + Budget + {{ viewingProject.budget ? formatCurrency(viewingProject.budget) : '-' }} + + + + Stundenkontingent + {{ viewingProject.hourContingent ? `${viewingProject.hourContingent} h` : '-' }} + + + + + Metadaten + + + Erstellt am + {{ formatDate(viewingProject.createdAt) || '-' }} + + + + Zuletzt geändert + {{ formatDate(viewingProject.updatedAt) || '-' }} + + + + + + + + + + + + + + + diff --git a/assets/js/views/ProjectStatusManagement.vue b/assets/js/views/ProjectStatusManagement.vue new file mode 100644 index 0000000..e1b69e4 --- /dev/null +++ b/assets/js/views/ProjectStatusManagement.vue @@ -0,0 +1,329 @@ + + + + + + + + {{ data.name }} + + + + + + + {{ data.color }} + + + + + {{ data.sortOrder }} + + + + + + + + + + + + {{ formatDate(data.createdAt) }} + + + + + + + + Name * + + Name ist erforderlich + + + + Farbe + + + + + + + + Sortierung + + Niedrigere Werte werden zuerst angezeigt + + + + + Als Standard-Status verwenden + + + + + Status ist aktiv + + + + + + + + + + + + + + Möchten Sie den Status {{ statusToDelete?.name }} wirklich löschen? + + + + + + + + + + + + diff --git a/migrations/Version20251111133233.php b/migrations/Version20251111133233.php new file mode 100644 index 0000000..46a7f16 --- /dev/null +++ b/migrations/Version20251111133233.php @@ -0,0 +1,33 @@ +addSql('CREATE TABLE projects (id INT AUTO_INCREMENT NOT NULL, customer_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, description LONGTEXT DEFAULT NULL, project_number VARCHAR(50) DEFAULT NULL, order_number VARCHAR(50) DEFAULT NULL, order_date DATE DEFAULT NULL COMMENT \'(DC2Type:date_immutable)\', start_date DATE DEFAULT NULL COMMENT \'(DC2Type:date_immutable)\', end_date DATE DEFAULT NULL COMMENT \'(DC2Type:date_immutable)\', budget NUMERIC(10, 2) DEFAULT NULL, hour_contingent NUMERIC(8, 2) DEFAULT NULL, is_private TINYINT(1) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_5C93B3A49395C3F3 (customer_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE projects ADD CONSTRAINT FK_5C93B3A49395C3F3 FOREIGN KEY (customer_id) REFERENCES contacts (id) ON DELETE SET NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE projects DROP FOREIGN KEY FK_5C93B3A49395C3F3'); + $this->addSql('DROP TABLE projects'); + } +} diff --git a/migrations/Version20251111142205.php b/migrations/Version20251111142205.php new file mode 100644 index 0000000..dc52b3b --- /dev/null +++ b/migrations/Version20251111142205.php @@ -0,0 +1,37 @@ +addSql('CREATE TABLE project_statuses (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(100) NOT NULL, color VARCHAR(7) DEFAULT NULL, sort_order INT NOT NULL, is_default TINYINT(1) NOT NULL, is_active TINYINT(1) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE projects ADD status_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE projects ADD CONSTRAINT FK_5C93B3A46BF700BD FOREIGN KEY (status_id) REFERENCES project_statuses (id) ON DELETE SET NULL'); + $this->addSql('CREATE INDEX IDX_5C93B3A46BF700BD ON projects (status_id)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE projects DROP FOREIGN KEY FK_5C93B3A46BF700BD'); + $this->addSql('DROP TABLE project_statuses'); + $this->addSql('DROP INDEX IDX_5C93B3A46BF700BD ON projects'); + $this->addSql('ALTER TABLE projects DROP status_id'); + } +} diff --git a/src/Controller/PermissionController.php b/src/Controller/PermissionController.php index ea14a33..0402c40 100644 --- a/src/Controller/PermissionController.php +++ b/src/Controller/PermissionController.php @@ -24,7 +24,7 @@ class PermissionController extends AbstractController $permissions = []; // Liste aller Module die geprüft werden sollen - $modules = ['contacts', 'users', 'roles', 'settings']; + $modules = ['contacts', 'projects', 'project_statuses', 'users', 'roles', 'settings']; foreach ($modules as $module) { $permissions[$module] = [ diff --git a/src/Entity/Contact.php b/src/Entity/Contact.php index a535999..f47500d 100644 --- a/src/Entity/Contact.php +++ b/src/Entity/Contact.php @@ -59,11 +59,11 @@ class Contact implements ModuleAwareInterface #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] - #[Groups(['contact:read'])] + #[Groups(['contact:read', 'project:read'])] private ?int $id = null; #[ORM\Column(length: 255)] - #[Groups(['contact:read', 'contact:write'])] + #[Groups(['contact:read', 'contact:write', 'project:read'])] #[Assert\NotBlank(message: 'Der Firmenname darf nicht leer sein')] #[Assert\Length(max: 255)] private ?string $companyName = null; diff --git a/src/Entity/Project.php b/src/Entity/Project.php new file mode 100644 index 0000000..f8b7be7 --- /dev/null +++ b/src/Entity/Project.php @@ -0,0 +1,318 @@ + ['project:read']], + denormalizationContext: ['groups' => ['project:write']], + order: ['startDate' => 'DESC'] +)] +#[ApiFilter(BooleanFilter::class, properties: ['isPrivate'])] +#[ApiFilter(SearchFilter::class, properties: ['name' => 'partial', 'projectNumber' => 'partial', 'orderNumber' => 'partial', 'customer.companyName' => 'partial'])] +#[ApiFilter(DateFilter::class, properties: ['startDate', 'endDate', 'orderDate'])] +class Project implements ModuleAwareInterface +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + #[Groups(['project:read'])] + private ?int $id = null; + + #[ORM\Column(length: 255)] + #[Groups(['project:read', 'project:write'])] + #[Assert\NotBlank(message: 'Der Projektname darf nicht leer sein')] + #[Assert\Length(max: 255)] + private ?string $name = null; + + #[ORM\Column(type: Types::TEXT, nullable: true)] + #[Groups(['project:read', 'project:write'])] + private ?string $description = null; + + #[ORM\ManyToOne(targetEntity: Contact::class)] + #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')] + #[Groups(['project:read', 'project:write'])] + private ?Contact $customer = null; + + #[ORM\ManyToOne(targetEntity: ProjectStatus::class)] + #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')] + #[Groups(['project:read', 'project:write'])] + private ?ProjectStatus $status = null; + + #[ORM\Column(length: 50, nullable: true)] + #[Groups(['project:read', 'project:write'])] + #[Assert\Length(max: 50)] + private ?string $projectNumber = null; + + #[ORM\Column(length: 50, nullable: true)] + #[Groups(['project:read', 'project:write'])] + #[Assert\Length(max: 50)] + private ?string $orderNumber = null; + + #[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)] + #[Groups(['project:read', 'project:write'])] + private ?\DateTimeImmutable $orderDate = null; + + #[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)] + #[Groups(['project:read', 'project:write'])] + private ?\DateTimeImmutable $startDate = null; + + #[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)] + #[Groups(['project:read', 'project:write'])] + private ?\DateTimeImmutable $endDate = null; + + #[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true)] + #[Groups(['project:read', 'project:write'])] + #[Assert\PositiveOrZero(message: 'Das Budget muss positiv sein')] + private ?string $budget = null; + + #[ORM\Column(type: Types::DECIMAL, precision: 8, scale: 2, nullable: true)] + #[Groups(['project:read', 'project:write'])] + #[Assert\PositiveOrZero(message: 'Das Stundenkontingent muss positiv sein')] + private ?string $hourContingent = null; + + #[ORM\Column] + #[Groups(['project:read', 'project:write'])] + private bool $isPrivate = false; + + #[ORM\Column] + #[Groups(['project:read'])] + private ?\DateTimeImmutable $createdAt = null; + + #[ORM\Column(nullable: true)] + #[Groups(['project:read'])] + private ?\DateTimeImmutable $updatedAt = null; + + public function __construct() + { + $this->createdAt = new \DateTimeImmutable(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): static + { + $this->description = $description; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getCustomer(): ?Contact + { + return $this->customer; + } + + public function setCustomer(?Contact $customer): static + { + $this->customer = $customer; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getStatus(): ?ProjectStatus + { + return $this->status; + } + + public function setStatus(?ProjectStatus $status): static + { + $this->status = $status; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getProjectNumber(): ?string + { + return $this->projectNumber; + } + + public function setProjectNumber(?string $projectNumber): static + { + $this->projectNumber = $projectNumber; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getOrderNumber(): ?string + { + return $this->orderNumber; + } + + public function setOrderNumber(?string $orderNumber): static + { + $this->orderNumber = $orderNumber; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getOrderDate(): ?\DateTimeImmutable + { + return $this->orderDate; + } + + public function setOrderDate(?\DateTimeImmutable $orderDate): static + { + $this->orderDate = $orderDate; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getStartDate(): ?\DateTimeImmutable + { + return $this->startDate; + } + + public function setStartDate(?\DateTimeImmutable $startDate): static + { + $this->startDate = $startDate; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getEndDate(): ?\DateTimeImmutable + { + return $this->endDate; + } + + public function setEndDate(?\DateTimeImmutable $endDate): static + { + $this->endDate = $endDate; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getBudget(): ?string + { + return $this->budget; + } + + public function setBudget(?string $budget): static + { + $this->budget = $budget; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getHourContingent(): ?string + { + return $this->hourContingent; + } + + public function setHourContingent(?string $hourContingent): static + { + $this->hourContingent = $hourContingent; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getIsPrivate(): bool + { + return $this->isPrivate; + } + + public function setIsPrivate(bool $isPrivate): static + { + $this->isPrivate = $isPrivate; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeImmutable $createdAt): static + { + $this->createdAt = $createdAt; + return $this; + } + + public function getUpdatedAt(): ?\DateTimeImmutable + { + return $this->updatedAt; + } + + public function setUpdatedAt(?\DateTimeImmutable $updatedAt): static + { + $this->updatedAt = $updatedAt; + return $this; + } + + public function __toString(): string + { + return $this->name ?? ''; + } + + /** + * Returns the module code this entity belongs to. + * Required by ModuleVoter for permission checks. + */ + public function getModuleName(): string + { + return 'projects'; + } +} diff --git a/src/Entity/ProjectStatus.php b/src/Entity/ProjectStatus.php new file mode 100644 index 0000000..330902a --- /dev/null +++ b/src/Entity/ProjectStatus.php @@ -0,0 +1,199 @@ + ['project_status:read']], + denormalizationContext: ['groups' => ['project_status:write']], + order: ['sortOrder' => 'ASC'] +)] +#[ApiFilter(BooleanFilter::class, properties: ['isActive', 'isDefault'])] +#[ApiFilter(SearchFilter::class, properties: ['name' => 'partial'])] +class ProjectStatus implements ModuleAwareInterface +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + #[Groups(['project_status:read', 'project:read'])] + private ?int $id = null; + + #[ORM\Column(length: 100)] + #[Groups(['project_status:read', 'project_status:write', 'project:read'])] + #[Assert\NotBlank(message: 'Der Statusname darf nicht leer sein')] + #[Assert\Length(max: 100)] + private ?string $name = null; + + #[ORM\Column(length: 7, nullable: true)] + #[Groups(['project_status:read', 'project_status:write', 'project:read'])] + #[Assert\Length(max: 7)] + #[Assert\Regex(pattern: '/^#[0-9A-Fa-f]{6}$/', message: 'Die Farbe muss im Format #RRGGBB sein')] + private ?string $color = null; + + #[ORM\Column] + #[Groups(['project_status:read', 'project_status:write'])] + private int $sortOrder = 0; + + #[ORM\Column] + #[Groups(['project_status:read', 'project_status:write'])] + private bool $isDefault = false; + + #[ORM\Column] + #[Groups(['project_status:read', 'project_status:write'])] + private bool $isActive = true; + + #[ORM\Column] + #[Groups(['project_status:read'])] + private ?\DateTimeImmutable $createdAt = null; + + #[ORM\Column(nullable: true)] + #[Groups(['project_status:read'])] + private ?\DateTimeImmutable $updatedAt = null; + + public function __construct() + { + $this->createdAt = new \DateTimeImmutable(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getColor(): ?string + { + return $this->color; + } + + public function setColor(?string $color): static + { + $this->color = $color; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getSortOrder(): int + { + return $this->sortOrder; + } + + public function setSortOrder(int $sortOrder): static + { + $this->sortOrder = $sortOrder; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getIsDefault(): bool + { + return $this->isDefault; + } + + public function setIsDefault(bool $isDefault): static + { + $this->isDefault = $isDefault; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getIsActive(): bool + { + return $this->isActive; + } + + public function setIsActive(bool $isActive): static + { + $this->isActive = $isActive; + $this->updatedAt = new \DateTimeImmutable(); + return $this; + } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeImmutable $createdAt): static + { + $this->createdAt = $createdAt; + return $this; + } + + public function getUpdatedAt(): ?\DateTimeImmutable + { + return $this->updatedAt; + } + + public function setUpdatedAt(?\DateTimeImmutable $updatedAt): static + { + $this->updatedAt = $updatedAt; + return $this; + } + + public function __toString(): string + { + return $this->name ?? ''; + } + + /** + * Returns the module code this entity belongs to. + * Required by ModuleVoter for permission checks. + */ + public function getModuleName(): string + { + return 'project_statuses'; + } +} diff --git a/src/Repository/ProjectRepository.php b/src/Repository/ProjectRepository.php new file mode 100644 index 0000000..2e1919e --- /dev/null +++ b/src/Repository/ProjectRepository.php @@ -0,0 +1,57 @@ + + */ +class ProjectRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Project::class); + } + + /** + * Find projects by customer + */ + public function findByCustomer(int $customerId): array + { + return $this->createQueryBuilder('p') + ->andWhere('p.customer = :customerId') + ->setParameter('customerId', $customerId) + ->orderBy('p.startDate', 'DESC') + ->getQuery() + ->getResult(); + } + + /** + * Find active projects (end date in future or null) + */ + public function findActiveProjects(): array + { + return $this->createQueryBuilder('p') + ->andWhere('p.endDate IS NULL OR p.endDate >= :today') + ->setParameter('today', new \DateTimeImmutable()) + ->orderBy('p.startDate', 'DESC') + ->getQuery() + ->getResult(); + } + + /** + * Find projects by private flag + */ + public function findByPrivateFlag(bool $isPrivate): array + { + return $this->createQueryBuilder('p') + ->andWhere('p.isPrivate = :isPrivate') + ->setParameter('isPrivate', $isPrivate) + ->orderBy('p.startDate', 'DESC') + ->getQuery() + ->getResult(); + } +} diff --git a/src/Repository/ProjectStatusRepository.php b/src/Repository/ProjectStatusRepository.php new file mode 100644 index 0000000..c4b0213 --- /dev/null +++ b/src/Repository/ProjectStatusRepository.php @@ -0,0 +1,45 @@ + + */ +class ProjectStatusRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, ProjectStatus::class); + } + + /** + * Find default status + */ + public function findDefault(): ?ProjectStatus + { + return $this->createQueryBuilder('ps') + ->andWhere('ps.isDefault = :true') + ->andWhere('ps.isActive = :true') + ->setParameter('true', true) + ->setMaxResults(1) + ->getQuery() + ->getOneOrNullResult(); + } + + /** + * Find all active statuses ordered by sortOrder + */ + public function findAllActive(): array + { + return $this->createQueryBuilder('ps') + ->andWhere('ps.isActive = :true') + ->setParameter('true', true) + ->orderBy('ps.sortOrder', 'ASC') + ->getQuery() + ->getResult(); + } +}
{{ data.color }}