getInitializedConfigurations(); $configurations = []; foreach ($configSettings as $configModel) { $configurations[] = [ 'model' => $configModel, 'form' => $this->createConfigurationsForm($configModel)->createView(), ]; } $page = new PageSetup('menu.system_configuration'); $page->setHelp('configurations.html'); return $this->render('system-configuration/index.html.twig', [ 'page_setup' => $page, 'sections' => $configurations, ]); } #[Route(path: '/edit/{section}', name: 'system_configuration_section', methods: ['GET'])] public function sectionAction(string $section): Response { $configSettings = $this->getInitializedConfigurations(); $configurations = []; foreach ($configSettings as $configModel) { if ($configModel->getSection() !== $section) { continue; } $configurations[] = [ 'model' => $configModel, 'form' => $this->createConfigurationsForm($configModel, true)->createView(), ]; } $page = new PageSetup('menu.system_configuration'); $page->setHelp('configurations.html'); return $this->render('system-configuration/section.html.twig', [ 'page_setup' => $page, 'sections' => $configurations, ]); } /** * @internal do not link directly to this route * @param Request $request * @param string $section * @return RedirectResponse|Response */ #[Route(path: '/update/{section}/{single}', defaults: ['single' => 0], name: 'system_configuration_update', methods: ['POST'])] public function configUpdate(Request $request, string $section, string $single): Response { $single = (bool) $single; $configModel = null; $configSettings = $this->getInitializedConfigurations(); foreach ($configSettings as $model) { if ($model->getSection() === $section) { $configModel = $model; break; } } if (null === $configModel) { throw $this->createNotFoundException('Could not find config model: ' . $section); } $form = $this->createConfigurationsForm($configModel, $single); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { try { /** @var SystemConfigurationModel $saveModel */ $saveModel = $form->getData(); $this->repository->saveSystemConfiguration($saveModel); $this->flashSuccess('action.update.success'); } catch (\Exception $ex) { $this->handleFormUpdateException($ex, $form); } if ($single) { return $this->redirectToRoute('system_configuration_section', ['section' => $section]); } return $this->redirectToRoute('system_configuration'); } $configSettings = $this->getInitializedConfigurations(); $configurations = []; foreach ($configSettings as $model) { if ($single && $section !== $model->getSection()) { continue; } if ($section !== $model->getSection()) { $form2 = $this->createConfigurationsForm($model, $single); } else { $form2 = $form; } $configurations[] = [ 'model' => $model, 'form' => $form2->createView(), ]; } return $this->render('system-configuration/index.html.twig', [ 'sections' => $configurations, ]); } private function createConfigurationsForm(SystemConfigurationModel $configuration, bool $isSingleSection = false): FormInterface { $options = [ 'action' => $this->generateUrl('system_configuration_update', ['section' => $configuration->getSection(), 'single' => $isSingleSection ? '1' : '0']), 'method' => 'POST', ]; return $this->container ->get('form.factory') ->createNamedBuilder('system_configuration_form_' . $configuration->getSection(), SystemConfigurationForm::class, $configuration, $options) ->getForm(); } /** * @return SystemConfigurationModel[] */ private function getInitializedConfigurations(): array { $types = $this->getConfigurationTypes(); $event = new SystemConfigurationEvent($types); $this->eventDispatcher->dispatch($event); foreach ($event->getConfigurations() as $configs) { foreach ($configs->getConfiguration() as $config) { if (!$this->systemConfiguration->has($config->getName())) { continue; } $configValue = $this->systemConfiguration->find($config->getName()); if (null !== $configValue) { $config->setValue($configValue); } } } return $event->getConfigurations(); } /** * @return SystemConfigurationModel[] */ private function getConfigurationTypes(): array { $user = $this->getUser(); $lockdownStartHelp = null; $lockdownEndHelp = null; $lockdownGraceHelp = null; $dateFormat = 'D, d M Y H:i:s'; if ($this->lockdownService->isLockdownActive()) { try { $start = $this->lockdownService->getLockdownStart($user); if ($start !== null) { $lockdownStartHelp = $start->format($dateFormat); } $end = $this->lockdownService->getLockdownEnd($user); if ($end !== null) { $lockdownEndHelp = $end->format($dateFormat); } $grace = $this->lockdownService->getLockdownGrace($user); if ($grace !== null) { $lockdownGraceHelp = $grace->format($dateFormat); } } catch (\Exception $ex) { $lockdownStartHelp = 'invalid'; } } $authentication = (new SystemConfigurationModel('authentication')) ->setConfiguration([ (new Configuration('user.login')) ->setLabel('user_auth_login') ->setTranslationDomain('system-configuration') ->setType(YesNoType::class), (new Configuration('user.registration')) ->setLabel('user_auth_registration') ->setTranslationDomain('system-configuration') ->setType(YesNoType::class), (new Configuration('user.password_reset')) ->setTranslationDomain('system-configuration') ->setLabel('user_auth_password_reset') ->setType(YesNoType::class), (new Configuration('user.password_reset_retry_ttl')) ->setTranslationDomain('system-configuration') ->setLabel('user_auth_password_reset_retry_ttl') ->setConstraints([new NotNull(), new GreaterThanOrEqual(['value' => 60])]) ->setType(IntegerType::class), (new Configuration('user.password_reset_token_ttl')) ->setTranslationDomain('system-configuration') ->setLabel('user_auth_password_reset_token_ttl') ->setConstraints([new NotNull(), new GreaterThanOrEqual(['value' => 60])]) ->setType(IntegerType::class), ]); $allowRegistration = $this->systemConfiguration->find('features.user_registration'); if ($allowRegistration === false) { $authentication->getConfigurationByName('user.registration')?->setEnabled(false); } if (!$this->systemConfiguration->isSamlActive()) { $authentication->getConfigurationByName('user.login')?->setEnabled(false); } if (!$this->systemConfiguration->isPasswordResetActive()) { $authentication->getConfigurationByName('user.password_reset_retry_ttl')?->setEnabled(false); $authentication->getConfigurationByName('user.password_reset_token_ttl')?->setEnabled(false); } return [ (new SystemConfigurationModel('timesheet')) ->setConfiguration([ (new Configuration('timesheet.mode')) ->setType(TrackingModeType::class) ->setTranslationDomain('system-configuration'), (new Configuration('timesheet.default_begin')) ->setType(DateTimeTextType::class) ->setConstraints([new DateTimeFormat(), new NotNull()]) ->setTranslationDomain('system-configuration'), (new Configuration('timesheet.rules.allow_future_times')) ->setType(YesNoType::class) ->setTranslationDomain('system-configuration'), (new Configuration('timesheet.rules.allow_zero_duration')) ->setType(YesNoType::class) ->setTranslationDomain('system-configuration'), (new Configuration('timesheet.rules.allow_overlapping_records')) ->setType(YesNoType::class) ->setTranslationDomain('system-configuration'), (new Configuration('timesheet.rules.allow_overbooking_budget')) ->setType(YesNoType::class) ->setTranslationDomain('system-configuration'), (new Configuration('timesheet.active_entries.hard_limit')) ->setType(IntegerType::class) ->setTranslationDomain('system-configuration') ->setConstraints([ new GreaterThanOrEqual(['value' => 1]) ]), (new Configuration('timesheet.time_increment')) ->setType(MinuteIncrementType::class) ->setTranslationDomain('system-configuration') ->setConstraints([ new Range(['min' => 0, 'max' => 60]) ]), (new Configuration('timesheet.duration_increment')) ->setType(MinuteIncrementType::class) ->setTranslationDomain('system-configuration') ->setConstraints([ new GreaterThanOrEqual(['value' => 0]) ]), /* (new Configuration('timesheet.rules.break_warning_duration')) ->setType(IntegerType::class) ->setTranslationDomain('system-configuration') ->setConstraints([ new GreaterThanOrEqual(['value' => 0]) ]), */ (new Configuration('timesheet.rules.long_running_duration')) ->setType(IntegerType::class) ->setTranslationDomain('system-configuration') ->setConstraints([ new GreaterThanOrEqual(['value' => 0]) ]), ]), (new SystemConfigurationModel('quick_entry')) ->setTranslation('quick_entry.title') ->setTranslationDomain('messages') ->setConfiguration([ (new Configuration('quick_entry.recent_activities')) ->setType(IntegerType::class) ->setTranslationDomain('system-configuration') ->setRequired(false) ->setConstraints([ new Range(['min' => 0, 'max' => 20]), ]), (new Configuration('quick_entry.recent_activity_weeks')) ->setType(IntegerType::class) ->setTranslationDomain('system-configuration') ->setRequired(false) ->setConstraints([ new Range(['min' => 0, 'max' => 20]), ]), (new Configuration('quick_entry.minimum_rows')) ->setType(IntegerType::class) ->setTranslationDomain('system-configuration') ->setConstraints([ new Range(['min' => 1, 'max' => 5]), ]), ]), (new SystemConfigurationModel('lockdown_period')) ->setConfiguration([ (new Configuration('timesheet.rules.lockdown_period_start')) ->setOptions(['help' => $lockdownStartHelp]) ->setType(TextType::class) ->setRequired(false) ->setConstraints([new DateTimeFormat(['separator' => ','])]) ->setTranslationDomain('system-configuration'), (new Configuration('timesheet.rules.lockdown_period_end')) ->setOptions(['help' => $lockdownEndHelp]) ->setType(TextType::class) ->setRequired(false) ->setConstraints([new DateTimeFormat(['separator' => ','])]) ->setTranslationDomain('system-configuration'), (new Configuration('timesheet.rules.lockdown_period_timezone')) ->setType(TimezoneType::class) ->setRequired(false) ->setTranslationDomain('system-configuration'), (new Configuration('timesheet.rules.lockdown_grace_period')) ->setOptions(['help' => $lockdownGraceHelp]) ->setType(TextType::class) ->setRequired(false) ->setConstraints([new DateTimeFormat()]) ->setTranslationDomain('system-configuration'), ]), (new SystemConfigurationModel('rounding')) ->setConfiguration([ (new Configuration('timesheet.rounding.default.mode')) ->setType(RoundingModeType::class) ->setTranslationDomain('system-configuration'), (new Configuration('timesheet.rounding.default.begin')) ->setType(IntegerType::class) ->setTranslationDomain('system-configuration') ->setConstraints([ new GreaterThanOrEqual(['value' => 0]) ]), (new Configuration('timesheet.rounding.default.end')) ->setType(IntegerType::class) ->setTranslationDomain('system-configuration') ->setConstraints([ new GreaterThanOrEqual(['value' => 0]) ]), (new Configuration('timesheet.rounding.default.duration')) ->setType(IntegerType::class) ->setTranslationDomain('system-configuration') ->setConstraints([ new GreaterThanOrEqual(['value' => 0]) ]), (new Configuration('timesheet.rounding.default.days')) ->setType(WeekDaysType::class) ->setRequired(false) ->setTranslationDomain('system-configuration'), ]), (new SystemConfigurationModel('invoice')) ->setTranslation('invoices') ->setTranslationDomain('messages') ->setConfiguration([ // TODO that should be a custom type with validation (new Configuration('invoice.number_format')) ->setLabel('invoice.number_format') ->setOptions([ 'help' => 'allowed_replacer', 'help_translation_parameters' => [ '%replacer%' => '{Y}, {y}, {M}, {m}, {D}, {d}, {date}, {cc}, {ccy}, {ccm}, {ccd}, {cu}, {cuy}, {cum}, {cud}, {ustaff}, {uid}, {c}, {cy}, {cm}, {cd}, {cname}, {cnumber}' ] ]) ->setRequired(true) ->setType(TextType::class) ->setConstraints([new NotBlank()]) ->setTranslationDomain('system-configuration'), ]), $authentication, (new SystemConfigurationModel('customer')) ->setConfiguration([ (new Configuration('defaults.customer.timezone')) ->setLabel('timezone') ->setType(TimezoneType::class) ->setValue(date_default_timezone_get()) ->setOptions(['help' => 'default_value_new']), (new Configuration('defaults.customer.country')) ->setLabel('country') ->setType(CountryType::class) ->setOptions(['help' => 'default_value_new']), (new Configuration('defaults.customer.currency')) ->setLabel('currency') ->setType(CurrencyType::class) ->setOptions(['help' => 'default_value_new']), (new Configuration('customer.choice_pattern')) ->setLabel('choice_pattern') ->setType(CustomerTypePatternType::class), (new Configuration('customer.number_format')) ->setLabel('customer.number_format') ->setOptions(['help' => 'allowed_replacer', 'help_translation_parameters' => ['%replacer%' => '{cc}']]) ->setRequired(true) ->setType(TextType::class) ->setTranslationDomain('system-configuration'), (new Configuration('customer.rules.allow_duplicate_number')) ->setLabel('customer.allow_duplicate_number') ->setType(YesNoType::class) ->setTranslationDomain('system-configuration'), ]), (new SystemConfigurationModel('project')) ->setConfiguration([ (new Configuration('project.choice_pattern')) ->setLabel('choice_pattern') ->setType(ProjectTypePatternType::class), (new Configuration('project.copy_teams_on_create')) ->setLabel('copy_teams_on_create') ->setType(YesNoType::class) ->setTranslationDomain('system-configuration'), ]), (new SystemConfigurationModel('activity')) ->setConfiguration([ (new Configuration('activity.choice_pattern')) ->setLabel('choice_pattern') ->setType(ActivityTypePatternType::class), // TODO see DependencyInjection/Configuration::getActivityNode() /* (new Configuration('activity.allow_inline_create')) ->setLabel('activity.allow_inline_create') ->setType(YesNoType::class) ->setTranslationDomain('system-configuration'), */ ]), (new SystemConfigurationModel('user')) ->setConfiguration([ (new Configuration('defaults.user.timezone')) ->setLabel('timezone') ->setType(TimezoneType::class) ->setValue(date_default_timezone_get()) ->setOptions(['help' => 'default_value_new']), (new Configuration('defaults.user.language')) ->setLabel('language') ->setType(LanguageType::class) ->setOptions(['help' => 'default_value_new']), (new Configuration('defaults.user.theme')) ->setLabel('skin') ->setType(SkinType::class) ->setOptions(['help' => 'default_value_new']), (new Configuration('defaults.user.currency')) ->setLabel('currency') ->setType(CurrencyType::class), (new Configuration('theme.avatar_url')) ->setRequired(false) ->setLabel('theme.avatar_url') ->setType(YesNoType::class) ->setTranslationDomain('system-configuration'), ]), (new SystemConfigurationModel('theme')) ->setConfiguration([ (new Configuration('timesheet.markdown_content')) ->setLabel('theme.markdown_content') ->setType(YesNoType::class) ->setTranslationDomain('system-configuration'), (new Configuration('theme.color_choices')) ->setRequired(false) ->setLabel('theme.color_choices') ->setType(ArrayToCommaStringType::class) ->setOptions(['help' => 'help.theme.color_choices']) ->setConstraints([new ColorChoices()]) ->setTranslationDomain('system-configuration'), ]), (new SystemConfigurationModel('calendar')) ->setTranslation('calendar') ->setTranslationDomain('messages') ->setConfiguration([ (new Configuration('calendar.week_numbers')) ->setTranslationDomain('system-configuration') ->setType(YesNoType::class), (new Configuration('calendar.weekends')) ->setTranslationDomain('system-configuration') ->setType(YesNoType::class), (new Configuration('calendar.businessHours.begin')) ->setTranslationDomain('system-configuration') ->setType(DayTimeType::class) ->setConstraints([new NotBlank(), new TimeFormat()]), (new Configuration('calendar.businessHours.end')) ->setTranslationDomain('system-configuration') ->setType(DayTimeType::class) ->setConstraints([new NotBlank(), new TimeFormat()]), (new Configuration('calendar.visibleHours.begin')) ->setTranslationDomain('system-configuration') ->setType(DayTimeType::class) ->setConstraints([new NotBlank(), new TimeFormat()]), (new Configuration('calendar.visibleHours.end')) ->setTranslationDomain('system-configuration') ->setType(DayTimeType::class) ->setConstraints([new NotBlank(), new TimeFormat()]), (new Configuration('calendar.slot_duration')) ->setTranslationDomain('system-configuration') ->setType(TextType::class) ->setConstraints([new Regex(['pattern' => '/[0-2]{1}[0-9]{1}:[0-9]{2}:[0-9]{2}/']), new NotNull()]), (new Configuration('calendar.dragdrop_amount')) ->setTranslationDomain('system-configuration') ->setType(IntegerType::class) ->setConstraints([new Range(['min' => 0, 'max' => 20]), new NotNull()]), (new Configuration('calendar.dragdrop_data')) ->setTranslationDomain('system-configuration') ->setType(YesNoType::class), (new Configuration('calendar.title_pattern')) ->setTranslationDomain('system-configuration') ->setType(CalendarTitlePatternType::class), ]), (new SystemConfigurationModel('branding')) ->setConfiguration([ (new Configuration('theme.branding.logo')) ->setTranslationDomain('system-configuration') ->setRequired(false) ->setType(TextType::class), (new Configuration('theme.branding.company')) ->setTranslationDomain('system-configuration') ->setRequired(false) ->setType(TextType::class), (new Configuration('company.financial_year')) ->setTranslationDomain('system-configuration') ->setRequired(false) ->setType(DatePickerType::class) ->setOptions(['input' => 'string']), ]), ]; } }