From 06e5ce519959efa2c4a6ed5b1116743c146da14a Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 10 Apr 2026 11:24:16 +0200 Subject: [PATCH 01/11] Add new fields and modify existing on ServiceAgreement --- migrations/Version20260410120000.php | 61 +++++++++ src/Entity/ServiceAgreement.php | 191 +++++++++++++++++++++++++-- src/Enum/ServerSizeEnum.php | 11 ++ src/Enum/SystemOwnerNoticeEnum.php | 6 +- 4 files changed, 258 insertions(+), 11 deletions(-) create mode 100644 migrations/Version20260410120000.php create mode 100644 src/Enum/ServerSizeEnum.php diff --git a/migrations/Version20260410120000.php b/migrations/Version20260410120000.php new file mode 100644 index 00000000..fd5f04d4 --- /dev/null +++ b/migrations/Version20260410120000.php @@ -0,0 +1,61 @@ +addSql('ALTER TABLE service_agreement DROP COLUMN system_owner_notice'); + $this->addSql('ALTER TABLE service_agreement ADD system_owner_notices JSON NOT NULL DEFAULT \'[]\''); + $this->addSql('UPDATE service_agreement SET system_owner_notices = \'[]\' WHERE system_owner_notices IS NULL'); + + // Make valid_to nullable + $this->addSql('ALTER TABLE service_agreement CHANGE valid_to valid_to DATETIME DEFAULT NULL'); + + // Add new fields + $this->addSql('ALTER TABLE service_agreement ADD is_eol TINYINT(1) NOT NULL DEFAULT 0'); + $this->addSql('ALTER TABLE service_agreement ADD leantime_url VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE service_agreement ADD client_contact_name VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE service_agreement ADD client_contact_email VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE service_agreement ADD dedicated_server TINYINT(1) NOT NULL DEFAULT 0'); + $this->addSql('ALTER TABLE service_agreement ADD server_size VARCHAR(255) DEFAULT NULL'); + $this->addSql('ALTER TABLE service_agreement ADD cybersecurity_price DOUBLE PRECISION DEFAULT NULL'); + $this->addSql('ALTER TABLE service_agreement ADD git_repos LONGTEXT DEFAULT NULL'); + $this->addSql('ALTER TABLE service_agreement ADD created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP'); + $this->addSql('ALTER TABLE service_agreement ADD updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP'); + } + + public function down(Schema $schema): void + { + // Remove new fields + $this->addSql('ALTER TABLE service_agreement DROP COLUMN is_eol'); + $this->addSql('ALTER TABLE service_agreement DROP COLUMN leantime_url'); + $this->addSql('ALTER TABLE service_agreement DROP COLUMN client_contact_name'); + $this->addSql('ALTER TABLE service_agreement DROP COLUMN client_contact_email'); + $this->addSql('ALTER TABLE service_agreement DROP COLUMN dedicated_server'); + $this->addSql('ALTER TABLE service_agreement DROP COLUMN server_size'); + $this->addSql('ALTER TABLE service_agreement DROP COLUMN cybersecurity_price'); + $this->addSql('ALTER TABLE service_agreement DROP COLUMN git_repos'); + $this->addSql('ALTER TABLE service_agreement DROP COLUMN created_at'); + $this->addSql('ALTER TABLE service_agreement DROP COLUMN updated_at'); + + // Revert valid_to to NOT NULL + $this->addSql('ALTER TABLE service_agreement CHANGE valid_to valid_to DATETIME NOT NULL'); + + // Revert system_owner_notices JSON back to system_owner_notice VARCHAR + $this->addSql('ALTER TABLE service_agreement DROP COLUMN system_owner_notices'); + $this->addSql('ALTER TABLE service_agreement ADD system_owner_notice VARCHAR(255) NOT NULL DEFAULT \'never\''); + } +} diff --git a/src/Entity/ServiceAgreement.php b/src/Entity/ServiceAgreement.php index 48a90acc..9aeac4f4 100644 --- a/src/Entity/ServiceAgreement.php +++ b/src/Entity/ServiceAgreement.php @@ -3,12 +3,16 @@ namespace App\Entity; use App\Enum\HostingProviderEnum; +use App\Enum\ServerSizeEnum; use App\Enum\SystemOwnerNoticeEnum; use App\Repository\ServiceAgreementRepository; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Context\ExecutionContextInterface; #[ORM\Entity(repositoryClass: ServiceAgreementRepository::class)] +#[ORM\HasLifecycleCallbacks] class ServiceAgreement { #[ORM\Id] @@ -44,14 +48,44 @@ class ServiceAgreement #[ORM\Column(type: Types::DATETIME_MUTABLE)] private ?\DateTimeInterface $validFrom = null; - #[ORM\Column(type: Types::DATETIME_MUTABLE)] + #[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)] private ?\DateTimeInterface $validTo = null; #[ORM\Column] private ?bool $isActive = null; - #[ORM\Column(enumType: SystemOwnerNoticeEnum::class)] - private ?SystemOwnerNoticeEnum $SystemOwnerNotice = null; + #[ORM\Column(type: Types::JSON)] + private array $systemOwnerNotices = []; + + #[ORM\Column] + private bool $isEol = false; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $leantimeUrl = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $clientContactName = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $clientContactEmail = null; + + #[ORM\Column] + private bool $dedicatedServer = false; + + #[ORM\Column(enumType: ServerSizeEnum::class, nullable: true)] + private ?ServerSizeEnum $serverSize = null; + + #[ORM\Column(nullable: true)] + private ?float $cybersecurityPrice = null; + + #[ORM\Column(type: Types::TEXT, nullable: true)] + private ?string $gitRepos = null; + + #[ORM\Column(type: Types::DATETIME_MUTABLE)] + private ?\DateTimeInterface $createdAt = null; + + #[ORM\Column(type: Types::DATETIME_MUTABLE)] + private ?\DateTimeInterface $updatedAt = null; public function getId(): ?int { @@ -159,7 +193,7 @@ public function getValidTo(): ?\DateTimeInterface return $this->validTo; } - public function setValidTo(\DateTimeInterface $validTo): static + public function setValidTo(?\DateTimeInterface $validTo): static { $this->validTo = $validTo; @@ -178,15 +212,156 @@ public function setIsActive(bool $isActive): static return $this; } - public function getSystemOwnerNotice(): ?SystemOwnerNoticeEnum + /** + * @return SystemOwnerNoticeEnum[] + */ + public function getSystemOwnerNotices(): array { - return $this->SystemOwnerNotice; + return array_map( + fn (string $value) => SystemOwnerNoticeEnum::from($value), + $this->systemOwnerNotices + ); } - public function setSystemOwnerNotice(SystemOwnerNoticeEnum $SystemOwnerNotice): static + /** + * @param SystemOwnerNoticeEnum[] $notices + */ + public function setSystemOwnerNotices(array $notices): static { - $this->SystemOwnerNotice = $SystemOwnerNotice; + $this->systemOwnerNotices = array_map( + fn (SystemOwnerNoticeEnum $notice) => $notice->value, + $notices + ); return $this; } + + public function isEol(): bool + { + return $this->isEol; + } + + public function setIsEol(bool $isEol): static + { + $this->isEol = $isEol; + + return $this; + } + + public function getLeantimeUrl(): ?string + { + return $this->leantimeUrl; + } + + public function setLeantimeUrl(?string $leantimeUrl): static + { + $this->leantimeUrl = $leantimeUrl; + + return $this; + } + + public function getClientContactName(): ?string + { + return $this->clientContactName; + } + + public function setClientContactName(?string $clientContactName): static + { + $this->clientContactName = $clientContactName; + + return $this; + } + + public function getClientContactEmail(): ?string + { + return $this->clientContactEmail; + } + + public function setClientContactEmail(?string $clientContactEmail): static + { + $this->clientContactEmail = $clientContactEmail; + + return $this; + } + + public function isDedicatedServer(): bool + { + return $this->dedicatedServer; + } + + public function setDedicatedServer(bool $dedicatedServer): static + { + $this->dedicatedServer = $dedicatedServer; + + return $this; + } + + public function getServerSize(): ?ServerSizeEnum + { + return $this->serverSize; + } + + public function setServerSize(?ServerSizeEnum $serverSize): static + { + $this->serverSize = $serverSize; + + return $this; + } + + public function getCybersecurityPrice(): ?float + { + return $this->cybersecurityPrice; + } + + public function setCybersecurityPrice(?float $cybersecurityPrice): static + { + $this->cybersecurityPrice = $cybersecurityPrice; + + return $this; + } + + public function getGitRepos(): ?string + { + return $this->gitRepos; + } + + public function setGitRepos(?string $gitRepos): static + { + $this->gitRepos = $gitRepos; + + return $this; + } + + public function getCreatedAt(): ?\DateTimeInterface + { + return $this->createdAt; + } + + public function getUpdatedAt(): ?\DateTimeInterface + { + return $this->updatedAt; + } + + #[ORM\PrePersist] + public function setCreatedAtValue(): void + { + $this->createdAt = new \DateTime(); + $this->updatedAt = new \DateTime(); + } + + #[ORM\PreUpdate] + public function setUpdatedAtValue(): void + { + $this->updatedAt = new \DateTime(); + } + + #[Assert\Callback] + public function validateValidTo(ExecutionContextInterface $context): void + { + if ($this->isEol && null === $this->validTo) { + $context->buildViolation('service_agreement.valid_to_required_when_eol') + ->atPath('validTo') + ->addViolation(); + } + } } diff --git a/src/Enum/ServerSizeEnum.php b/src/Enum/ServerSizeEnum.php new file mode 100644 index 00000000..3f0cd919 --- /dev/null +++ b/src/Enum/ServerSizeEnum.php @@ -0,0 +1,11 @@ + Date: Fri, 10 Apr 2026 11:24:29 +0200 Subject: [PATCH 02/11] Update forms with new fields and multi-select checkboxes --- src/Form/CombinedServiceAgreementType.php | 1 - src/Form/ServiceAgreementType.php | 144 +++++++++++++++++----- 2 files changed, 112 insertions(+), 33 deletions(-) diff --git a/src/Form/CombinedServiceAgreementType.php b/src/Form/CombinedServiceAgreementType.php index 4861081b..e14b95eb 100644 --- a/src/Form/CombinedServiceAgreementType.php +++ b/src/Form/CombinedServiceAgreementType.php @@ -23,7 +23,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'label_attr' => ['class' => 'label toggle-label'], 'help_attr' => ['class' => 'form-help'], 'row_attr' => ['class' => 'form-row select-none'], - 'attr' => ['style' => 'margin-left: 10px;'], 'data' => $options['data']['attachCybersecurityAgreement'] ?? false, ]) ->add('cybersecurityAgreement', CybersecurityAgreementType::class, [ diff --git a/src/Form/ServiceAgreementType.php b/src/Form/ServiceAgreementType.php index 1c86316d..f0ccfda5 100644 --- a/src/Form/ServiceAgreementType.php +++ b/src/Form/ServiceAgreementType.php @@ -7,14 +7,17 @@ use App\Entity\ServiceAgreement; use App\Entity\Worker; use App\Enum\HostingProviderEnum; +use App\Enum\ServerSizeEnum; use App\Enum\SystemOwnerNoticeEnum; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\DateType; -use Symfony\Component\Form\Extension\Core\Type\EnumType; +use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\NumberType; +use Symfony\Component\Form\Extension\Core\Type\TextareaType; +use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\UrlType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -34,6 +37,34 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'help_attr' => ['class' => 'form-help'], 'row_attr' => ['class' => 'form-row'], ]) + ->add('isActive', CheckboxType::class, [ + 'label' => 'service_agreement.is_active', + 'label_attr' => ['class' => 'label toggle-label'], + 'help_attr' => ['class' => 'form-help'], + 'row_attr' => ['class' => 'form-row select-none'], + 'required' => false, + 'data' => true, + ]) + ->add('isEol', CheckboxType::class, [ + 'label' => 'service_agreement.is_eol', + 'label_attr' => ['class' => 'label toggle-label'], + 'help_attr' => ['class' => 'form-help'], + 'row_attr' => ['class' => 'form-row select-none'], + 'attr' => [ + 'data-eol-required-target' => 'checkbox', + 'data-action' => 'eol-required#toggle', + ], + 'required' => false, + ]) + ->add('leantimeUrl', UrlType::class, [ + 'label' => 'service_agreement.leantime_url', + 'label_attr' => ['class' => 'label'], + 'attr' => ['class' => 'form-element'], + 'constraints' => new Url(), + 'help_attr' => ['class' => 'form-help'], + 'row_attr' => ['class' => 'form-row'], + 'required' => false, + ]) ->add('client', EntityType::class, [ 'class' => Client::class, 'label' => 'service_agreement.client', @@ -42,6 +73,59 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'help_attr' => ['class' => 'form-help'], 'row_attr' => ['class' => 'form-row'], ]) + ->add('clientContactName', TextType::class, [ + 'label' => 'service_agreement.client_contact_name', + 'label_attr' => ['class' => 'label'], + 'attr' => ['class' => 'form-element'], + 'help_attr' => ['class' => 'form-help'], + 'row_attr' => ['class' => 'form-row'], + 'required' => false, + ]) + ->add('clientContactEmail', EmailType::class, [ + 'label' => 'service_agreement.client_contact_email', + 'label_attr' => ['class' => 'label'], + 'attr' => ['class' => 'form-element'], + 'help_attr' => ['class' => 'form-help'], + 'row_attr' => ['class' => 'form-row'], + 'required' => false, + ]) + ->add('systemOwnerNotices', ChoiceType::class, [ + 'choices' => SystemOwnerNoticeEnum::cases(), + 'choice_label' => fn (SystemOwnerNoticeEnum $choice) => match ($choice) { + SystemOwnerNoticeEnum::SERVERFLYTNING => 'system_owner_notice_enum.serverflytning', + SystemOwnerNoticeEnum::SIKKERHEDSPATCH => 'system_owner_notice_enum.sikkerhedspatch', + SystemOwnerNoticeEnum::CYBERSIKKERSHEDSOPDATERING => 'system_owner_notice_enum.cybersikkershedsopdatering', + }, + 'choice_value' => fn (?SystemOwnerNoticeEnum $choice) => $choice?->value, + 'multiple' => true, + 'expanded' => true, + 'label' => 'service_agreement.system_owner_notices', + 'label_attr' => ['class' => 'label'], + 'attr' => ['class' => 'checkbox-inline-group'], + 'help_attr' => ['class' => 'form-help'], + 'row_attr' => ['class' => 'form-row'], + 'required' => false, + ]) + ->add('validFrom', DateType::class, [ + 'widget' => 'single_text', + 'label' => 'service_agreement.valid_from', + 'label_attr' => ['class' => 'label'], + 'attr' => ['class' => 'form-element'], + 'help_attr' => ['class' => 'form-help'], + 'row_attr' => ['class' => 'form-row'], + ]) + ->add('validTo', DateType::class, [ + 'widget' => 'single_text', + 'label' => 'service_agreement.valid_to', + 'label_attr' => ['class' => 'label'], + 'attr' => [ + 'class' => 'form-element', + 'data-eol-required-target' => 'dateField', + ], + 'help_attr' => ['class' => 'form-help'], + 'row_attr' => ['class' => 'form-row'], + 'required' => false, + ]) ->add('hostingProvider', ChoiceType::class, [ 'choices' => HostingProviderEnum::cases(), 'choice_label' => fn ($choice) => $choice->value, @@ -51,27 +135,23 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'help_attr' => ['class' => 'form-help'], 'row_attr' => ['class' => 'form-row'], ]) - ->add('price', NumberType::class, [ - 'label' => 'service_agreement.price', - 'label_attr' => ['class' => 'label'], - 'attr' => ['class' => 'form-element'], + ->add('dedicatedServer', CheckboxType::class, [ + 'label' => 'service_agreement.dedicated_server', + 'label_attr' => ['class' => 'label toggle-label'], 'help_attr' => ['class' => 'form-help'], - 'row_attr' => ['class' => 'form-row service-agreement-price'], - 'html5' => true, + 'row_attr' => ['class' => 'form-row select-none'], + 'required' => false, ]) - ->add('SystemOwnerNotice', EnumType::class, [ - 'class' => SystemOwnerNoticeEnum::class, - 'label' => 'service_agreement.system_owner_notice', + ->add('serverSize', ChoiceType::class, [ + 'choices' => ServerSizeEnum::cases(), + 'choice_label' => fn (ServerSizeEnum $choice) => 'server_size_enum.'.$choice->value, + 'choice_value' => fn (?ServerSizeEnum $choice) => $choice?->value, + 'label' => 'service_agreement.server_size', 'label_attr' => ['class' => 'label'], - 'choice_label' => fn ($choice) => match ($choice) { - SystemOwnerNoticeEnum::ON_UPDATE => 'system_owner_notice_enum.on_update', - SystemOwnerNoticeEnum::ON_SERVER => 'system_owner_notice_enum.on_server', - SystemOwnerNoticeEnum::NEVER => 'system_owner_notice_enum.never', - default => null, - }, 'attr' => ['class' => 'form-element'], 'help_attr' => ['class' => 'form-help'], - 'row_attr' => ['class' => 'form-row service-agreement-price'], + 'row_attr' => ['class' => 'form-row'], + 'required' => false, ]) ->add('documentUrl', UrlType::class, [ 'label' => 'service_agreement.document_url', @@ -82,6 +162,14 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'row_attr' => ['class' => 'form-row'], 'required' => false, ]) + ->add('price', NumberType::class, [ + 'label' => 'service_agreement.operations_price', + 'label_attr' => ['class' => 'label'], + 'attr' => ['class' => 'form-element'], + 'help_attr' => ['class' => 'form-help'], + 'row_attr' => ['class' => 'form-row service-agreement-price'], + 'html5' => true, + ]) ->add('projectLead', EntityType::class, [ 'class' => Worker::class, 'label' => 'service_agreement.project_lead_id', @@ -90,30 +178,22 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'help_attr' => ['class' => 'form-help'], 'row_attr' => ['class' => 'form-row'], ]) - ->add('validFrom', DateType::class, [ - 'widget' => 'single_text', - 'label' => 'service_agreement.valid_from', + ->add('cybersecurityPrice', NumberType::class, [ + 'label' => 'service_agreement.cybersecurity_price', 'label_attr' => ['class' => 'label'], 'attr' => ['class' => 'form-element'], 'help_attr' => ['class' => 'form-help'], 'row_attr' => ['class' => 'form-row'], + 'html5' => true, + 'required' => false, ]) - ->add('validTo', DateType::class, [ - 'widget' => 'single_text', - 'label' => 'service_agreement.valid_to', + ->add('gitRepos', TextareaType::class, [ + 'label' => 'service_agreement.git_repos', 'label_attr' => ['class' => 'label'], - 'attr' => ['class' => 'form-element'], + 'attr' => ['class' => 'form-element', 'style' => 'height: 200px;'], 'help_attr' => ['class' => 'form-help'], 'row_attr' => ['class' => 'form-row'], - ]) - ->add('isActive', CheckboxType::class, [ - 'label' => 'service_agreement.is_active', - 'label_attr' => ['class' => 'label toggle-label'], - 'help_attr' => ['class' => 'form-help'], - 'row_attr' => ['class' => 'form-row select-none'], - 'attr' => ['style' => 'margin-left: 10px;'], 'required' => false, - 'data' => true, ]); } From 72eadab2d15d2490e8171e101d69ce3848143020 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 10 Apr 2026 11:24:51 +0200 Subject: [PATCH 03/11] Add Stimulus controller to require valid-to when EoL is checked --- assets/controllers/eol-required_controller.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 assets/controllers/eol-required_controller.js diff --git a/assets/controllers/eol-required_controller.js b/assets/controllers/eol-required_controller.js new file mode 100644 index 00000000..2e111859 --- /dev/null +++ b/assets/controllers/eol-required_controller.js @@ -0,0 +1,13 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + static targets = ["checkbox", "dateField"]; + + connect() { + this.toggle(); + } + + toggle() { + this.dateFieldTarget.required = this.checkboxTarget.checked; + } +} From 092b56fa40da759f02ccfd992b9fd904c3b00bc2 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 10 Apr 2026 11:25:14 +0200 Subject: [PATCH 04/11] Update templates with eol-required controller and rename price label --- templates/service_agreement/edit.html.twig | 2 +- templates/service_agreement/index.html.twig | 2 +- templates/service_agreement/new.html.twig | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/service_agreement/edit.html.twig b/templates/service_agreement/edit.html.twig index 4e46b8ab..ea7d1eeb 100644 --- a/templates/service_agreement/edit.html.twig +++ b/templates/service_agreement/edit.html.twig @@ -6,7 +6,7 @@

