diff --git a/CHANGELOG.md b/CHANGELOG.md index e4f40848..f7367d55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* Added commands to manage data providers. * Changed how errors are handled in Leantime api calls. * Modified getSprintReportData to work with Leantime data * Added project lead to client when syncing projects. @@ -30,6 +31,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Refactored error handling. * Added support for multiple data providers * Removed project creator for Jira. +* Added client view. +* Added account view. +* Added leantime support for projects and project sync. * RELEASE NOTES: diff --git a/migrations/Version20240118132209.php b/migrations/Version20240118132209.php new file mode 100644 index 00000000..6ff93bde --- /dev/null +++ b/migrations/Version20240118132209.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE data_provider ADD enabled TINYINT(1) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE data_provider DROP enabled'); + } +} diff --git a/src/Command/DataProviderCreateCommand.php b/src/Command/DataProviderCreateCommand.php index 70966f6f..57614e00 100644 --- a/src/Command/DataProviderCreateCommand.php +++ b/src/Command/DataProviderCreateCommand.php @@ -11,8 +11,8 @@ use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand( - name: 'app:project-tracker:create', - description: 'Create a new Project Tracker', + name: 'app:data-provider:create', + description: 'Create a new Data Provider', )] class DataProviderCreateCommand extends Command { @@ -37,7 +37,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $question->setAutocompleterValues(DataProviderService::IMPLEMENTATIONS); $class = $io->askQuestion($question); - $dataProvider = $this->dataProviderService->createDataProvider($name, $class, $url, $secret, false, false); + $dataProvider = $this->dataProviderService->createDataProvider($name, $class, $url, $secret); + $dataProvider->setCreatedBy('CLI'); + $dataProvider->setUpdatedBy('CLI'); $text = "Created the following data provider\n\n"; $text .= 'ID: '.$dataProvider->getId()."\n"; diff --git a/src/Command/DataProviderListCommand.php b/src/Command/DataProviderListCommand.php new file mode 100644 index 00000000..8420f777 --- /dev/null +++ b/src/Command/DataProviderListCommand.php @@ -0,0 +1,48 @@ +getName()); + } + + protected function configure(): void + { + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $dataProviders = $this->dataProviderRepository->findAll(); + + /* @var DataProvider $provider */ + $io->table(['id', 'name', 'enabled', 'class', 'url', 'sync clients', 'sync accounts'], array_map(fn ($provider) => [ + $provider->getId(), + $provider->getName(), + $provider->isEnabled() ? 'true' : 'false', + $provider->getClass(), + $provider->getUrl(), + $provider->isEnableClientSync(), + $provider->isEnableAccountSync(), + ], $dataProviders)); + + return Command::SUCCESS; + } +} diff --git a/src/Command/DataProviderSetEnableCommand.php b/src/Command/DataProviderSetEnableCommand.php new file mode 100644 index 00000000..76b50220 --- /dev/null +++ b/src/Command/DataProviderSetEnableCommand.php @@ -0,0 +1,54 @@ +getName()); + } + + protected function configure(): void + { + $this->addArgument('id', InputArgument::REQUIRED, 'data provider id'); + $this->addArgument('enable', InputArgument::REQUIRED, 'data provider enable'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $id = (int) $input->getArgument('id'); + $enable = $input->getArgument('enable'); + + $dataProvider = $this->dataProviderRepository->find($id); + + if (null != $dataProvider) { + $dataProvider->setEnabled('true' == $enable); + $dataProvider->setUpdatedBy('CLI'); + $this->dataProviderRepository->save($dataProvider, true); + + $io->info('Data provider with id: '.$dataProvider->getId().' '.($dataProvider->isEnabled() ? 'enabled' : 'disabled')); + + return Command::SUCCESS; + } else { + $io->error('Data provider not found'); + + return Command::FAILURE; + } + } +} diff --git a/src/Command/SyncAccountsCommand.php b/src/Command/SyncAccountsCommand.php index aa3f2440..7eb1cf67 100644 --- a/src/Command/SyncAccountsCommand.php +++ b/src/Command/SyncAccountsCommand.php @@ -35,7 +35,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - $dataProviders = $this->dataProviderRepository->findAll(); + $dataProviders = $this->dataProviderRepository->findBy(['enabled' => true]); foreach ($dataProviders as $dataProvider) { $io->info('Processing accounts in '.$dataProvider->getName()); diff --git a/src/Command/SyncCommand.php b/src/Command/SyncCommand.php index 54d362b3..ad66c60e 100644 --- a/src/Command/SyncCommand.php +++ b/src/Command/SyncCommand.php @@ -41,7 +41,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->info('Processing projects'); - $dataProviders = $this->dataProviderRepository->findAll(); + $dataProviders = $this->dataProviderRepository->findBy(['enabled' => true]); foreach ($dataProviders as $dataProvider) { $this->dataSynchronizationService->syncProjects(function ($i, $length) use ($io) { diff --git a/src/Command/SyncIssuesCommand.php b/src/Command/SyncIssuesCommand.php index 3310e0c2..a82e6bde 100644 --- a/src/Command/SyncIssuesCommand.php +++ b/src/Command/SyncIssuesCommand.php @@ -39,7 +39,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - $dataProviders = $this->dataProviderRepository->findAll(); + $dataProviders = $this->dataProviderRepository->findBy(['enabled' => true]); foreach ($dataProviders as $dataProvider) { $projects = $this->projectRepository->findBy(['include' => true, 'dataProvider' => $dataProvider]); diff --git a/src/Command/SyncProjectsCommand.php b/src/Command/SyncProjectsCommand.php index ea882c90..bf347d2c 100644 --- a/src/Command/SyncProjectsCommand.php +++ b/src/Command/SyncProjectsCommand.php @@ -35,7 +35,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - $dataProviders = $this->dataProviderRepository->findAll(); + $dataProviders = $this->dataProviderRepository->findBy(['enabled' => true]); foreach ($dataProviders as $dataProvider) { $this->dataSynchronizationService->syncProjects(function ($i, $length) use ($io) { diff --git a/src/Command/SyncWorklogsCommand.php b/src/Command/SyncWorklogsCommand.php index 5a51c288..4363b577 100644 --- a/src/Command/SyncWorklogsCommand.php +++ b/src/Command/SyncWorklogsCommand.php @@ -39,7 +39,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - $dataProviders = $this->dataProviderRepository->findAll(); + $dataProviders = $this->dataProviderRepository->findBy(['enabled' => true]); foreach ($dataProviders as $dataProvider) { $projects = $this->projectRepository->findBy(['include' => true, 'dataProvider' => $dataProvider]); diff --git a/src/Controller/AccountController.php b/src/Controller/AccountController.php new file mode 100644 index 00000000..5cfbe4b2 --- /dev/null +++ b/src/Controller/AccountController.php @@ -0,0 +1,82 @@ +render('account/index.html.twig', [ + 'accounts' => $accountRepository->findAll(), + ]); + } + + #[Route('/new', name: 'app_account_new', methods: ['GET', 'POST'])] + public function new(Request $request, EntityManagerInterface $entityManager): Response + { + $account = new Account(); + $form = $this->createForm(AccountType::class, $account); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->persist($account); + $entityManager->flush(); + + return $this->redirectToRoute('app_account_index', [], Response::HTTP_SEE_OTHER); + } + + return $this->render('account/new.html.twig', [ + 'account' => $account, + 'form' => $form, + ]); + } + + #[Route('/{id}', name: 'app_account_show', methods: ['GET'])] + public function show(Account $account): Response + { + return $this->render('account/show.html.twig', [ + 'account' => $account, + ]); + } + + #[Route('/{id}/edit', name: 'app_account_edit', methods: ['GET', 'POST'])] + public function edit(Request $request, Account $account, EntityManagerInterface $entityManager): Response + { + $form = $this->createForm(AccountType::class, $account); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->flush(); + + return $this->redirectToRoute('app_account_index', [], Response::HTTP_SEE_OTHER); + } + + return $this->render('account/edit.html.twig', [ + 'account' => $account, + 'form' => $form, + ]); + } + + #[Route('/{id}', name: 'app_account_delete', methods: ['POST'])] + public function delete(Request $request, Account $account, EntityManagerInterface $entityManager): Response + { + $token = $request->request->get('_token'); + if (is_string($token) && $this->isCsrfTokenValid('delete'.$account->getId(), $token)) { + $entityManager->remove($account); + $entityManager->flush(); + } + + return $this->redirectToRoute('app_account_index', [], Response::HTTP_SEE_OTHER); + } +} diff --git a/src/Controller/ClientController.php b/src/Controller/ClientController.php new file mode 100644 index 00000000..c556a7b0 --- /dev/null +++ b/src/Controller/ClientController.php @@ -0,0 +1,82 @@ +render('client/index.html.twig', [ + 'clients' => $clientRepository->findAll(), + ]); + } + + #[Route('/new', name: 'app_client_new', methods: ['GET', 'POST'])] + public function new(Request $request, EntityManagerInterface $entityManager): Response + { + $client = new Client(); + $form = $this->createForm(ClientType::class, $client); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->persist($client); + $entityManager->flush(); + + return $this->redirectToRoute('app_client_index', [], Response::HTTP_SEE_OTHER); + } + + return $this->render('client/new.html.twig', [ + 'client' => $client, + 'form' => $form, + ]); + } + + #[Route('/{id}', name: 'app_client_show', methods: ['GET'])] + public function show(Client $client): Response + { + return $this->render('client/show.html.twig', [ + 'client' => $client, + ]); + } + + #[Route('/{id}/edit', name: 'app_client_edit', methods: ['GET', 'POST'])] + public function edit(Request $request, Client $client, EntityManagerInterface $entityManager): Response + { + $form = $this->createForm(ClientType::class, $client); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $entityManager->flush(); + + return $this->redirectToRoute('app_client_index', [], Response::HTTP_SEE_OTHER); + } + + return $this->render('client/edit.html.twig', [ + 'client' => $client, + 'form' => $form, + ]); + } + + #[Route('/{id}', name: 'app_client_delete', methods: ['POST'])] + public function delete(Request $request, Client $client, EntityManagerInterface $entityManager): Response + { + $token = $request->request->get('_token'); + if (is_string($token) && $this->isCsrfTokenValid('delete'.$client->getId(), $token)) { + $entityManager->remove($client); + $entityManager->flush(); + } + + return $this->redirectToRoute('app_client_index', [], Response::HTTP_SEE_OTHER); + } +} diff --git a/src/Controller/SprintReportController.php b/src/Controller/SprintReportController.php index d3d1938b..cb1d093e 100644 --- a/src/Controller/SprintReportController.php +++ b/src/Controller/SprintReportController.php @@ -98,11 +98,11 @@ public function index(Request $request): Response ]); if (!empty($requestData['projectId'])) { - $versionCollection = $service->getSprintReportProjectVersions($requestData['projectId']); + $versionCollection = $service->getSprintReportVersions($requestData['projectId']); $versionChoices = []; foreach ($versionCollection->versions as $version) { - $versionChoices[$version->headline] = $version->id; + $versionChoices[$version->name] = $version->id; } // Override versionId with element with choices. diff --git a/src/Entity/DataProvider.php b/src/Entity/DataProvider.php index e47bd0d0..43e7190b 100644 --- a/src/Entity/DataProvider.php +++ b/src/Entity/DataProvider.php @@ -26,6 +26,9 @@ class DataProvider extends AbstractBaseEntity #[ORM\Column(nullable: true)] private ?bool $enableAccountSync = null; + #[ORM\Column(nullable: true)] + private ?bool $enabled = null; + public function getName(): ?string { return $this->name; @@ -102,4 +105,16 @@ public function __toString(): string { return $this->getName() ?? ''.$this->getId(); } + + public function isEnabled(): ?bool + { + return $this->enabled; + } + + public function setEnabled(?bool $enabled): static + { + $this->enabled = $enabled; + + return $this; + } } diff --git a/src/Form/AccountType.php b/src/Form/AccountType.php index 4db65f6c..629719d3 100644 --- a/src/Form/AccountType.php +++ b/src/Form/AccountType.php @@ -4,6 +4,7 @@ use App\Entity\Account; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -12,23 +13,42 @@ class AccountType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { $builder - ->add('name', null, [ - 'required' => false, - 'label' => 'accounts.name', + ->add('name', TextType::class, [ + 'label' => 'create_account_form.account_name_label', 'label_attr' => ['class' => 'label'], - 'row_attr' => ['class' => 'form-row'], 'attr' => ['class' => 'form-element'], - 'help' => 'accounts.name_helptext', + 'help_attr' => ['class' => 'form-help'], + 'help' => 'create_account_form.account_name_help', + 'required' => true, + 'row_attr' => ['class' => 'form-row'], ]) - ->add('value', null, [ - 'required' => false, - 'label' => 'accounts.value', + ->add('value', TextType::class, [ + 'label' => 'create_account_form.account_value_label', 'label_attr' => ['class' => 'label'], + 'attr' => ['class' => 'form-element'], + 'help_attr' => ['class' => 'form-help'], + 'help' => 'create_account_form.account_value_help', + 'required' => true, 'row_attr' => ['class' => 'form-row'], + ]) + ->add('status', TextType::class, [ + 'label' => 'create_account_form.account_status_label', + 'label_attr' => ['class' => 'label'], 'attr' => ['class' => 'form-element'], - 'help' => 'accounts.value_helptext', + 'help_attr' => ['class' => 'form-help'], + 'help' => 'create_account_form.account_status_help', + 'required' => true, + 'row_attr' => ['class' => 'form-row'], ]) - ; + ->add('category', TextType::class, [ + 'label' => 'create_account_form.account_category_label', + 'label_attr' => ['class' => 'label'], + 'attr' => ['class' => 'form-element'], + 'help_attr' => ['class' => 'form-help'], + 'help' => 'create_account_form.account_category_help', + 'required' => true, + 'row_attr' => ['class' => 'form-row'], + ]); } public function configureOptions(OptionsResolver $resolver): void diff --git a/src/Form/ClientType.php b/src/Form/ClientType.php new file mode 100644 index 00000000..0597105d --- /dev/null +++ b/src/Form/ClientType.php @@ -0,0 +1,119 @@ +add('name', TextType::class, [ + 'label' => 'create_client_form.client_name.label', + 'label_attr' => ['class' => 'label'], + 'attr' => ['class' => 'form-element'], + 'help_attr' => ['class' => 'form-help'], + 'help' => 'create_client_form.client_name.help', + 'required' => true, + 'row_attr' => ['class' => 'form-row'], + ]) + ->add('contact', TextType::class, [ + 'label' => 'create_client_form.client_contact.label', + 'label_attr' => ['class' => 'label'], + 'attr' => ['class' => 'form-element'], + 'help_attr' => ['class' => 'form-help'], + 'help' => 'create_client_form.client_contact.help', + 'required' => true, + 'row_attr' => ['class' => 'form-row'], + ]) + ->add('standardPrice', NumberType::class, [ + 'label' => 'create_client_form.client_standardPrice.label', + 'label_attr' => ['class' => 'label'], + 'attr' => ['class' => 'form-element', 'min' => 0], + 'help_attr' => ['class' => 'form-help'], + 'help' => 'create_client_form.client_standardPrice.help', + 'required' => true, + 'row_attr' => ['class' => 'form-row'], + 'html5' => true, + ]) + ->add('type', EnumType::class, [ + 'class' => ClientTypeEnum::class, + 'label' => 'create_client_form.type.label', + 'label_attr' => ['class' => 'label'], + 'choice_label' => fn ($choice) => match ($choice) { + ClientTypeEnum::INTERNAL => 'client_type_enum.internal', + ClientTypeEnum::EXTERNAL => 'client_type_enum.external', + default => null, + }, + 'attr' => ['class' => 'form-element'], + 'help_attr' => ['class' => 'form-help'], + 'help' => 'create_client_form.type.help', + 'required' => true, + 'row_attr' => ['class' => 'form-row'], + ]) + ->add('account', TextType::class, [ + 'label' => 'create_client_form.client_account.label', + 'label_attr' => ['class' => 'label'], + 'attr' => ['class' => 'form-element'], + 'help_attr' => ['class' => 'form-help'], + 'help' => 'create_client_form.client_account.help', + 'required' => true, + 'row_attr' => ['class' => 'form-row'], + ]) + ->add('salesChannel', ChoiceType::class, [ + 'label' => 'create_client_form.sales_channel.label', + 'label_attr' => ['class' => 'label'], + 'attr' => ['class' => 'form-element'], + 'help_attr' => ['class' => 'form-help'], + 'help' => 'create_client_form.sales_channel.help', + 'required' => true, + 'row_attr' => ['class' => 'form-row'], + 'choices' => [ + 10 => 10, + 20 => 20, + ], + ]) + ->add('customerKey', TextType::class, [ + 'label' => 'create_client_form.customer_key.label', + 'label_attr' => ['class' => 'label'], + 'attr' => ['class' => 'form-element'], + 'help_attr' => ['class' => 'form-help'], + 'help' => 'create_client_form.customer_key.help', + 'row_attr' => ['class' => 'form-row'], + ]) + ->add('psp', TextType::class, [ + 'label' => 'create_client_form.client_psp.label', + 'label_attr' => ['class' => 'label'], + 'attr' => ['class' => 'form-element'], + 'help_attr' => ['class' => 'form-help'], + 'help' => 'create_client_form.client_psp.help', + 'row_attr' => ['class' => 'form-row'], + 'required' => false, + ]) + ->add('ean', TextType::class, [ + 'label' => 'create_client_form.client_ean.label', + 'label_attr' => ['class' => 'label'], + 'attr' => ['class' => 'form-element'], + 'help_attr' => ['class' => 'form-help'], + 'help' => 'create_client_form.client_ean.help', + 'row_attr' => ['class' => 'form-row'], + 'required' => false, + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Client::class, + ]); + } +} diff --git a/src/Interface/DataProviderServiceInterface.php b/src/Interface/DataProviderServiceInterface.php index d82da75d..5d774a0c 100644 --- a/src/Interface/DataProviderServiceInterface.php +++ b/src/Interface/DataProviderServiceInterface.php @@ -5,8 +5,8 @@ use App\Model\Invoices\AccountData; use App\Model\Invoices\ClientData; use App\Model\Invoices\PagedResult; -use App\Model\Invoices\ProjectData; -use App\Model\Invoices\WorklogData; +use App\Model\Invoices\ProjectDataCollection; +use App\Model\Invoices\WorklogDataCollection; use App\Model\Planning\PlanningData; use App\Model\SprintReport\SprintReportData; use App\Model\SprintReport\SprintReportProjects; @@ -16,11 +16,6 @@ interface DataProviderServiceInterface { public function getPlanningData(): PlanningData; - /** - * @return array - */ - public function getAllProjectData(): array; - /** * @return array */ @@ -33,12 +28,13 @@ public function getAllAccountData(): array; public function getIssuesDataForProjectPaged(string $projectId, int $startAt = 0, $maxResults = 50): PagedResult; - /** @return array */ - public function getWorklogDataForProject(string $projectId): array; - public function getSprintReportData(string $projectId, string $versionId): SprintReportData; public function getSprintReportProjects(): SprintReportProjects; - public function getSprintReportProjectVersions(string $projectId): SprintReportVersions; + public function getSprintReportVersions(string $projectId): SprintReportVersions; + + public function getProjectDataCollection(): ProjectDataCollection; + + public function getWorklogDataCollection(string $projectId): WorklogDataCollection; } diff --git a/src/Model/Invoices/IssueData.php b/src/Model/Invoices/IssueData.php index a2463454..74ad44be 100644 --- a/src/Model/Invoices/IssueData.php +++ b/src/Model/Invoices/IssueData.php @@ -17,7 +17,7 @@ class IssueData public ?string $epicName = null; public ?string $epicKey = null; /** @var Collection */ - public Collection $versions; + public ?Collection $versions; public ?\DateTime $resolutionDate = null; public string $projectId; diff --git a/src/Model/Invoices/IssueDataCollection.php b/src/Model/Invoices/IssueDataCollection.php new file mode 100644 index 00000000..9520c145 --- /dev/null +++ b/src/Model/Invoices/IssueDataCollection.php @@ -0,0 +1,17 @@ + */ + public Collection $issueData; + + public function __construct() + { + $this->issueData = new ArrayCollection(); + } +} diff --git a/src/Model/Invoices/ProjectDataCollection.php b/src/Model/Invoices/ProjectDataCollection.php new file mode 100644 index 00000000..449ae11d --- /dev/null +++ b/src/Model/Invoices/ProjectDataCollection.php @@ -0,0 +1,17 @@ + */ + public Collection $projectData; + + public function __construct() + { + $this->projectData = new ArrayCollection(); + } +} diff --git a/src/Model/Invoices/WorklogDataCollection.php b/src/Model/Invoices/WorklogDataCollection.php new file mode 100644 index 00000000..fe536300 --- /dev/null +++ b/src/Model/Invoices/WorklogDataCollection.php @@ -0,0 +1,17 @@ + */ + public Collection $worklogData; + + public function __construct() + { + $this->worklogData = new ArrayCollection(); + } +} diff --git a/src/Model/SprintReport/SprintReportProject.php b/src/Model/SprintReport/SprintReportProject.php index e48be0d7..79d51a96 100644 --- a/src/Model/SprintReport/SprintReportProject.php +++ b/src/Model/SprintReport/SprintReportProject.php @@ -6,10 +6,4 @@ class SprintReportProject { public string $id; public string $name; - - public function __construct(string $id, string $name) - { - $this->id = $id; - $this->name = $name; - } } diff --git a/src/Model/SprintReport/SprintReportVersion.php b/src/Model/SprintReport/SprintReportVersion.php index ecd8a0ef..5b365a6b 100644 --- a/src/Model/SprintReport/SprintReportVersion.php +++ b/src/Model/SprintReport/SprintReportVersion.php @@ -5,11 +5,6 @@ class SprintReportVersion { public string $id; - public string $headline; - - public function __construct(string $id, string $headline) - { - $this->id = $id; - $this->headline = $headline; - } + public string $name; + public string $projectTrackerId; } diff --git a/src/Repository/DataProviderRepository.php b/src/Repository/DataProviderRepository.php index c3ac3a57..9fdca1ea 100644 --- a/src/Repository/DataProviderRepository.php +++ b/src/Repository/DataProviderRepository.php @@ -20,4 +20,22 @@ public function __construct(ManagerRegistry $registry) { parent::__construct($registry, DataProvider::class); } + + public function save(DataProvider $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(DataProvider $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } } diff --git a/src/Service/DataProviderService.php b/src/Service/DataProviderService.php index f2d173be..01a53691 100644 --- a/src/Service/DataProviderService.php +++ b/src/Service/DataProviderService.php @@ -89,6 +89,7 @@ public function createDataProvider(string $name, string $class, string $url, str $dataProvider->setUrl($url); $dataProvider->setSecret($secret); $dataProvider->setClass($class); + $dataProvider->setEnabled(true); $dataProvider->setEnableClientSync($enableClientSync); $dataProvider->setEnableAccountSync($enableAccountSync); diff --git a/src/Service/DataSynchronizationService.php b/src/Service/DataSynchronizationService.php index bb82ebf5..65e2e944 100644 --- a/src/Service/DataSynchronizationService.php +++ b/src/Service/DataSynchronizationService.php @@ -55,9 +55,8 @@ public function syncProjects(callable $progressCallback, DataProvider $dataProvi $service = $this->dataProviderService->getService($dataProvider); // Get all projects from ApiService. - $allProjectData = $service->getAllProjectData(); - - foreach ($allProjectData as $index => $projectDatum) { + $allProjectData = $service->getProjectDataCollection(); + foreach ($allProjectData->projectData as $index => $projectDatum) { $project = $this->projectRepository->findOneBy(['projectTrackerId' => $projectDatum->projectTrackerId, 'dataProvider' => $dataProvider]); $dataProvider = $this->dataProviderRepository->find($dataProviderId); @@ -71,19 +70,20 @@ public function syncProjects(callable $progressCallback, DataProvider $dataProvi $project->setProjectTrackerId($projectDatum->projectTrackerId); $project->setProjectTrackerKey($projectDatum->projectTrackerKey); $project->setProjectTrackerProjectUrl($projectDatum->projectTrackerProjectUrl); - foreach ($projectDatum->versions as $versionData) { - $version = $this->versionRepository->findOneBy(['projectTrackerId' => $versionData->projectTrackerId, 'dataProvider' => $dataProvider]); + foreach ($versionData as $versionDatum) { + $version = $this->versionRepository->findOneBy(['projectTrackerId' => $versionDatum->projectTrackerId, 'dataProvider' => $dataProvider]); - if (!$version) { - $version = new Version(); - $version->setDataProvider($dataProvider); - $this->entityManager->persist($version); - } + if (!$version) { + $version = new Version(); + $version->setDataProvider($dataProvider); + $this->entityManager->persist($version); + } - $version->setName($versionData->name); - $version->setProjectTrackerId($versionData->projectTrackerId); - $version->setProject($project); + $version->setName($versionDatum->name); + $version->setProjectTrackerId($versionDatum->projectTrackerId); + $version->setProject($project); + } } // Only synchronize clients if this is enabled. @@ -124,7 +124,7 @@ public function syncProjects(callable $progressCallback, DataProvider $dataProvi $this->entityManager->clear(); } - $progressCallback($index, count($allProjectData)); + $progressCallback($index, count($allProjectData->projectData)); } $this->entityManager->flush(); @@ -204,11 +204,9 @@ public function syncIssuesForProject(int $projectId, callable $progressCallback $issuesProcessed = 0; $startAt = 0; - do { $dataProvider = $this->dataProviderRepository->find($dataProviderId); $project = $this->projectRepository->find($projectId); - if (!$project) { throw new EconomicsException($this->translator->trans('exception.project_not_found')); } @@ -216,9 +214,7 @@ public function syncIssuesForProject(int $projectId, callable $progressCallback $pagedIssueData = $service->getIssuesDataForProjectPaged($projectTrackerId, $startAt, self::MAX_RESULTS); $total = $pagedIssueData->total; - $issueData = $pagedIssueData->items; - - foreach ($issueData as $issueDatum) { + foreach ($pagedIssueData->items as $issueDatum) { $issue = $this->issueRepository->findOneBy(['projectTrackerId' => $issueDatum->projectTrackerId]); if (!$issue) { @@ -227,7 +223,6 @@ public function syncIssuesForProject(int $projectId, callable $progressCallback $this->entityManager->persist($issue); } - $issue->setName($issueDatum->name); $issue->setAccountId($issueDatum->accountId); $issue->setAccountKey($issueDatum->accountKey); @@ -287,10 +282,9 @@ public function syncWorklogsForProject(int $projectId, callable $progressCallbac throw new EconomicsException($this->translator->trans('exception.project_tracker_id_not_set')); } - $worklogData = $service->getWorklogDataForProject($projectTrackerId); + $worklogData = $service->getWorklogDataCollection($projectTrackerId); $worklogsAdded = 0; - - foreach ($worklogData as $worklogDatum) { + foreach ($worklogData->worklogData as $worklogDatum) { $project = $this->projectRepository->find($projectId); if (!$project) { @@ -298,7 +292,6 @@ public function syncWorklogsForProject(int $projectId, callable $progressCallbac } $worklog = $this->worklogRepository->findOneBy(['worklogId' => $worklogDatum->projectTrackerId]); - if (!$worklog) { $worklog = new Worklog(); @@ -308,7 +301,6 @@ public function syncWorklogsForProject(int $projectId, callable $progressCallbac $this->entityManager->persist($worklog); } - $worklog->setWorklogId($worklogDatum->projectTrackerId); $worklog->setDescription($worklogDatum->comment); $worklog->setWorker($worklogDatum->worker); @@ -329,7 +321,7 @@ public function syncWorklogsForProject(int $projectId, callable $progressCallbac $project->addWorklog($worklog); if (null !== $progressCallback) { - $progressCallback($worklogsAdded, count($worklogData)); + $progressCallback($worklogsAdded, count($worklogData->worklogData)); ++$worklogsAdded; } diff --git a/src/Service/JiraApiService.php b/src/Service/JiraApiService.php index e14da285..5e83019b 100644 --- a/src/Service/JiraApiService.php +++ b/src/Service/JiraApiService.php @@ -8,10 +8,13 @@ use App\Model\Invoices\AccountData; use App\Model\Invoices\ClientData; use App\Model\Invoices\IssueData; +use App\Model\Invoices\IssueDataCollection; use App\Model\Invoices\PagedResult; use App\Model\Invoices\ProjectData; +use App\Model\Invoices\ProjectDataCollection; use App\Model\Invoices\VersionData; use App\Model\Invoices\WorklogData; +use App\Model\Invoices\WorklogDataCollection; use App\Model\Planning\Assignee; use App\Model\Planning\AssigneeProject; use App\Model\Planning\Issue; @@ -83,30 +86,28 @@ public function getSprintReportProjects(): SprintReportProjects $projects = $this->getAllProjects(); foreach ($projects as $project) { - $sprintReportProjects->projects->add( - new SprintReportProject( - $project->id, - $project->name - ) - ); + $sprintReportProject = new SprintReportProject(); + $sprintReportProject->id = $project->id; + $sprintReportProject->name = $project->name; + $sprintReportProjects->projects->add($sprintReportProject); } return $sprintReportProjects; } - public function getSprintReportProjectVersions(string $projectId): SprintReportVersions + public function getSprintReportVersions(string $projectId): SprintReportVersions { $sprintReportVersions = new SprintReportVersions(); $project = $this->getProject($projectId); $projectVersions = $project->versions ?? []; foreach ($projectVersions as $projectVersion) { - $sprintReportVersions->versions->add( - new SprintReportVersion( - $projectVersion->id, - $projectVersion->name, - ) - ); + $sprintReportVersion = new SprintReportVersion(); + $sprintReportVersion->id = $projectVersion->id; + $sprintReportVersion->name = $projectVersion->name; + $sprintReportVersion->projectTrackerId = $projectVersion->projectId; + + $sprintReportVersions->versions->add($sprintReportVersion); } return $sprintReportVersions; @@ -771,30 +772,27 @@ public function getClientDataForProject(string $projectId): array return $clients; } - public function getAllProjectData(): array + public function getProjectDataCollection(): ProjectDataCollection { - $projects = []; - - $trackerProjects = $this->getAllProjects(); - - foreach ($trackerProjects as $trackerProject) { - $project = $this->getProject($trackerProject->id); - $projectVersions = $project->versions ?? []; + $projectDataCollection = new ProjectDataCollection(); + $projects = $this->getAllProjects(); + foreach ($projects as $project) { $projectData = new ProjectData(); - $projectData->name = $trackerProject->name; - $projectData->projectTrackerId = $trackerProject->id; - $projectData->projectTrackerKey = $trackerProject->key; - $projectData->projectTrackerProjectUrl = $trackerProject->self; + $projectData->name = $project->name; + $projectData->projectTrackerId = $project->id; + $projectData->projectTrackerKey = $project->key; + $projectData->projectTrackerProjectUrl = $project->self; + $projectVersions = $this->getSprintReportVersions($project->id); foreach ($projectVersions as $projectVersion) { - $projectData->versions->add(new VersionData($projectVersion->id, $projectVersion->name)); + $projectData->versions?->add($projectVersion); } - $projects[] = $projectData; + $projectDataCollection->projectData->add($projectData); } - return $projects; + return $projectDataCollection; } /** @@ -818,32 +816,35 @@ public function getProjectWorklogs($projectId, string $from = '2000-01-01', stri ]); } - public function getWorklogDataForProject(string $projectId): array + public function getWorklogDataCollection(string $projectId): WorklogDataCollection { - $worklogsResult = []; - + $worklogDataCollection = new WorklogDataCollection(); $worklogs = $this->getProjectWorklogs($projectId); foreach ($worklogs as $worklog) { $worklogData = new WorklogData(); $worklogData->projectTrackerId = $worklog->tempoWorklogId; - $worklogData->worker = $worklog->worker; - $worklogData->timeSpentSeconds = $worklog->timeSpentSeconds; $worklogData->comment = $worklog->comment; + $worklogData->worker = $worklog->worker; + $worklogData->timeSpentSeconds = (int) $worklog->timeSpentSeconds; $worklogData->started = new \DateTime($worklog->started); + $worklogData->projectTrackerIsBilled = false; $worklogData->projectTrackerIssueId = $worklog->issue->id; + $worklogDataCollection->worklogData->add($worklogData); + // TODO: Is this synchronization relevant? if (isset($worklog->attributes->_Billed_) && '_Billed_' == $worklog->attributes->_Billed_->key) { $worklogData->projectTrackerIsBilled = 'true' == $worklog->attributes->_Billed_->value; } - - $worklogsResult[] = $worklogData; } - return $worklogsResult; + return $worklogDataCollection; } + /** + * @throws ApiServiceException + */ public function getAllAccountData(): array { $accountsResult = []; @@ -927,7 +928,7 @@ public function getIssuesDataForProjectPaged(string $projectId, $startAt = 0, $m } foreach ($fields->fixVersions ?? [] as $fixVersion) { - $issueData->versions->add(new VersionData($fixVersion->id, $fixVersion->name)); + $issueData->versions?->add(new VersionData($fixVersion->id, $fixVersion->name)); } $result[] = $issueData; @@ -936,6 +937,15 @@ public function getIssuesDataForProjectPaged(string $projectId, $startAt = 0, $m return new PagedResult($result, $startAt, $maxResults, $pagedResult['total']); } + /** + * @throws ApiServiceException + * @throws \Exception + */ + public function getIssueDataCollection(string $projectId): IssueDataCollection + { + throw new ApiServiceException('Method not implemented', 501); + } + /** * @throws ApiServiceException */ diff --git a/src/Service/LeantimeApiService.php b/src/Service/LeantimeApiService.php index 1356dbf0..f3212dc0 100644 --- a/src/Service/LeantimeApiService.php +++ b/src/Service/LeantimeApiService.php @@ -5,7 +5,13 @@ use App\Exception\ApiServiceException; use App\Exception\EconomicsException; use App\Interface\DataProviderServiceInterface; +use App\Model\Invoices\IssueData; use App\Model\Invoices\PagedResult; +use App\Model\Invoices\ProjectData; +use App\Model\Invoices\ProjectDataCollection; +use App\Model\Invoices\VersionData; +use App\Model\Invoices\WorklogData; +use App\Model\Invoices\WorklogDataCollection; use App\Model\Planning\Assignee; use App\Model\Planning\AssigneeProject; use App\Model\Planning\Issue; @@ -23,6 +29,7 @@ use App\Model\SprintReport\SprintReportVersions; use App\Model\SprintReport\SprintStateEnum; use Doctrine\Common\Collections\ArrayCollection; +use Symfony\Component\Uid\Ulid; use Symfony\Contracts\HttpClient\HttpClientInterface; class LeantimeApiService implements DataProviderServiceInterface @@ -42,29 +49,38 @@ public function __construct( ) { } - public function getAllProjectData(): array + public function getEndpoints(): array { - throw new \Exception('Not implemented'); + return [ + 'base' => $this->leantimeUrl, + ]; } - public function getClientDataForProject(string $projectId): array - { - throw new \Exception('Not implemented'); - } - - public function getAllAccountData(): array - { - throw new \Exception('Not implemented'); - } - - public function getIssuesDataForProjectPaged(string $projectId, int $startAt = 0, $maxResults = 50): PagedResult + /** + * Get all projects, including archived. + * + * @throws ApiServiceException|EconomicsException + */ + public function getAllProjects(): mixed { - throw new \Exception('Not implemented'); + return $this->request(self::API_PATH_JSONRPC, 'POST', 'leantime.rpc.projects.getAll', []); } - public function getWorklogDataForProject(string $projectId): array + /** + * Get all worklogs for project. + * + * @param $projectId + * @param string $from + * @param string $to + * + * @return mixed + * + * @throws ApiServiceException + * @throws EconomicsException + */ + public function getProjectWorklogs($projectId, string $from = '2000-01-01', string $to = '3000-01-01'): mixed { - throw new \Exception('Not implemented'); + return $this->request(self::API_PATH_JSONRPC, 'POST', 'leantime.rpc.timesheets.getAll', ['invEmpl' => '-1', 'invComp' => '-1', 'paid' => '-1', 'id' => $projectId]); } /** @@ -72,7 +88,7 @@ public function getWorklogDataForProject(string $projectId): array * * @return SprintReportProjects array of SprintReportProjects * - * @throws ApiServiceException + * @throws ApiServiceException|EconomicsException */ public function getSprintReportProjects(): SprintReportProjects { @@ -80,17 +96,111 @@ public function getSprintReportProjects(): SprintReportProjects $projects = $this->request(self::API_PATH_JSONRPC, 'POST', 'leantime.rpc.projects.getAll', []); foreach ($projects as $project) { - $sprintReportProjects->projects->add( - new SprintReportProject( - $project->id, - $project->name - ) - ); + $sprintReportProject = new SprintReportProject(); + $sprintReportProject->id = $project->id; + $sprintReportProject->name = $project->name; + + $sprintReportProjects->projects->add($sprintReportProject); } return $sprintReportProjects; } + /** + * Get projectV2. + * + * @param string $projectId A project key or id + * + * @return SprintReportProject SprintReportProject + * + * @throws ApiServiceException|EconomicsException + */ + public function getSprintReportProject(string $projectId): SprintReportProject + { + $project = $this->request(self::API_PATH_JSONRPC, 'POST', 'leantime.rpc.projects.getProject', ['id' => $projectId]); + + $sprintReportProject = new SprintReportProject(); + $sprintReportProject->id = $project->id; + $sprintReportProject->name = $project->name; + + return $sprintReportProject; + } + + /** + * @throws ApiServiceException + * @throws EconomicsException + */ + private function getProjectIssuesPaged($projectId, $startAt, $maxResults = 50): array + { + // TODO: Implement pagination. + return $this->request(self::API_PATH_JSONRPC, 'POST', 'leantime.rpc.tickets.getAll', ['searchCriteria' => ['currentProject' => $projectId]]); + } + + /** + * @throws ApiServiceException + * @throws EconomicsException + */ + public function getIssuesDataForProjectPaged(string $projectId, $startAt = 0, $maxResults = 50): PagedResult + { + $result = []; + + $issues = $this->getProjectIssuesPaged($projectId, $startAt, $maxResults); + + // Filter out all worklogs that do not belong to the project. + // TODO: Remove filter when issues are filtered correctly by projectId in the API. + $issues = array_filter($issues, fn ($issue) => $issue->projectId == $projectId); + + foreach ($issues as $issue) { + $issueData = new IssueData(); + + $issueData->name = $issue->headline; + $issueData->status = $issue->status; + $issueData->projectTrackerId = $issue->id; + // Leantime does not have a key for each issue. + $issueData->projectTrackerKey = $issue->id; + $issueData->accountId = ''; + $issueData->accountKey = ''; + $issueData->epicKey = $issue->tags; + $issueData->epicName = $issue->tags; + if (isset($issue->milestoneid) && isset($issue->milestoneHeadline)) { + $issueData->versions?->add(new VersionData($issue->milestoneid, $issue->milestoneHeadline)); + } + $issueData->projectId = $issue->projectId; + $result[] = $issueData; + } + + return new PagedResult($result, $startAt, count($issues), count($issues)); + } + + public function getProjectDataCollection(): ProjectDataCollection + { + $projectDataCollection = new ProjectDataCollection(); + $projects = $this->getAllProjects(); + + foreach ($projects as $project) { + $projectData = new ProjectData(); + $projectData->name = $project->name; + $projectData->projectTrackerId = $project->id; + // Leantime does not have a key for each project. + $projectData->projectTrackerKey = $project->id; + $projectData->projectTrackerProjectUrl = $this->leantimeUrl.'#/tickets/showTicket/'.$project->id; + + $projectVersions = $this->getSprintReportVersions($project->id); + foreach ($projectVersions as $projectVersion) { + $projectData->versions?->add($projectVersion); + } + + $projectDataCollection->projectData->add($projectData); + } + + return $projectDataCollection; + } + + public function getClientDataForProject(string $projectId): array + { + throw new ApiServiceException('Method not implemented', 501); + } + /** * Get project. * @@ -104,6 +214,11 @@ public function getProject($key): mixed return $this->request(self::API_PATH_JSONRPC, 'POST', 'leantime.rpc.projects.getProject', ['id' => $key]); } + public function getAllAccountData(): array + { + return []; + } + /** * Get milestone. * @@ -112,28 +227,63 @@ public function getProject($key): mixed * * @throws ApiServiceException */ - private function getMilestone($key): mixed + public function getMilestone($key): mixed { return $this->request(self::API_PATH_JSONRPC, 'POST', 'leantime.rpc.tickets.getTicket', ['id' => $key]); } - public function getSprintReportProjectVersions(string $projectId): SprintReportVersions + public function getSprintReportVersions(string $projectId): SprintReportVersions { $sprintReportVersions = new SprintReportVersions(); $projectVersions = $this->request(self::API_PATH_JSONRPC, 'POST', 'leantime.rpc.tickets.getAllMilestones', ['searchCriteria' => ['currentProject' => $projectId, 'type' => 'milestone']]); foreach ($projectVersions as $projectVersion) { - $sprintReportVersions->versions->add( - new SprintReportVersion( - $projectVersion->id, - $projectVersion->headline - ) - ); + $sprintReportVersion = new SprintReportVersion(); + $sprintReportVersion->id = $projectVersion->id; + $sprintReportVersion->name = $projectVersion->headline; + $sprintReportVersion->projectTrackerId = $projectVersion->projectId; + + $sprintReportVersions->versions->add($sprintReportVersion); } return $sprintReportVersions; } + public function getWorklogDataCollection(string $projectId): WorklogDataCollection + { + $worklogDataCollection = new WorklogDataCollection(); + $worklogs = $this->getProjectWorklogs($projectId); + + $workersData = $this->request(self::API_PATH_JSONRPC, 'POST', 'leantime.rpc.users.getAll'); + + $workers = array_reduce($workersData, function ($carry, $item) { + $carry[$item->id] = $item->username; + + return $carry; + }, []); + + // Filter out all worklogs that do not belong to the project. + // TODO: Remove filter when worklogs are filtered correctly by projectId in the API. + $worklogs = array_filter($worklogs, fn ($worklog) => $worklog->projectId == $projectId); + + foreach ($worklogs as $worklog) { + $worklogData = new WorklogData(); + if (isset($worklog->ticketId)) { + $worklogData->projectTrackerId = $worklog->id; + $worklogData->comment = $worklog->description ?? ''; + $worklogData->worker = $workers[$worklog->userId]; + $worklogData->timeSpentSeconds = (int) ($worklog->hours * 60 * 60); + $worklogData->started = new \DateTime($worklog->workDate); + $worklogData->projectTrackerIsBilled = false; + $worklogData->projectTrackerIssueId = $worklog->ticketId; + + $worklogDataCollection->worklogData->add($worklogData); + } + } + + return $worklogDataCollection; + } + /** * Get all sprints for a given board. * @@ -155,11 +305,7 @@ public function getAllSprints(): array */ public function getTicketsInSprint(string $sprintId): array { - $result = $this->request(self::API_PATH_JSONRPC, 'POST', 'leantime.rpc.tickets.getAll', [ - 'searchCriteria' => [ - 'sprint' => $sprintId, - ], - ]); + $result = $this->request(self::API_PATH_JSONRPC, 'POST', 'leantime.rpc.tickets.getAll', ['sprint' => $sprintId]); return $result; } @@ -169,7 +315,11 @@ public function getTicketsInSprint(string $sprintId): array */ private function getIssueSprint($issueEntry): SprintReportSprint { - $sprint = $this->request(self::API_PATH_JSONRPC, 'POST', 'leantime.rpc.sprints.getSprint', ['id' => $issueEntry->sprint]); + $sprint = false; + + if ((bool) $issueEntry->sprint) { + $sprint = $this->request(self::API_PATH_JSONRPC, 'POST', 'leantime.rpc.sprints.getSprint', ['id' => $issueEntry->sprint]); + } if ($sprint) { $sprintState = SprintStateEnum::OTHER; @@ -379,11 +529,19 @@ public function getPlanningData(): PlanningData return $planning; } - public function getTimesheetsForTicket($ticketId): mixed + public function getTimesheetsForTicket(string $ticketId): mixed { return $this->request(self::API_PATH_JSONRPC, 'POST', 'leantime.rpc.timesheets.getAll', ['invEmpl' => '-1', 'invComp' => '-1', 'paid' => '-1', 'ticketFilter' => $ticketId]); } + /** + * @throws ApiServiceException + */ + private function getIssuesForProjectMilestone($projectId, $milestoneId): array + { + return $this->request(self::API_PATH_JSONRPC, 'POST', 'leantime.rpc.tickets.getAll', ['currentProject' => $projectId, 'milestone' => $milestoneId]); + } + /** * @throws ApiServiceException * @throws \Exception @@ -428,18 +586,20 @@ public function getSprintReportData(string $projectId, string $versionId): Sprin $issue->epic = $tag; // Get sprint for issue. - try { - $issueSprint = $this->getIssueSprint($issueEntry); + if ((bool) $issueEntry->sprint) { + try { + $issueSprint = $this->getIssueSprint($issueEntry); - if (!$sprints->containsKey($issueSprint->id)) { - $sprints->set($issueSprint->id, $issueSprint); - } - // Set which sprint the issue is assigned to. - if (SprintStateEnum::ACTIVE === $issueSprint->state || SprintStateEnum::FUTURE === $issueSprint->state) { - $issue->assignedToSprint = $issueSprint; + if (!$sprints->containsKey($issueSprint->id)) { + $sprints->set($issueSprint->id, $issueSprint); + } + // Set which sprint the issue is assigned to. + if (SprintStateEnum::ACTIVE === $issueSprint->state || SprintStateEnum::FUTURE === $issueSprint->state) { + $issue->assignedToSprint = $issueSprint; + } + } catch (ApiServiceException) { + // Ignore if sprint is not found. } - } catch (ApiServiceException) { - // Ignore if sprint is not found. } $worklogs = $this->getTimesheetsForTicket($issueEntry->id); @@ -521,14 +681,6 @@ public function getSprintReportData(string $projectId, string $versionId): Sprin return $sprintReportData; } - /** - * @throws ApiServiceException - */ - private function getIssuesForProjectMilestone($projectId, $milestoneId): array - { - return $this->request(self::API_PATH_JSONRPC, 'POST', 'leantime.rpc.tickets.getAll', ['searchCriteria' => ['currentProject' => $projectId, 'milestone' => $milestoneId]]); - } - /** * Get from Leantime. * @@ -542,7 +694,7 @@ private function request(string $path, string $type, string $method, array $para ['json' => [ 'jsonrpc' => '2.0', 'method' => $method, - 'id' => '1', + 'id' => (new Ulid())->jsonSerialize(), 'params' => $params, ]] ); diff --git a/templates/account/_delete_form.html.twig b/templates/account/_delete_form.html.twig new file mode 100644 index 00000000..1b090eea --- /dev/null +++ b/templates/account/_delete_form.html.twig @@ -0,0 +1,4 @@ +
+ + +
diff --git a/templates/account/edit.html.twig b/templates/account/edit.html.twig new file mode 100644 index 00000000..6a4799b2 --- /dev/null +++ b/templates/account/edit.html.twig @@ -0,0 +1,19 @@ +{% extends 'base.html.twig' %} + +{% block title %}{{ 'accounts.edit'|trans}}{% endblock %} + +{% block content %} +

