├── public ├── jobs.svg └── dashboard.css ├── contao ├── templates │ └── _new │ │ ├── .twig-root │ │ ├── jobs_basic │ │ ├── plenta_jobs_basic_form.html.twig │ │ ├── plenta_jobs_basic_job_offer_empty.html.twig │ │ └── plenta_jobs_basic_offer_default.html.twig │ │ ├── jobs_basic_reader_parts │ │ ├── plenta_jobs_basic_reader_image.html.twig │ │ ├── plenta_jobs_basic_reader_attribute.html.twig │ │ ├── plenta_jobs_basic_reader_teaser.html.twig │ │ ├── plenta_jobs_basic_reader_description.html.twig │ │ ├── plenta_jobs_basic_reader_salary.html.twig │ │ └── plenta_jobs_basic_reader_job_location.html.twig │ │ ├── content_element │ │ ├── plenta_jobs_basic_job_offer_details.html.twig │ │ └── plenta_jobs_basic_job_offer_teaser.html.twig │ │ └── frontend_module │ │ ├── plenta_jobs_basic_offer_reader.html.twig │ │ ├── plenta_jobs_basic_offer_list.html.twig │ │ └── plenta_jobs_basic_filter.html.twig ├── languages │ ├── de │ │ ├── tl_user_group.xlf │ │ ├── tl_plenta_jobs_basic_settings_employment_type.xlf │ │ ├── tl_plenta_jobs_basic_organization.xlf │ │ ├── tl_content.xlf │ │ ├── modules.xlf │ │ ├── default.xlf │ │ └── tl_plenta_jobs_basic_job_location.xlf │ └── en │ │ ├── tl_module.xlf │ │ ├── default.xlf │ │ └── modules.xlf ├── dca │ ├── tl_user_group.php │ ├── tl_content.php │ ├── tl_plenta_jobs_basic_organization.php │ ├── tl_plenta_jobs_basic_settings_employment_type.php │ └── tl_plenta_jobs_basic_job_location.php └── config │ └── config.php ├── docs ├── contao-jobs-basic-backend-offer.png └── contao-jobs-basic-backend-listing.png ├── config ├── config.yml ├── routing.yml ├── listener.yml └── services.yml ├── src ├── Contao │ └── Model │ │ ├── PlentaJobsBasicOrganizationModel.php │ │ ├── PlentaJobsBasicSettingsEmploymentTypeModel.php │ │ └── PlentaJobsBasicJobLocationModel.php ├── Events │ ├── JobOfferModelKeywordFieldsEvent.php │ ├── JobOfferListAfterFormBuildEvent.php │ ├── JobOfferFilterAfterFormBuildEvent.php │ ├── JobOfferDataManipulatorEvent.php │ ├── JobOfferReaderContentPartEvent.php │ ├── Model │ │ └── FindAllPublishedByTypesAndLocationEvent.php │ ├── JobOfferListBeforeParseTemplateEvent.php │ └── JobOfferReaderBeforeParseTemplateEvent.php ├── EventListener │ └── Contao │ │ ├── DCA │ │ ├── JobOfferFields.php │ │ ├── TlContent.php │ │ ├── TlPlentaJobsBasicJobLocation.php │ │ ├── SetPtableForContentListener.php │ │ ├── TlPlentaJobsBasicSettingsEmploymentType.php │ │ ├── TlModule.php │ │ └── TlPlentaJobsBasicOffer.php │ │ ├── Hooks │ │ └── ChangelanguageNavigationListener.php │ │ ├── SitemapListener.php │ │ ├── BackendMenuListener.php │ │ └── InsertTagListener.php ├── Controller │ ├── JobsOfferFilterRequestController.php │ └── Contao │ │ ├── BackendController.php │ │ ├── AjaxController.php │ │ ├── BackendModule │ │ └── SettingsController.php │ │ └── ContentElement │ │ ├── PlentaJobsBasicJobOfferTeaserController.php │ │ └── PlentaJobsBasicJobOfferDetailsController.php ├── Helper │ ├── SortingHelper.php │ ├── PermissionsHelper.php │ ├── DataTypeMapper.php │ ├── NumberHelper.php │ ├── CountJobsHelper.php │ ├── EmploymentType.php │ └── MetaFieldsHelper.php ├── PlentaContaoJobsBasicBundle.php ├── Form │ └── Type │ │ ├── HtmlType.php │ │ ├── ContaoRequestTokenType.php │ │ ├── JobSortingType.php │ │ └── JobOfferFilterType.php ├── Csrf │ └── JobsBasicCsrfTokenManager.php ├── DependencyInjection │ └── PlentaContaoJobsBasicExtension.php ├── Migration │ ├── JobLocationCountryUppercaseMigration.php │ ├── RemoveFkConstraintsMigration.php │ ├── BoolCharToIntMigration.php │ ├── RenameDatabaseColumnsMigration.php │ ├── RefactorTranslationsMigration.php │ └── MoveRemoteJobsMigration.php ├── InsertTag │ └── JobsCountInsertTag.php └── ContaoManager │ └── Plugin.php ├── templates ├── be_plenta_jobs_basic_settings.html.twig ├── be_plenta_info.html.twig └── forms │ └── form_layout.html.twig ├── README.md ├── composer.json └── LICENSE /public/jobs.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /contao/templates/_new/.twig-root: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /contao/templates/_new/jobs_basic/plenta_jobs_basic_form.html.twig: -------------------------------------------------------------------------------- 1 | {{ form(form) }} -------------------------------------------------------------------------------- /docs/contao-jobs-basic-backend-offer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plenta/contao-jobs-basic-bundle/HEAD/docs/contao-jobs-basic-backend-offer.png -------------------------------------------------------------------------------- /docs/contao-jobs-basic-backend-listing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plenta/contao-jobs-basic-bundle/HEAD/docs/contao-jobs-basic-backend-listing.png -------------------------------------------------------------------------------- /config/config.yml: -------------------------------------------------------------------------------- 1 | twig: 2 | form_themes: ['@PlentaContaoJobsBasic/forms/form_layout.html.twig'] 3 | globals: 4 | backend_route: '%contao.backend.route_prefix%' -------------------------------------------------------------------------------- /contao/templates/_new/jobs_basic_reader_parts/plenta_jobs_basic_reader_image.html.twig: -------------------------------------------------------------------------------- 1 | {% block content %} 2 | {{ content_element('image', data) }} 3 | {% endblock %} -------------------------------------------------------------------------------- /contao/templates/_new/content_element/plenta_jobs_basic_job_offer_details.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@Contao/content_element/_base.html.twig' %} 2 | 3 | {% block content %} 4 | {{ content|default('')|raw }} 5 | {% endblock %} -------------------------------------------------------------------------------- /contao/templates/_new/jobs_basic/plenta_jobs_basic_job_offer_empty.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@Contao/frontend_module/_base.html.twig' %} 2 | 3 | {% block content %} 4 |

{{ empty }}

5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /contao/templates/_new/jobs_basic_reader_parts/plenta_jobs_basic_reader_attribute.html.twig: -------------------------------------------------------------------------------- 1 | {% block content %} 2 | {{ label }}: {{ value }}

3 | {% endblock %} -------------------------------------------------------------------------------- /contao/templates/_new/jobs_basic_reader_parts/plenta_jobs_basic_reader_teaser.html.twig: -------------------------------------------------------------------------------- 1 | {% block content %} 2 | {{ content_element('text', { 3 | text: text, 4 | id: null, 5 | cssID: ['', class], 6 | }) }} 7 | {% endblock %} -------------------------------------------------------------------------------- /contao/templates/_new/jobs_basic_reader_parts/plenta_jobs_basic_reader_description.html.twig: -------------------------------------------------------------------------------- 1 | {% block content %} 2 | {{ content_element('text', { 3 | text: text, 4 | id: null, 5 | cssID: ['', class], 6 | }) }} 7 | {% endblock %} -------------------------------------------------------------------------------- /contao/templates/_new/jobs_basic_reader_parts/plenta_jobs_basic_reader_salary.html.twig: -------------------------------------------------------------------------------- 1 | {% block content %} 2 |

3 | {{ 'tl_plenta_jobs_basic_offer.salaryValue.0'|trans({}, 'contao_tl_plenta_jobs_basic_offer') }}: {{ salary }}/{{ unit }} 4 |

5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /contao/templates/_new/frontend_module/plenta_jobs_basic_offer_reader.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@Contao/frontend_module/_base.html.twig' %} 2 | 3 | {% block content %} 4 | {{ content|raw }} 5 | 6 | {% if back|default %} 7 | 8 |

{{ back }}