{{ 'service_agreement.edit'|trans }}

{{ form_start(form) }} -
+
{{ form_rest(form) }}
diff --git a/templates/service_agreement/index.html.twig b/templates/service_agreement/index.html.twig index 141ce416..9582686a 100644 --- a/templates/service_agreement/index.html.twig +++ b/templates/service_agreement/index.html.twig @@ -34,7 +34,7 @@ {{ knp_pagination_sortable(service_agreements, 'service_agreement.cybersecurity_agreement'|trans, 'cybersecurityAgreement.id') }} {{ knp_pagination_sortable(service_agreements, 'service_agreement.hosting_provider'|trans, 'service_agreement.hostingProvider') }} {{ knp_pagination_sortable(service_agreements, 'service_agreement.document_url'|trans, 'service_agreement.documentUrl') }} - {{ knp_pagination_sortable(service_agreements, 'service_agreement.price'|trans, 'service_agreement.price') }} + {{ knp_pagination_sortable(service_agreements, 'service_agreement.operations_price'|trans, 'service_agreement.price') }} {{ 'service_agreement.cyber_security_agreement.quarterly_hours'|trans }} {{ knp_pagination_sortable(service_agreements, 'service_agreement.project_lead'|trans, 'projectLead.name') }} {{ knp_pagination_sortable(service_agreements, 'service_agreement.valid_from'|trans, 'service_agreement.validFrom') }} diff --git a/templates/service_agreement/new.html.twig b/templates/service_agreement/new.html.twig index 717a53ef..8d056c17 100644 --- a/templates/service_agreement/new.html.twig +++ b/templates/service_agreement/new.html.twig @@ -6,7 +6,7 @@