{{ 'accounts.edit'|trans}}

+ + {{ form_start(form) }} + {{ form_widget(form) }} +
+ +
+ {{ form_end(form) }} + +
+ {{ 'accounts.back_to_list'|trans }} + {{ include('account/_delete_form.html.twig') }} +
+{% endblock %} diff --git a/templates/account/index.html.twig b/templates/account/index.html.twig new file mode 100644 index 00000000..217055f4 --- /dev/null +++ b/templates/account/index.html.twig @@ -0,0 +1,53 @@ +{% extends 'base.html.twig' %} + +{% block title %}{{ 'account.title'|trans }}{% endblock %} + +{% block content %} + {{ 'accounts.action_create_new'|trans }} + +

{{ 'account.title'|trans }}

+ + {% set numberOfColumns = 6 %} + + {# TODO: Add pagination, sorting and filters #} + + + + + + + + + + + + + + {% for index, account in accounts %} + + + + + + + + + {% else %} + + + + {% endfor %} + + + + +
{{ 'accounts.id'|trans }}{{ 'accounts.name'|trans }}{{ 'accounts.value'|trans }}{{ 'accounts.status'|trans }}{{ 'accounts.category'|trans }}{{ 'accounts.actions'|trans }}
{{ account.id }}{{ account.name }}{{ account.value }}{{ account.status }}{{ account.category }} + {{ 'accounts.action_edit'|trans }} +
{{ 'accounts.list_no_records_found'|trans }}
+ + {{ 'accounts.action_create_new'|trans }} + +
+{% endblock %} diff --git a/templates/account/new.html.twig b/templates/account/new.html.twig new file mode 100644 index 00000000..76ee1d01 --- /dev/null +++ b/templates/account/new.html.twig @@ -0,0 +1,18 @@ +{% extends 'base.html.twig' %} + +{% block title %}{{ 'account.new'|trans }}{% endblock %} + +{% block content %} +

{{ 'account.new'|trans }}

+ + {{ form_start(form) }} +
+ {{ form_rest(form) }} + +
+ {{ form_end(form) }} + + +{% endblock %} diff --git a/templates/client/_delete_form.html.twig b/templates/client/_delete_form.html.twig new file mode 100644 index 00000000..c40d9154 --- /dev/null +++ b/templates/client/_delete_form.html.twig @@ -0,0 +1,4 @@ +
+ + +
diff --git a/templates/client/edit.html.twig b/templates/client/edit.html.twig new file mode 100644 index 00000000..4e7d6c43 --- /dev/null +++ b/templates/client/edit.html.twig @@ -0,0 +1,17 @@ +{% extends 'base.html.twig' %} + +{% block title %}{{ 'clients.edit'|trans}}{% endblock %} + +{% block content %} +

{{ 'clients.edit'|trans}}

+ + {{ form_start(form) }} + {{ form_widget(form) }} +
+ +
+ {{ form_end(form) }} + + {{ include('client/_delete_form.html.twig') }} + {{ 'client.back_to_list'|trans}} +{% endblock %} diff --git a/templates/client/index.html.twig b/templates/client/index.html.twig new file mode 100644 index 00000000..4e8b5adb --- /dev/null +++ b/templates/client/index.html.twig @@ -0,0 +1,67 @@ +{% extends 'base.html.twig' %} + +{% block title %}{{ 'client.title'|trans }}{% endblock %} + +{% block content %} + {{ 'clients.action_create_new'|trans }} + +

{{ 'client.title'|trans }}

+ + {% set numberOfColumns = 11 %} + + {# TODO: Add pagination, sorting and filters #} + + + + + + + + + + + + + + + + + + + {% for index, client in clients %} + + + + + + {% if client.type is not null %} + + {% else %} + + {% endif %} + + + + + + + + {% else %} + + + + {% endfor %} + + + + +
{{ 'clients.id'|trans }}{{ 'clients.name'|trans }}{{ 'clients.contact'|trans }}{{ 'clients.standard_price'|trans }}{{ 'clients.type'|trans }}{{ 'clients.account'|trans }}{{ 'clients.psp'|trans }}{{ 'clients.ean'|trans }}{{ 'clients.sales_channel'|trans }}{{ 'clients.customer_key'|trans }}{{ 'clients.actions'|trans }}
{{ client.id }}{{ client.name }}{{ client.contact }}{{ client.standardPrice }}{{ client.type.value }}{{ client.account }}{{ client.psp }}{{ client.ean }}{{ client.salesChannel }}{{ client.customerKey }} + {{ 'clients.action_edit'|trans }} +
{{ 'clients.list_no_records_found'|trans }}
+ + {{ 'clients.action_create_new'|trans }} + +
+{% endblock %} diff --git a/templates/client/new.html.twig b/templates/client/new.html.twig new file mode 100644 index 00000000..3d3ef11a --- /dev/null +++ b/templates/client/new.html.twig @@ -0,0 +1,18 @@ +{% extends 'base.html.twig' %} + +{% block title %}{{ 'client.new'|trans }}{% endblock %} + +{% block content %} +

{{ 'client.new'|trans }}

+ + {{ form_start(form) }} +
+ {{ form_rest(form) }} + +
+ {{ form_end(form) }} + + +{% endblock %} diff --git a/templates/components/navigation.html.twig b/templates/components/navigation.html.twig index c325fa44..7508050c 100644 --- a/templates/components/navigation.html.twig +++ b/templates/components/navigation.html.twig @@ -53,6 +53,22 @@ {{ 'navigation.projects'|trans }} +
  • + + + + + {{ 'navigation.client'|trans }} + +
  • +
  • + + + + + {{ 'navigation.account'|trans }} + +
  • diff --git a/templates/home/index.html.twig b/templates/home/index.html.twig index 4825d506..2c1a1499 100644 --- a/templates/home/index.html.twig +++ b/templates/home/index.html.twig @@ -53,5 +53,23 @@ {{ 'home.projects'|trans }} +
    + +
    {{ 'home.clients'|trans }}
    +
    +

    {{ 'home.clients_description'|trans }}

    + + {{ 'home.clients'|trans }} + +
    +
    + +
    {{ 'home.accounts'|trans }}
    +
    +

    {{ 'home.accounts_description'|trans }}

    + + {{ 'home.accounts'|trans }} + +
    {% endblock %} diff --git a/templates/invoice_entry/_delete_form.html.twig b/templates/invoice_entry/_delete_form.html.twig index d6a496c3..52b8fee2 100644 --- a/templates/invoice_entry/_delete_form.html.twig +++ b/templates/invoice_entry/_delete_form.html.twig @@ -1,4 +1,4 @@ -
    +
    diff --git a/templates/invoice_entry/_form.html.twig b/templates/invoice_entry/_form.html.twig deleted file mode 100644 index bf20b98f..00000000 --- a/templates/invoice_entry/_form.html.twig +++ /dev/null @@ -1,4 +0,0 @@ -{{ form_start(form) }} - {{ form_widget(form) }} - -{{ form_end(form) }} diff --git a/templates/invoices/_delete_form.html.twig b/templates/invoices/_delete_form.html.twig index 27014753..8e9ca63d 100644 --- a/templates/invoices/_delete_form.html.twig +++ b/templates/invoices/_delete_form.html.twig @@ -1,4 +1,4 @@ -
    +
    diff --git a/templates/project_billing/_delete_form.html.twig b/templates/project_billing/_delete_form.html.twig index 79f5d2ea..0a9eab55 100644 --- a/templates/project_billing/_delete_form.html.twig +++ b/templates/project_billing/_delete_form.html.twig @@ -1,4 +1,4 @@ -
    +
    diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index 371744bf..27f6381a 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -1,184 +1,265 @@ sprint_report: - data_provider_helptext: '' - data_provider: 'Vælg datakilde' - select_project: Vælg projekt - select_version: Vælg version - select_an_option: Vælg en mulighed - save_budget: Gem - title: Sprintrapport - heading: Sprintrapport - heading_overview: Overblik - heading_phases: Projektets faser - heading_spent_hours: Forbrugt pr. uge - heading_planned_hours: Planlagt pr. uge - pdf_title: Sprint rapport eksport - render_as_pdf: Hent som PDF + data_provider_helptext: "" + data_provider: "Vælg datakilde" + select_project: "Vælg projekt" + select_version: "Vælg version" + select_an_option: "Vælg en mulighed" + save_budget: "Gem" + title: "Sprintrapport" + heading: "Sprintrapport" + heading_overview: "Overblik" + heading_phases: "Projektets faser" + heading_spent_hours: "Forbrugt pr. uge" + heading_planned_hours: "Planlagt pr. uge" + pdf_title: "Sprint rapport eksport" + render_as_pdf: "Hent som PDF" sub: - project: Projekt - version: Bestilling - budget: Salgsbudget - spent_hours: Timer reg. total - remaining_hours: Resterende timer planlagt total - project_hours: Projektets timer - finished_degree: Færdiggørelsesgrad (%) - project_forecast: Projekt total forecast - over_under_index: Over/underløb - index + project: "Projekt" + version: "Bestilling" + budget: "Salgsbudget" + spent_hours: "Timer reg. total" + remaining_hours: "Resterende timer planlagt total" + project_hours: "Projektets timer" + finished_degree: "Færdiggørelsesgrad (%)" + project_forecast: "Projekt total forecast" + over_under_index: "Over/underløb - index" table: - epic: Epic - estimate: Est. - remaining_hours: Resterende arbejde est. - remaining_hours_short: Rest. arb. - registered_hours: Registreret - reg: Reg. - no_sprint: Ingen sprint - planned_work_sum: Sum af est. planlagt arb. + epic: "Epic" + estimate: "Est." + remaining_hours: "Resterende arbejde est." + remaining_hours_short: "Rest. arb." + registered_hours: "Registreret" + reg: "Reg." + no_sprint: "Ingen sprint" + planned_work_sum: "Sum af est. planlagt arb." navigation: - home: Forside - create_project: 'Projektopretter' - invoices: 'Faktura' - planning: 'Planlægning' - sprint_report: 'Sprintrapport' - title: 'Economics' - accounts: 'Konto' - project_billing: 'Projektfakturering' - projects: 'Projekter' + home: "Forside" + create_project: "Projektopretter" + invoices: "Faktura" + planning: "Planlægning" + sprint_report: "Sprintrapport" + title: "Economics" + accounts: "Konto" + project_billing: "Projektfakturering" + projects: "Projekter" + client: "Kunder" + account: "Konti" + pricelist: "Prisliste" planning: - title: 'Planlægning' - assignees: 'Brugere' - projects: 'Projekter' - hidden-entries: 'Skjulte brugere' - data_provider_helptext: '' - data_provider: 'Vælg datakilde' - search: 'Vis planlægning' + title: "Planlægning" + assignees: "Brugere" + projects: "Projekter" + hidden-entries: "Skjulte brugere" + data_provider_helptext: "" + data_provider: "Vælg datakilde" + search: "Vis planlægning" + +client: + title: "Kunder" + create_new: "Opret kunde" + no_records_found: "Ingen kunder at vise" + new: "Opret kunde" + client_type_select: "Vælg kundetype" + action_create: "Opret kunde" + back_to_list: "Tilbage til listen" + +price_list: + title: "Prisliste" + create_new: "Opret pris" + no_records_found: "Ingen priser at vise" + new: "Opret pris" + client_type_select: "Vælg kundetype" + action_create: "Opret pris" + back_to_list: "Tilbage til listen" + +create_pricelist_form: + price_list_name: + label: "Navn" + help: "" + price_list_price: + label: "Pris" + help: "" + +create_client_form: + client_name: + label: "Navn" + help: "" + client_contact: + label: "Kontakt" + help: "" + client_standardPrice: + label: "Standardpris" + help: "" + type: + label: "Kundetype" + help: "" + client_account: + label: "Konto" + help: "" + client_psp: + label: "PSP-element" + help: "" + client_ean: + label: "EAN-nummer" + help: "" + client_projectTrackerId: + label: "ProjectTrackerId" + help: "" + sales_channel: + label: "Salgskanal" + help: "" + customer_key: + label: "Ordregiver/Bestiller" + help: "" + +account: + title: "Konti" + create_new: "Opret konto" + no_records_found: "Ingen konti at vise" + new: "Opret konto" + action_create: "Opret konto" + back_to_list: "Tilbage til listen" + +create_account_form: + account_name_label: "Navn" + account_name_help: "" + account_value_label: "Værdi" + account_value_help: "" + account_project_tracker_id_label: "Projekt" + account_project_tracker_id_help: "" + account_status_label: "Status" + account_status_help: "" + account_category_label: "Kategori" + account_category_help: "" invoices: - title: 'Faktura' - list_id: '#' - list_name: 'Faktura' - list_project: 'Projekt' - list_created_by: 'Opretter' - list_updated_at: 'Dato' - list_total_price: 'Beløb' - list_actions: 'Handlinger' - list_no_records_found: 'Ingen fundet' - action_create_new: 'Opret ny faktura' - id: 'Faktura' - status: 'Status' - status_posted: 'Bogført' - status_not_posted: 'Ikke bogført' - action_update: 'Gem ændringer' - edit: 'Rediger faktura' - client_information: 'Kundekontoinformationer' + title: "Faktura" + list_id: "#" + list_name: "Faktura" + list_project: "Projekt" + list_created_by: "Opretter" + list_updated_at: "Dato" + list_total_price: "Beløb" + list_actions: "Handlinger" + list_no_records_found: "Ingen fundet" + action_create_new: "Opret ny faktura" + id: "Faktura" + status: "Status" + status_posted: "Bogført" + status_not_posted: "Ikke bogført" + action_update: "Gem ændringer" + edit: "Rediger faktura" + client_information: "Kundeinformationer" invoice_for: "Faktura for [%project%] (%projectTrackerId%)" - no_client: 'Ingen kundekonto valgt' - client_psp: 'PSP' - client_account: 'Konto' - client_type: 'Type' - client_standard_price: 'Standardpris' - client_contact: 'Kontakt' - client_name: 'Kundekonto' - info: 'Faktura' - create_new: 'Opret faktura' - action_edit: 'Redigér' - action_save: 'Gem faktura' - action_delete: 'Slet faktura' - action_create: 'Opret faktura' - back_to_list: 'Tilbage til listen' - search: 'Filtrér' - recorded_true: 'Bogført' - recorded_false: 'Ikke bogført' - recorded: 'Bogført' - created_by: 'Opretter' - invoice_entry_receiver_acccount: 'Til konto' - invoice_entry_material_number: 'Materialenummer' - invoice_entry_product: 'Vare' - invoice_entry_amount: 'Antal' - invoice_entry_price: 'Pris pr. stk. (DKK)' - invoice_entry_total_price: 'Samlet pris (DKK)' - action_create_new_invoice_entry: 'Opret ny fakturalinje' - invoice_entries: 'Fakturalinjer' - back_to_invoice: 'Tilbage til faktura' - entry_new: 'Opret ny fakturalinje' - entry_edit: 'Redigér fakturalinje' - entry_for_invoice: 'Faktura: %invoice%' - name: 'Navn' - description: 'Beskrivelse' - project: 'Projekt' - client: 'Kundekonto' - default_material_number: 'Materialenummer' - default_receiver_account: 'Til konto' - paid_by_account: 'Betales af konto' - period_from: 'Periode fra' - period_to: 'Periode til' - period_from_helptext: 'Dette felt skal udfyldes på eksterne fakturaer.' - period_to_helptext: 'Dette felt skal udfyldes på eksterne fakturaer.' - name_helptext: 'Vælg faktuens navn.' - description_helptext: 'Indsættes i tekstfeltet i fakturaen. Autoudfyld er tilgængelig, hvis der er valgt en kundekonto med en project lead.' - project_helptext: 'Vælg projekt.' - client_helptext: 'Vælg hvilken kundekonto fakturaen skal udstedes til.' - payer_account_helptext: 'Anvendes kun når der er tale om ITK-projektposteringer på PSP-element. Hvis valgt indsættes følgende i starten af tekstfeltet i fakturaen: "Betales af [KONTO]".' - default_material_number_helptext: 'Dette bliver sat automatisk i alle fakturaindgange.' - default_receiver_account_helptext: 'Dette bliver sat automatisk i alle fakturaindgange.' - client_type_internal: 'Intern' - client_type_external: 'Ekstern' - client_ean: 'EAN' - invoice_entry_type: 'Type' - invoice_entry_type_manual: 'Manuel' - invoice_entry_type_worklog: 'Worklog' - action_create_new_invoice_entry_worklog: 'Tilføj worklog linje' - action_create_new_invoice_entry_manual: 'Tilføj manuel linje' - action_delete_invoice_entry: 'Slet fakturalinje' - action_save_invoice_entry: 'Gem fakturalinje' - invoice_entry_material_number_helptext: 'Denne værdi kan kun ændres på fakturaniveau.' - invoice_entry_receiver_account_helptext: 'Denne værdi kan kun ændres på fakturaniveau.' - invoice_entry_product_helptext: 'Beskriv fakturalinjen.' - invoice_entry_price_helptext: 'Vælg prisen pr. stk.' - invoice_entry_amount_helptext: 'Vælg antal.' - invoice_entry_type_heading: 'Type: %type%' - add_worklogs: 'Vælg worklogs' - confirm_record_title: 'Bogfør faktura' - confirm_record_are_you_sure: 'Er du sikker på at du vil bogføre fakturaen "%invoiceName%"?' - confirm_record_cannot_be_undone: 'Når fakturaen er bogført kan den ikke ændres mere.' - record_save: 'Gennemfør valg' - record_invoice_false: 'Nej. Gå tilbage.' - record_invoice_true: 'Ja. Bogfør faktura.' + no_client: "Ingen kunde valgt" + client_psp: "PSP" + client_account: "Konto" + client_type: "Type" + client_standard_price: "Standardpris" + client_contact: "Kontakt" + client_name: "Kunde" + info: "Faktura" + create_new: "Opret faktura" + action_edit: "Redigér" + action_save: "Gem faktura" + action_delete: "Slet faktura" + action_create: "Opret faktura" + back_to_list: "Tilbage til listen" + search: "Filtrér" + recorded_true: "Bogført" + recorded_false: "Ikke bogført" + recorded: "Bogført" + created_by: "Opretter" + invoice_entry_receiver_acccount: "Til konto" + invoice_entry_material_number: "Materialenummer" + invoice_entry_product: "Vare" + invoice_entry_amount: "Antal" + invoice_entry_price: "Pris pr. stk. (DKK)" + invoice_entry_total_price: "Samlet pris (DKK)" + action_create_new_invoice_entry: "Opret ny fakturalinje" + invoice_entries: "Fakturalinjer" + back_to_invoice: "Tilbage til faktura" + entry_new: "Opret ny fakturalinje" + entry_edit: "Redigér fakturalinje" + entry_for_invoice: "Faktura: %invoice%" + name: "Navn" + description: "Beskrivelse" + project: "Projekt" + client: "Kunde" + default_material_number: "Materialenummer" + default_receiver_account: "Til konto" + paid_by_account: "Betales af konto" + period_from: "Periode fra" + period_to: "Periode til" + period_from_helptext: "Dette felt skal udfyldes på eksterne fakturaer." + period_to_helptext: "Dette felt skal udfyldes på eksterne fakturaer." + name_helptext: "Vælg faktuens navn." + description_helptext: "Indsættes i tekstfeltet i fakturaen. Autoudfyld er tilgængelig, hvis der er valgt en kundekonto med en project lead." + project_helptext: "Vælg projekt." + client_helptext: "Vælg hvilken kunde fakturaen skal udstedes til." + payer_account_helptext: "Anvendes kun når der er tale om ITK-projektposteringer på PSP-element. Hvis valgt indsættes følgende i starten af tekstfeltet i fakturaen: \"Betales af [KONTO]\"." + default_material_number_helptext: "Dette bliver sat automatisk i alle fakturaindgange." + default_receiver_account_helptext: "Dette bliver sat automatisk i alle fakturaindgange." + client_type_internal: "Intern" + client_type_external: "Ekstern" + client_ean: "EAN" + invoice_entry_type: "Type" + invoice_entry_type_manual: "Manuel" + invoice_entry_type_worklog: "Worklog" + action_create_new_invoice_entry_worklog: "Tilføj worklog linje" + action_create_new_invoice_entry_manual: "Tilføj manuel linje" + action_delete_invoice_entry: "Slet fakturalinje" + action_save_invoice_entry: "Gem fakturalinje" + invoice_entry_material_number_helptext: "Denne værdi kan kun ændres på fakturaniveau." + invoice_entry_receiver_account_helptext: "Denne værdi kan kun ændres på fakturaniveau." + invoice_entry_product_helptext: "Beskriv fakturalinjen." + invoice_entry_price_helptext: "Vælg prisen pr. stk." + invoice_entry_amount_helptext: "Vælg antal." + invoice_entry_type_heading: "Type: %type%" + add_worklogs: "Vælg worklogs" + confirm_record_title: "Bogfør faktura" + confirm_record_are_you_sure: "Er du sikker på at du vil bogføre fakturaen \"%invoiceName%\"?" + confirm_record_cannot_be_undone: "Når fakturaen er bogført kan den ikke ændres mere." + record_save: "Gennemfør valg" + record_invoice_false: "Nej. Gå tilbage." + record_invoice_true: "Ja. Bogfør faktura." record_invoice: "Bogfør faktura?" - record_invoice_helptext: '' - cannot_record_errors_exist: 'Kan ikke bogføre faktura, idet der er fejl i data:' - already_recorded: 'Fakturaen er allerede bogført' - action_show_export: 'Vis eksport' - action_record: 'Bogfør faktura' - locked_client_values: 'Låste fakturaværdier. Disse værdier bruges i fakturaen og ændres ikke selvom kundekontoen bliver opdateret.' - locked_type: 'Type' - locked_sales_channel: 'Salgskanal' - locked_account_key: 'Konto' - locked_contact_name: 'Kontakt' - locked_customer_key: 'Ordregiver/Bestiller' - client_sales_channel: 'Salgskanal' - client_customer_key: 'Ordregiver/Bestiller' - all_worklogs_will_be_marked_as_billed: 'Alle worklogs i fakturaen vil blive markeret som faktureret.' - show_worklogs: 'Vis worklogs' - export_title: 'Eksport' - action_export: 'Download .csv' - back_to_invoice_entry: 'Tilbage til fakturalinje' - invoice_info: 'Fakturainformation' - project_billing: 'PF' - include_project_billing: 'Projektfakturering' - with_project_billing: 'Inkludér' - without_project_billing: 'Udelad' - only_project_billing: 'Kun PF' - list_recorded: 'Bogført' - export_only_available_when_mandatory_fields_are_set: 'Eksport kun muligt når der er sat en kundekonto' - action_export_selected: 'Eksporter valg' - action_view: 'Se' - list_exported_date: 'Eksportdato' - client_type_is_null: '' - generate_description: 'Autoudfyld' - client_project_lead: 'Project Lead' - required_fields_not_set_for_creating_entries: '"Til konto" og "Materialenummer" skal være valgt før der kan oprettes fakturalinjer.' - type: 'Kundekonto type' + record_invoice_helptext: "" + cannot_record_errors_exist: "Kan ikke bogføre faktura, idet der er fejl i data:" + already_recorded: "Fakturaen er allerede bogført" + action_show_export: "Vis eksport" + action_record: "Bogfør faktura" + locked_client_values: "Låste fakturaværdier. Disse værdier bruges i fakturaen og ændres ikke selvom kunden bliver opdateret." + locked_type: "Type" + locked_sales_channel: "Salgskanal" + locked_account_key: "Konto" + locked_contact_name: "Kontakt" + locked_customer_key: "Ordregiver/Bestiller" + client_sales_channel: "Salgskanal" + client_customer_key: "Ordregiver/Bestiller" + all_worklogs_will_be_marked_as_billed: "Alle worklogs i fakturaen vil blive markeret som faktureret." + show_worklogs: "Vis worklogs" + export_title: "Eksport" + action_export: "Download .csv" + back_to_invoice_entry: "Tilbage til fakturalinje" + invoice_info: "Fakturainformation" + project_billing: "PF" + include_project_billing: "Projektfakturering" + with_project_billing: "Inkludér" + without_project_billing: "Udelad" + only_project_billing: "Kun PF" + list_recorded: "Bogført" + export_only_available_when_mandatory_fields_are_set: "Eksport kun muligt når der er sat en kundekonto" + action_export_selected: "Eksporter valg" + action_view: "Se" + list_exported_date: "Eksportdato" + client_type_is_null: "" + generate_description: "Autoudfyld" + client_project_lead: "Project Lead" + required_fields_not_set_for_creating_entries: "\"Til konto\" og \"Materialenummer\" skal være valgt før der kan oprettes fakturalinjer." + type: "Kundekonto type" material_number_enum: internal: "Interne: 103361" @@ -186,201 +267,236 @@ material_number_enum: external_without_moms: "Eksterne u/moms: 100008" accounts: - id: '#' - create_new: 'Opret ny konto' - no_records_found: 'Ingen fundet' - edit: 'Redigér' - value: 'Værdi' - actions: 'Handlinger' - name: 'Navn' - title: 'Konto' - back_to_list: 'Tilbage til listen' - action_save: 'Gem' - name_helptext: 'Vælg et navn' - value_helptext: 'Vælg kontoens værdi, som sættes ind i fakturaen.' - delete: 'Slet konto' - created_by: 'Oprettet af' - source: 'Kilde' - manual: 'Manuel' + id: "#" + create_new: "Opret ny konto" + no_records_found: "Ingen fundet" + edit: "Redigér" + value: "Værdi" + actions: "Handlinger" + name: "Navn" + title: "Konto" + back_to_list: "Tilbage til listen" + action_save: "Gem" + name_helptext: "Vælg et navn" + value_helptext: "Vælg kontoens værdi, som sættes ind i fakturaen." + delete: "Slet konto" + created_by: "Oprettet af" + source: "Kilde" + manual: "Manuel" + action_delete_account: "Slet konto" + list_no_records_found: "Ingen konti" + status: "Status" + category: "Kategori" + action_create_new: "Opret ny konto" + action_edit: "Redigér" worklog: - title: 'Tilføj worklogs til fakturalinje' - name: 'Worklog' - is_billed: 'Faktureret' - epic: 'Epic' - version: 'Version' - worker: 'Arbejder' - time_spent: 'Timer' - date: 'Dato' - is_billed_true: 'Ja' - is_billed_false: 'Nej' + title: "Tilføj worklogs til fakturalinje" + name: "Worklog" + is_billed: "Faktureret" + epic: "Epic" + version: "Version" + worker: "Arbejder" + time_spent: "Timer" + date: "Dato" + is_billed_true: "Ja" + is_billed_false: "Nej" period_from: "Periode fra" period_to: "Periode til" - is_billed_helptext: '' - period_from_helptext: '' - period_to_helptext: '' - worker_helptext: '' - version_helptext: '' - epic_helptext: '' - owned_by_other: 'Anden faktura' - action_save: 'Gem valgte worklogs' - error_already_billed: 'Fejl: Indeholder worklog der allerede er faktureret. Prøv at genindlæse siden...' - error_added_to_other_invoice_entry: 'Fejl: Indeholder worklog der er bundet til anden fakturaindgang. Prøv at genindlæse siden...' - only_available_false: 'Nej. Vis alle.' - only_available_true: 'Ja' - only_available: 'Kun "frie" worklogs' - only_available_helptext: 'Worklogs der ikke er i en anden fakturalinje.' - title_show: 'Worklogs' + is_billed_helptext: "" + period_from_helptext: "" + period_to_helptext: "" + worker_helptext: "" + version_helptext: "" + epic_helptext: "" + owned_by_other: "Anden faktura" + action_save: "Gem valgte worklogs" + error_already_billed: "Fejl: Indeholder worklog der allerede er faktureret. Prøv at genindlæse siden..." + error_added_to_other_invoice_entry: "Fejl: Indeholder worklog der er bundet til anden fakturaindgang. Prøv at genindlæse siden..." + only_available_false: "Nej. Vis alle." + only_available_true: "Ja" + only_available: "Kun \"frie\" worklogs" + only_available_helptext: "Worklogs der ikke er i en anden fakturalinje." + title_show: "Worklogs" invoice_entry: - amount: 'Antal enheder' - amount_worklogs: 'Antal worklogs tilkoblet' - new_worklog_information: 'Bemærk! Der kan først tilkobles worklogs efter linjen er gemt første gang.' + amount: "Antal enheder" + amount_worklogs: "Antal worklogs tilkoblet" + new_worklog_information: "Bemærk! Der kan først tilkobles worklogs efter linjen er gemt første gang." invoice_recordable: - error_no_client: 'Ingen klientkonto tilkoblet' - error_no_account: 'Ingen konto' - error_no_contact: 'Ingen kontakt' - error_no_type: 'Ingen type' - error_empty_invoice_entry: 'Fakturaindgangen med id: %invoiceEntryId% har et tomt antal' + error_no_client: "Ingen kunde tilkoblet" + error_no_account: "Ingen konto" + error_no_contact: "Ingen kontakt" + error_no_type: "Ingen type" + error_empty_invoice_entry: "Fakturaindgangen med id: %invoiceEntryId% har et tomt antal" -title: 'Economics' +title: "Economics" home: - title: 'Hjem' - invoice: 'Faktura' - invoice_desctiption: 'Opret og rediger fakturaer baseret på opgaver og worklogs i Project Tracker.' - planning: 'Planlægning' - planning_description: 'Planlægningsoversigt baseret på opgaver i Project Tracker.' - sprint_report: 'Sprintrapport' - sprint_report_description: 'Opret sprintrapporter baseret på opgaver og worklogs i Project Tracker.' - create_project: 'Projektopretter' - create_project_description: 'Hjælper med at oprette projekter i Project Tracker.' - accounts: 'Konto' - account_details: 'Liste over kontoer der bruges i faktura.' - project_billing: 'Projektfakturering' - project_billing_description: 'Fakturér alle kunder i et projekt for en given periode.' - projects: 'Projekter' - projects_description: 'Liste over projekter. Vælg hvilke skal vises i resten af systemet.' + title: "Hjem" + invoice: "Faktura" + invoice_desctiption: "Opret og rediger fakturaer baseret på opgaver og worklogs i Project Tracker." + planning: "Planlægning" + planning_description: "Planlægningsoversigt baseret på opgaver i Project Tracker." + sprint_report: "Sprintrapport" + sprint_report_description: "Opret sprintrapporter baseret på opgaver og worklogs i Project Tracker." + create_project: "Projektopretter" + create_project_description: "Hjælper med at oprette projekter i Project Tracker." + accounts: "Konti" + accounts_description: "Liste over konti. Bruges i valglister." + account_details: "Liste over kontoer der bruges i faktura." + project_billing: "Projektfakturering" + project_billing_description: "Fakturér alle kunder i et projekt for en given periode." + projects: "Projekter" + projects_description: "Liste over projekter. Vælg hvilke skal vises i resten af systemet." + clients_description: "Liste over kunder, som kan kobles på projekter." + clients: "Kunder" frontpage: - title: 'Velkommen til Economics' - login: 'Log ind' + title: "Velkommen til Economics" + login: "Log ind" project_billing: - title: 'Projektfakturering' - list_id: '#' - list_name: 'Navn' - list_project: 'Projekt' - list_created_by: 'Opretter' - list_updated_at: 'Dato' - list_actions: 'Handlinger' - action_edit: 'Redigér' - list_no_records_found: 'Ingen fundet' - action_create_new: 'Opret ny projektfakturering' - edit: 'Redigér projektfakturering' - new: 'Opret ny projektfakturering' - back_to_list: 'Tilbage til listen' - action_save: 'Gem' - action_create: 'Opret' - name: 'Navn' - name_helptext: 'Navn på projektfakturering' - invoices: 'Tilkoblede fakturaer' - action_delete: 'Slet projektfakturering' - field_name: 'Navn' - field_name_helptext: 'Skriv projektfaktureringens navn' - field_project: 'Projekt' - field_project_helptext: 'Vælg projek der skal projektfaktureres' - field_period_start: 'Fra' - field_period_start_helptext: 'Vælg hvor perioden skal starte' - field_period_end: 'Til' - field_period_end_helptext: 'Vælg hvor perioden skal strække sig til' - field_description: 'Beskrivelse' - field_description_help: 'Teksten sættes efter "Projektnavn: Accountnavn (faktureringsperiode)." i H på faktura.' - action_export: 'Eksportér' - action_record: 'Bogfør projektfakturering' - action_show_export: 'Vis eksport' - back_to_invoice: 'Tilbage til projektfakturering' - confirm_record_are_you_sure: 'Er du sikker på at du vil bogføre projektfaktureringen "%projectBillingName%"?' - all_worklogs_will_be_marked_as_billed: 'Alle worklogs og fakturaer i projektfaktureringen vil blive markeret som faktureret.' - confirm_record_cannot_be_undone: 'Når fakturaen er bogført kan den ikke ændres mere.' - confirm_record_title: 'Bogfør projektfaktureringen' - record_save: 'Gennemfør valg' - record_project_billing_false: 'Nej. Gå tilbage.' - record_project_billing_true: 'Ja. Bogfør projektfaktureringen.' + title: "Projektfakturering" + list_id: "#" + list_name: "Navn" + list_project: "Projekt" + list_created_by: "Opretter" + list_updated_at: "Dato" + list_actions: "Handlinger" + action_edit: "Redigér" + list_no_records_found: "Ingen fundet" + action_create_new: "Opret ny projektfakturering" + edit: "Redigér projektfakturering" + new: "Opret ny projektfakturering" + back_to_list: "Tilbage til listen" + action_save: "Gem" + action_create: "Opret" + name: "Navn" + name_helptext: "Navn på projektfakturering" + invoices: "Tilkoblede fakturaer" + action_delete: "Slet projektfakturering" + field_name: "Navn" + field_name_helptext: "Skriv projektfaktureringens navn" + field_project: "Projekt" + field_project_helptext: "Vælg projek der skal projektfaktureres" + field_period_start: "Fra" + field_period_start_helptext: "Vælg hvor perioden skal starte" + field_period_end: "Til" + field_period_end_helptext: "Vælg hvor perioden skal strække sig til" + field_description: "Beskrivelse" + field_description_help: "Teksten sættes efter \"Projektnavn: Accountnavn (faktureringsperiode).\" i H på faktura." + action_export: "Eksportér" + action_record: "Bogfør projektfakturering" + action_show_export: "Vis eksport" + back_to_invoice: "Tilbage til projektfakturering" + confirm_record_are_you_sure: "Er du sikker på at du vil bogføre projektfaktureringen \"%projectBillingName%\"?" + all_worklogs_will_be_marked_as_billed: "Alle worklogs og fakturaer i projektfaktureringen vil blive markeret som faktureret." + confirm_record_cannot_be_undone: "Når fakturaen er bogført kan den ikke ændres mere." + confirm_record_title: "Bogfør projektfaktureringen" + record_save: "Gennemfør valg" + record_project_billing_false: "Nej. Gå tilbage." + record_project_billing_true: "Ja. Bogfør projektfaktureringen." record_project_billing: "Bogfør projektfakturering?" - record_project_billing_helptext: '' - cannot_record_errors_exist: 'Kan ikke bogføre projektfaktureringen, idet der er fejl i dataen:' - already_recorded: 'Projektfaktureringen er allerede bogført' - export_title: 'Eksport' - recorded: 'Ja' - list_recorded: 'Bogført' - issues_not_included: 'Opgaver der ikke er inkluderet fordi de mangler konto' - issues_not_included_helptext: 'Sæt konto på opgaven for få den med i projektfaktureringen. Synkronisér derefter projektet i "Projekter", og tryk derefter "Gem" på denne projektfaktureringen for at få opgaverne med.' + record_project_billing_helptext: "" + cannot_record_errors_exist: "Kan ikke bogføre projektfaktureringen, idet der er fejl i dataen:" + already_recorded: "Projektfaktureringen er allerede bogført" + export_title: "Eksport" + recorded: "Ja" + list_recorded: "Bogført" + issues_not_included: "Opgaver der ikke er inkluderet fordi de mangler konto" + issues_not_included_helptext: "Sæt konto på opgaven for få den med i projektfaktureringen. Synkronisér derefter projektet i \"Projekter\", og tryk derefter \"Gem\" på denne projektfaktureringen for at få opgaverne med." issue: - name: 'navn' - status: 'status' - key: 'nøgle' - search: 'Filtrér' - form_recorded: 'Bogført' - form_created_by: 'Opretter' - existing_errors_before_on_record: 'Følgende fejl skal rettes før projektfakturering kan bogføres.' - on_record_errors_invoice_id: 'Faktura ID' - on_record_errors_error: 'Fejlbesked' - on_record_errors_invoice_link: 'Link til faktura' - action_record_internal: 'Eksportér interne' - action_record_external: 'Eksportér eksterne' + name: "navn" + status: "status" + key: "nøgle" + search: "Filtrér" + form_recorded: "Bogført" + form_created_by: "Opretter" + existing_errors_before_on_record: "Følgende fejl skal rettes før projektfakturering kan bogføres." + on_record_errors_invoice_id: "Faktura ID" + on_record_errors_error: "Fejlbesked" + on_record_errors_invoice_link: "Link til faktura" + action_record_internal: "Eksportér interne" + action_record_external: "Eksportér eksterne" project: - title: 'Projekter' - include: 'Inkludér' - id: '#' - name: 'Navn' - project_tracker_key: 'Nøgle' - updated_at: 'Opdateret' - list_no_records_found: 'Ingen projekter fundet' - sync: 'Synkronisér' - sync_action: 'Start' - search: 'Søg' - include_null: '' - include_false: 'Ikke inkluderet' - include_true: 'Inkluderet' - form_name: 'Navn' - form_key: 'Nøgle' - form_include: 'Inkluderet' - information: 'Kun inkluderede projekter bliver vist og synkroniseret i resten af system.' + title: "Projekter" + include: "Inkludér" + id: "#" + name: "Navn" + project_tracker_key: "Nøgle" + updated_at: "Opdateret" + list_no_records_found: "Ingen projekter fundet" + sync: "Synkronisér" + sync_action: "Start" + search: "Søg" + include_null: "" + include_false: "Ikke inkluderet" + include_true: "Inkluderet" + form_name: "Navn" + form_key: "Nøgle" + form_include: "Inkluderet" + information: "Kun inkluderede projekter bliver vist og synkroniseret i resten af system." exception: - invoices_record_entry_amount_not_set: 'Faktura can ikke bogføres, da den indeholder fakturalinjer hvor antal ikke er sat. Se: %invoiceEntryUrl%.' - invoices_record_part_of_project_billing: 'Fakturaen er en del af en projektfakturering og kan kun bogføres separat.' - invoices_export_must_be_on_record: 'Faktura skal være en bogført før den kan eksporteres til .csv.' - invoices_export_part_of_project_billing: 'Faktura kan ikke eksporteres da den er en del af en projektfakturering.' - invoices_export_failure: 'Eksport fejlede.' - invoices_delete_on_record: 'Faktura kan ikke slettes, da den er bogført.' - invoices_delete_part_of_project_billing: 'Faktura kan ikke slettes, da den er en del af projektfakturering.' - invoices_export_failure_could_not_generate_html: 'Faktura kan ikke eksporteres. HTML eksport fejlede.' - project_not_found: 'Projektet blev ikke fundet.' - project_tracker_id_not_set: 'Project Tracker id er ikke sat.' - invoices_part_of_project_billing_cannot_edit: 'Fakturaen er en del af en projektfakturering og kan derfor ikke redigeres.' - invoices_on_record_cannot_edit: 'Fakturaen er bogført og kan derfor ikke redigeres.' - invoice_entry_invoice_on_record_cannot_add_entries: 'Fakturaen er bogført og kan derfor ikke få tilføjet fakturalinjer.' - invoice_entry_invoice_on_record_cannot_edit_entries: 'Fakturaen er bogført og kan derfor ikke redigeres.' - invoice_entry_invoice_on_record_cannot_delete_entries: 'Fakturaen er bogført og kan derfor ikke slettes.' - invoice_entry_not_worklog_type: 'Fakturalinjen er ikke af worklog typen.' - invoice_entry_project_not_set_on_invoice: 'Projekt er ikke sat på fakturaen.' - worklog_not_found: 'Worklog blev ikke fundet.' - project_billing_on_record_cannot_delete: 'Projektfaktureringen er bogført og kan derfor ikke slettes.' - project_billing_on_record_cannot_edit: 'Projektfaktureringen er bogført og kan derfor ikke redigeres.' - project_billing_invoice_on_record_cannot_delete: 'Projektfaktureringen er bogført og derfor kan fakturaen ikke slettes.' - project_billing_no_entity_found: 'Projektfakturering ikke fundet.' - project_billing_no_project_project_tracker_id: 'Projektets project tracker id ikke fundet.' - project_billing_no_project_selected: 'Projekt ikke valgt.' - project_billing_period_cannot_be_null: 'Perioden skal være valgt.' - billing_cannot_put_invoice_on_record_errors_found: 'Kan ikke bogføre fakturaen %invoiceName% (%invoiceId%). Følgende fejl er ikke håndteret: %errors%' - invoice_client_must_be_set: 'Kundekonto skal være valgt.' - invoice_errors_exit_cannot_put_on_record: 'Der er problemer med projektfaktureringen. Kan ikke bogføre den.' + invoices_record_entry_amount_not_set: "Faktura can ikke bogføres, da den indeholder fakturalinjer hvor antal ikke er sat. Se: %invoiceEntryUrl%." + invoices_record_part_of_project_billing: "Fakturaen er en del af en projektfakturering og kan kun bogføres separat." + invoices_export_must_be_on_record: "Faktura skal være en bogført før den kan eksporteres til .csv." + invoices_export_part_of_project_billing: "Faktura kan ikke eksporteres da den er en del af en projektfakturering." + invoices_export_failure: "Eksport fejlede." + invoices_delete_on_record: "Faktura kan ikke slettes, da den er bogført." + invoices_delete_part_of_project_billing: "Faktura kan ikke slettes, da den er en del af projektfakturering." + invoices_export_failure_could_not_generate_html: "Faktura kan ikke eksporteres. HTML eksport fejlede." + project_not_found: "Projektet blev ikke fundet." + project_tracker_id_not_set: "Project Tracker id er ikke sat." + invoices_part_of_project_billing_cannot_edit: "Fakturaen er en del af en projektfakturering og kan derfor ikke redigeres." + invoices_on_record_cannot_edit: "Fakturaen er bogført og kan derfor ikke redigeres." + invoice_entry_invoice_on_record_cannot_add_entries: "Fakturaen er bogført og kan derfor ikke få tilføjet fakturalinjer." + invoice_entry_invoice_on_record_cannot_edit_entries: "Fakturaen er bogført og kan derfor ikke redigeres." + invoice_entry_invoice_on_record_cannot_delete_entries: "Fakturaen er bogført og kan derfor ikke slettes." + invoice_entry_not_worklog_type: "Fakturalinjen er ikke af worklog typen." + invoice_entry_project_not_set_on_invoice: "Projekt er ikke sat på fakturaen." + worklog_not_found: "Worklog blev ikke fundet." + project_billing_on_record_cannot_delete: "Projektfaktureringen er bogført og kan derfor ikke slettes." + project_billing_on_record_cannot_edit: "Projektfaktureringen er bogført og kan derfor ikke redigeres." + project_billing_invoice_on_record_cannot_delete: "Projektfaktureringen er bogført og derfor kan fakturaen ikke slettes." + project_billing_no_entity_found: "Projektfakturering ikke fundet." + project_billing_no_project_project_tracker_id: "Projektets project tracker id ikke fundet." + project_billing_no_project_selected: "Projekt ikke valgt." + project_billing_period_cannot_be_null: "Perioden skal være valgt." + billing_cannot_put_invoice_on_record_errors_found: "Kan ikke bogføre fakturaen %invoiceName% (%invoiceId%). Følgende fejl er ikke håndteret: %errors%" + invoice_client_must_be_set: "Kunde skal være valgt." + invoice_errors_exit_cannot_put_on_record: "Der er problemer med projektfaktureringen. Kan ikke bogføre den." error: - title: 'Der opstod en fejl' - code: 'Fejlkode' - message: 'Besked' + title: "Der opstod en fejl" + code: "Fejlkode" + message: "Besked" + +clients: + action_delete_account: "Slet kunde" + edit: "Redigér kunde" + action_save: "Redigér" + action_create_new: "Opret ny kunde" + id: "#" + name: "Navn" + contact: "Kontakt" + standard_price: "Standardpris" + type: "Type" + account: "Konto" + psp: "PSP" + ean: "EAN" + sales_channel: "Salgskanal" + customer_key: "Ordregiver/Bestiller" + actions: "Handlinger" + action_edit: "Redigér" + list_no_records_found: "Ingen kunder" + +delete: + confirm: "Er du sikker på at du vil slette dette?" + +client_type_enum: + internal: "Intern" + external: "Ekstern"