9 | 10 | {% endif %} 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /src/Contao/Model/PlentaJobsBasicOrganizationModel.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Contao\Model; 14 | 15 | use Contao\Model; 16 | 17 | class PlentaJobsBasicOrganizationModel extends Model 18 | { 19 | protected static $strTable = 'tl_plenta_jobs_basic_organization'; 20 | } 21 | -------------------------------------------------------------------------------- /src/Contao/Model/PlentaJobsBasicSettingsEmploymentTypeModel.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Contao\Model; 14 | 15 | use Contao\Model; 16 | 17 | class PlentaJobsBasicSettingsEmploymentTypeModel extends Model 18 | { 19 | protected static $strTable = 'tl_plenta_jobs_basic_settings_employment_type'; 20 | } 21 | -------------------------------------------------------------------------------- /src/Events/JobOfferModelKeywordFieldsEvent.php: -------------------------------------------------------------------------------- 1 | fields; 18 | } 19 | 20 | public function setFields(array $fields): JobOfferModelKeywordFieldsEvent 21 | { 22 | $this->fields = $fields; 23 | return $this; 24 | } 25 | } -------------------------------------------------------------------------------- /config/routing.yml: -------------------------------------------------------------------------------- 1 | plenta_jobs_basic.offer_filter: 2 | path: /_plenta-jobs-basic/offer/filter 3 | defaults: 4 | _scope: frontend 5 | _token_check: false 6 | _controller: Plenta\ContaoJobsBasic\Controller\JobsOfferFilterRequestController::filterOffersAction 7 | 8 | Plenta\ContaoJobsBasic\Controller\Contao\BackendModule\SettingsController: 9 | path: /%contao.backend.route_prefix%/plentajobsbasic/settings 10 | defaults: 11 | _scope: backend 12 | _controller: Plenta\ContaoJobsBasic\Controller\Contao\BackendModule\SettingsController::showSettings 13 | 14 | jobs_basic: 15 | resource: ../src/Controller 16 | type: attribute 17 | -------------------------------------------------------------------------------- /public/dashboard.css: -------------------------------------------------------------------------------- 1 | .plenta-wrapper { 2 | display: flex; 3 | flex-wrap: wrap; 4 | } 5 | 6 | .plenta-box { 7 | flex: 50%; 8 | } 9 | 10 | .plenta-box a { 11 | text-decoration: underline; 12 | } 13 | 14 | .plenta-box a:hover { 15 | text-decoration: underline; 16 | color: var(--contao); 17 | } 18 | 19 | .plenta-box.last { 20 | text-align: right; 21 | } 22 | 23 | #plentaJobsInfoPanel { 24 | padding: 15px; 25 | border-bottom: 1px solid var(--blue); 26 | } 27 | 28 | html[data-color-scheme="dark"] { 29 | .plenta-box, 30 | .plenta-box a { 31 | color: #ffffff; 32 | } 33 | 34 | .plenta-box a:hover { 35 | color: var(--contao); 36 | } 37 | } -------------------------------------------------------------------------------- /templates/be_plenta_jobs_basic_settings.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "@Contao/be_main.html.twig" %} 2 | {% set type = '' %} 3 | 4 | {% block main_content %} 5 | {% include('@PlentaContaoJobsBasic/be_plenta_info.html.twig') %} 6 | 7 |
8 | 9 | 10 | {% for key, mod in mods %} 11 | 12 | 13 | 14 | {% endfor %} 15 | 16 |
{{ mod.title.0 }}
17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /contao/languages/de/tl_user_group.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Jobs (Basic) Module 7 | 8 | 9 | Hier können Sie den Zugriff auf ein oder mehrere Jobs (Basic) Module erlauben. 10 | 11 | 12 | Einstellungen 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/EventListener/Contao/DCA/JobOfferFields.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\EventListener\Contao\DCA; 14 | 15 | class JobOfferFields 16 | { 17 | public static function getFields() 18 | { 19 | return ['title', 'tstamp', 'datePosted', 'jobLocation']; 20 | } 21 | 22 | public static function getParts() 23 | { 24 | return ['image', 'teaser', 'company', 'jobLocation', 'publicationDate', 'employmentType']; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Contao/Model/PlentaJobsBasicJobLocationModel.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Contao\Model; 14 | 15 | use Contao\Model; 16 | 17 | class PlentaJobsBasicJobLocationModel extends Model 18 | { 19 | protected static $strTable = 'tl_plenta_jobs_basic_job_location'; 20 | 21 | public static function findByMultiplePids(array $pids) 22 | { 23 | $criteria = array_fill(0, count($pids), 'tl_plenta_jobs_basic_job_location.pid = ?'); 24 | 25 | $cols = ['(' . implode(' OR ', $criteria) . ')']; 26 | 27 | return self::findBy($cols, $pids); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Controller/JobsOfferFilterRequestController.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Controller; 14 | 15 | use Contao\Module; 16 | use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; 17 | use Symfony\Component\HttpFoundation\Request; 18 | use Symfony\Component\HttpFoundation\Response; 19 | 20 | class JobsOfferFilterRequestController extends AbstractController 21 | { 22 | public function filterOffersAction(Request $request): Response 23 | { 24 | $module = Module::getFrontendModule($request->get('id')); 25 | 26 | return new Response($module); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Helper/SortingHelper.php: -------------------------------------------------------------------------------- 1 | plentaJobsBasic_filterSort) { 12 | usort($options, function ($a, $b) use ($sorting) { 13 | return match ($sorting) { 14 | 'az' => strnatcasecmp($a['name'], $b['name']), 15 | 'za' => strnatcasecmp($b['name'], $a['name']), 16 | '09' => $a['count'] <=> $b['count'], 17 | '90' => $b['count'] <=> $a['count'], 18 | }; 19 | }); 20 | } 21 | 22 | foreach ($options as $option) { 23 | $return[$option['id']] = $option['name']; 24 | } 25 | 26 | return $return; 27 | } 28 | } -------------------------------------------------------------------------------- /src/PlentaContaoJobsBasicBundle.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic; 14 | 15 | use Plenta\ContaoJobsBasic\DependencyInjection\PlentaContaoJobsBasicExtension; 16 | use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; 17 | use Symfony\Component\HttpKernel\Bundle\AbstractBundle; 18 | use Symfony\Component\HttpKernel\Bundle\Bundle; 19 | 20 | /** 21 | * Configures the bundle. 22 | */ 23 | class PlentaContaoJobsBasicBundle extends AbstractBundle 24 | { 25 | public function getContainerExtension(): ?ExtensionInterface 26 | { 27 | return new PlentaContaoJobsBasicExtension(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /contao/dca/tl_user_group.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | use Contao\CoreBundle\DataContainer\PaletteManipulator; 14 | 15 | $GLOBALS['TL_DCA']['tl_user_group']['fields']['plenta_jobs_basic_settings'] = [ 16 | 'inputType' => 'checkbox', 17 | 'exclude' => true, 18 | 'options' => [ 19 | 'settings', 20 | ], 21 | 'reference' => &$GLOBALS['TL_LANG']['tl_user_group']['plenta_jobs_basic_settings'], 22 | 'eval' => [ 23 | 'multiple' => true, 24 | ], 25 | 'sql' => 'blob NULL', 26 | ]; 27 | 28 | PaletteManipulator::create() 29 | ->addField('plenta_jobs_basic_settings', 'modules', PaletteManipulator::POSITION_AFTER) 30 | ->applyToPalette('default', 'tl_user_group') 31 | ; 32 | -------------------------------------------------------------------------------- /src/Form/Type/HtmlType.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Form\Type; 14 | 15 | use Symfony\Component\Form\AbstractType; 16 | use Symfony\Component\Form\FormInterface; 17 | use Symfony\Component\Form\FormView; 18 | use Symfony\Component\OptionsResolver\OptionsResolver; 19 | 20 | class HtmlType extends AbstractType 21 | { 22 | public function configureOptions(OptionsResolver $resolver): void 23 | { 24 | $resolver->setDefaults([ 25 | 'html' => '', 26 | 'label' => false, 27 | ]); 28 | } 29 | 30 | public function buildView(FormView $view, FormInterface $form, array $options) 31 | { 32 | $view->vars['html'] = $options['html']; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Csrf/JobsBasicCsrfTokenManager.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Csrf; 14 | 15 | use Contao\CoreBundle\Csrf\ContaoCsrfTokenManager; 16 | 17 | class JobsBasicCsrfTokenManager 18 | { 19 | private ContaoCsrfTokenManager $csrfTokenManager; 20 | private string $csrfTokenName; 21 | 22 | public function __construct( 23 | ContaoCsrfTokenManager $csrfTokenManager, 24 | string $csrfTokenName 25 | ) { 26 | $this->csrfTokenName = $csrfTokenName; 27 | $this->csrfTokenManager = $csrfTokenManager; 28 | } 29 | 30 | public function generateToken(): string 31 | { 32 | return $this->csrfTokenManager->getToken($this->csrfTokenName)->getValue(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/EventListener/Contao/DCA/TlContent.php: -------------------------------------------------------------------------------- 1 | requestStack = $requestStack; 20 | } 21 | 22 | public function __invoke(DataContainer $dc = null): void 23 | { 24 | if (null === $dc || !$dc->id || 'edit' !== $this->requestStack->getCurrentRequest()->query->get('act')) { 25 | return; 26 | } 27 | 28 | $element = ContentModel::findById($dc->id); 29 | 30 | if (null === $element || 'plenta_jobs_basic_job_offer_teaser' !== $element->type) { 31 | return; 32 | } 33 | 34 | $GLOBALS['TL_DCA']['tl_content']['fields']['text']['eval']['mandatory'] = false; 35 | } 36 | } -------------------------------------------------------------------------------- /src/EventListener/Contao/DCA/TlPlentaJobsBasicJobLocation.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\EventListener\Contao\DCA; 14 | 15 | class TlPlentaJobsBasicJobLocation 16 | { 17 | public function listLocations(array $arrRow): string 18 | { 19 | if ($arrRow['title']) { 20 | return $arrRow['title']; 21 | } 22 | 23 | if ('onPremise' === $arrRow['jobTypeLocation']) { 24 | return '
'.$arrRow['addressLocality'].', '.$arrRow['postalCode'].(!empty($arrRow['streetAddress']) ? ', '.$arrRow['streetAddress'] : '').'
'; 25 | } 26 | 27 | $return = '
'.$GLOBALS['TL_LANG']['MSC']['PLENTA_JOBS']['remote']; 28 | 29 | $return .= ' ['.$arrRow['requirementValue'].']'; 30 | 31 | $return .= '
'; 32 | 33 | return $return; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Events/JobOfferListAfterFormBuildEvent.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Events; 14 | 15 | use Plenta\ContaoJobsBasic\Form\Type\JobOfferFilterType; 16 | use Symfony\Component\Form\FormInterface; 17 | use Symfony\Contracts\EventDispatcher\Event; 18 | 19 | class JobOfferListAfterFormBuildEvent extends Event 20 | { 21 | public const NAME = 'plenta_jobs_basic.job_offer_list.after_form_build'; 22 | 23 | protected FormInterface $form; 24 | 25 | /** 26 | * @return JobOfferFilterType 27 | */ 28 | public function getForm(): FormInterface 29 | { 30 | return $this->form; 31 | } 32 | 33 | /** 34 | * @param FormInterface $form 35 | * 36 | * @return JobOfferListAfterFormBuildEvent 37 | */ 38 | public function setForm(FormInterface $form): self 39 | { 40 | $this->form = $form; 41 | 42 | return $this; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Contao Jobs Basic Bundle 2 | 3 | The perfect solution for your job offer site. 4 | Ideal for job offers in multiple languages. 5 | 6 | Basic Job Manager for Contao (including Google Jobs) 7 | Open source and free. 🚀 8 | 9 | 👉 [Full documentation](https://plenta.io/contao-erweiterungen/jobs-basic) 10 | 11 | ## Installation 12 | 13 | ### Install using Contao Manager 14 | 15 | Search for **job** or **jobs** and you will find this extension. 16 | 17 | ### Install using Composer 18 | 19 | ```bash 20 | composer require plenta/contao-jobs-basic-bundle 21 | ``` 22 | 23 | 24 | ## System requirements 25 | 26 | - PHP: `^7.4 || ^8.1` 27 | - Contao: `^4.13` || `^5` 28 | - mvo/contao-group-widget `^1.4` 29 | 30 | ## Which version is right for you? 31 | 32 | | Contao Version | PHP Version | Jobs Version | 33 | |----------------|-------------|--------------| 34 | | 4.13.* | \>= 7.4 | 2.* | 35 | | 5.* | \>= 8.1 | 3.* | 36 | 37 | 38 | ## Screenshots 39 | 40 | ### Detail view 41 | 42 | ![Jobs-Backend-View](docs/contao-jobs-basic-backend-offer.png) 43 | 44 | ### Listing 45 | 46 | ![Jobs-Backend-View](docs/contao-jobs-basic-backend-listing.png) -------------------------------------------------------------------------------- /src/Helper/PermissionsHelper.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Helper; 14 | 15 | use Contao\BackendUser; 16 | use Contao\StringUtil; 17 | use Contao\UserGroupModel; 18 | 19 | class PermissionsHelper 20 | { 21 | public static function canAccessModule($name, $module = 'plenta_jobs_basic_settings'): bool 22 | { 23 | $user = BackendUser::getInstance(); 24 | 25 | if ($user->isAdmin) { 26 | return true; 27 | } 28 | if (!empty($user->groups)) { 29 | foreach ($user->groups as $groupId) { 30 | $group = UserGroupModel::findByIdOrAlias($groupId); 31 | $moduleArr = StringUtil::deserialize($group->{$module}); 32 | if (\is_array($moduleArr) && \in_array($name, $moduleArr, true)) { 33 | return true; 34 | } 35 | } 36 | } 37 | 38 | return false; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Helper/DataTypeMapper.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Helper; 14 | 15 | use Contao\CoreBundle\Framework\ContaoFramework; 16 | use Contao\StringUtil; 17 | 18 | class DataTypeMapper 19 | { 20 | private ContaoFramework $framework; 21 | 22 | public function __construct(ContaoFramework $framework) 23 | { 24 | $this->framework = $framework; 25 | } 26 | 27 | public function serializedToJson(string $serializedData): string 28 | { 29 | /** @var StringUtil $stringUtil */ 30 | $stringUtil = $this->framework->getAdapter(StringUtil::class); 31 | 32 | $data = $stringUtil->deserialize($serializedData); 33 | 34 | return json_encode($data); 35 | } 36 | 37 | public function jsonToSerialized(?string $jsonData): ?string 38 | { 39 | if (null === $jsonData) { 40 | return serialize([]); 41 | } 42 | 43 | return serialize(json_decode($jsonData, true)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/DependencyInjection/PlentaContaoJobsBasicExtension.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\DependencyInjection; 14 | 15 | use Exception; 16 | use Symfony\Component\Config\FileLocator; 17 | use Symfony\Component\DependencyInjection\ContainerBuilder; 18 | use Symfony\Component\DependencyInjection\Extension\Extension; 19 | use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; 20 | use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; 21 | 22 | class PlentaContaoJobsBasicExtension extends Extension 23 | { 24 | /** 25 | * @param array $configs 26 | * @param ContainerBuilder $container 27 | * 28 | * @throws Exception 29 | */ 30 | public function load(array $configs, ContainerBuilder $container): void 31 | { 32 | $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../../config')); 33 | $loader->load('services.yml'); 34 | $loader->load('listener.yml'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Controller/Contao/BackendController.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Controller\Contao; 14 | 15 | use Contao\CoreBundle\Controller\AbstractBackendController; 16 | use Contao\System; 17 | use Plenta\ContaoJobsBasic\Contao\Model\PlentaJobsBasicOfferModel; 18 | use Symfony\Component\HttpFoundation\Request; 19 | use Symfony\Component\HttpFoundation\RequestStack; 20 | use Symfony\Component\Routing\Attribute\Route; 21 | 22 | #[Route('%contao.backend.route_prefix%/_jobs', defaults: ['_scope' => 'backend'])] 23 | class BackendController extends AbstractBackendController 24 | { 25 | #[Route('/renewDatePosted', name: 'jobsBasic_renewDatePosted')] 26 | public function renewDatePosted(Request $request, RequestStack $requestStack) 27 | { 28 | $id = $request->get('id'); 29 | $objJobOffer = PlentaJobsBasicOfferModel::findByPk($id); 30 | $objJobOffer->datePosted = time(); 31 | $objJobOffer->save(); 32 | 33 | return $this->redirect(System::getReferer()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /config/listener.yml: -------------------------------------------------------------------------------- 1 | services: 2 | Plenta\ContaoJobsBasic\EventListener\Contao\DCA\TlPlentaJobsBasicOffer: 3 | public: true 4 | arguments: 5 | - '@Plenta\ContaoJobsBasic\Helper\EmploymentType' 6 | - '@contao.slug' 7 | - '@request_stack' 8 | - '@twig' 9 | 10 | Plenta\ContaoJobsBasic\EventListener\Contao\DCA\TlPlentaJobsBasicJobLocation: 11 | public: true 12 | 13 | Plenta\ContaoJobsBasic\EventListener\Contao\DCA\TlPlentaJobsBasicSettingsEmploymentType: 14 | public: true 15 | arguments: 16 | - '@Plenta\ContaoJobsBasic\Helper\DataTypeMapper' 17 | - '@Plenta\ContaoJobsBasic\Helper\EmploymentType' 18 | 19 | # Contao Hooks 20 | Plenta\ContaoJobsBasic\EventListener\Contao\DCA\SetPtableForContentListener: 21 | tags: 22 | - { name: contao.hook, hook: loadDataContainer, method: setPtableForContentListener, priority: 0 } 23 | arguments: 24 | - '@request_stack' 25 | - '@contao.routing.scope_matcher' 26 | 27 | Plenta\ContaoJobsBasic\EventListener\Contao\BackendMenuListener: 28 | public: true 29 | tags: 30 | - { name: kernel.event_listener, event: contao.backend_menu_build, priority: -255 } 31 | arguments: 32 | - '@router' 33 | - '@request_stack' 34 | -------------------------------------------------------------------------------- /src/Controller/Contao/AjaxController.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Controller\Contao; 14 | 15 | use Contao\CoreBundle\Controller\AbstractController; 16 | use Contao\CoreBundle\String\SimpleTokenParser; 17 | use Contao\ModuleModel; 18 | use Plenta\ContaoJobsBasic\Helper\CountJobsHelper; 19 | use Symfony\Component\HttpFoundation\Request; 20 | use Symfony\Component\HttpFoundation\Response; 21 | use Symfony\Component\Routing\Attribute\Route; 22 | 23 | #[Route('_plenta-jobs-basic/offer', defaults: ['_scope' => 'frontend'])] 24 | class AjaxController extends AbstractController 25 | { 26 | #[Route('/count', methods: ['GET'])] 27 | public function count(Request $request, CountJobsHelper $countJobsHelper, SimpleTokenParser $tokenParser) 28 | { 29 | $module = ModuleModel::findByPk($request->get('module')); 30 | 31 | if (!$module) { 32 | throw new \Exception('Module not found'); 33 | } 34 | 35 | $request->attributes->set('moduleModel', $module); 36 | 37 | return new Response($tokenParser->parse($module->plentaJobsBasicSubmit, ['count' => $countJobsHelper->countJobs()])); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Events/JobOfferFilterAfterFormBuildEvent.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Events; 14 | 15 | use Symfony\Component\Form\FormInterface; 16 | use Symfony\Component\Routing\Attribute\Route; 17 | use Symfony\Contracts\EventDispatcher\Event; 18 | 19 | class JobOfferFilterAfterFormBuildEvent extends Event 20 | { 21 | public const NAME = 'plenta_jobs_basic.job_offer_filter.after_form_build'; 22 | 23 | protected FormInterface $form; 24 | 25 | protected string $route; 26 | 27 | /** 28 | * @return FormInterface 29 | */ 30 | public function getForm(): FormInterface 31 | { 32 | return $this->form; 33 | } 34 | 35 | /** 36 | * @param FormInterface $form 37 | * 38 | * @return JobOfferListAfterFormBuildEvent 39 | */ 40 | public function setForm(FormInterface $form): self 41 | { 42 | $this->form = $form; 43 | 44 | return $this; 45 | } 46 | 47 | public function getRoute(): string 48 | { 49 | return $this->route; 50 | } 51 | 52 | public function setRoute(string $route): JobOfferFilterAfterFormBuildEvent 53 | { 54 | $this->route = $route; 55 | return $this; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /templates/be_plenta_info.html.twig: -------------------------------------------------------------------------------- 1 |
2 | 13 |
-------------------------------------------------------------------------------- /contao/languages/de/tl_plenta_jobs_basic_settings_employment_type.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Einstellungen 7 | 8 | 9 | Name 10 | 11 | 12 | Bitte geben Sie den Namen der Stellenart ein. 13 | 14 | 15 | Google For Jobs - Zuordung 16 | 17 | 18 | Bitte wählen Sie eine Zuordnung. 19 | 20 | 21 | Übersetzung 22 | 23 | 24 | Hier können Sie Übersetzungen anlegen. 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /contao/templates/_new/content_element/plenta_jobs_basic_job_offer_teaser.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@Contao/content_element/_base.html.twig' %} 2 | 3 | {% block content %} 4 | {% if data.text %} 5 |
6 | {{ data.text|raw }} 7 |
8 | {% endif %} 9 | {% if 'image' in parts %} 10 | {{ jobOfferMeta.image|raw }} 11 | {% endif %} 12 |
13 | <{{ data.plentaJobsBasicHeadlineTag }} class="title"> 14 | {{ jobOfferMeta.title }} 15 | 16 | {% if 'company' in parts %} 17 |
18 | {{ jobOfferMeta.company }} 19 |
20 | {% endif %} 21 | {% if 'jobLocation' in parts %} 22 |
23 | {{ jobOfferMeta.addressLocalityFormatted }} 24 |
25 | {% endif %} 26 | {% if 'teaser' in parts %} 27 |
28 | {{ jobOfferMeta.teaser }} 29 |
30 | {% endif %} 31 |
32 |
33 | {% if 'publicationDate' in parts %} 34 |
35 | {{ jobOfferMeta.publicationDateFormatted }} 36 |
37 | {% endif %} 38 | {% if 'employmentType' in parts %} 39 |
40 | {{ jobOfferMeta.employmentTypeFormatted }} 41 |
42 | {% endif %} 43 |
44 | {% endblock %} -------------------------------------------------------------------------------- /src/EventListener/Contao/DCA/SetPtableForContentListener.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\EventListener\Contao\DCA; 14 | 15 | use Contao\CoreBundle\Routing\ScopeMatcher; 16 | use Symfony\Component\HttpFoundation\RequestStack; 17 | 18 | class SetPtableForContentListener 19 | { 20 | private RequestStack $requestStack; 21 | private ScopeMatcher $scopeMatcher; 22 | 23 | public function __construct(RequestStack $requestStack, ScopeMatcher $scopeMatcher) 24 | { 25 | $this->requestStack = $requestStack; 26 | $this->scopeMatcher = $scopeMatcher; 27 | } 28 | 29 | public function setPtableForContentListener(string $table): void 30 | { 31 | // We only want to adjust the DCA of tl_content 32 | if ('tl_content' !== $table) { 33 | return; 34 | } 35 | 36 | $request = $this->requestStack->getCurrentRequest(); 37 | 38 | // Check if this is a back end request 39 | if (null === $request || !$this->scopeMatcher->isBackendRequest($request)) { 40 | return; 41 | } 42 | 43 | if ('plenta_jobs_basic_offers' === $request->query->get('do')) { 44 | $GLOBALS['TL_DCA']['tl_content']['config']['ptable'] = 'tl_plenta_jobs_basic_offer'; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /contao/languages/en/tl_module.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Modification date (ascending) 7 | 8 | 9 | Publication date (ascending) 10 | 11 | 12 | A-Z 13 | 14 | 15 | Modification date (descending) 16 | 17 | 18 | Publication date (descending) 19 | 20 | 21 | Z-A 22 | 23 | 24 | Job location (ascending) 25 | 26 | 27 | Job location (descending) 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Events/JobOfferDataManipulatorEvent.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Events; 14 | 15 | use Contao\Model; 16 | use Plenta\ContaoJobsBasic\Contao\Model\PlentaJobsBasicOfferModel; 17 | use Symfony\Contracts\EventDispatcher\Event; 18 | 19 | class JobOfferDataManipulatorEvent extends Event 20 | { 21 | public const NAME = 'plenta_jobs_basic.job_offer_list.offer_data_manipulator'; 22 | 23 | protected array $data; 24 | protected PlentaJobsBasicOfferModel $jobOffer; 25 | protected Model $model; 26 | 27 | public function __construct() 28 | { 29 | } 30 | 31 | public function getJob(): PlentaJobsBasicOfferModel 32 | { 33 | return $this->jobOffer; 34 | } 35 | 36 | public function setJob(PlentaJobsBasicOfferModel $jobOffer): self 37 | { 38 | $this->jobOffer = $jobOffer; 39 | 40 | return $this; 41 | } 42 | 43 | public function getData(): array 44 | { 45 | return $this->data; 46 | } 47 | 48 | public function setData(array $data): self 49 | { 50 | $this->data = $data; 51 | 52 | return $this; 53 | } 54 | 55 | public function setModel(Model $model): self 56 | { 57 | $this->model = $model; 58 | 59 | return $this; 60 | } 61 | 62 | public function getModel(): Model 63 | { 64 | return $this->model; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Form/Type/ContaoRequestTokenType.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Form\Type; 14 | 15 | use Plenta\ContaoJobsBasic\Csrf\JobsBasicCsrfTokenManager; 16 | use Symfony\Component\Form\AbstractType; 17 | use Symfony\Component\Form\Extension\Core\Type\HiddenType; 18 | use Symfony\Component\Form\FormInterface; 19 | use Symfony\Component\Form\FormView; 20 | use Symfony\Component\OptionsResolver\OptionsResolver; 21 | 22 | class ContaoRequestTokenType extends AbstractType 23 | { 24 | private JobsBasicCsrfTokenManager $csrfTokenManager; 25 | 26 | public function __construct( 27 | JobsBasicCsrfTokenManager $csrfTokenManager 28 | ) { 29 | $this->csrfTokenManager = $csrfTokenManager; 30 | } 31 | 32 | public function configureOptions(OptionsResolver $resolver): void 33 | { 34 | $resolver->setDefaults([ 35 | 'data' => $this->csrfTokenManager->generateToken(), 36 | 'empty_data' => $this->csrfTokenManager->generateToken(), 37 | 'mapped' => false, 38 | ]); 39 | } 40 | 41 | public function getParent(): string 42 | { 43 | return HiddenType::class; 44 | } 45 | 46 | public function buildView(FormView $view, FormInterface $form, array $options): void 47 | { 48 | parent::buildView($view, $form, $options); 49 | 50 | $view->vars['full_name'] = 'REQUEST_TOKEN'; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Migration/JobLocationCountryUppercaseMigration.php: -------------------------------------------------------------------------------- 1 | connection->createSchemaManager(); 34 | 35 | if (!$schemaManager->tablesExist(['tl_plenta_jobs_basic_job_location'])) { 36 | return false; 37 | } 38 | 39 | if (!isset($schemaManager->listTableColumns('tl_plenta_jobs_basic_job_location')['addresscountry'])) { 40 | return false; 41 | } 42 | 43 | $test = $this->connection->fetchOne('SELECT TRUE FROM tl_plenta_jobs_basic_job_location WHERE BINARY addressCountry!=BINARY UPPER(addressCountry) LIMIT 1'); 44 | 45 | return false !== $test; 46 | } 47 | 48 | public function run(): MigrationResult 49 | { 50 | $this->connection->executeStatement('UPDATE tl_plenta_jobs_basic_job_location SET addressCountry=UPPER(addressCountry) WHERE BINARY addressCountry!=BINARY UPPER(addressCountry)'); 51 | 52 | return $this->createResult(true); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/InsertTag/JobsCountInsertTag.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\InsertTag; 14 | 15 | use Contao\ArticleModel; 16 | use Contao\ContentModel; 17 | use Contao\CoreBundle\DependencyInjection\Attribute\AsInsertTag; 18 | use Contao\CoreBundle\InsertTag\InsertTagResult; 19 | use Contao\CoreBundle\InsertTag\ResolvedInsertTag; 20 | use Contao\CoreBundle\InsertTag\Resolver\InsertTagResolverNestedResolvedInterface; 21 | use Contao\CoreBundle\Routing\PageFinder; 22 | use Contao\ModuleModel; 23 | use Contao\StringUtil; 24 | use Plenta\ContaoJobsBasic\Contao\Model\PlentaJobsBasicJobLocationModel; 25 | use Plenta\ContaoJobsBasic\Contao\Model\PlentaJobsBasicOfferModel; 26 | use Plenta\ContaoJobsBasic\Controller\Contao\FrontendModule\JobOfferListController; 27 | use Plenta\ContaoJobsBasic\Helper\CountJobsHelper; 28 | use Symfony\Component\HttpFoundation\RequestStack; 29 | 30 | #[AsInsertTag('jobs')] 31 | class JobsCountInsertTag implements InsertTagResolverNestedResolvedInterface 32 | { 33 | public function __construct(protected CountJobsHelper $countJobsHelper) 34 | { 35 | } 36 | 37 | public function __invoke(ResolvedInsertTag $insertTag): InsertTagResult 38 | { 39 | if ('count' === $insertTag->getParameters()->get(0)) { 40 | $filtered = 'filtered' === $insertTag->getParameters()->get(1); 41 | 42 | return new InsertTagResult((string) $this->countJobsHelper->countJobs($filtered)); 43 | } 44 | 45 | return new InsertTagResult(''); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /config/services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | autowire: true 4 | autoconfigure: true 5 | public: false 6 | bind: 7 | $csrfTokenName: '%contao.csrf_token_name%' 8 | 9 | Plenta\ContaoJobsBasic\: 10 | resource: ../src/* 11 | exclude: ../src/{Entity,Migrations,Resources,Tests} 12 | 13 | Plenta\ContaoJobsBasic\Helper\EmploymentType: 14 | class: Plenta\ContaoJobsBasic\Helper\EmploymentType 15 | arguments: 16 | - '@translator' 17 | - '@request_stack' 18 | 19 | Plenta\ContaoJobsBasic\Helper\DataTypeMapper: 20 | class: Plenta\ContaoJobsBasic\Helper\DataTypeMapper 21 | arguments: 22 | - '@contao.framework' 23 | 24 | Plenta\ContaoJobsBasic\GoogleForJobs\GoogleForJobs: 25 | public: true 26 | class: Plenta\ContaoJobsBasic\GoogleForJobs\GoogleForJobs 27 | arguments: 28 | - '@contao.image.picture_factory' 29 | - '@contao.assets.files_context' 30 | - '@Plenta\ContaoJobsBasic\Helper\EmploymentType' 31 | - '%kernel.project_dir%' 32 | 33 | # Migrations 34 | Plenta\ContaoJobsBasic\Migration\RenameDatabaseColumnsMigration: 35 | public: true 36 | tags: 37 | - { name: contao.migration, priority: 0 } 38 | 39 | 40 | Plenta\ContaoJobsBasic\Migration\MoveRemoteJobsMigration: 41 | public: true 42 | tags: 43 | - { name: contao.migration, priority: 0 } 44 | 45 | Plenta\ContaoJobsBasic\Migration\JobLocationCountryUppercaseMigration: 46 | public: true 47 | tags: 48 | - { name: contao.migration, priority: 0 } 49 | 50 | Plenta\ContaoJobsBasic\EventListener\Contao\Hooks\ChangelanguageNavigationListener: 51 | public: true -------------------------------------------------------------------------------- /src/EventListener/Contao/DCA/TlPlentaJobsBasicSettingsEmploymentType.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\EventListener\Contao\DCA; 14 | 15 | use Contao\DataContainer; 16 | use Plenta\ContaoJobsBasic\Helper\DataTypeMapper; 17 | use Plenta\ContaoJobsBasic\Helper\EmploymentType; 18 | 19 | class TlPlentaJobsBasicSettingsEmploymentType 20 | { 21 | protected DataTypeMapper $dataTypeMapper; 22 | protected EmploymentType $employmentTypeHelper; 23 | 24 | public function __construct( 25 | DataTypeMapper $dataTypeMapper, 26 | EmploymentType $employmentTypeHelper 27 | ) { 28 | $this->dataTypeMapper = $dataTypeMapper; 29 | $this->employmentTypeHelper = $employmentTypeHelper; 30 | } 31 | 32 | public function translationSaveCallback($value, DataContainer $dc): string 33 | { 34 | return $this->dataTypeMapper->serializedToJson($value); 35 | } 36 | 37 | public function translationLoadCallback($value, DataContainer $dc): string 38 | { 39 | return $this->dataTypeMapper->jsonToSerialized($value); 40 | } 41 | 42 | public function googleForJobsMappingOptionsCallback(): array 43 | { 44 | $employmentTypes = $this->employmentTypeHelper->getGoogleForJobsEmploymentTypes(); 45 | 46 | $return = []; 47 | foreach ($employmentTypes as $employmentType) { 48 | $return[$employmentType] = $this->employmentTypeHelper->getEmploymentTypeName($employmentType); 49 | } 50 | 51 | return $return; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /contao/languages/de/tl_plenta_jobs_basic_organization.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Einstellungen 7 | 8 | 9 | Name 10 | 11 | 12 | Bitte geben Sie den Namen des Unternehmens ein. 13 | 14 | 15 | Unternehmens-Url 16 | 17 | 18 | Bitte geben Sie die Website-Adresse des Unternehmens ein. 19 | 20 | 21 | Firmenlogo 22 | 23 | 24 | Firmenlogo 25 | 26 | 27 | Bitte wählen Sie eine Datei aus der Dateiübersicht. 28 | 29 | 30 | Neues Unternehmen 31 | 32 | 33 | Ein neues Unternehmen anlegen 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/Form/Type/JobSortingType.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Form\Type; 14 | 15 | use Symfony\Component\Form\AbstractType; 16 | use Symfony\Component\Form\Extension\Core\Type\ChoiceType; 17 | use Symfony\Component\Form\FormBuilderInterface; 18 | use Symfony\Component\HttpFoundation\RequestStack; 19 | use Symfony\Component\OptionsResolver\OptionsResolver; 20 | 21 | class JobSortingType extends AbstractType 22 | { 23 | protected RequestStack $requestStack; 24 | 25 | public function __construct(RequestStack $requestStack) 26 | { 27 | $this->requestStack = $requestStack; 28 | } 29 | 30 | public function buildForm(FormBuilderInterface $builder, array $options): void 31 | { 32 | $request = $this->requestStack->getMainRequest(); 33 | $sortBy = $request->get('sortBy', 'title'); 34 | $order = $request->get('order', 'ASC'); 35 | 36 | $builder->add('REQUEST_TOKEN', ContaoRequestTokenType::class); 37 | $builder->add('sort', ChoiceType::class, [ 38 | 'choices' => $options['sortingOptions'], 39 | 'choice_label' => fn ($item) => 'tl_module.plentaJobsBasicSortingFields.fields.'.$item, 40 | 'translation_domain' => 'contao_tl_module', 41 | 'label' => false, 42 | 'data' => $sortBy.'__'.$order, 43 | ]); 44 | } 45 | 46 | public function getBlockPrefix() 47 | { 48 | return ''; 49 | } 50 | 51 | public function configureOptions(OptionsResolver $resolver): void 52 | { 53 | $resolver->setDefaults([ 54 | 'sortingOptions' => null, 55 | ]); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/EventListener/Contao/Hooks/ChangelanguageNavigationListener.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\EventListener\Contao\Hooks; 14 | 15 | use Contao\Config; 16 | use Contao\Input; 17 | use Plenta\ContaoJobsBasic\Contao\Model\PlentaJobsBasicOfferModel; 18 | use Symfony\Component\HttpFoundation\RequestStack; 19 | use Terminal42\ChangeLanguage\Event\ChangelanguageNavigationEvent; 20 | 21 | class ChangelanguageNavigationListener 22 | { 23 | protected RequestStack $requestStack; 24 | 25 | public function __construct(RequestStack $requestStack) 26 | { 27 | $this->requestStack = $requestStack; 28 | } 29 | 30 | public function onChangelanguageNavigation(ChangelanguageNavigationEvent $event): void 31 | { 32 | $targetRoot = $event->getNavigationItem()->getRootPage(); 33 | $language = $targetRoot->language; 34 | 35 | $alias = Input::get('auto_item', false, true); 36 | 37 | if ($alias) { 38 | $jobOffer = PlentaJobsBasicOfferModel::findPublishedByIdOrAlias($alias); 39 | 40 | if (null !== $jobOffer) { 41 | if ($targetRoot->rootIsFallback) { 42 | $newAlias = $jobOffer->alias; 43 | } else { 44 | $translation = $jobOffer->getTranslation($language); 45 | if (!$translation) { 46 | $newAlias = $jobOffer->alias; 47 | } else { 48 | $newAlias = $translation['alias']; 49 | } 50 | } 51 | 52 | $event->getUrlParameterBag()->setUrlAttribute('auto_item', $newAlias); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/ContaoManager/Plugin.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\ContaoManager; 14 | 15 | use Contao\CoreBundle\ContaoCoreBundle; 16 | use Contao\ManagerPlugin\Bundle\BundlePluginInterface; 17 | use Contao\ManagerPlugin\Bundle\Config\BundleConfig; 18 | use Contao\ManagerPlugin\Bundle\Parser\ParserInterface; 19 | use Contao\ManagerPlugin\Config\ConfigPluginInterface; 20 | use Contao\ManagerPlugin\Routing\RoutingPluginInterface; 21 | use Plenta\ContaoJobsBasic\PlentaContaoJobsBasicBundle; 22 | use Symfony\Component\Config\Loader\LoaderInterface; 23 | use Symfony\Component\Config\Loader\LoaderResolverInterface; 24 | use Symfony\Component\HttpKernel\KernelInterface; 25 | 26 | /** 27 | * Class ContaoManagerPlugin. 28 | */ 29 | class Plugin implements BundlePluginInterface, RoutingPluginInterface, ConfigPluginInterface 30 | { 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function getBundles(ParserInterface $parser): array 35 | { 36 | return [ 37 | BundleConfig::create(PlentaContaoJobsBasicBundle::class) 38 | ->setLoadAfter([ 39 | ContaoCoreBundle::class, 40 | ]), 41 | ]; 42 | } 43 | 44 | /** 45 | * @throws \Exception 46 | */ 47 | public function registerContainerConfiguration(LoaderInterface $loader, array $managerConfig): void 48 | { 49 | $loader->load(__DIR__.'/../../config/config.yml'); 50 | } 51 | 52 | public function getRouteCollection(LoaderResolverInterface $resolver, KernelInterface $kernel) 53 | { 54 | return $resolver 55 | ->resolve(__DIR__.'/../../config/routing.yml') 56 | ->load(__DIR__.'/../../config/routing.yml') 57 | ; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /contao/templates/_new/jobs_basic/plenta_jobs_basic_offer_default.html.twig: -------------------------------------------------------------------------------- 1 | {% block content %} 2 |
3 | {% block image %} 4 | {% if 'image' in parts %} 5 |
6 | {{ jobOfferMeta.image|raw }} 7 |
8 | {% endif %} 9 | {% endblock %} 10 |
11 | {% block headline %} 12 | <{{ headlineUnit }} class="title"> 13 | {{ jobOfferMeta.title|raw }} 14 | 15 | {% endblock %} 16 | {% block company %} 17 | {% if 'company' in parts %} 18 |
19 | {{ jobOfferMeta.company }} 20 |
21 | {% endif %} 22 | {% endblock %} 23 | {% block location %} 24 | {% if 'jobLocation' in parts %} 25 |
26 | {{ jobOfferMeta.addressLocalityFormatted }} 27 |
28 | {% endif %} 29 | {% endblock %} 30 | {% block teaser %} 31 | {% if 'teaser' in parts %} 32 |
33 | {{ jobOfferMeta.teaser|raw }} 34 |
35 | {% endif %} 36 | {% endblock %} 37 |
38 | {% block publicationDate %} 39 | {% if 'publicationDate' in parts or 'employmentType' in parts %} 40 |
41 | {% if 'publicationDate' in parts %} 42 |
43 | {{ jobOfferMeta.publicationDateFormatted }} 44 |
45 | {% endif %} 46 | {% if 'employmentType' in parts %} 47 |
48 | {{ jobOfferMeta.employmentTypeFormatted }} 49 |
50 | {% endif %} 51 |
52 | {% endif %} 53 | {% endblock %} 54 |
55 | {% endblock %} -------------------------------------------------------------------------------- /contao/templates/_new/jobs_basic_reader_parts/plenta_jobs_basic_reader_job_location.html.twig: -------------------------------------------------------------------------------- 1 | {% trans_default_domain 'contao_tl_plenta_jobs_basic_offer' %} 2 | 3 | {% block content %} 4 | {% if locations %} 5 |

{{ 'tl_plenta_jobs_basic_offer.jobLocation.0'|trans }}:

6 | {% for key, location in locations %} 7 |
8 | {% if key in imgs|keys %} 9 | {{ attribute(imgs, key)|raw }} 10 | {% endif %} 11 | {% if showCompanyName %} 12 | {{ attribute(attribute(organizations, key), 'name') }} 13 | {% endif %} 14 |
15 | {% set remote = [] %} 16 | {% for loc in location %} 17 | {% if loc.jobTypeLocation == 'onPremise' %} 18 |
19 | {% if loc.title|default %} 20 | {{ loc.title }}
21 | {% endif %} 22 | {% if loc.streetAddress|trim %} 23 | {{ loc.streetAddress }}
24 | {% endif %} 25 | {{ loc.postalCode }} {{ loc.addressLocality }} 26 |
27 | {% else %} 28 | {% set remote = remote|merge([loc.requirementValue]) %} 29 | {% endif %} 30 | {% endfor %} 31 | {% if remote|length > 0 %} 32 |
33 | {% if plentaJobsBasicHideRemoteRequirements %} 34 | {{ 'tl_plenta_jobs_basic_offer.remoteIsPossible'|trans }} 35 | {% else %} 36 | {{ 'tl_plenta_jobs_basic_offer.remoteIsPossibleWithRequirements'|trans([remote|join(', ')]) }} 37 | {% endif %} 38 |
39 | {% endif %} 40 | {% endfor %} 41 | {% endif %} 42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /src/EventListener/Contao/SitemapListener.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\EventListener\Contao; 14 | 15 | use Contao\ArticleModel; 16 | use Contao\ContentModel; 17 | use Contao\CoreBundle\Event\SitemapEvent; 18 | use Contao\CoreBundle\Framework\ContaoFramework; 19 | use Contao\Database; 20 | use Contao\ModuleModel; 21 | use Contao\PageModel; 22 | use Plenta\ContaoJobsBasic\Contao\Model\PlentaJobsBasicOfferModel; 23 | use Symfony\Component\EventDispatcher\Attribute\AsEventListener; 24 | 25 | #[AsEventListener(event: SitemapEvent::class)] 26 | class SitemapListener 27 | { 28 | public function __construct(private ContaoFramework $framework) 29 | { 30 | } 31 | 32 | public function __invoke(SitemapEvent $event): void 33 | { 34 | $arrRoot = $event->getRootPageIds(); 35 | 36 | if (empty($arrRoot)) { 37 | return; 38 | } 39 | 40 | $arrPages = []; 41 | $jobs = PlentaJobsBasicOfferModel::findAllPublished(); 42 | 43 | foreach ($arrRoot as $pageId) { 44 | $objPage = $this->framework->getAdapter(PageModel::class)->findPublishedById($pageId)?->loadDetails(); 45 | 46 | if (null === $objPage) { 47 | continue; 48 | } 49 | 50 | if ($jobs) { 51 | foreach ($jobs as $job) { 52 | if ('noindex,nofollow' === $job->robots) { 53 | continue; 54 | } 55 | 56 | if ($page = $job->getAbsoluteUrl($objPage->language)) { 57 | if (!\in_array($page, $arrPages, true)) { 58 | $arrPages[] = $page; 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | foreach ($arrPages as $strUrl) { 66 | $event->addUrlToDefaultUrlSet($strUrl); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/EventListener/Contao/BackendMenuListener.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\EventListener\Contao; 14 | 15 | use Contao\CoreBundle\Event\MenuEvent; 16 | use Plenta\ContaoJobsBasic\Controller\Contao\BackendModule\SettingsController; 17 | use Plenta\ContaoJobsBasic\Helper\PermissionsHelper; 18 | use Symfony\Component\HttpFoundation\RequestStack; 19 | use Symfony\Component\Routing\RouterInterface; 20 | 21 | class BackendMenuListener 22 | { 23 | protected $router; 24 | protected $requestStack; 25 | 26 | public function __construct(RouterInterface $router, RequestStack $requestStack) 27 | { 28 | $this->router = $router; 29 | $this->requestStack = $requestStack; 30 | } 31 | 32 | public function __invoke(MenuEvent $event): void 33 | { 34 | $factory = $event->getFactory(); 35 | $tree = $event->getTree(); 36 | 37 | if ('mainMenu' !== $tree->getName()) { 38 | return; 39 | } 40 | 41 | $contentNode = $tree->getChild('plenta_jobs_basic'); 42 | 43 | if (!$contentNode) { 44 | return; 45 | } 46 | 47 | if (PermissionsHelper::canAccessModule('settings')) { 48 | $node = $factory 49 | ->createItem('plenta-jobs-basic-settings') 50 | ->setUri($this->router->generate(SettingsController::class)) 51 | ->setLabel($GLOBALS['TL_LANG']['MOD']['plenta_jobs_basic_settings'][0]) 52 | ->setLinkAttribute('title', $GLOBALS['TL_LANG']['MOD']['plenta_jobs_basic_settings'][1]) 53 | ->setLinkAttribute('class', 'plenta-jobs-basic-settings') 54 | ->setCurrent($this->requestStack->getCurrentRequest()->get('_controller') === SettingsController::class.'::showSettings' || SettingsController::isActive($this->requestStack)) 55 | ; 56 | 57 | $contentNode->addChild($node); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /contao/languages/de/tl_content.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Eigenschaften 7 | 8 | 9 | Bitte wählen sie die auszugebenden Eigenschaften. 10 | 11 | 12 | Überschriftentyp (Anzeigen-Titel) 13 | 14 | 15 | Bitte wählen Sie einen Überschriftentyp aus. 16 | 17 | 18 | Stellenanzeige 19 | 20 | 21 | Bitte wählen Sie eine Stellenanzeige aus. 22 | 23 | 24 | Eigenschaften 25 | 26 | 27 | Bitte wählen Sie die Eigenschaften aus, die angezeigt werden sollen. 28 | 29 | 30 | Hinweis für nicht verfügbare Stellenanzeige 31 | 32 | 33 | Hier haben Sie die Möglichkeit, einen Hinweistext einzufügen, der angezeigt wird, wenn die ausgewählte Stellenanzeige nicht mehr verfügbar ist. 34 | 35 | 36 | Stellenanzeige 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/Migration/RemoveFkConstraintsMigration.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Migration; 14 | 15 | use Contao\CoreBundle\Migration\MigrationResult; 16 | use Doctrine\DBAL\Connection; 17 | 18 | class RemoveFkConstraintsMigration extends \Contao\CoreBundle\Migration\AbstractMigration 19 | { 20 | protected $tables = ['tl_plenta_jobs_basic_offer_translation', 'tl_plenta_jobs_basic_job_location']; 21 | 22 | protected Connection $database; 23 | 24 | public function __construct(Connection $connection) 25 | { 26 | $this->database = $connection; 27 | } 28 | 29 | public function getName(): string 30 | { 31 | return 'Plenta Jobs Basic Bundle 2.0 Update - Foreign Key Constraints'; 32 | } 33 | 34 | public function shouldRun(): bool 35 | { 36 | $schemaManager = $this->database->getSchemaManager(); 37 | 38 | foreach ($this->tables as $table) { 39 | if ($schemaManager->tablesExist([$table])) { 40 | $foreignKeys = $schemaManager->listTableForeignKeys($table); 41 | if (!empty($foreignKeys)) { 42 | return true; 43 | } 44 | } 45 | } 46 | 47 | return false; 48 | } 49 | 50 | public function run(): MigrationResult 51 | { 52 | $schemaManager = $this->database->getSchemaManager(); 53 | 54 | foreach ($this->tables as $table) { 55 | if ($schemaManager->tablesExist([$table])) { 56 | $foreignKeys = $schemaManager->listTableForeignKeys($table); 57 | if (!empty($foreignKeys)) { 58 | foreach ($foreignKeys as $foreignKey) { 59 | $schemaManager->dropForeignKey($foreignKey, $table); 60 | } 61 | } 62 | } 63 | } 64 | 65 | return new MigrationResult(true, 'Foreign key constraints have successfully been dropped.'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /contao/config/config.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | use Composer\InstalledVersions; 14 | use Contao\ArrayUtil; 15 | use Plenta\ContaoJobsBasic\Contao\Model\PlentaJobsBasicJobLocationModel; 16 | use Plenta\ContaoJobsBasic\Contao\Model\PlentaJobsBasicOfferModel; 17 | use Plenta\ContaoJobsBasic\Contao\Model\PlentaJobsBasicOrganizationModel; 18 | use Plenta\ContaoJobsBasic\Contao\Model\PlentaJobsBasicSettingsEmploymentTypeModel; 19 | use Plenta\ContaoJobsBasic\EventListener\Contao\Hooks\ChangelanguageNavigationListener; 20 | 21 | ArrayUtil::arrayInsert($GLOBALS['BE_MOD'], 1, [ 22 | 'plenta_jobs_basic' => [ 23 | 'plenta_jobs_basic_offers' => [ 24 | 'tables' => ['tl_plenta_jobs_basic_offer', 'tl_content'], 25 | ], 26 | 'plenta_jobs_basic_organizations' => [ 27 | 'tables' => ['tl_plenta_jobs_basic_organization', 'tl_plenta_jobs_basic_job_location'], 28 | 'hideInNavigation' => true, 29 | ], 30 | 'plenta_jobs_basic_settings_employment_type' => [ 31 | 'tables' => ['tl_plenta_jobs_basic_settings_employment_type'], 32 | 'hideInNavigation' => true, 33 | ], 34 | ], 35 | ]); 36 | 37 | if (defined('TL_MODE') && TL_MODE == 'BE') { 38 | $GLOBALS['TL_CSS'][] = 'bundles/plentacontaojobsbasic/backend.css|static'; 39 | } 40 | 41 | $GLOBALS['TL_MODELS'][PlentaJobsBasicOfferModel::getTable()] = PlentaJobsBasicOfferModel::class; 42 | $GLOBALS['TL_MODELS'][PlentaJobsBasicJobLocationModel::getTable()] = PlentaJobsBasicJobLocationModel::class; 43 | $GLOBALS['TL_MODELS'][PlentaJobsBasicOrganizationModel::getTable()] = PlentaJobsBasicOrganizationModel::class; 44 | $GLOBALS['TL_MODELS'][PlentaJobsBasicSettingsEmploymentTypeModel::getTable()] = PlentaJobsBasicSettingsEmploymentTypeModel::class; 45 | 46 | if (InstalledVersions::isInstalled('terminal42/contao-changelanguage')) { 47 | $GLOBALS['TL_HOOKS']['changelanguageNavigation'][] = [ChangelanguageNavigationListener::class, 'onChangelanguageNavigation']; 48 | } 49 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "plenta/contao-jobs-basic-bundle", 3 | "type": "contao-bundle", 4 | "description": "Basic Job Manager for Contao (including Google Jobs).", 5 | "license": "LGPL-3.0-or-later", 6 | "authors": [ 7 | { 8 | "name": "Plenta.io", 9 | "homepage": "https://plenta.io" 10 | } 11 | ], 12 | "funding": [{ 13 | "type": "github", 14 | "url": "https://github.com/sponsors/plenta" 15 | }], 16 | "support":{ 17 | "issues": "https://github.com/plenta/contao-jobs-basic-bundle/issues", 18 | "source": "https://github.com/plenta/contao-jobs-basic-bundle", 19 | "docs": "https://plenta.io/contao-erweiterungen/jobs-basic" 20 | }, 21 | "require": { 22 | "php": "^8.1", 23 | "ext-json": "*", 24 | "contao/conflicts": "@dev", 25 | "contao/core-bundle": "^5.3", 26 | "doctrine/dbal": "^2.10.1 || ^3.2.0", 27 | "mvo/contao-group-widget": "^1.5", 28 | "symfony/intl": "^6.0 || ^7.0 ", 29 | "symfony/form": "^6.0 || ^7.0" 30 | }, 31 | "require-dev": { 32 | "contao/manager-bundle": "^5.0", 33 | "phpunit/phpunit": "^9.5.10", 34 | "contao/test-case": "^5.0", 35 | "symfony/phpunit-bridge": "^v6.4.9", 36 | "friendsofphp/php-cs-fixer": "^3.2", 37 | "contao/maker-bundle": "*" 38 | }, 39 | "suggest": { 40 | "plenta/contao-jobs-basic-contact-bundle": "Contact persons for Contao Jobs Basic", 41 | "plenta/contao-jobs-basic-categories-bundle": "Categories for Contao Jobs Basic", 42 | "plenta/contao-jobs-basic-geo-search-bundle": "GEO search for Contao Jobs Basic", 43 | "plenta/contao-jobs-basic-zvoove-bundle": "zvoove Connector for Contao Jobs Basic", 44 | "plenta/contao-jobs-basic-hansalog-hr-bundle": "HANSALOG HR Connector für Contao Jobs Basic" 45 | }, 46 | "extra": { 47 | "contao-manager-plugin": "Plenta\\ContaoJobsBasic\\ContaoManager\\Plugin" 48 | }, 49 | "autoload": { 50 | "psr-4": { 51 | "Plenta\\ContaoJobsBasic\\": "src" 52 | } 53 | }, 54 | "config": { 55 | "allow-plugins": { 56 | "composer/package-versions-deprecated": true, 57 | "contao-components/installer": true, 58 | "contao/manager-plugin": true, 59 | "contao-community-alliance/composer-plugin": true, 60 | "php-http/discovery": true 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Events/JobOfferReaderContentPartEvent.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Events; 14 | 15 | use Contao\ModuleModel; 16 | use Plenta\ContaoJobsBasic\Contao\Model\PlentaJobsBasicOfferModel; 17 | use Symfony\Component\HttpFoundation\Request; 18 | use Symfony\Contracts\EventDispatcher\Event; 19 | 20 | class JobOfferReaderContentPartEvent extends Event 21 | { 22 | public const NAME = 'plenta_jobs_basic.job_offer_reader.content_part'; 23 | 24 | private PlentaJobsBasicOfferModel $jobOffer; 25 | 26 | private ModuleModel $model; 27 | 28 | private Request $request; 29 | 30 | private string $contentResponse = ''; 31 | 32 | private string $part = ''; 33 | 34 | public function getJobOffer(): PlentaJobsBasicOfferModel 35 | { 36 | return $this->jobOffer; 37 | } 38 | 39 | public function setJobOffer(PlentaJobsBasicOfferModel $jobOffer): self 40 | { 41 | $this->jobOffer = $jobOffer; 42 | 43 | return $this; 44 | } 45 | 46 | public function getModel(): ModuleModel 47 | { 48 | return $this->model; 49 | } 50 | 51 | public function setModel(ModuleModel $model): self 52 | { 53 | $this->model = $model; 54 | 55 | return $this; 56 | } 57 | 58 | public function getRequest(): Request 59 | { 60 | return $this->request; 61 | } 62 | 63 | public function setRequest(Request $request): self 64 | { 65 | $this->request = $request; 66 | 67 | return $this; 68 | } 69 | 70 | public function getContentResponse(): string 71 | { 72 | return $this->contentResponse; 73 | } 74 | 75 | public function setContentResponse(string $contentResponse): self 76 | { 77 | $this->contentResponse = $contentResponse; 78 | 79 | return $this; 80 | } 81 | 82 | public function getPart(): string 83 | { 84 | return $this->part; 85 | } 86 | 87 | public function setPart(string $part): self 88 | { 89 | $this->part = $part; 90 | 91 | return $this; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /contao/templates/_new/frontend_module/plenta_jobs_basic_offer_list.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@Contao/frontend_module/_base.html.twig' %} 2 | 3 | {% block content %} 4 | {% block sorting %} 5 | {% if showSorting|default(false) %} 6 | {{ sortingForm|raw }} 7 | 8 | (function ($) { 9 | $('.form_{{ formId }} select#sort').on('change', function () { 10 | let searchParams = new URLSearchParams(location.search); 11 | let vals = $(this).val().split('__'); 12 | searchParams.set('sortBy', vals[0]); 13 | searchParams.set('order', vals[1]); 14 | window.location.href = location.pathname + '?' + searchParams.toString(); 15 | }) 16 | })(jQuery); 17 | 18 | {% endif %} 19 | {% endblock %} 20 | 21 | {% for item in items %} 22 | {{ item|raw }} 23 | {% else %} 24 |

{{ empty }}

25 | {% endfor %} 26 | {% if pagination is defined %} 27 | {{ pagination|raw }} 28 | {% endif %} 29 | {% endblock %} 30 | {#
cssID ?>style): ?> style="style ?>"attributes ? ' '.$this->attributes : ''; ?> > 31 | 32 | block('headline'); ?> 33 | headline): ?> 34 | <hl ?>>headline ?>hl ?>> 35 | 36 | endblock(); ?> 37 | 38 | block('sorting'); ?> 39 | showSorting): ?> 40 | sortingForm; ?> 41 | 42 | 43 | 44 | endblock(); ?> 45 | 46 | block('content'); ?> 47 | items)): ?> 48 |

empty ?>

49 | 50 | items) ?> 51 | pagination): ?> 52 | pagination ?> 53 | 54 | 55 | endblock(); ?> 56 |
57 | hallo#} 58 | -------------------------------------------------------------------------------- /contao/languages/en/default.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Full time 7 | 8 | 9 | Part time 10 | 11 | 12 | Contractor 13 | 14 | 15 | Temporary 16 | 17 | 18 | Intern 19 | 20 | 21 | Volunteer 22 | 23 | 24 | Per diem 25 | 26 | 27 | Other 28 | 29 | 30 | Jobs (Basic) 31 | 32 | 33 | Job offer details 34 | 35 | 36 | Job offer details 37 | 38 | 39 | There are no jobs available at the moment. 40 | 41 | 42 | Documentation 43 | 44 | 45 | Report a error 46 | 47 | 48 | Become a sponsor 49 | 50 | 51 | Add-ons 52 | 53 | 54 | Remote 55 | 56 | 57 | Remote in 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/EventListener/Contao/DCA/TlModule.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\EventListener\Contao\DCA; 14 | 15 | use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback; 16 | use Contao\CoreBundle\Twig\Finder\FinderFactory; 17 | use Contao\DataContainer; 18 | use Contao\StringUtil; 19 | use Plenta\ContaoJobsBasic\Contao\Model\PlentaJobsBasicJobLocationModel; 20 | 21 | class TlModule 22 | { 23 | public function __construct(protected FinderFactory $finderFactory) 24 | { 25 | } 26 | 27 | public function jobLocationOptionsCallback(DataContainer $dc): array 28 | { 29 | if ($dc->activeRecord->plentaJobsBasicCompanies) { 30 | $companies = StringUtil::deserialize($dc->activeRecord->plentaJobsBasicCompanies); 31 | 32 | if (!empty($companies)) { 33 | $jobLocations = PlentaJobsBasicJobLocationModel::findByMultiplePids($companies); 34 | } 35 | } 36 | 37 | if (empty($jobLocations)) { 38 | $jobLocations = PlentaJobsBasicJobLocationModel::findAll(); 39 | } 40 | 41 | $return = []; 42 | foreach ($jobLocations as $jobLocation) { 43 | $return[$jobLocation->id] = $jobLocation->getRelated('pid')->name.': '; 44 | if ('onPremise' === $jobLocation->jobTypeLocation) { 45 | $return[$jobLocation->id] .= $jobLocation->streetAddress; 46 | 47 | if ('' !== $jobLocation->addressLocality) { 48 | $return[$jobLocation->id] .= ', '.$jobLocation->addressLocality; 49 | } 50 | } else { 51 | $return[$jobLocation->id] .= $GLOBALS['TL_LANG']['MSC']['PLENTA_JOBS']['remote'].' ['.$jobLocation->requirementValue.']'; 52 | } 53 | } 54 | 55 | return $return; 56 | } 57 | 58 | #[AsCallback(table: 'tl_module', target: 'fields.plentaJobsBasicElementTpl.options')] 59 | public function onElementTplOptionsCallback() 60 | { 61 | return $this->finderFactory 62 | ->create() 63 | ->identifier('jobs_basic/plenta_jobs_basic_offer_default') 64 | ->extension('html.twig') 65 | ->withVariants() 66 | ->asTemplateOptions(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Controller/Contao/BackendModule/SettingsController.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Controller\Contao\BackendModule; 14 | 15 | use Composer\InstalledVersions; 16 | use Contao\BackendUser; 17 | use Contao\CoreBundle\Controller\AbstractBackendController; 18 | use Contao\CoreBundle\Exception\AccessDeniedException; 19 | use Contao\System; 20 | use Plenta\ContaoJobsBasic\Helper\PermissionsHelper; 21 | use Symfony\Component\HttpFoundation\RequestStack; 22 | use Symfony\Component\HttpFoundation\Response; 23 | use Twig\Environment as TwigEnvironment; 24 | 25 | class SettingsController extends AbstractBackendController 26 | { 27 | public function showSettings(): Response 28 | { 29 | if (!PermissionsHelper::canAccessModule('settings')) { 30 | throw new AccessDeniedException('The settings module of the Plenta Jobs Basic Bundle is not allowed for user "'.BackendUser::getInstance()->username.'".'); 31 | } 32 | 33 | System::loadLanguageFile('modules'); 34 | 35 | $GLOBALS['TL_CSS'][] = 'bundles/plentacontaojobsbasic/dashboard.css'; 36 | 37 | $mods = []; 38 | 39 | foreach ($GLOBALS['BE_MOD']['plenta_jobs_basic'] as $key => $mod) { 40 | if (isset($mod['hideInNavigation']) && $mod['hideInNavigation']) { 41 | if (!PermissionsHelper::canAccessModule($key, 'modules')) { 42 | continue; 43 | } 44 | $mod['title'] = $GLOBALS['TL_LANG']['MOD'][$key]; 45 | $mods[$key] = $mod; 46 | } 47 | } 48 | 49 | return $this->render( 50 | '@PlentaContaoJobsBasic/be_plenta_jobs_basic_settings.html.twig', 51 | [ 52 | 'title' => $GLOBALS['TL_LANG']['MOD']['plenta_jobs_basic_settings'][0], 53 | 'mods' => $mods, 54 | 'version' => InstalledVersions::getVersion('plenta/contao-jobs-basic-bundle'), 55 | ] 56 | ); 57 | } 58 | 59 | public static function isActive(RequestStack $requestStack) 60 | { 61 | $do = $requestStack->getCurrentRequest()->get('do'); 62 | if (isset($GLOBALS['BE_MOD']['plenta_jobs_basic'][$do], $GLOBALS['BE_MOD']['plenta_jobs_basic'][$do]['hideInNavigation']) && $GLOBALS['BE_MOD']['plenta_jobs_basic'][$do]['hideInNavigation']) { 63 | return true; 64 | } 65 | 66 | return false; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Migration/BoolCharToIntMigration.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Migration; 14 | 15 | use Contao\CoreBundle\Migration\AbstractMigration; 16 | use Contao\CoreBundle\Migration\MigrationResult; 17 | use Doctrine\DBAL\Connection; 18 | use Doctrine\DBAL\Types\BooleanType; 19 | 20 | class BoolCharToIntMigration extends AbstractMigration 21 | { 22 | private Connection $database; 23 | 24 | private array $columns = ['addImage', 'addSalary']; 25 | 26 | public function __construct(Connection $database) 27 | { 28 | $this->database = $database; 29 | } 30 | 31 | public function getName(): string 32 | { 33 | return 'Plenta Jobs Basic Bundle 1.4.5 Update'; 34 | } 35 | 36 | public function shouldRun(): bool 37 | { 38 | $schemaManager = $this->database->getSchemaManager(); 39 | 40 | if (!$schemaManager->tablesExist(['tl_plenta_jobs_basic_offer'])) { 41 | return false; 42 | } 43 | 44 | $columns = $schemaManager->listTableColumns('tl_plenta_jobs_basic_offer'); 45 | 46 | $shouldRun = false; 47 | 48 | foreach ($columns as $currentColumn) { 49 | if (is_a($currentColumn->getType(), BooleanType::class)) { 50 | continue; 51 | } 52 | $currentColumnName = $currentColumn->getName(); 53 | 54 | if (true === \in_array($currentColumnName, $this->columns, true)) { 55 | if (true === (bool) $this->database->executeQuery('SELECT EXISTS (SELECT id FROM tl_plenta_jobs_basic_offer WHERE '.$currentColumnName." = '')")->fetchOne()) { 56 | $shouldRun = true; 57 | break; 58 | } 59 | } 60 | } 61 | 62 | return $shouldRun; 63 | } 64 | 65 | public function run(): MigrationResult 66 | { 67 | $schemaManager = $this->database->getSchemaManager(); 68 | $columns = $schemaManager->listTableColumns('tl_plenta_jobs_basic_offer'); 69 | 70 | foreach ($columns as $currentColumn) { 71 | $currentColumnName = $currentColumn->getName(); 72 | 73 | if (true === \in_array($currentColumnName, $this->columns, true)) { 74 | $this->database 75 | ->executeQuery( 76 | 'UPDATE tl_plenta_jobs_basic_offer SET '.$currentColumnName.' = 0 WHERE '.$currentColumnName." = ''" 77 | ) 78 | ; 79 | } 80 | } 81 | 82 | return $this->createResult( 83 | true, 84 | 'All empty boolean values have been changed to 0.' 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Events/Model/FindAllPublishedByTypesAndLocationEvent.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Events\Model; 14 | 15 | use Contao\ModuleModel; 16 | use Symfony\Contracts\EventDispatcher\Event; 17 | 18 | class FindAllPublishedByTypesAndLocationEvent extends Event 19 | { 20 | public const NAME = 'plenta_jobs_basic.model.find_all_published_by_types_and_location'; 21 | 22 | private array $columns; 23 | 24 | private array $values; 25 | 26 | private array $options; 27 | 28 | private bool $applyRequestFilters = false; 29 | 30 | private ?ModuleModel $model = null; 31 | 32 | public function __construct() 33 | { 34 | } 35 | 36 | /** 37 | * @return array 38 | */ 39 | public function getColumns(): array 40 | { 41 | return $this->columns; 42 | } 43 | 44 | /** 45 | * @param array $columns 46 | * 47 | * @return FindAllPublishedByTypesAndLocationEvent 48 | */ 49 | public function setColumns(array $columns): self 50 | { 51 | $this->columns = $columns; 52 | 53 | return $this; 54 | } 55 | 56 | /** 57 | * @return array 58 | */ 59 | public function getValues(): array 60 | { 61 | return $this->values; 62 | } 63 | 64 | /** 65 | * @param array $values 66 | * 67 | * @return FindAllPublishedByTypesAndLocationEvent 68 | */ 69 | public function setValues(array $values): self 70 | { 71 | $this->values = $values; 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * @return array 78 | */ 79 | public function getOptions(): array 80 | { 81 | return $this->options; 82 | } 83 | 84 | /** 85 | * @param array $options 86 | * 87 | * @return FindAllPublishedByTypesAndLocationEvent 88 | */ 89 | public function setOptions(array $options): self 90 | { 91 | $this->options = $options; 92 | 93 | return $this; 94 | } 95 | 96 | public function isApplyRequestFilters(): bool 97 | { 98 | return $this->applyRequestFilters; 99 | } 100 | 101 | public function setApplyRequestFilters(bool $applyRequestFilters): FindAllPublishedByTypesAndLocationEvent 102 | { 103 | $this->applyRequestFilters = $applyRequestFilters; 104 | return $this; 105 | } 106 | 107 | public function getModel(): ?ModuleModel 108 | { 109 | return $this->model; 110 | } 111 | 112 | public function setModel(?ModuleModel $model): FindAllPublishedByTypesAndLocationEvent 113 | { 114 | $this->model = $model; 115 | return $this; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /contao/languages/en/modules.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Jobs (Basic) 7 | 8 | 9 | Jobs 10 | 11 | 12 | Manage job offers 13 | 14 | 15 | Companies 16 | 17 | 18 | Manage companies 19 | 20 | 21 | Locations 22 | 23 | 24 | Locations 25 | 26 | 27 | Manage locations 28 | 29 | 30 | Settings 31 | 32 | 33 | Manage settings 34 | 35 | 36 | Dashboard 37 | 38 | 39 | Support 40 | 41 | 42 | Here you will find support and options for assistance. 43 | 44 | 45 | Jobs (Basic) 46 | 47 | 48 | Job offer list 49 | 50 | 51 | Job offer list 52 | 53 | 54 | Job offer reader 55 | 56 | 57 | Job offer reader 58 | 59 | 60 | Job offer filter 61 | 62 | 63 | Job offer filter 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /contao/dca/tl_content.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | use Plenta\ContaoJobsBasic\EventListener\Contao\DCA\JobOfferFields; 14 | 15 | $GLOBALS['TL_DCA']['tl_content']['palettes']['plenta_jobs_basic_job_offer_details'] = '{type_legend},type,plenta_jobs_basic_job_offer_details;{image_legend},size;{expert_legend},guests,cssID;{invisible_legend:hide},invisible,start,stop'; 16 | $GLOBALS['TL_DCA']['tl_content']['palettes']['plenta_jobs_basic_job_offer_teaser'] = '{type_legend},type,headline;{plenta_jobs_basic_legend},text,plentaJobsBasicHeadlineTag,plentaJobsBasicJobOffer,plentaJobsBasicJobOfferTeaserParts,plentaJobsBasicNotice;{image_legend},size;{expert_legend},guests,cssID;{invisible_legend:hide},invisible,start,stop'; 17 | 18 | $GLOBALS['TL_DCA']['tl_content']['fields']['plenta_jobs_basic_job_offer_details'] = [ 19 | 'exclude' => true, 20 | 'inputType' => 'checkboxWizard', 21 | 'options' => [ 22 | 'employmentTypeFormatted' => 'Stellenarten', 23 | 'description' => 'Beschreibung', 24 | 'publicationDateFormatted' => 'Veröffentlichungsdatum', 25 | 'title' => 'Titel', 26 | 'addressLocalityFormatted' => 'Arbeitsort', 27 | 'image' => 'Bild', 28 | ], 29 | 'eval' => ['multiple' => true, 'tl_class' => 'clr'], 30 | 'sql' => 'blob NULL', 31 | ]; 32 | 33 | $GLOBALS['TL_DCA']['tl_content']['fields']['plentaJobsBasicJobOffer'] = [ 34 | 'exclude' => true, 35 | 'inputType' => 'select', 36 | 'foreignKey' => 'tl_plenta_jobs_basic_offer.title', 37 | 'eval' => [ 38 | 'chosen' => true, 39 | 'tl_class' => 'clr', 40 | ], 41 | 'sql' => 'int(10) unsigned NOT NULL default 0', 42 | ]; 43 | 44 | $GLOBALS['TL_DCA']['tl_content']['fields']['plentaJobsBasicJobOfferTeaserParts'] = [ 45 | 'exclude' => true, 46 | 'inputType' => 'checkbox', 47 | 'options' => JobOfferFields::getParts(), 48 | 'eval' => [ 49 | 'multiple' => true, 50 | ], 51 | 'reference' => &$GLOBALS['TL_LANG']['MSC']['PLENTA_JOBS']['offerParts'], 52 | 'sql' => "varchar(255) COLLATE ascii_bin NOT NULL default '".serialize(['jobLocation', 'publicationDate', 'employmentType'])."'", 53 | ]; 54 | 55 | $GLOBALS['TL_DCA']['tl_content']['fields']['plentaJobsBasicHeadlineTag'] = [ 56 | 'exclude' => true, 57 | 'search' => true, 58 | 'inputType' => 'select', 59 | 'options' => ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div'], 60 | 'eval' => ['maxlength' => 8, 'tl_class' => 'w50 clr'], 61 | 'sql' => "varchar(3) NOT NULL default 'h2' COLLATE ascii_bin", 62 | ]; 63 | 64 | $GLOBALS['TL_DCA']['tl_content']['fields']['plentaJobsBasicNotice'] = [ 65 | 'inputType' => 'text', 66 | 'exclude' => true, 67 | 'eval' => [ 68 | 'maxlength' => 255, 69 | 'tl_class' => 'long', 70 | ], 71 | 'sql' => [ 72 | 'type' => 'string', 73 | 'length' => 255, 74 | 'default' => '', 75 | ], 76 | ]; 77 | -------------------------------------------------------------------------------- /contao/languages/de/modules.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Stellenanzeigen 7 | 8 | 9 | Stellenanzeigen 10 | 11 | 12 | Stellenanzeigen verwalten 13 | 14 | 15 | Unternehmen verwalten 16 | 17 | 18 | Unternehmen verwalten 19 | 20 | 21 | Standort 22 | 23 | 24 | Standorte 25 | 26 | 27 | Arbeitsorte verwalten 28 | 29 | 30 | Einstellungen 31 | 32 | 33 | Einstellungen verwalten 34 | 35 | 36 | Zusätzliche Stellenarten verwalten 37 | 38 | 39 | Hier können Sie weitere Stellenarten konfigurieren. 40 | 41 | 42 | Jobs (Basic) 43 | 44 | 45 | Stellenanzeigen Liste 46 | 47 | 48 | Stellenanzeigen Liste 49 | 50 | 51 | Stellenanzeigen Detailansicht 52 | 53 | 54 | Stellenanzeigen Detailansicht 55 | 56 | 57 | Stellenanzeigen Filter 58 | 59 | 60 | Stellenangebot Filter 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/Events/JobOfferListBeforeParseTemplateEvent.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Events; 14 | 15 | use Contao\ModuleModel; 16 | use Contao\Template; 17 | use Plenta\ContaoJobsBasic\Controller\Contao\FrontendModule\JobOfferListController; 18 | use Symfony\Contracts\EventDispatcher\Event; 19 | 20 | class JobOfferListBeforeParseTemplateEvent extends Event 21 | { 22 | public const NAME = 'plenta_jobs_basic.job_offer_list.before_parse_template'; 23 | 24 | private Template $template; 25 | 26 | private $jobOffers; 27 | 28 | private JobOfferListController $objModule; 29 | 30 | private ModuleModel $model; 31 | 32 | public function __construct($jobOffers, Template $template, ModuleModel $model, JobOfferListController $objModule) 33 | { 34 | $this->jobOffers = $jobOffers; 35 | $this->template = $template; 36 | $this->model = $model; 37 | $this->objModule = $objModule; 38 | } 39 | 40 | /** 41 | * @return Template 42 | */ 43 | public function getTemplate(): Template 44 | { 45 | return $this->template; 46 | } 47 | 48 | /** 49 | * @param Template $template 50 | * 51 | * @return JobOfferListBeforeParseTemplateEvent 52 | */ 53 | public function setTemplate(Template $template): self 54 | { 55 | $this->template = $template; 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * @return mixed 62 | */ 63 | public function getJobOffers() 64 | { 65 | return $this->jobOffers; 66 | } 67 | 68 | /** 69 | * @param mixed $jobOffers 70 | * 71 | * @return JobOfferListBeforeParseTemplateEvent 72 | */ 73 | public function setJobOffers($jobOffers) 74 | { 75 | $this->jobOffers = $jobOffers; 76 | 77 | return $this; 78 | } 79 | 80 | /** 81 | * @return JobOfferListController 82 | */ 83 | public function getObjModule(): JobOfferListController 84 | { 85 | return $this->objModule; 86 | } 87 | 88 | /** 89 | * @param JobOfferListController $objModule 90 | * 91 | * @return JobOfferListBeforeParseTemplateEvent 92 | */ 93 | public function setObjModule(JobOfferListController $objModule): self 94 | { 95 | $this->objModule = $objModule; 96 | 97 | return $this; 98 | } 99 | 100 | /** 101 | * @return ModuleModel 102 | */ 103 | public function getModel(): ModuleModel 104 | { 105 | return $this->model; 106 | } 107 | 108 | /** 109 | * @param ModuleModel $model 110 | * 111 | * @return JobOfferListBeforeParseTemplateEvent 112 | */ 113 | public function setModel(ModuleModel $model): self 114 | { 115 | $this->model = $model; 116 | 117 | return $this; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Migration/RenameDatabaseColumnsMigration.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Migration; 14 | 15 | use Contao\CoreBundle\Migration\AbstractMigration; 16 | use Contao\CoreBundle\Migration\MigrationResult; 17 | use Doctrine\DBAL\Connection; 18 | 19 | class RenameDatabaseColumnsMigration extends AbstractMigration 20 | { 21 | private Connection $database; 22 | private array $oldColumnNames = [ 23 | 'plentaJobsMethod' => "varchar(12) NOT NULL default 'POST'", 24 | 'plentaJobsShowButton' => "char(1) NOT NULL default ''", 25 | 'plentaJobsSubmit' => "varchar(255) NOT NULL default ''", 26 | 'plentaJobsShowTypes' => "char(1) NOT NULL default ''", 27 | 'plentaJobsTypesHeadline' => "varchar(255) NOT NULL default ''", 28 | 'plentaJobsShowAllTypes' => "char(1) NOT NULL default ''", 29 | 'plentaJobsShowQuantity' => "char(1) NOT NULL default ''", 30 | 'plentaJobsShowLocations' => "char(1) NOT NULL default ''", 31 | 'plentaJobsLocationsHeadline' => "varchar(255) NOT NULL default ''", 32 | 'plentaJobsShowAllLocations' => "char(1) NOT NULL default ''", 33 | 'plentaJobsShowLocationQuantity' => "char(1) NOT NULL default ''", 34 | ]; 35 | 36 | public function __construct(Connection $database) 37 | { 38 | $this->database = $database; 39 | } 40 | 41 | public function shouldRun(): bool 42 | { 43 | $schemaManager = $this->database->getSchemaManager(); 44 | 45 | if (!$schemaManager->tablesExist(['tl_module'])) { 46 | return false; 47 | } 48 | 49 | $columns = $schemaManager->listTableColumns('tl_module'); 50 | $shouldRun = false; 51 | 52 | foreach ($columns as $currentColumn) { 53 | $currentColumnName = $currentColumn->getName(); 54 | 55 | if (true === \array_key_exists($currentColumnName, $this->oldColumnNames)) { 56 | $shouldRun = true; 57 | break; 58 | } 59 | } 60 | 61 | return $shouldRun; 62 | } 63 | 64 | public function run(): MigrationResult 65 | { 66 | $schemaManager = $this->database->getSchemaManager(); 67 | $columns = $schemaManager->listTableColumns('tl_module'); 68 | 69 | foreach ($columns as $currentColumn) { 70 | $currentColumnName = $currentColumn->getName(); 71 | 72 | if (true === \array_key_exists($currentColumnName, $this->oldColumnNames)) { 73 | $newColumnName = str_replace('plentaJobs', 'plentaJobsBasic', $currentColumnName); 74 | 75 | $this->database 76 | ->executeQuery( 77 | 'ALTER TABLE tl_module 78 | CHANGE '.$currentColumnName.' '.$newColumnName.' '.$this->oldColumnNames[$currentColumnName] 79 | ) 80 | ; 81 | } 82 | } 83 | 84 | return $this->createResult( 85 | true, 86 | 'All Plenta Jobs Basic Columns have been renamed.' 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Helper/NumberHelper.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Helper; 14 | 15 | use Symfony\Component\Intl\Currencies; 16 | 17 | class NumberHelper 18 | { 19 | protected string $currency; 20 | protected string $locale; 21 | 22 | public function __construct($currency, $locale) 23 | { 24 | $this->currency = $currency; 25 | $this->locale = $locale; 26 | } 27 | 28 | public function reformatDecimalForDb($value): ?int 29 | { 30 | if (null === $value || '' === $value) { 31 | return null; 32 | } 33 | 34 | $decimalPlaces = Currencies::getFractionDigits($this->currency); 35 | $exp = 10 ** $decimalPlaces; 36 | $threshold = $decimalPlaces + 1; 37 | 38 | // No decimal places 39 | if (false === strpos($value, '.')) { 40 | return (int) ($value * $exp); 41 | } 42 | 43 | $pos = \strlen($value) - strpos($value, '.'); 44 | 45 | // Remove dot 46 | $value = str_replace('.', '', $value); 47 | 48 | // One decimal place 49 | if ($pos < $threshold) { 50 | return (int) ($value * (10 ** ($threshold - $pos))); 51 | } 52 | 53 | if ($pos > $threshold) { 54 | $value = substr($value, 0, -($pos - $threshold)); 55 | } 56 | 57 | return (int) $value; 58 | } 59 | 60 | public function formatNumberFromDbForDCAField(?string $number): ?string 61 | { 62 | if (null === $number) { 63 | return null; 64 | } 65 | 66 | $decimalPlaces = Currencies::getFractionDigits($this->currency); 67 | $exp = 10 ** $decimalPlaces; 68 | 69 | $thousandSeparator = ''; 70 | $decimalSeparator = '.'; 71 | 72 | return number_format($number / $exp, $decimalPlaces, $decimalSeparator, $thousandSeparator); 73 | } 74 | 75 | public function formatNumberFromDb(?int $number): ?string 76 | { 77 | if (null === $number) { 78 | return null; 79 | } 80 | 81 | $numberFormatter = \NumberFormatter::create($this->locale, \NumberFormatter::DECIMAL_SEPARATOR_SYMBOL); 82 | $thousandSeparator = $numberFormatter->getSymbol(\NumberFormatter::GROUPING_SEPARATOR_SYMBOL); 83 | $decimalSeparator = $numberFormatter->getSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL); 84 | 85 | $decimalPlaces = Currencies::getFractionDigits($this->currency); 86 | $exp = 10 ** $decimalPlaces; 87 | 88 | return number_format($number / $exp, $decimalPlaces, $decimalSeparator, $thousandSeparator); 89 | } 90 | 91 | public function formatCurrency(?int $number): ?string 92 | { 93 | if (null === $number) { 94 | return null; 95 | } 96 | 97 | $numberFormatter = \NumberFormatter::create($this->locale, \NumberFormatter::CURRENCY); 98 | $decimalPlaces = Currencies::getFractionDigits($this->currency); 99 | $exp = 10 ** $decimalPlaces; 100 | return $numberFormatter->formatCurrency($number / $exp, $this->currency); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Helper/CountJobsHelper.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Helper; 14 | 15 | use Contao\ArticleModel; 16 | use Contao\ContentModel; 17 | use Contao\ModuleModel; 18 | use Contao\StringUtil; 19 | use Plenta\ContaoJobsBasic\Contao\Model\PlentaJobsBasicJobLocationModel; 20 | use Plenta\ContaoJobsBasic\Contao\Model\PlentaJobsBasicOfferModel; 21 | use Symfony\Component\HttpFoundation\RequestStack; 22 | 23 | class CountJobsHelper 24 | { 25 | public function __construct(protected RequestStack $requestStack) 26 | { 27 | } 28 | 29 | public function countJobs(bool $filtered = true) 30 | { 31 | $request = $this->requestStack->getCurrentRequest(); 32 | 33 | $model = $request->attributes->get('moduleModel'); 34 | 35 | if ($model && !$model instanceof ModuleModel) { 36 | $model = ModuleModel::findByPk($model); 37 | } 38 | 39 | if ('plenta_jobs_basic_offer_list' !== $model?->type && 'plenta_jobs_basic_filter' !== $model?->type) { 40 | $model = null; 41 | } 42 | 43 | if (!$model) { 44 | $page = $request->attributes->get('pageModel'); 45 | 46 | $articles = ArticleModel::findByPid($page->id); 47 | 48 | foreach ($articles as $article) { 49 | $contents = ContentModel::findPublishedByPidAndTable($article->id, 'tl_article'); 50 | 51 | foreach ($contents as $content) { 52 | if ('module' === $content->type) { 53 | $module = ModuleModel::findByPk($content->module); 54 | 55 | if ('plenta_jobs_basic_offer_list' === $module->type) { 56 | $model = $module; 57 | break 2; 58 | } 59 | } 60 | } 61 | } 62 | } 63 | 64 | $types = []; 65 | $locations = []; 66 | 67 | if ($model) { 68 | $types = StringUtil::deserialize($model->plentaJobsBasicEmploymentTypes) ?? []; 69 | $locations = StringUtil::deserialize($model->plentaJobsBasicLocations) ?? []; 70 | 71 | if (empty($locations) && !empty($model->plentaJobsBasicCompanies)) { 72 | $locationObjs = PlentaJobsBasicJobLocationModel::findByMultiplePids(StringUtil::deserialize($model->plentaJobsBasicCompanies, true)); 73 | 74 | foreach ($locationObjs as $locationObj) { 75 | $locations[] = $locationObj->id; 76 | } 77 | } 78 | } 79 | 80 | if ($filtered) { 81 | $requestData = $request->query->all(); 82 | 83 | $queryTypes = $requestData['types'] ?? []; 84 | $queryLocations = $requestData['location'] ?? []; 85 | 86 | $types = array_merge($types, (is_array($queryTypes) || empty($queryTypes) ? $queryTypes : [$queryTypes])); 87 | $locations = array_merge($locations, (is_array($queryLocations) || empty($queryLocations) ? $queryLocations : [$queryLocations])); 88 | 89 | return PlentaJobsBasicOfferModel::countAllPublishedByTypesAndLocation($types, $locations, true, $model); 90 | } 91 | 92 | return PlentaJobsBasicOfferModel::countAllPublishedByTypesAndLocation($types, $locations, true, $model, false); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /templates/forms/form_layout.html.twig: -------------------------------------------------------------------------------- 1 | {% use "form_div_layout.html.twig" %} 2 | 3 | 4 | {# Rows #} 5 | {%- block form_row -%} 6 | {% set row_attr = row_attr|merge({class: (row_attr.class|default('') ~ ' widget')|trim}) %} 7 | {% if form.vars.block_prefixes.1 is defined %} 8 | {% set row_attr = row_attr|merge({class: (row_attr.class|default('') ~ ' widget-' ~ form.vars.block_prefixes.1)|trim}) %} 9 | {% endif %} 10 | {% if form.vars.block_prefixes.2 is defined %} 11 | {% if form.vars.block_prefixes.2 != form.vars.unique_block_prefix %} 12 | {% set row_attr = row_attr|merge({class: (row_attr.class|default('') ~ ' widget-' ~ form.vars.block_prefixes.2)|trim}) %} 13 | {% endif %} 14 | {% endif %} 15 | {{- parent() -}} 16 | {%- endblock form_row -%} 17 | 18 | {%- block button_row -%} 19 | {% set row_attr = row_attr|merge({class: (row_attr.class|default('') ~ ' widget')|trim}) %} 20 | {% if form.vars.block_prefixes.1 is defined %} 21 | {% set row_attr = row_attr|merge({class: (row_attr.class|default('') ~ ' widget-' ~ form.vars.block_prefixes.1)|trim}) %} 22 | {% endif %} 23 | {% if form.vars.block_prefixes.2 is defined %} 24 | {% if form.vars.block_prefixes.2 != form.vars.unique_block_prefix %} 25 | {% set row_attr = row_attr|merge({class: (row_attr.class|default('') ~ ' widget-' ~ form.vars.block_prefixes.2)|trim}) %} 26 | {% endif %} 27 | {% endif %} 28 | {{- parent() -}} 29 | {%- endblock button_row -%} 30 | 31 | {# Form label #} 32 | {%- block form_label -%} 33 | {% if label is not same as(false) -%} 34 | {% if not compound -%} 35 | {% set label_attr = label_attr|merge({'for': id}) %} 36 | {%- endif -%} 37 | {% if required -%} 38 | {% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' required')|trim}) %} 39 | {%- endif -%} 40 | {% if label is empty -%} 41 | {%- if label_format is not empty -%} 42 | {% set label = label_format|replace({ 43 | '%name%': name, 44 | '%id%': id, 45 | }) %} 46 | {%- else -%} 47 | {% set label = name|humanize %} 48 | {%- endif -%} 49 | {%- endif -%} 50 | <{{ element|default('label') }}{% if label_attr %}{% with { attr: label_attr } %}{{ block('attributes') }}{% endwith %}{% endif %}> 51 | {% if form.vars.required ?? false %} 52 | 53 | {% endif %} 54 | {%- if translation_domain is same as(false) -%} 55 | {{- label|raw -}} 56 | {%- else -%} 57 | {{- label|trans({}, translation_domain)|raw -}} 58 | {%- endif -%} 59 | {% if form.vars.required ?? false %}*{% endif %} 60 | 61 | {%- endif -%} 62 | {%- endblock form_label -%} 63 | 64 | 65 | {# Form errors #} 66 | {%- block form_errors -%} 67 | {%- if errors|length > 0 -%} 68 | {%- for error in errors -%} 69 |

{{ error.message }}

70 | {%- endfor -%} 71 | {%- endif -%} 72 | {%- endblock form_errors -%} 73 | 74 | {%- block choice_widget_expanded -%} 75 |
76 | {%- for child in form %} 77 | 78 | {{- form_widget(child) -}} 79 | {{- form_label(child, null, {translation_domain: choice_translation_domain}) -}} 80 | 81 | {% endfor -%} 82 |
83 | {%- endblock choice_widget_expanded -%} 84 | 85 | {% block html_widget %} 86 | {{ html|raw }} 87 | {% endblock %} -------------------------------------------------------------------------------- /src/Controller/Contao/ContentElement/PlentaJobsBasicJobOfferTeaserController.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Controller\Contao\ContentElement; 14 | 15 | use Contao\ContentModel; 16 | use Contao\CoreBundle\Controller\ContentElement\AbstractContentElementController; 17 | use Contao\CoreBundle\DependencyInjection\Attribute\AsContentElement; 18 | use Contao\CoreBundle\Routing\ScopeMatcher; 19 | use Contao\CoreBundle\ServiceAnnotation\ContentElement; 20 | use Contao\CoreBundle\Twig\FragmentTemplate; 21 | use Contao\StringUtil; 22 | use Contao\Template; 23 | use Contao\FrontendTemplate; 24 | use Plenta\ContaoJobsBasic\Contao\Model\PlentaJobsBasicOfferModel; 25 | use Plenta\ContaoJobsBasic\Helper\MetaFieldsHelper; 26 | use Symfony\Component\HttpFoundation\Request; 27 | use Symfony\Component\HttpFoundation\RequestStack; 28 | use Symfony\Component\HttpFoundation\Response; 29 | use Symfony\Contracts\Translation\TranslatorInterface; 30 | 31 | #[AsContentElement(category: 'plentaJobsBasic')] 32 | class PlentaJobsBasicJobOfferTeaserController extends AbstractContentElementController 33 | { 34 | protected $metaFields; 35 | 36 | public function __construct( 37 | protected MetaFieldsHelper $metaFieldsHelper, 38 | protected TranslatorInterface $translator 39 | ) { 40 | } 41 | 42 | public function getMetaFields(ContentModel $model, PlentaJobsBasicOfferModel $jobOffer): array 43 | { 44 | if (null !== $this->metaFields) { 45 | return $this->metaFields; 46 | } 47 | 48 | $this->metaFields = $this->metaFieldsHelper->getMetaFields($jobOffer, $model->size); 49 | 50 | return $this->metaFields; 51 | } 52 | 53 | public function getResponse(FragmentTemplate $template, ContentModel $model, Request $request): Response 54 | { 55 | $jobOffer = PlentaJobsBasicOfferModel::findPublishedByIdOrAlias($model->plentaJobsBasicJobOffer); 56 | 57 | if (!$jobOffer) { 58 | $headlineData = StringUtil::deserialize($model->headline ?? [] ?: '', true); 59 | $attributesData = StringUtil::deserialize($model->cssID ?? [] ?: '', true); 60 | 61 | $template = new FrontendTemplate('jobs_basic/plenta_jobs_basic_job_offer_empty'); 62 | $template->setData([ 63 | 'type' => $this->getType(), 64 | 'template' => $template->getName(), 65 | 'data' => $model, 66 | 'element_html_id' => $attributesData[0] ?? null, 67 | 'element_css_classes' => trim($attributesData[1] ?? ''), 68 | 'empty' => $model->plentaJobsBasicNotice ?: 69 | $this->translator->trans('MSC.PLENTA_JOBS.emptyList', [], 'contao_default'), 70 | 'headline' => [ 71 | 'text' => $headlineData['value'] ?? '', 72 | 'tag_name' => $headlineData['unit'] ?? 'h1', 73 | ], 74 | ]); 75 | 76 | return $template->getResponse(); 77 | } 78 | 79 | $template->jobOffer = $jobOffer; 80 | $parts = StringUtil::deserialize($model->plentaJobsBasicJobOfferTeaserParts); 81 | 82 | if (!\is_array($parts)) { 83 | $parts = []; 84 | } 85 | 86 | $template->parts = $parts; 87 | $template->jobOfferMeta = $this->getMetaFields($model, $jobOffer); 88 | $template->link = $jobOffer->getFrontendUrl($request->getLocale()); 89 | 90 | $this->tagResponse('contao.db.tl_plenta_jobs_basic_offer.'.$jobOffer->id); 91 | 92 | return $template->getResponse(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Migration/RefactorTranslationsMigration.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Migration; 14 | 15 | use Contao\CoreBundle\Migration\MigrationResult; 16 | use Doctrine\DBAL\Connection; 17 | use Doctrine\DBAL\Types\TextType; 18 | 19 | class RefactorTranslationsMigration extends \Contao\CoreBundle\Migration\AbstractMigration 20 | { 21 | protected Connection $database; 22 | 23 | public function __construct(Connection $connection) 24 | { 25 | $this->database = $connection; 26 | } 27 | 28 | public function getName(): string 29 | { 30 | return 'Plenta Jobs Basic Bundle 2.0 Update - Job Offer Translations'; 31 | } 32 | 33 | public function shouldRun(): bool 34 | { 35 | $schemaManager = $this->database->getSchemaManager(); 36 | 37 | if (!$schemaManager->tablesExist(['tl_plenta_jobs_basic_offer_translation', 'tl_plenta_jobs_basic_offer'])) { 38 | return false; 39 | } 40 | 41 | $columns = $schemaManager->listTableColumns('tl_plenta_jobs_basic_offer'); 42 | 43 | if (!\array_key_exists('translations', $columns)) { 44 | return false; 45 | } 46 | 47 | foreach ($columns as $column) { 48 | if ('translations' !== $column->getName()) { 49 | continue; 50 | } 51 | 52 | if (!is_a($column->getType(), TextType::class)) { 53 | return false; 54 | } 55 | } 56 | 57 | if (true === (bool) $this->database 58 | ->executeQuery(' 59 | SELECT EXISTS( 60 | SELECT id 61 | FROM tl_plenta_jobs_basic_offer_translation 62 | ) 63 | ') 64 | ->fetchOne() 65 | ) { 66 | return true; 67 | } 68 | 69 | return false; 70 | } 71 | 72 | public function run(): MigrationResult 73 | { 74 | $offers = $this->database->executeQuery('SELECT * FROM tl_plenta_jobs_basic_offer')->fetchAllAssociative(); 75 | 76 | foreach ($offers as $offer) { 77 | $translations = $this->database->prepare('SELECT * FROM tl_plenta_jobs_basic_offer_translation WHERE offer_id = ?')->executeQuery([$offer['id']])->fetchAllAssociative(); 78 | if (!empty($translations)) { 79 | $translationsArr = []; 80 | foreach ($translations as $translation) { 81 | $translationArr = [ 82 | 'description' => $translation['description'], 83 | 'title' => $translation['title'], 84 | 'alias' => $translation['alias'], 85 | 'language' => $translation['language'], 86 | ]; 87 | if (empty($translationsArr)) { 88 | $translationsArr[1] = $translationArr; 89 | } else { 90 | $translationsArr[] = $translationArr; 91 | } 92 | } 93 | 94 | $this->database->prepare('UPDATE tl_plenta_jobs_basic_offer SET translations = ? WHERE id = ?')->executeStatement([serialize($translationsArr), $offer['id']]); 95 | $this->database->prepare('DELETE FROM tl_plenta_jobs_basic_offer_translation WHERE offer_id = ?')->executeStatement([$offer['id']]); 96 | } 97 | } 98 | 99 | return new MigrationResult(true, 'Translations have successfully been moved from entities to serialized arrays.'); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Events/JobOfferReaderBeforeParseTemplateEvent.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Events; 14 | 15 | use Contao\ModuleModel; 16 | use Contao\Template; 17 | use Plenta\ContaoJobsBasic\Contao\Model\PlentaJobsBasicOfferModel; 18 | use Plenta\ContaoJobsBasic\Controller\Contao\FrontendModule\JobOfferReaderController; 19 | use Symfony\Contracts\EventDispatcher\Event; 20 | 21 | class JobOfferReaderBeforeParseTemplateEvent extends Event 22 | { 23 | public const NAME = 'plenta_jobs_basic.job_offer_reader.before_parse_template'; 24 | 25 | private Template $template; 26 | 27 | private PlentaJobsBasicOfferModel $jobOffer; 28 | 29 | private JobOfferReaderController $objModule; 30 | 31 | private ModuleModel $model; 32 | 33 | private ?string $structuredData; 34 | 35 | public function __construct(PlentaJobsBasicOfferModel $jobOffer, Template $template, ModuleModel $model, JobOfferReaderController $objModule, ?string $structuredData) 36 | { 37 | $this->jobOffer = $jobOffer; 38 | $this->template = $template; 39 | $this->model = $model; 40 | $this->objModule = $objModule; 41 | $this->structuredData = $structuredData; 42 | } 43 | 44 | /** 45 | * @return Template 46 | */ 47 | public function getTemplate(): Template 48 | { 49 | return $this->template; 50 | } 51 | 52 | /** 53 | * @param Template $template 54 | * 55 | * @return JobOfferReaderBeforeParseTemplateEvent 56 | */ 57 | public function setTemplate(Template $template): self 58 | { 59 | $this->template = $template; 60 | 61 | return $this; 62 | } 63 | 64 | /** 65 | * @return PlentaJobsBasicOfferModel 66 | */ 67 | public function getJobOffer(): PlentaJobsBasicOfferModel 68 | { 69 | return $this->jobOffer; 70 | } 71 | 72 | /** 73 | * @param PlentaJobsBasicOfferModel $jobOffer 74 | * 75 | * @return JobOfferReaderBeforeParseTemplateEvent 76 | */ 77 | public function setJobOffer(PlentaJobsBasicOfferModel $jobOffer): self 78 | { 79 | $this->jobOffer = $jobOffer; 80 | 81 | return $this; 82 | } 83 | 84 | /** 85 | * @return JobOfferReaderController 86 | */ 87 | public function getObjModule(): JobOfferReaderController 88 | { 89 | return $this->objModule; 90 | } 91 | 92 | /** 93 | * @param JobOfferReaderController $objModule 94 | * 95 | * @return JobOfferReaderBeforeParseTemplateEvent 96 | */ 97 | public function setObjModule(JobOfferReaderController $objModule): self 98 | { 99 | $this->objModule = $objModule; 100 | 101 | return $this; 102 | } 103 | 104 | /** 105 | * @return ModuleModel 106 | */ 107 | public function getModel(): ModuleModel 108 | { 109 | return $this->model; 110 | } 111 | 112 | /** 113 | * @param ModuleModel $model 114 | * 115 | * @return JobOfferReaderBeforeParseTemplateEvent 116 | */ 117 | public function setModel(ModuleModel $model): self 118 | { 119 | $this->model = $model; 120 | 121 | return $this; 122 | } 123 | 124 | /** 125 | * @return string|null 126 | */ 127 | public function getStructuredData(): ?string 128 | { 129 | return $this->structuredData; 130 | } 131 | 132 | /** 133 | * @param string|null $structuredData 134 | * 135 | * @return JobOfferReaderBeforeParseTemplateEvent 136 | */ 137 | public function setStructuredData(?string $structuredData): self 138 | { 139 | $this->structuredData = $structuredData; 140 | 141 | return $this; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /contao/dca/tl_plenta_jobs_basic_organization.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | use Contao\DC_Table; 14 | 15 | $GLOBALS['TL_DCA']['tl_plenta_jobs_basic_organization'] = [ 16 | // Config 17 | 'config' => [ 18 | 'dataContainer' => DC_Table::class, 19 | 'ctable' => ['tl_plenta_jobs_basic_job_location'], 20 | 'switchToEdit' => true, 21 | 'enableVersioning' => true, 22 | 'sql' => [ 23 | 'keys' => [ 24 | 'id' => 'primary', 25 | ], 26 | ], 27 | ], 28 | 29 | 'list' => [ 30 | 'sorting' => [ 31 | 'mode' => 2, 32 | 'fields' => ['name'], 33 | 'flag' => 1, 34 | 'disableGrouping' => true, 35 | ], 36 | 'label' => [ 37 | 'fields' => ['name'], 38 | 'format' => '%s', 39 | ], 40 | 'global_operations' => [ 41 | 'back' => [ 42 | 'route' => 'Plenta\ContaoJobsBasic\Controller\Contao\BackendModule\SettingsController', 43 | 'label' => &$GLOBALS['TL_LANG']['MSC']['backBT'], 44 | 'icon' => 'back.svg', 45 | ], 46 | 'all' => [ 47 | 'href' => 'act=select', 48 | 'class' => 'header_edit_all', 49 | 'attributes' => 'onclick="Backend.getScrollOffset()" accesskey="e"', 50 | ], 51 | ], 52 | 'operations' => [ 53 | 'edit', 54 | 'children', 55 | 'delete' 56 | ], 57 | ], 58 | 59 | // Palettes 60 | 'palettes' => [ 61 | 'default' => '{organization_legend},name,sameAs;{logo_legend},logo', 62 | ], 63 | 64 | // Fields 65 | 'fields' => [ 66 | 'id' => [ 67 | 'sql' => [ 68 | 'type' => 'integer', 69 | 'unsigned' => true, 70 | 'autoincrement' => true, 71 | ], 72 | ], 73 | 'tstamp' => [ 74 | 'sql' => [ 75 | 'type' => 'integer', 76 | 'unsigned' => true, 77 | 'default' => 0, 78 | ], 79 | ], 80 | 'name' => [ 81 | 'label' => &$GLOBALS['TL_LANG']['tl_plenta_jobs_basic_organization']['name'], 82 | 'exclude' => true, 83 | 'search' => true, 84 | 'inputType' => 'text', 85 | 'default' => '', 86 | 'eval' => [ 87 | 'maxlength' => 255, 88 | 'tl_class' => 'w50', 89 | 'mandatory' => true, 90 | ], 91 | 'sql' => [ 92 | 'type' => 'string', 93 | 'length' => 255, 94 | 'default' => '', 95 | ], 96 | ], 97 | 'sameAs' => [ 98 | 'label' => &$GLOBALS['TL_LANG']['tl_plenta_jobs_basic_organization']['sameAs'], 99 | 'exclude' => true, 100 | 'inputType' => 'text', 101 | 'default' => '', 102 | 'eval' => [ 103 | 'maxlength' => 255, 104 | 'tl_class' => 'w50', 105 | 'rgxp' => 'url', 106 | ], 107 | 'sql' => [ 108 | 'type' => 'string', 109 | 'length' => 255, 110 | 'default' => '', 111 | ], 112 | ], 113 | 'logo' => [ 114 | 'exclude' => true, 115 | 'inputType' => 'fileTree', 116 | 'eval' => [ 117 | 'fieldType' => 'radio', 118 | 'filesOnly' => true, 119 | 'extensions' => Contao\Config::get('validImageTypes'), 120 | ], 121 | 'sql' => [ 122 | 'type' => 'binary_string', 123 | 'notnull' => false, 124 | 'default' => null, 125 | ], 126 | ], 127 | ], 128 | ]; 129 | -------------------------------------------------------------------------------- /src/Controller/Contao/ContentElement/PlentaJobsBasicJobOfferDetailsController.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Controller\Contao\ContentElement; 14 | 15 | use Contao\Config; 16 | use Contao\ContentModel; 17 | use Contao\CoreBundle\Controller\ContentElement\AbstractContentElementController; 18 | use Contao\CoreBundle\DependencyInjection\Attribute\AsContentElement; 19 | use Contao\CoreBundle\Routing\ScopeMatcher; 20 | use Contao\CoreBundle\ServiceAnnotation\ContentElement; 21 | use Contao\CoreBundle\Twig\FragmentTemplate; 22 | use Contao\Input; 23 | use Contao\StringUtil; 24 | use Contao\Template; 25 | use Plenta\ContaoJobsBasic\Contao\Model\PlentaJobsBasicOfferModel; 26 | use Plenta\ContaoJobsBasic\Helper\MetaFieldsHelper; 27 | use Symfony\Component\HttpFoundation\Request; 28 | use Symfony\Component\HttpFoundation\RequestStack; 29 | use Symfony\Component\HttpFoundation\Response; 30 | 31 | #[AsContentElement(category: 'plentaJobsBasic')] 32 | class PlentaJobsBasicJobOfferDetailsController extends AbstractContentElementController 33 | { 34 | 35 | protected ?PlentaJobsBasicOfferModel $jobOffer = null; 36 | 37 | protected ?array $metaFields = null; 38 | 39 | public function __construct( 40 | protected MetaFieldsHelper $metaFieldsHelper, 41 | protected RequestStack $requestStack, 42 | protected ScopeMatcher $matcher 43 | ) { 44 | } 45 | 46 | public function getMetaFields(ContentModel $model): array 47 | { 48 | if (null !== $this->metaFields) { 49 | return $this->metaFields; 50 | } 51 | 52 | $this->metaFields = $this->metaFieldsHelper->getMetaFields($this->getJobOffer(), $model->size); 53 | 54 | return $this->metaFields; 55 | } 56 | 57 | public function getJobOffer($language = null): ?PlentaJobsBasicOfferModel 58 | { 59 | if (null !== $this->jobOffer) { 60 | return $this->jobOffer; 61 | } 62 | 63 | $alias = Input::get('auto_item'); 64 | 65 | if (null === $alias) { 66 | return null; 67 | } 68 | 69 | $this->jobOffer = PlentaJobsBasicOfferModel::findPublishedByIdOrAlias($alias); 70 | 71 | return $this->jobOffer; 72 | } 73 | 74 | public function renderDetails(string $data, string $class): string 75 | { 76 | if (empty($data)) { 77 | return ''; 78 | } 79 | 80 | return '
'.$data.'
'; 81 | } 82 | 83 | protected function getResponse(FragmentTemplate $template, ContentModel $model, Request $request): Response 84 | { 85 | if ($this->matcher->isFrontendRequest($this->requestStack->getCurrentRequest())) { 86 | if (null === $this->getJobOffer($request->getLocale())) { 87 | return new Response(''); 88 | } 89 | 90 | $metaFields = $this->getMetaFields($model); 91 | $template->content = ''; 92 | 93 | if (!empty($model->plenta_jobs_basic_job_offer_details)) { 94 | $detailsSelected = StringUtil::deserialize($model->plenta_jobs_basic_job_offer_details); 95 | 96 | foreach ($detailsSelected as $details) { 97 | $cssClass = $details; 98 | 99 | if ('description' === $details) { 100 | $cssClass .= ' ce_text'; 101 | } 102 | 103 | $template->content .= $this->renderDetails($metaFields[$details], $cssClass); 104 | } 105 | } 106 | 107 | $template->jobOfferMeta = $this->getMetaFields($model); 108 | $template->jobOffer = $this->getJobOffer(); 109 | 110 | $this->tagResponse('contao.db.tl_plenta_jobs_basic_offer.'.$this->jobOffer->id); 111 | } 112 | 113 | return $template->getResponse(); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/EventListener/Contao/InsertTagListener.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\EventListener\Contao; 14 | 15 | use Composer\InstalledVersions; 16 | use Contao\Config; 17 | use Contao\CoreBundle\ServiceAnnotation\Hook; 18 | use Contao\Date; 19 | use Contao\Input; 20 | use Contao\UserModel; 21 | use Plenta\ContaoJobsBasic\Contao\Model\PlentaJobsBasicOfferModel; 22 | use Plenta\ContaoJobsBasic\Helper\MetaFieldsHelper; 23 | use Symfony\Component\HttpFoundation\RequestStack; 24 | 25 | /** 26 | * @Hook("replaceInsertTags") 27 | */ 28 | class InsertTagListener 29 | { 30 | public const TAG = 'job'; 31 | 32 | protected RequestStack $requestStack; 33 | 34 | protected MetaFieldsHelper $metaFieldsHelper; 35 | 36 | /** 37 | * @var string|null 38 | */ 39 | protected ?string $autoItem = null; 40 | 41 | public function __construct(RequestStack $requestStack, MetaFieldsHelper $metaFieldsHelper) 42 | { 43 | $this->requestStack = $requestStack; 44 | $this->metaFieldsHelper = $metaFieldsHelper; 45 | } 46 | 47 | public function __invoke(string $tag) 48 | { 49 | $chunks = explode('::', $tag); 50 | 51 | if (self::TAG !== $chunks[0]) { 52 | return false; 53 | } 54 | 55 | $this->handleAutoItem(); 56 | 57 | if (!$this->autoItem) { 58 | return false; 59 | } 60 | 61 | $jobOfferData = PlentaJobsBasicOfferModel::findPublishedByIdOrAlias($this->autoItem); 62 | $language = $this->requestStack->getCurrentRequest()->getLocale(); 63 | 64 | if (null !== $jobOfferData) { 65 | $translation = $jobOfferData->getTranslation($language); 66 | 67 | if ('id' === $chunks[1]) { 68 | return (string) $jobOfferData->id; 69 | } 70 | 71 | if ('teasertext' === $chunks[1]) { 72 | return $translation['teaser'] ?? (string) $jobOfferData->teaser; 73 | } 74 | if ('description' === $chunks[1]) { 75 | return $translation['description'] ?? (string) $jobOfferData->description; 76 | } 77 | 78 | if ('title' === $chunks[1]) { 79 | return $translation['title'] ?? (string) $jobOfferData->title; 80 | } 81 | 82 | if ('alias' === $chunks[1]) { 83 | return $translation['alias'] ?? (string) $jobOfferData->alias; 84 | } 85 | 86 | if ('datePosted' === $chunks[1]) { 87 | $objPage = $GLOBALS['objPage'] ?? null; 88 | 89 | return Date::parse($objPage->dateFormat ?? Config::get('dateFormat'), $jobOfferData->datePosted); 90 | } 91 | 92 | if ('author' === $chunks[1] && isset($chunks[2])) { 93 | $author = UserModel::findById((int) $jobOfferData->author); 94 | 95 | if (null !== $author) { 96 | if ('name' === $chunks[2]) { 97 | return (string) $author->name; 98 | } 99 | 100 | if ('email' === $chunks[2]) { 101 | return (string) $author->email; 102 | } 103 | 104 | if ('username' === $chunks[2]) { 105 | return (string) $author->username; 106 | } 107 | } 108 | } 109 | 110 | if ('location' === $chunks[1]) { 111 | return $this->metaFieldsHelper->formatAddressLocality($jobOfferData) ?? ''; 112 | } 113 | 114 | if ('locationTitle' === $chunks[1]) { 115 | return $this->metaFieldsHelper->formatAddressLocalityTitle($jobOfferData) ?? ''; 116 | } 117 | } 118 | 119 | return false; 120 | } 121 | 122 | public function handleAutoItem(): void 123 | { 124 | if (null === $this->autoItem) { 125 | $this->autoItem = Input::get('auto_item', false, true); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /contao/templates/_new/frontend_module/plenta_jobs_basic_filter.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@Contao/frontend_module/_base.html.twig' %} 2 | {% set searchable = false %} 3 | 4 | {% block content %} 5 | 6 | {{ form(form) }} 7 | 8 | 9 | (function ($) { 10 | let timer = null; 11 | let delay = 1000; 12 | let jobList = null; 13 | 14 | document.addEventListener('DOMContentLoaded', function () { 15 | 16 | $('input, select').on('change', function () { 17 | if (this.type === 'text') { 18 | return; 19 | } 20 | 21 | clearTimeout(timer); 22 | timer = setTimeout(doAjax, delay); 23 | }) 24 | 25 | $('input[type=text]').on('keyup', function () { 26 | clearTimeout(timer); 27 | timer = setTimeout(doAjax, delay); 28 | }) 29 | }); 30 | 31 | function doAjax() 32 | { 33 | jobList = document.querySelector('.module-plenta-jobs-basic-offer-list'); 34 | 35 | if (null !== jobList) { 36 | let args = {}; 37 | $('input:checked').each(function () { 38 | let name = $(this).prop('name'); 39 | if (name.endsWith('[]')) { 40 | name = name.replace('[]', ''); 41 | if (typeof (args[name]) === 'undefined') { 42 | args[name] = []; 43 | } 44 | args[name].push($(this).val()); 45 | } else { 46 | args[name] = $(this).val(); 47 | } 48 | }) 49 | 50 | $('input[type=text]').each(function () { 51 | if (this.value) { 52 | args[this.name] = this.value; 53 | } 54 | }) 55 | 56 | $('select').each(function () { 57 | args[this.name] = $(this).val(); 58 | }) 59 | 60 | let searchParams = new URLSearchParams(location.search); 61 | if (searchParams.has('sortBy')) { 62 | args['sortBy'] = searchParams.get('sortBy'); 63 | } 64 | if (searchParams.has('order')) { 65 | args['order'] = searchParams.get('order'); 66 | } 67 | let str_args = ''; 68 | Object.entries(args).forEach(function (item) { 69 | if (typeof (item[1]) === 'object') { 70 | item[1].forEach(function (arrItem) { 71 | str_args += str_args === '' ? '?' : '&'; 72 | str_args += item[0].replace('[]', '') + '[]=' + arrItem; 73 | }) 74 | } else { 75 | str_args += str_args === '' ? '?' : '&'; 76 | str_args += item[0] + '=' + item[1]; 77 | } 78 | }); 79 | history.replaceState(null, null, location.pathname + str_args); 80 | args['id'] = jobList.dataset.id; 81 | args['page'] = {{ page }}; 82 | 83 | {% block beforeAjax %} 84 | {% endblock %} 85 | 86 | $.ajax('{{ ajaxRoute }}', { 87 | method: 'GET', 88 | data: args, 89 | headers: { 90 | 'Accept-Language': '{{ locale }}' 91 | }, 92 | success: function (data) { 93 | data = data.replaceAll('{{ ajaxRoute }}'.replace(/^\//, ''), location.pathname.replace(/^\//, '')); 94 | $('.module-plenta-jobs-basic-offer-list:not(.no-filter)').replaceWith(data); 95 | document.dispatchEvent(new Event('plentaJobsBasic:updateList')); 96 | 97 | {% block afterAjax %} 98 | {% endblock %} 99 | } 100 | }) 101 | } 102 | 103 | {% if data.plentaJobsBasicShowButton|default and data.plentaJobsBasicDynamicButton|default %} 104 | let form = document.querySelector('.module-plenta-jobs-basic-filter form'); 105 | let formData = new FormData(form); 106 | 107 | fetch('/_plenta-jobs-basic/offer/count?' + new URLSearchParams(formData).toString() + '&module=' + {{ data.id }}, { 108 | }).then(r => r.text()).then(r => { 109 | form.querySelector('button#submit').innerText = r; 110 | }) 111 | {% endif %} 112 | } 113 | })(jQuery); 114 | 115 | 116 | {% endblock %} 117 | -------------------------------------------------------------------------------- /contao/dca/tl_plenta_jobs_basic_settings_employment_type.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | use Contao\DC_Table; 14 | use Plenta\ContaoJobsBasic\EventListener\Contao\DCA\TlPlentaJobsBasicSettingsEmploymentType; 15 | 16 | $GLOBALS['TL_DCA']['tl_plenta_jobs_basic_settings_employment_type'] = [ 17 | 'config' => [ 18 | 'dataContainer' => DC_Table::class, 19 | 'switchToEdit' => true, 20 | 'markAsCopy' => 'title', 21 | 'enableVersioning' => true, 22 | 'sql' => [ 23 | 'keys' => [ 24 | 'id' => 'primary', 25 | ], 26 | ], 27 | ], 28 | 29 | 'list' => [ 30 | 'sorting' => [ 31 | 'mode' => 1, 32 | 'fields' => ['title'], 33 | 'flag' => 1, 34 | 'panelLayout' => 'filter;search,sort,limit', 35 | ], 36 | 'label' => [ 37 | 'fields' => ['title'], 38 | 'showColumns' => false, 39 | ], 40 | 'global_operations' => [ 41 | 'back' => [ 42 | 'route' => 'Plenta\ContaoJobsBasic\Controller\Contao\BackendModule\SettingsController', 43 | 'label' => &$GLOBALS['TL_LANG']['MSC']['backBT'], 44 | 'icon' => 'back.svg', 45 | ], 46 | 'all' => [ 47 | 'href' => 'act=select', 48 | 'class' => 'header_edit_all', 49 | 'attributes' => 'onclick="Backend.getScrollOffset()" accesskey="e"', 50 | ], 51 | ], 52 | 'operations' => [ 53 | 'edit' => [ 54 | 'href' => 'act=edit', 55 | 'icon' => 'edit.svg', 56 | ], 57 | 'copy' => [ 58 | 'href' => 'act=copy', 59 | 'icon' => 'copy.svg', 60 | ], 61 | 'delete' => [ 62 | 'href' => 'act=delete', 63 | 'icon' => 'delete.svg', 64 | 'attributes' => 'onclick="if(!confirm(\''.($GLOBALS['TL_LANG']['MSC']['deleteConfirm'] ?? null).'\'))return false;Backend.getScrollOffset()"', 65 | ], 66 | ], 67 | ], 68 | 69 | // Palettes 70 | 'palettes' => [ 71 | 'default' => '{settings_legend},title,google_for_jobs_mapping;translation', 72 | ], 73 | 74 | // Fields 75 | 'fields' => [ 76 | 'id' => [ 77 | 'sql' => [ 78 | 'type' => 'integer', 79 | 'unsigned' => true, 80 | 'autoincrement' => true, 81 | ], 82 | ], 83 | 'tstamp' => [ 84 | 'sql' => [ 85 | 'type' => 'integer', 86 | 'unsigned' => true, 87 | 'default' => 0, 88 | ], 89 | ], 90 | 'title' => [ 91 | 'exclude' => true, 92 | 'search' => true, 93 | 'inputType' => 'text', 94 | 'eval' => [ 95 | 'maxlength' => 255, 96 | 'tl_class' => 'w50', 97 | 'mandatory' => true, 98 | ], 99 | 'sql' => [ 100 | 'type' => 'string', 101 | 'length' => 255, 102 | 'default' => '', 103 | ], 104 | ], 105 | 'google_for_jobs_mapping' => [ 106 | 'exclude' => true, 107 | 'inputType' => 'select', 108 | 'options_callback' => [ 109 | TlPlentaJobsBasicSettingsEmploymentType::class, 110 | 'googleForJobsMappingOptionsCallback', 111 | ], 112 | 'eval' => [ 113 | 'tl_class' => 'w50', 114 | 'mandatory' => true, 115 | ], 116 | 'sql' => [ 117 | 'type' => 'string', 118 | 'length' => 32, 119 | 'default' => 'OTHER', 120 | ], 121 | ], 122 | 'translation' => [ 123 | 'inputType' => 'metaWizard', 124 | 'eval' => [ 125 | 'class' => 'clr', 126 | 'allowHtml' => true, 127 | 'multiple' => true, 128 | 'metaFields' => [ 129 | 'title' => 'maxlength="255"', 130 | ], 131 | ], 132 | 'load_callback' => [[ 133 | TlPlentaJobsBasicSettingsEmploymentType::class, 134 | 'translationLoadCallback', 135 | ]], 136 | 'save_callback' => [[ 137 | TlPlentaJobsBasicSettingsEmploymentType::class, 138 | 'translationSaveCallback', 139 | ]], 140 | 'sql' => 'JSON NULL default NULL', 141 | ], 142 | ], 143 | ]; 144 | -------------------------------------------------------------------------------- /src/Form/Type/JobOfferFilterType.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Form\Type; 14 | 15 | use Contao\CoreBundle\InsertTag\InsertTagParser; 16 | use Contao\CoreBundle\String\SimpleTokenParser; 17 | use Contao\ModuleModel; 18 | use Contao\StringUtil; 19 | use Plenta\ContaoJobsBasic\Helper\CountJobsHelper; 20 | use Symfony\Component\Form\AbstractType; 21 | use Symfony\Component\Form\Extension\Core\Type\ChoiceType; 22 | use Symfony\Component\Form\Extension\Core\Type\SubmitType; 23 | use Symfony\Component\Form\Extension\Core\Type\TextType; 24 | use Symfony\Component\Form\FormBuilderInterface; 25 | use Symfony\Component\HttpFoundation\RequestStack; 26 | use Symfony\Component\OptionsResolver\OptionsResolver; 27 | 28 | class JobOfferFilterType extends AbstractType 29 | { 30 | 31 | public function __construct( 32 | protected InsertTagParser $insertTagParser, 33 | protected RequestStack $requestStack, 34 | protected CountJobsHelper $countJobsHelper, 35 | protected SimpleTokenParser $simpleTokenParser 36 | ) { 37 | } 38 | 39 | public function buildForm(FormBuilderInterface $builder, array $options): void 40 | { 41 | /** @var ModuleModel $model */ 42 | $model = $options['fmd']; 43 | if ($model->plentaJobsBasicShowKeyword) { 44 | $builder->add('keywordHeadline', HtmlType::class, [ 45 | 'html' => $this->getHeadlineHtml($model->plentaJobsBasicKeywordHeadline, 'keyword'), 46 | 'priority' => 110, 47 | ]); 48 | $builder->add('keyword', TextType::class, [ 49 | 'label' => false, 50 | 'required' => false, 51 | 'row_attr' => [ 52 | 'class' => 'widget-text', 53 | ], 54 | 'priority' => 109, 55 | ]); 56 | } 57 | if ($model->plentaJobsBasicShowTypes) { 58 | $builder->add('typesHeadline', HtmlType::class, [ 59 | 'html' => $this->getHeadlineHtml($model->plentaJobsBasicTypesHeadline, 'jobTypes'), 60 | 'priority' => 100, 61 | ]); 62 | $builder->add('types', ChoiceType::class, [ 63 | 'choices' => array_flip($options['types']), 64 | 'multiple' => true, 65 | 'expanded' => true, 66 | 'label' => false, 67 | 'required' => false, 68 | 'row_attr' => [ 69 | 'class' => 'widget-checkbox', 70 | ], 71 | 'data' => $this->requestStack->getMainRequest()->get('types', []), 72 | 'priority' => 99, 73 | ]); 74 | } 75 | 76 | if ($model->plentaJobsBasicShowLocations) { 77 | $builder->add('locationsHeadline', HtmlType::class, [ 78 | 'html' => $this->getHeadlineHtml($model->plentaJobsBasicLocationsHeadline, 'jobLocation'), 79 | 'priority' => 90, 80 | ]); 81 | $builder->add('location', ChoiceType::class, [ 82 | 'choices' => array_flip($options['locations']), 83 | 'multiple' => !$model->plentaJobsBasicDisableMultipleLocations, 84 | 'expanded' => true, 85 | 'label' => false, 86 | 'required' => false, 87 | 'row_attr' => [ 88 | 'class' => 'widget-checkbox', 89 | ], 90 | 'placeholder' => 'MSC.PLENTA_JOBS.filterForm.locationPlaceholder', 91 | 'translation_domain' => 'contao_default', 92 | 'data' => $this->requestStack->getMainRequest()->get('location', $model->plentaJobsBasicDisableMultipleLocations ? '' : []), 93 | 'priority' => 89, 94 | ]); 95 | } 96 | 97 | if ($model->plentaJobsBasicShowButton) { 98 | $builder->add('submit', SubmitType::class, [ 99 | 'label' => $model->plentaJobsBasicDynamicButton ? $this->simpleTokenParser->parse($model->plentaJobsBasicSubmit, ['count' => $this->countJobsHelper->countJobs()]) : $model->plentaJobsBasicSubmit, 100 | 'priority' => 0, 101 | ]); 102 | } 103 | } 104 | 105 | public function configureOptions(OptionsResolver $resolver): void 106 | { 107 | $resolver->setDefaults([ 108 | 'fmd' => null, 109 | 'types' => [], 110 | 'locations' => [], 111 | 'method' => 'GET', 112 | 'csrf_protection' => false, 113 | ]); 114 | } 115 | 116 | public function getHeadlineHtml(?string $content, string $type): string 117 | { 118 | if (empty($content)) { 119 | return ''; 120 | } 121 | 122 | $return = '
'; 123 | $return .= $this->insertTagParser->replace($content); 124 | $return .= '
'; 125 | 126 | return $return; 127 | } 128 | 129 | public function getBlockPrefix() 130 | { 131 | return ''; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /contao/languages/de/default.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vollzeit 7 | 8 | 9 | Teilzeit 10 | 11 | 12 | Befristet 13 | 14 | 15 | Leiharbeit 16 | 17 | 18 | Praktikum 19 | 20 | 21 | Ehrenamt 22 | 23 | 24 | Tageweise 25 | 26 | 27 | Andere 28 | 29 | 30 | Jobs (Basic) 31 | 32 | 33 | Details zum Jobangebot 34 | 35 | 36 | Details zum Jobangebot 37 | 38 | 39 | Stellenanzeigen-Teaser 40 | 41 | 42 | Stellenanzeigen-Teaser 43 | 44 | 45 | Es sind momentan keine Stellenanzeigen vorhanden. 46 | 47 | 48 | Dokumentation 49 | 50 | 51 | Fehler melden 52 | 53 | 54 | Supporter werden 55 | 56 | 57 | Add-ons 58 | 59 | 60 | Homeoffice 61 | 62 | 63 | Remote in 64 | 65 | 66 | Stellentitel 67 | 68 | 69 | Stellenbild 70 | 71 | 72 | Stellenbeschreibung 73 | 74 | 75 | Stellenart 76 | 77 | 78 | Bewerbungsfrist 79 | 80 | 81 | Arbeitsort 82 | 83 | 84 | Teaser-Text 85 | 86 | 87 | Veröffentlichungsdatum 88 | 89 | 90 | Unternehmen 91 | 92 | 93 | Zurück-Link 94 | 95 | 96 | Inhaltselemente 97 | 98 | 99 | Gehalt 100 | 101 | 102 | Teaser-Text 103 | 104 | 105 | Eintrittsdatum 106 | 107 | 108 | Alle Standorte 109 | 110 | 111 | ab sofort 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /contao/languages/de/tl_plenta_jobs_basic_job_location.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Adressdaten 7 | 8 | 9 | Straße/Hausnummer 10 | 11 | 12 | Bitte geben Sie die Straße und Hausnummer ein. 13 | 14 | 15 | Stadt 16 | 17 | 18 | Bitte geben Sie die Stadt ein. 19 | 20 | 21 | Bundesland/Kanton 22 | 23 | 24 | Bitte geben Sie das Bundesland oder den Kanton ein. 25 | 26 | 27 | Postleitzahl 28 | 29 | 30 | Bitte geben Sie Postleitzahl ein. 31 | 32 | 33 | Land 34 | 35 | 36 | Bitte wählen Sie ein Land aus. 37 | 38 | 39 | Firmenlogo 40 | 41 | 42 | Neuer Standort 43 | 44 | 45 | Einen neuen Standorte anlegen 46 | 47 | 48 | Ortsbeschränkungen 49 | 50 | 51 | Legen Sie für jeden möglichen Ort einen Eintrag an. 52 | 53 | 54 | Verwaltungseinheit 55 | 56 | 57 | Bitte wählen Sie die Art der Verwaltungseinheit aus der Liste aus. 58 | 59 | 60 | Name 61 | 62 | 63 | Bitte geben Sie den Namen der Verwaltungseinheit an (z.B. Deutschland). 64 | 65 | 66 | Land 67 | 68 | 69 | Bundesland/Kanton 70 | 71 | 72 | Stadt 73 | 74 | 75 | Schulbezirk 76 | 77 | 78 | Standort-Art 79 | 80 | 81 | Bitte wählen Sie eine Art aus der Liste aus. 82 | 83 | 84 | Vor Ort 85 | 86 | 87 | Homeoffice 88 | 89 | 90 | Typen-Einstellungen 91 | 92 | 93 | Standort-Einstellungen 94 | 95 | 96 | Titel 97 | 98 | 99 | Bitte geben Sie einen Titel ein. 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /src/Migration/MoveRemoteJobsMigration.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Migration; 14 | 15 | use Contao\CoreBundle\Migration\MigrationResult; 16 | use Contao\StringUtil; 17 | use Contao\System; 18 | use Doctrine\DBAL\Connection; 19 | use Plenta\ContaoJobsBasic\GoogleForJobs\GoogleForJobs; 20 | 21 | class MoveRemoteJobsMigration extends \Contao\CoreBundle\Migration\AbstractMigration 22 | { 23 | protected Connection $database; 24 | 25 | public function __construct(Connection $connection) 26 | { 27 | $this->database = $connection; 28 | } 29 | 30 | public function getName(): string 31 | { 32 | return 'Plenta Jobs Basic Bundle 2.0 Update - Remote jobs'; 33 | } 34 | 35 | public function shouldRun(): bool 36 | { 37 | $schemaManager = $this->database->getSchemaManager(); 38 | 39 | if (!$schemaManager->tablesExist(['tl_plenta_jobs_basic_offer', 'tl_plenta_jobs_basic_job_location'])) { 40 | return false; 41 | } 42 | 43 | $columns = $schemaManager->listTableColumns('tl_plenta_jobs_basic_offer'); 44 | $columnsLocation = $schemaManager->listTableColumns('tl_plenta_jobs_basic_job_location'); 45 | 46 | if (!isset($columns['isremote']) || !isset($columnsLocation['requirementtype'])) { 47 | return false; 48 | } 49 | 50 | if (true === (bool) $this->database 51 | ->executeQuery(" 52 | SELECT EXISTS( 53 | SELECT id 54 | FROM tl_plenta_jobs_basic_offer 55 | WHERE 56 | isRemote = '1' 57 | ) 58 | ") 59 | ->fetchOne() 60 | ) { 61 | return true; 62 | } 63 | 64 | return false; 65 | } 66 | 67 | public function run(): MigrationResult 68 | { 69 | $offers = $this->database->executeQuery("SELECT * FROM tl_plenta_jobs_basic_offer WHERE isRemote = '1'")->fetchAllAssociative(); 70 | $rootPage = $this->database->executeQuery("SELECT * FROM tl_page WHERE type = 'root' AND fallback = '1' LIMIT 1")->fetchAssociative(); 71 | System::loadLanguageFile('countries', $rootPage['language'] ?? 'en'); 72 | 73 | foreach ($offers as $offer) { 74 | $locations = StringUtil::deserialize($offer['jobLocation']); 75 | $newLocation = null; 76 | foreach ($locations as $location) { 77 | $locationArr = $this->database->prepare('SELECT * FROM tl_plenta_jobs_basic_job_location WHERE id = ?')->executeQuery([$location])->fetchAssociative(); 78 | if ('Telecommute' === $locationArr['jobTypeLocation']) { 79 | $newLocation = null; 80 | break; 81 | } 82 | if (null !== $newLocation) { 83 | continue; 84 | } 85 | 86 | $requirements = StringUtil::deserialize($offer['applicantLocationRequirements']); 87 | if (!\is_array($requirements)) { 88 | $requirements = []; 89 | } 90 | 91 | $requirements = array_filter($requirements, fn ($item) => \in_array($item['key'], GoogleForJobs::ALLOWED_TYPES, true)); 92 | 93 | if (empty($requirements)) { 94 | $requirements = [ 95 | [ 96 | 'key' => 'Country', 97 | 'value' => $GLOBALS['TL_LANG']['CNT'][$locationArr['addressCountry']], 98 | ], 99 | ]; 100 | } 101 | 102 | foreach ($requirements as $requirement) { 103 | $remoteLocation = $this->database->prepare('SELECT * FROM tl_plenta_jobs_basic_job_location WHERE jobTypeLocation = ? AND pid = ? AND requirementType = ? AND requirementValue = ?')->executeQuery(['Telecommute', $locationArr['pid'], $requirement['key'], $requirement['value']])->fetchAssociative(); 104 | if ($remoteLocation) { 105 | $newLocation = $remoteLocation['id']; 106 | } else { 107 | $this->database->insert('tl_plenta_jobs_basic_job_location', [ 108 | 'pid' => $locationArr['pid'], 109 | 'requirementType' => $requirement['key'], 110 | 'requirementValue' => $requirement['value'], 111 | 'tstamp' => time(), 112 | 'jobTypeLocation' => 'Telecommute', 113 | ]); 114 | $newLocation = $this->database->lastInsertId(); 115 | } 116 | $locations[] = $newLocation; 117 | } 118 | } 119 | 120 | if (null !== $newLocation) { 121 | $this->database->update( 122 | 'tl_plenta_jobs_basic_offer', 123 | [ 124 | 'isRemote' => 0, 125 | 'applicantLocationRequirements' => null, 126 | 'jobLocation' => serialize($locations), 127 | ], 128 | [ 129 | 'id' => $offer['id'], 130 | ] 131 | ); 132 | } 133 | } 134 | 135 | return new MigrationResult(true, 'Remote jobs have successfully been migrated to having designated remote job locations.'); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Helper/EmploymentType.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Helper; 14 | 15 | use Composer\InstalledVersions; 16 | use Plenta\ContaoJobsBasic\Contao\Model\PlentaJobsBasicSettingsEmploymentTypeModel; 17 | use Symfony\Component\HttpFoundation\RequestStack; 18 | use Symfony\Contracts\Translation\LocaleAwareInterface; 19 | 20 | class EmploymentType 21 | { 22 | protected LocaleAwareInterface $translator; 23 | protected RequestStack $requestStack; 24 | protected string $customEmploymentTypePrefix = 'CUSTOM_'; 25 | private ?array $customEmploymentTypes = null; 26 | 27 | public function __construct( 28 | LocaleAwareInterface $translator, 29 | RequestStack $requestStack 30 | ) { 31 | $this->translator = $translator; 32 | $this->requestStack = $requestStack; 33 | } 34 | 35 | public function getGoogleForJobsEmploymentTypes(): array 36 | { 37 | return [ 38 | 'FULL_TIME', 39 | 'PART_TIME', 40 | 'CONTRACTOR', 41 | 'TEMPORARY', 42 | 'INTERN', 43 | 'VOLUNTEER', 44 | 'PER_DIEM', 45 | 'OTHER', 46 | ]; 47 | } 48 | 49 | public function getCustomEmploymentTypes(): array 50 | { 51 | $customEmploymentTypes = $this->getAndSetCustomEmploymentTypes(); 52 | $return = []; 53 | 54 | if (empty($customEmploymentTypes)) { 55 | return $return; 56 | } 57 | 58 | foreach ($customEmploymentTypes as $customEmploymentType) { 59 | $return[] = $this->customEmploymentTypePrefix.$customEmploymentType->id; 60 | } 61 | 62 | return $return; 63 | } 64 | 65 | /** 66 | * @return PlentaJobsBasicSettingsEmploymentTypeModel[] 67 | */ 68 | public function getAndSetCustomEmploymentTypes() 69 | { 70 | if (null === $this->customEmploymentTypes) { 71 | $employmentTypes = []; 72 | $objEmploymentTypes = PlentaJobsBasicSettingsEmploymentTypeModel::findAll(); 73 | if ($objEmploymentTypes) { 74 | foreach ($objEmploymentTypes as $employmentType) { 75 | $employmentTypes[$employmentType->id] = $employmentType; 76 | } 77 | } 78 | $this->customEmploymentTypes = $employmentTypes; 79 | } 80 | 81 | return $this->customEmploymentTypes; 82 | } 83 | 84 | /** 85 | * @return string[] 86 | */ 87 | public function getEmploymentTypes(): array 88 | { 89 | return array_merge( 90 | $this->getGoogleForJobsEmploymentTypes(), 91 | $this->getCustomEmploymentTypes() 92 | ); 93 | } 94 | 95 | public function getEmploymentTypeName(string $employmentType): ?string 96 | { 97 | if (0 === strpos($employmentType, $this->customEmploymentTypePrefix)) { 98 | $translation = $this->getEmploymentTypeNameFromDatabase($employmentType); 99 | } else { 100 | $translation = $this->getEmploymentTypeNameFromTranslator($employmentType); 101 | } 102 | 103 | return $translation; 104 | } 105 | 106 | public function getEmploymentTypeNameFromDatabase(string $identifier): ?string 107 | { 108 | /** @var PlentaJobsBasicSettingsEmploymentTypeModel[] $employmentTypes */ 109 | $employmentTypes = $this->getAndSetCustomEmploymentTypes(); 110 | 111 | if (empty($employmentTypes)) { 112 | return null; 113 | } 114 | 115 | $employmentTypeId = str_replace($this->customEmploymentTypePrefix, '', $identifier); 116 | 117 | if (false === isset($employmentTypes[$employmentTypeId])) { 118 | return null; 119 | } 120 | 121 | $request = $this->requestStack->getCurrentRequest(); 122 | 123 | $language = substr( 124 | $request->getLocale(), 125 | 0, 126 | 2 127 | ); 128 | 129 | if (!is_null($employmentTypes[$employmentTypeId]->translation) && 130 | false === empty($translatedTitle = json_decode($employmentTypes[$employmentTypeId]->translation, true)[$language]['title'] ?? null)) { 131 | $translation = $translatedTitle; 132 | } else { 133 | $translation = $employmentTypes[$employmentTypeId]->title; 134 | } 135 | 136 | return $translation; 137 | } 138 | 139 | public function getEmploymentTypeNameFromTranslator(string $employmentType): ?string 140 | { 141 | $translation = $this->translator->trans( 142 | 'MSC.PLENTA_JOBS.'.$employmentType, 143 | [], 144 | 'contao_default' 145 | ); 146 | 147 | if ($translation === 'MSC.PLENTA_JOBS.'.$employmentType) { 148 | return null; 149 | } 150 | 151 | return $translation; 152 | } 153 | 154 | public function getEmploymentTypesFormatted(?array $employmentTypes): string 155 | { 156 | if (null === $employmentTypes) { 157 | return ''; 158 | } 159 | 160 | $employmentTypesTemp = []; 161 | 162 | foreach ($employmentTypes as $employmentType) { 163 | $employmentTypesTemp[] = $this->getEmploymentTypeName($employmentType); 164 | } 165 | 166 | return implode(', ', array_filter($employmentTypesTemp)); 167 | } 168 | 169 | public function getCustomEmploymentTypePrefix(): string 170 | { 171 | return $this->customEmploymentTypePrefix; 172 | } 173 | 174 | public function getMappedEmploymentTypesForGoogleForJobs(array $employmentTypesUnmapped): array 175 | { 176 | $employmentTypes = $this->getAndSetCustomEmploymentTypes(); 177 | $return = []; 178 | 179 | foreach ($employmentTypesUnmapped as $employmentTypeUnmapped) { 180 | if (0 === strpos($employmentTypeUnmapped, $this->customEmploymentTypePrefix)) { 181 | $employmentTypeId = str_replace($this->customEmploymentTypePrefix, '', $employmentTypeUnmapped); 182 | 183 | if (false === isset($employmentTypes[$employmentTypeId])) { 184 | continue; 185 | } 186 | 187 | $return[] = $employmentTypes[$employmentTypeId]->google_for_jobs_mapping; 188 | } else { 189 | $return[] = $employmentTypeUnmapped; 190 | } 191 | } 192 | 193 | return array_unique($return); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /src/Helper/MetaFieldsHelper.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\Helper; 14 | 15 | use Contao\CoreBundle\File\Metadata; 16 | use Contao\CoreBundle\Image\Studio\Studio; 17 | use Contao\CoreBundle\InsertTag\InsertTagParser; 18 | use Contao\Date; 19 | use Contao\FilesModel; 20 | use Contao\Frontend; 21 | use Contao\FrontendTemplate; 22 | use Contao\StringUtil; 23 | use Contao\System; 24 | use Plenta\ContaoJobsBasic\Contao\Model\PlentaJobsBasicJobLocationModel; 25 | use Plenta\ContaoJobsBasic\Contao\Model\PlentaJobsBasicOfferModel; 26 | use Plenta\ContaoJobsBasic\Contao\Model\PlentaJobsBasicOrganizationModel; 27 | use Symfony\Component\HttpFoundation\RequestStack; 28 | use Symfony\Component\Intl\Countries; 29 | use Symfony\Contracts\Translation\TranslatorInterface; 30 | 31 | class MetaFieldsHelper 32 | { 33 | public function __construct( 34 | protected EmploymentType $employmentTypeHelper, 35 | protected RequestStack $requestStack, 36 | protected InsertTagParser $insertTagParser, 37 | protected TranslatorInterface $translator, 38 | ) { 39 | } 40 | 41 | public function getMetaFields(PlentaJobsBasicOfferModel $jobOffer, $imageSize = null): array 42 | { 43 | $metaFields = []; 44 | 45 | $translation = $jobOffer->getTranslation($this->requestStack->getCurrentRequest()->getLocale()); 46 | 47 | $metaFields['publicationDateFormatted'] = Date::parse(Date::getNumericDateFormat(), $jobOffer->datePosted); 48 | $metaFields['employmentTypeFormatted'] = $this->employmentTypeHelper->getEmploymentTypesFormatted(json_decode($jobOffer->employmentType, true)); 49 | $metaFields['locationFormatted'] = $this->formatLocation($jobOffer); 50 | $metaFields['addressLocalityFormatted'] = $this->formatAddressLocality($jobOffer); 51 | $metaFields['addressCountryFormatted'] = $this->formatAddressCountry($jobOffer); 52 | $metaFields['title'] = $this->insertTagParser->replace($translation['title'] ?? $jobOffer->title); 53 | $metaFields['description'] = $this->insertTagParser->replace(StringUtil::restoreBasicEntities($translation['description'] ?? $jobOffer->description)); 54 | $metaFields['alias'] = $translation['alias'] ?? $jobOffer->alias; 55 | $metaFields['company'] = $this->formatCompany($jobOffer); 56 | $metaFields['teaser'] = $this->insertTagParser->replace($translation['teaser'] ?? $jobOffer->teaser ?? ''); 57 | 58 | if ($jobOffer->entryDate) { 59 | $metaFields['entryDateFormatted'] = Date::parse(Date::getNumericDateFormat(), $jobOffer->entryDate); 60 | } elseif ($jobOffer->startNow) { 61 | $metaFields['entryDateFormatted'] = $this->translator->trans('MSC.PLENTA_JOBS.startNow', [], 'contao_default'); 62 | } 63 | 64 | if ($imageSize && $jobOffer->addImage) { 65 | $file = FilesModel::findByUuid(StringUtil::binToUuid($jobOffer->singleSRC)); 66 | if ($file) { 67 | $tpl = new FrontendTemplate('jobs_basic_reader_parts/plenta_jobs_basic_reader_image'); 68 | $meta = []; 69 | if ($jobOffer->overwriteMeta) { 70 | $request = System::getContainer()->get('request_stack')->getCurrentRequest(); 71 | if ($request && System::getContainer()->get('contao.routing.scope_matcher')->isBackendRequest($request)) { 72 | $language = $request->getLocale(); 73 | } else { 74 | global $objPage; 75 | $language = $objPage->language; 76 | } 77 | $arrMeta = Frontend::getMetaData($file->meta, $language); 78 | $meta = [ 79 | 'alt' => $jobOffer->alt ?: ($arrMeta['alt'] ?? ''), 80 | 'imageTitle' => $jobOffer->imageTitle ?: ($arrMeta['title'] ?? ''), 81 | 'imageUrl' => $jobOffer->imageUrl ?: ($arrMeta['link'] ?? ''), 82 | 'caption' => $jobOffer->caption ?: ($arrMeta['caption'] ?? ''), 83 | ]; 84 | } 85 | $data = [ 86 | 'id' => null, 87 | 'singleSRC' => $jobOffer->singleSRC, 88 | 'sortBy' => 'custom', 89 | 'fullsize' => false, 90 | 'size' => $imageSize, 91 | ]; 92 | 93 | if (!empty($meta)) { 94 | $data['overwriteMeta'] = true; 95 | $data['alt'] = $meta['alt'] ?? ''; 96 | $data['imageTitle'] = $meta['imageTitle'] ?? ''; 97 | $data['imageUrl'] = $meta['imageUrl'] ?? ''; 98 | $data['caption'] = $meta['caption'] ?? ''; 99 | } 100 | 101 | $tpl->data = $data; 102 | 103 | $metaFields['image'] = $tpl->parse(); 104 | } 105 | } 106 | 107 | if (!isset($metaFields['image'])) { 108 | $metaFields['image'] = ''; 109 | } 110 | 111 | return $metaFields; 112 | } 113 | 114 | public function formatLocation(PlentaJobsBasicOfferModel $jobOffer): string 115 | { 116 | return $this->formatAddressLocality($jobOffer); 117 | } 118 | 119 | public function formatAddressLocality(PlentaJobsBasicOfferModel $jobOffer): string 120 | { 121 | $locationsTemp = []; 122 | 123 | $locations = StringUtil::deserialize($jobOffer->jobLocation); 124 | 125 | foreach ($locations as $location) { 126 | $objLocation = PlentaJobsBasicJobLocationModel::findByPk($location); 127 | $name = 'onPremise' === $objLocation->jobTypeLocation ? $objLocation->addressLocality : ('Country' === $objLocation->requirementType ? $objLocation->requirementValue : null); 128 | if (!\in_array($name, $locationsTemp, true)) { 129 | $locationsTemp[] = $name; 130 | } 131 | } 132 | 133 | return implode(', ', $locationsTemp); 134 | } 135 | 136 | public function formatAddressCountry(PlentaJobsBasicOfferModel $jobOffer): string 137 | { 138 | $countriesTemp = []; 139 | $locations = StringUtil::deserialize($jobOffer->jobLocation); 140 | 141 | System::loadLanguageFile('countries'); 142 | 143 | foreach ($locations as $location) { 144 | $objLocation = PlentaJobsBasicJobLocationModel::findByPk($location); 145 | $name = 'onPremise' === $objLocation->jobTypeLocation ? Countries::getName($objLocation->addressCountry) : ('Country' === $objLocation->requirementType ? $objLocation->requirementValue : null); 146 | if ($name && !\in_array($name, $countriesTemp, true)) { 147 | $countriesTemp[] = $name; 148 | } 149 | } 150 | 151 | return implode(', ', $countriesTemp); 152 | } 153 | 154 | public function formatCompany(PlentaJobsBasicOfferModel $jobOffer): string 155 | { 156 | $company = []; 157 | $locations = StringUtil::deserialize($jobOffer->jobLocation); 158 | foreach ($locations as $location) { 159 | $objLocation = PlentaJobsBasicJobLocationModel::findByPk($location); 160 | if (!\in_array($objLocation->pid, $company, true)) { 161 | $company[$objLocation->pid] = PlentaJobsBasicOrganizationModel::findByPk($objLocation->pid)->name; 162 | } 163 | } 164 | 165 | return implode(', ', $company); 166 | } 167 | 168 | public function formatAddressLocalityTitle(PlentaJobsBasicOfferModel $jobOffer): string 169 | { 170 | $locationsTemp = []; 171 | 172 | $locations = StringUtil::deserialize($jobOffer->jobLocation); 173 | 174 | foreach ($locations as $location) { 175 | $objLocation = PlentaJobsBasicJobLocationModel::findByPk($location); 176 | $name = 'onPremise' === $objLocation->jobTypeLocation ? $objLocation->title : ''; 177 | if (!\in_array($name, $locationsTemp, true)) { 178 | $locationsTemp[] = $name; 179 | } 180 | } 181 | 182 | return implode(', ', $locationsTemp); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /contao/dca/tl_plenta_jobs_basic_job_location.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | use Contao\DC_Table; 14 | use Contao\System; 15 | use Plenta\ContaoJobsBasic\EventListener\Contao\DCA\TlPlentaJobsBasicJobLocation; 16 | use Plenta\ContaoJobsBasic\GoogleForJobs\GoogleForJobs; 17 | 18 | $GLOBALS['TL_DCA']['tl_plenta_jobs_basic_job_location'] = [ 19 | // Config 20 | 'config' => [ 21 | 'dataContainer' => DC_Table::class, 22 | 'ptable' => 'tl_plenta_jobs_basic_organization', 23 | 'switchToEdit' => true, 24 | 'enableVersioning' => true, 25 | 'sql' => [ 26 | 'keys' => [ 27 | 'id' => 'primary', 28 | ], 29 | ], 30 | ], 31 | 32 | 'list' => [ 33 | 'sorting' => [ 34 | 'mode' => 4, 35 | 'flag' => 1, 36 | 'fields' => ['pid'], 37 | 'headerFields' => ['name'], 38 | 'child_record_callback' => [TlPlentaJobsBasicJobLocation::class, 'listLocations'], 39 | 'child_record_class' => 'no_padding', 40 | 'panelLayout' => 'filter;sort,search,limit', 41 | 'disableGrouping' => true, 42 | ], 43 | 'label' => [ 44 | 'fields' => [ 45 | 'streetAddress', 46 | 'postalCode', 47 | 'addressLocality', 48 | ], 49 | 'format' => '%s, %s %s', 50 | ], 51 | 'global_operations' => [ 52 | 'all' => [ 53 | 'href' => 'act=select', 54 | 'class' => 'header_edit_all', 55 | 'attributes' => 'onclick="Backend.getScrollOffset()" accesskey="e"', 56 | ], 57 | ], 58 | 'operations' => [ 59 | 'edit' => [ 60 | 'label' => &$GLOBALS['TL_LANG']['tl_jobs_product_config']['edit'], 61 | 'href' => 'act=edit', 62 | 'icon' => 'edit.svg', 63 | ], 64 | 'delete' => [ 65 | 'href' => 'act=delete', 66 | 'icon' => 'delete.svg', 67 | 'attributes' => 'onclick="if(!confirm(\''.($GLOBALS['TL_LANG']['MSC']['deleteConfirm'] ?? null).'\'))return false;Backend.getScrollOffset()"', 68 | ], 69 | 'show' => [ 70 | 'href' => 'act=show', 71 | 'icon' => 'show.svg', 72 | ], 73 | ], 74 | ], 75 | 76 | // Palettes 77 | 'palettes' => [ 78 | '__selector__' => ['jobTypeLocation'], 79 | 'default' => '{type_legend},jobTypeLocation,title', 80 | 'onPremise' => '{type_legend},jobTypeLocation,title;{address_legend},streetAddress,postalCode,addressLocality,addressRegion,addressCountry', 81 | 'Telecommute' => '{type_legend},jobTypeLocation,title;{location_legend},requirementType,requirementValue', 82 | ], 83 | 84 | // Fields 85 | 'fields' => [ 86 | 'id' => [ 87 | 'search' => true, 88 | 'sql' => [ 89 | 'type' => 'integer', 90 | 'unsigned' => true, 91 | 'autoincrement' => true, 92 | ], 93 | ], 94 | 'pid' => [ 95 | 'foreignKey' => 'tl_plenta_jobs_basic_organization.name', 96 | 'relation' => [ 97 | 'type' => 'belongsTo', 98 | 'load' => 'lazy', 99 | ], 100 | 'sql' => [ 101 | 'type' => 'integer', 102 | 'unsigned' => true, 103 | 'notnull' => false, 104 | ], 105 | ], 106 | 'tstamp' => [ 107 | 'sorting' => true, 108 | 'flag' => 6, 109 | 'sql' => [ 110 | 'type' => 'integer', 111 | 'unsigned' => true, 112 | 'default' => 0, 113 | ], 114 | ], 115 | 'streetAddress' => [ 116 | 'exclude' => true, 117 | 'search' => true, 118 | 'inputType' => 'text', 119 | 'default' => '', 120 | 'eval' => [ 121 | 'maxlength' => 255, 122 | 'tl_class' => 'w50', 123 | ], 124 | 'sql' => [ 125 | 'type' => 'string', 126 | 'length' => 255, 127 | 'default' => '', 128 | ], 129 | ], 130 | 'postalCode' => [ 131 | 'exclude' => true, 132 | 'search' => true, 133 | 'inputType' => 'text', 134 | 'default' => '', 135 | 'eval' => [ 136 | 'mandatory' => true, 137 | 'maxlength' => 32, 138 | 'tl_class' => 'w50 clr', 139 | ], 140 | 'sql' => [ 141 | 'type' => 'string', 142 | 'length' => 32, 143 | 'default' => '', 144 | ], 145 | ], 146 | 'addressLocality' => [ 147 | 'exclude' => true, 148 | 'sorting' => true, 149 | 'search' => true, 150 | 'flag' => 5, 151 | 'inputType' => 'text', 152 | 'default' => '', 153 | 'eval' => [ 154 | 'mandatory' => true, 155 | 'maxlength' => 255, 156 | 'tl_class' => 'w50', 157 | ], 158 | 'sql' => [ 159 | 'type' => 'string', 160 | 'length' => 255, 161 | 'default' => '', 162 | ], 163 | ], 164 | 'addressRegion' => [ 165 | 'exclude' => true, 166 | 'search' => true, 167 | 'inputType' => 'text', 168 | 'default' => '', 169 | 'eval' => [ 170 | 'maxlength' => 255, 171 | 'tl_class' => 'w50 clr', 172 | ], 173 | 'sql' => [ 174 | 'type' => 'string', 175 | 'length' => 255, 176 | 'default' => '', 177 | ], 178 | ], 179 | 'addressCountry' => [ 180 | 'exclude' => true, 181 | 'inputType' => 'select', 182 | 'options' => System::getContainer()->get('contao.intl.countries')->getCountries(), 183 | 'eval' => [ 184 | 'mandatory' => true, 185 | 'includeBlankOption' => true, 186 | 'chosen' => true, 187 | 'tl_class' => 'w50', 188 | ], 189 | 'sql' => [ 190 | 'type' => 'string', 191 | 'length' => 2, 192 | 'default' => '', 193 | ], 194 | ], 195 | 'jobTypeLocation' => [ 196 | 'exclude' => true, 197 | 'inputType' => 'select', 198 | 'options' => ['onPremise', 'Telecommute'], 199 | 'reference' => &$GLOBALS['TL_LANG']['tl_plenta_jobs_basic_job_location']['jobTypeLocationOptions'], 200 | 'eval' => [ 201 | 'submitOnChange' => true, 202 | ], 203 | 'sql' => [ 204 | 'type' => 'string', 205 | 'length' => 32, 206 | 'default' => 'onPremise', 207 | ], 208 | ], 209 | 'requirementType' => [ 210 | 'exclude' => true, 211 | 'inputType' => 'select', 212 | 'options' => GoogleForJobs::ALLOWED_TYPES, 213 | 'eval' => ['tl_class' => 'w50', 'mandatory' => true], 214 | 'reference' => &$GLOBALS['TL_LANG']['tl_plenta_jobs_basic_job_location']['administrativeAreas'], 215 | 'sql' => [ 216 | 'type' => 'string', 217 | 'length' => 32, 218 | 'default' => '', 219 | ], 220 | ], 221 | 'requirementValue' => [ 222 | 'exclude' => true, 223 | 'inputType' => 'text', 224 | 'eval' => ['mandatory' => true, 'tl_class' => 'w50'], 225 | 'sql' => [ 226 | 'type' => 'string', 227 | 'length' => 255, 228 | 'default' => '', 229 | ], 230 | ], 231 | 'title' => [ 232 | 'exclude' => true, 233 | 'search' => true, 234 | 'inputType' => 'text', 235 | 'eval' => ['tl_class' => 'w50'], 236 | 'sql' => [ 237 | 'type' => 'string', 238 | 'length' => 255, 239 | 'default' => '', 240 | ], 241 | ], 242 | ], 243 | ]; 244 | -------------------------------------------------------------------------------- /src/EventListener/Contao/DCA/TlPlentaJobsBasicOffer.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/plenta/ 11 | */ 12 | 13 | namespace Plenta\ContaoJobsBasic\EventListener\Contao\DCA; 14 | 15 | use Composer\InstalledVersions; 16 | use Contao\CoreBundle\Slug\Slug; 17 | use Contao\CoreBundle\Util\PackageUtil; 18 | use Contao\DataContainer; 19 | use Contao\Input; 20 | use Contao\Message; 21 | use Contao\PageModel; 22 | use Contao\StringUtil; 23 | use Contao\System; 24 | use Exception; 25 | use Plenta\ContaoJobsBasic\Contao\Model\PlentaJobsBasicJobLocationModel; 26 | use Plenta\ContaoJobsBasic\Contao\Model\PlentaJobsBasicOfferModel; 27 | use Plenta\ContaoJobsBasic\Helper\EmploymentType; 28 | use Plenta\ContaoJobsBasic\Helper\NumberHelper; 29 | use Symfony\Component\HttpFoundation\RequestStack; 30 | use Twig\Environment as TwigEnvironment; 31 | 32 | class TlPlentaJobsBasicOffer 33 | { 34 | protected EmploymentType $employmentTypeHelper; 35 | 36 | protected Slug $slugGenerator; 37 | 38 | protected RequestStack $requestStack; 39 | 40 | protected TwigEnvironment $twig; 41 | 42 | public function __construct( 43 | EmploymentType $employmentTypeHelper, 44 | Slug $slugGenerator, 45 | RequestStack $requestStack, 46 | TwigEnvironment $twig 47 | ) { 48 | $this->employmentTypeHelper = $employmentTypeHelper; 49 | $this->slugGenerator = $slugGenerator; 50 | $this->requestStack = $requestStack; 51 | $this->twig = $twig; 52 | } 53 | 54 | /** 55 | * @param mixed $varValue 56 | * 57 | * @throws Exception 58 | */ 59 | public function aliasSaveCallback($varValue, DataContainer $dc): string 60 | { 61 | $lang = null; 62 | 63 | if ('alias' === $dc->inputName) { 64 | $title = $dc->activeRecord->title; 65 | $aliasExists = fn (string $alias): bool => PlentaJobsBasicOfferModel::doesAliasExist($alias, (int) $dc->activeRecord->id); 66 | } else { 67 | $index = str_replace('translations__alias__', '', $dc->inputName); 68 | $title = Input::post('translations__title__'.$index); 69 | $lang = Input::post('translations__language__'.$index); 70 | $aliasExists = fn (string $alias): bool => PlentaJobsBasicOfferModel::doesAliasExist($alias) || PlentaJobsBasicOfferModel::doesAliasExist($alias, (int) $dc->activeRecord->id, $lang); 71 | } 72 | 73 | if (empty($varValue)) { 74 | $rootPages = PageModel::findPublishedRootPages(); 75 | $fallback = null; 76 | $rootPage = null; 77 | 78 | foreach ($rootPages as $rootP) { 79 | if ($rootP->fallback) { 80 | $fallback = $rootP->id; 81 | } 82 | 83 | if ($dc->inputName === 'alias' && $rootP->fallback) { 84 | $rootPage = $fallback; 85 | break; 86 | } 87 | 88 | if ($rootP->language === $lang) { 89 | $rootPage = $rootP->id; 90 | break; 91 | } 92 | } 93 | 94 | if (empty($rootPage)) { 95 | $rootPage = $fallback; 96 | } 97 | 98 | $varValue = $this->slugGenerator->generate( 99 | $title, 100 | $rootPage, 101 | $aliasExists 102 | ); 103 | } elseif (preg_match('/^[1-9]\d*$/', $varValue)) { 104 | throw new Exception(sprintf($GLOBALS['TL_LANG']['ERR']['aliasNumeric'], $varValue)); 105 | } elseif ($aliasExists($varValue)) { 106 | throw new Exception(sprintf($GLOBALS['TL_LANG']['ERR']['aliasExists'], $varValue)); 107 | } 108 | 109 | return $varValue; 110 | } 111 | 112 | public function jobLocationOptionsCallback(): array 113 | { 114 | $jobLocations = PlentaJobsBasicJobLocationModel::findAll(); 115 | 116 | $return = []; 117 | foreach ($jobLocations as $jobLocation) { 118 | $return[$jobLocation->id] = $jobLocation->getRelated('pid')->name.': '; 119 | 120 | if ($jobLocation->title) { 121 | $return[$jobLocation->id] .= $jobLocation->title; 122 | } 123 | elseif ('onPremise' === $jobLocation->jobTypeLocation) { 124 | $return[$jobLocation->id] .= $jobLocation->streetAddress; 125 | 126 | if ('' !== $jobLocation->addressLocality) { 127 | $return[$jobLocation->id] .= ($jobLocation->streetAddress ? ', ' : '').$jobLocation->addressLocality; 128 | } 129 | } else { 130 | $return[$jobLocation->id] .= $GLOBALS['TL_LANG']['MSC']['PLENTA_JOBS']['remote'].' ['.$jobLocation->requirementValue.']'; 131 | } 132 | } 133 | 134 | return $return; 135 | } 136 | 137 | public function employmentTypeOptionsCallback(): array 138 | { 139 | $employmentTypes = $this->employmentTypeHelper->getEmploymentTypes(); 140 | 141 | $return = []; 142 | foreach ($employmentTypes as $employmentType) { 143 | $return[$employmentType] = $this->employmentTypeHelper->getEmploymentTypeName($employmentType); 144 | } 145 | 146 | return $return; 147 | } 148 | 149 | public function employmentTypeSaveCallback($value, DataContainer $dc): string 150 | { 151 | $value = StringUtil::deserialize($value); 152 | 153 | return json_encode($value); 154 | } 155 | 156 | public function employmentTypeLoadCallback($value, DataContainer $dc): string 157 | { 158 | if (null === $value) { 159 | return serialize([]); 160 | } 161 | 162 | return serialize(json_decode($value)); 163 | } 164 | 165 | public function saveCallbackGlobal(DataContainer $dc): void 166 | { 167 | // Front end call 168 | if (!$dc instanceof DataContainer) { 169 | return; 170 | } 171 | 172 | if (!$dc->activeRecord) { 173 | return; 174 | } 175 | 176 | if (null === $dc->activeRecord->datePosted && !empty(Input::post('published'))) { 177 | $offer = PlentaJobsBasicOfferModel::findByPk($dc->activeRecord->id); 178 | $offer->datePosted = time(); 179 | $offer->save(); 180 | } 181 | } 182 | 183 | public function salaryOnLoad($value, DataContainer $dc): string 184 | { 185 | $numberHelper = new NumberHelper($dc->activeRecord->salaryCurrency, $this->requestStack->getCurrentRequest()->getLocale()); 186 | $value = $numberHelper->formatNumberFromDbForDCAField((string) $value); 187 | 188 | return $value; 189 | } 190 | 191 | public function salaryOnSave($value, DataContainer $dc): int 192 | { 193 | $numberHelper = new NumberHelper($dc->activeRecord->salaryCurrency, $this->requestStack->getCurrentRequest()->getLocale()); 194 | 195 | return $numberHelper->reformatDecimalForDb($value); 196 | } 197 | 198 | public function onShowInfoCallback(DataContainer $dc = null): void 199 | { 200 | $GLOBALS['TL_CSS'][] = 'bundles/plentacontaojobsbasic/dashboard.css'; 201 | $info = $this->twig->render('@PlentaContaoJobsBasic/be_plenta_info.html.twig', [ 202 | 'version' => InstalledVersions::getVersion('plenta/contao-jobs-basic-bundle'), 203 | ]); 204 | 205 | Message::addRaw($info); 206 | } 207 | 208 | public function getLanguages(): array 209 | { 210 | return System::getContainer()->get('contao.intl.locales')->getLanguages(); 211 | } 212 | 213 | public function labelCallback(array $row, string $label, DataContainer $dc, array $labels): string 214 | { 215 | $jobLocations = []; 216 | $locations = $this->jobLocationOptionsCallback(); 217 | $locationsArr = StringUtil::deserialize($row['jobLocation']); 218 | foreach ($locationsArr as $location) { 219 | $jobLocations[] = $locations[$location]; 220 | } 221 | 222 | $jobEmploymentTypes = []; 223 | $employmentTypes = $this->employmentTypeOptionsCallback(); 224 | $typesArr = StringUtil::deserialize($this->employmentTypeLoadCallback($row['employmentType'], $dc)); 225 | foreach ($typesArr as $type) { 226 | $jobEmploymentTypes[] = $employmentTypes[$type]; 227 | } 228 | 229 | $label = '

'.$row['title'].'

'; 230 | $label .= implode(' | ', $jobLocations); 231 | $label .= ' | '.implode(', ', $jobEmploymentTypes); 232 | 233 | return $label; 234 | } 235 | 236 | public function getSerpUrl(PlentaJobsBasicOfferModel $offer): string 237 | { 238 | $strSuffix = '/'; 239 | 240 | return sprintf(preg_replace('/%(?!s)/', '%%', $strSuffix), $offer->alias ?: $offer->id); 241 | } 242 | } 243 | --------------------------------------------------------------------------------