{{ 'service_agreement.new'|trans }}

{{ form_start(form) }} -
+
{{ form_rest(form) }}
From c86bf44eaf2308dc4d30f3180bb0614c0c0c0bb3 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 10 Apr 2026 11:25:26 +0200 Subject: [PATCH 05/11] Add translations for new fields and update system owner notice enum --- translations/messages.da.yaml | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index c4a0b4a0..59f7520f 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -281,9 +281,15 @@ material_number_enum: external_without_moms: "Eksterne u/moms: 100008" system_owner_notice_enum: - on_update: "Opdateringer af systemet" - on_server: "På server" - never: "Aldrig" + serverflytning: "Serverflytning" + sikkerhedspatch: "Sikkerhedspatch" + cybersikkershedsopdatering: "Cybersikkershedsopdatering" + +server_size_enum: + lille: "Lille" + mellem: "Mellem" + stor: "Stor" + custom: "Custom" accounts: id: "#" create_new: "Opret ny konto" @@ -901,6 +907,7 @@ service_agreement: action_edit: "Rediger driftsaftale" has_cybersecurity_agreement_true: "✅" has_cybersecurity_agreement_false: "❌" + edit: "Rediger driftsaftale" new: "Ny driftsaftale" action_create: "Gem" action_delete: "Slet driftsaftale" @@ -909,6 +916,19 @@ service_agreement: no: "Nej" no_records_found: "Ingen driftsaftaler" system_owner_notice: "Systemejer skal inddrages ved ændringer" + system_owner_notices: "Systemejer notifikation" + operations_price: "Driftsaftale Pris" + is_eol: "EoL Aftale" + leantime_url: "Leantime Projekt" + client_contact_name: "Kontakt hos kunden (Navn)" + client_contact_email: "Kontakt hos kunden (Email)" + dedicated_server: "Dedikeret Server" + server_size: "Serverstørrelse" + cybersecurity_price: "Cybersikkerhedsaftale Pris" + git_repos: "GIT Repos" + created_at: "Oprettet" + updated_at: "Opdateret" + valid_to_required_when_eol: "Gyldig til er påkrævet når EoL er aktiveret" hours_per_quarter: "timer/kvartal" search: "Søg" cyber_security_agreement: From 734d76d81a0728aee6b3d1eaab73afd0c954c800 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 10 Apr 2026 11:25:32 +0200 Subject: [PATCH 06/11] Add checkbox styling and inline notification checkboxes --- assets/styles/app.css | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/assets/styles/app.css b/assets/styles/app.css index 0275079a..3df47c76 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -324,7 +324,15 @@ } .toggle-label { - @apply relative inline-flex items-center cursor-pointer; + @apply relative inline-flex items-center cursor-pointer select-none; + } + + .form-row.select-none { + @apply flex flex-row-reverse justify-end items-center gap-1; + } + + .form-row.select-none .label { + @apply mb-0; } .selections .choices__list .choices__input { @@ -469,4 +477,12 @@ .service_agreement_filter > div.form-default { grid-template-columns: repeat(8, minmax(0, 1fr)); } + + .checkbox-inline-group { + @apply flex items-center; + } + + .checkbox-inline-group label { + @apply mr-4 ml-1 text-sm font-medium text-gray-900 dark:text-white select-none cursor-pointer; + } } From 3599f699eef4786b848f291e65742c5a8084b1af Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 10 Apr 2026 11:32:14 +0200 Subject: [PATCH 07/11] Updated changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index de47f022..412ea41f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* [PR-280](https://github.com/itk-dev/economics/pull/280) + Extended service agreement overview with new and updated fields. + Tweaked form styling. * [PR-276](https://github.com/itk-dev/economics/pull/276) Changed to use supercronic instead of woodpecker for crontabs. * [PR-277](https://github.com/itk-dev/economics/pull/277) From 5802a01f3f49ea670aeb1fbbb9b3ee8b3088cff0 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 10 Apr 2026 12:28:45 +0200 Subject: [PATCH 08/11] Removed manually created createdAt/updatedAt and extended AbstractBaseEntity for the TimestampableEntity. --- src/Entity/ServiceAgreement.php | 42 +-------------------------------- 1 file changed, 1 insertion(+), 41 deletions(-) diff --git a/src/Entity/ServiceAgreement.php b/src/Entity/ServiceAgreement.php index 9aeac4f4..eb5a6d27 100644 --- a/src/Entity/ServiceAgreement.php +++ b/src/Entity/ServiceAgreement.php @@ -12,14 +12,8 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; #[ORM\Entity(repositoryClass: ServiceAgreementRepository::class)] -#[ORM\HasLifecycleCallbacks] -class ServiceAgreement +class ServiceAgreement extends AbstractBaseEntity { - #[ORM\Id] - #[ORM\GeneratedValue] - #[ORM\Column] - private ?int $id = null; - #[ORM\ManyToOne(targetEntity: Project::class)] #[ORM\JoinColumn(nullable: false)] private ?Project $project = null; @@ -81,17 +75,6 @@ class ServiceAgreement #[ORM\Column(type: Types::TEXT, nullable: true)] private ?string $gitRepos = null; - #[ORM\Column(type: Types::DATETIME_MUTABLE)] - private ?\DateTimeInterface $createdAt = null; - - #[ORM\Column(type: Types::DATETIME_MUTABLE)] - private ?\DateTimeInterface $updatedAt = null; - - public function getId(): ?int - { - return $this->id; - } - public function getProject(): ?Project { return $this->project; @@ -332,29 +315,6 @@ public function setGitRepos(?string $gitRepos): static return $this; } - public function getCreatedAt(): ?\DateTimeInterface - { - return $this->createdAt; - } - - public function getUpdatedAt(): ?\DateTimeInterface - { - return $this->updatedAt; - } - - #[ORM\PrePersist] - public function setCreatedAtValue(): void - { - $this->createdAt = new \DateTime(); - $this->updatedAt = new \DateTime(); - } - - #[ORM\PreUpdate] - public function setUpdatedAtValue(): void - { - $this->updatedAt = new \DateTime(); - } - #[Assert\Callback] public function validateValidTo(ExecutionContextInterface $context): void { From 219e95b8160de6850ed3e0fe665e6b604fd92438 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 10 Apr 2026 12:29:00 +0200 Subject: [PATCH 09/11] Created new migration --- migrations/Version20260410102558.php | 31 ++++++++++++++ migrations/Version20260410120000.php | 61 ---------------------------- 2 files changed, 31 insertions(+), 61 deletions(-) create mode 100644 migrations/Version20260410102558.php delete mode 100644 migrations/Version20260410120000.php diff --git a/migrations/Version20260410102558.php b/migrations/Version20260410102558.php new file mode 100644 index 00000000..06665fa7 --- /dev/null +++ b/migrations/Version20260410102558.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE service_agreement ADD created_by VARCHAR(255) DEFAULT NULL, ADD updated_by VARCHAR(255) DEFAULT NULL, CHANGE system_owner_notices system_owner_notices JSON NOT NULL COMMENT \'(DC2Type:json)\', CHANGE is_eol is_eol TINYINT(1) NOT NULL, CHANGE dedicated_server dedicated_server TINYINT(1) NOT NULL, CHANGE created_at created_at DATETIME NOT NULL, CHANGE updated_at updated_at DATETIME NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE service_agreement DROP created_by, DROP updated_by, CHANGE system_owner_notices system_owner_notices JSON DEFAULT \'[]\' NOT NULL COMMENT \'(DC2Type:json)\', CHANGE is_eol is_eol TINYINT(1) DEFAULT 0 NOT NULL, CHANGE dedicated_server dedicated_server TINYINT(1) DEFAULT 0 NOT NULL, CHANGE created_at created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CHANGE updated_at updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL'); + } +} diff --git a/migrations/Version20260410120000.php b/migrations/Version20260410120000.php deleted file mode 100644 index fd5f04d4..00000000 --- a/migrations/Version20260410120000.php +++ /dev/null @@ -1,61 +0,0 @@ -addSql('ALTER TABLE service_agreement DROP COLUMN system_owner_notice'); - $this->addSql('ALTER TABLE service_agreement ADD system_owner_notices JSON NOT NULL DEFAULT \'[]\''); - $this->addSql('UPDATE service_agreement SET system_owner_notices = \'[]\' WHERE system_owner_notices IS NULL'); - - // Make valid_to nullable - $this->addSql('ALTER TABLE service_agreement CHANGE valid_to valid_to DATETIME DEFAULT NULL'); - - // Add new fields - $this->addSql('ALTER TABLE service_agreement ADD is_eol TINYINT(1) NOT NULL DEFAULT 0'); - $this->addSql('ALTER TABLE service_agreement ADD leantime_url VARCHAR(255) DEFAULT NULL'); - $this->addSql('ALTER TABLE service_agreement ADD client_contact_name VARCHAR(255) DEFAULT NULL'); - $this->addSql('ALTER TABLE service_agreement ADD client_contact_email VARCHAR(255) DEFAULT NULL'); - $this->addSql('ALTER TABLE service_agreement ADD dedicated_server TINYINT(1) NOT NULL DEFAULT 0'); - $this->addSql('ALTER TABLE service_agreement ADD server_size VARCHAR(255) DEFAULT NULL'); - $this->addSql('ALTER TABLE service_agreement ADD cybersecurity_price DOUBLE PRECISION DEFAULT NULL'); - $this->addSql('ALTER TABLE service_agreement ADD git_repos LONGTEXT DEFAULT NULL'); - $this->addSql('ALTER TABLE service_agreement ADD created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP'); - $this->addSql('ALTER TABLE service_agreement ADD updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP'); - } - - public function down(Schema $schema): void - { - // Remove new fields - $this->addSql('ALTER TABLE service_agreement DROP COLUMN is_eol'); - $this->addSql('ALTER TABLE service_agreement DROP COLUMN leantime_url'); - $this->addSql('ALTER TABLE service_agreement DROP COLUMN client_contact_name'); - $this->addSql('ALTER TABLE service_agreement DROP COLUMN client_contact_email'); - $this->addSql('ALTER TABLE service_agreement DROP COLUMN dedicated_server'); - $this->addSql('ALTER TABLE service_agreement DROP COLUMN server_size'); - $this->addSql('ALTER TABLE service_agreement DROP COLUMN cybersecurity_price'); - $this->addSql('ALTER TABLE service_agreement DROP COLUMN git_repos'); - $this->addSql('ALTER TABLE service_agreement DROP COLUMN created_at'); - $this->addSql('ALTER TABLE service_agreement DROP COLUMN updated_at'); - - // Revert valid_to to NOT NULL - $this->addSql('ALTER TABLE service_agreement CHANGE valid_to valid_to DATETIME NOT NULL'); - - // Revert system_owner_notices JSON back to system_owner_notice VARCHAR - $this->addSql('ALTER TABLE service_agreement DROP COLUMN system_owner_notices'); - $this->addSql('ALTER TABLE service_agreement ADD system_owner_notice VARCHAR(255) NOT NULL DEFAULT \'never\''); - } -} From 7f3e5c22fb8e436c498faa095dd117ccfd71f4d0 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 10 Apr 2026 12:42:26 +0200 Subject: [PATCH 10/11] Redid migrations to correctly remove system_owner_notice --- migrations/Version20260410102558.php | 31 ---------------------------- migrations/Version20260410104113.php | 31 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 31 deletions(-) delete mode 100644 migrations/Version20260410102558.php create mode 100644 migrations/Version20260410104113.php diff --git a/migrations/Version20260410102558.php b/migrations/Version20260410102558.php deleted file mode 100644 index 06665fa7..00000000 --- a/migrations/Version20260410102558.php +++ /dev/null @@ -1,31 +0,0 @@ -addSql('ALTER TABLE service_agreement ADD created_by VARCHAR(255) DEFAULT NULL, ADD updated_by VARCHAR(255) DEFAULT NULL, CHANGE system_owner_notices system_owner_notices JSON NOT NULL COMMENT \'(DC2Type:json)\', CHANGE is_eol is_eol TINYINT(1) NOT NULL, CHANGE dedicated_server dedicated_server TINYINT(1) NOT NULL, CHANGE created_at created_at DATETIME NOT NULL, CHANGE updated_at updated_at DATETIME NOT NULL'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE service_agreement DROP created_by, DROP updated_by, CHANGE system_owner_notices system_owner_notices JSON DEFAULT \'[]\' NOT NULL COMMENT \'(DC2Type:json)\', CHANGE is_eol is_eol TINYINT(1) DEFAULT 0 NOT NULL, CHANGE dedicated_server dedicated_server TINYINT(1) DEFAULT 0 NOT NULL, CHANGE created_at created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CHANGE updated_at updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL'); - } -} diff --git a/migrations/Version20260410104113.php b/migrations/Version20260410104113.php new file mode 100644 index 00000000..20b0c919 --- /dev/null +++ b/migrations/Version20260410104113.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE service_agreement ADD system_owner_notices JSON NOT NULL COMMENT \'(DC2Type:json)\', ADD is_eol TINYINT(1) NOT NULL, ADD leantime_url VARCHAR(255) DEFAULT NULL, ADD client_contact_name VARCHAR(255) DEFAULT NULL, ADD client_contact_email VARCHAR(255) DEFAULT NULL, ADD dedicated_server TINYINT(1) NOT NULL, ADD server_size VARCHAR(255) DEFAULT NULL, ADD cybersecurity_price DOUBLE PRECISION DEFAULT NULL, ADD git_repos LONGTEXT DEFAULT NULL, ADD created_by VARCHAR(255) DEFAULT NULL, ADD updated_by VARCHAR(255) DEFAULT NULL, ADD created_at DATETIME NOT NULL, ADD updated_at DATETIME NOT NULL, DROP system_owner_notice, CHANGE valid_to valid_to DATETIME DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE service_agreement ADD system_owner_notice VARCHAR(255) NOT NULL, DROP system_owner_notices, DROP is_eol, DROP leantime_url, DROP client_contact_name, DROP client_contact_email, DROP dedicated_server, DROP server_size, DROP cybersecurity_price, DROP git_repos, DROP created_by, DROP updated_by, DROP created_at, DROP updated_at, CHANGE valid_to valid_to DATETIME NOT NULL'); + } +} From f16aaf1afd482bb5245e22784f5a8ea76c0e86d2 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Fri, 10 Apr 2026 12:42:44 +0200 Subject: [PATCH 11/11] Removed translation for system_owner_notice --- translations/messages.da.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index 59f7520f..24d863f0 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -915,7 +915,6 @@ service_agreement: yes: "Ja" no: "Nej" no_records_found: "Ingen driftsaftaler" - system_owner_notice: "Systemejer skal inddrages ved ændringer" system_owner_notices: "Systemejer notifikation" operations_price: "Driftsaftale Pris" is_eol: "EoL Aftale"