Skip to content

Add salary grade structure for jobs - #1

Merged
sfroemkenjw merged 26 commits into
mainfrom
feature/salary-grades
Jul 30, 2026
Merged

Add salary grade structure for jobs#1
sfroemkenjw merged 26 commits into
mainfrom
feature/salary-grades

Conversation

@sfroemkenjw

@sfroemkenjw sfroemkenjw commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds a new salary grade structure (SalaryTable -> SalaryGrade -> SalaryStep) so a Job's salary can reference a maintained pay-scale grade instead of only free-text min/max values, including a locale-aware decimal input (3.842,17) for all amount fields.
  • Cleans up the extension's TCA across the board: native TYPO3 fields (sys_language_uid, hidden, starttime/endtime, ...) removed from columns in favor of core's TcaEnrichment, types/typeicon_classes keys typed and sorted correctly, columns order aligned with showitem, redundant foreign_field/ext_tables.sql boilerplate removed wherever DefaultTcaSchema already derives it, and the access tab switched to TYPO3's own native labels instead of custom translations.
  • Adds the missing test/build tooling for this extension: .editorconfig, .gitignore/.gitattributes, .phpstorm.meta.php, Build/ (runTests.sh, CGL config, PHPUnit bootstrap/config) and GitHub Actions workflows (CI, backport, TER release).
  • Minor metadata cleanup: composer.json (SPDX license id, description, require-dev), ext_emconf.php author, LICENSE renamed to LICENSE.txt.

Test plan

  • All changed TCA/PHP files linted (php -l) and XLF files validated (xmllint, EN/DE trans-unit counts matched).
  • composer validate passes.
  • Verified live in the TYPO3 backend: SalaryTable/SalaryGrade/SalaryStep record titles, inline editing, the Job salary-grade group field lookup, and the "Zugriff" (access) tab labels/descriptions on all four affected tables.
  • CI workflow (ci.yml) has not run yet in this repo - first PR to exercise it.

sfroemkenjw and others added 26 commits July 29, 2026 16:36
Introduce a salary grade catalog (SalaryTable, SalaryGrade,
SalaryStep) so editors can attach either a tariff-based
civil-service pay grade with seniority steps, or a free min/max
salary range, to a job posting. This is required by an upcoming
law that forbids displaying job postings without salary
information; SalaryGrade uses TYPO3's native starttime/endtime so
an expired grade automatically makes the referencing job
unresolvable via Extbase relation loading.

Job's ctrl[type] switches from is_import to salary_mode so the
backend can show distinct icons and field sets for grade-based
versus free-entry salary jobs. is_import becomes a plain field
that now drives a displayCond on vacancy_id instead of driving
the record type. This is a breaking change for any project still
relying on the previous is_import-based type icon behaviour.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Switch Job's salary_grade from a plain select dropdown to a group
field. selectSingle has no search/filter, and once several
SalaryTable catalogs exist in parallel, grade titles like "A7"
can no longer be told apart. Group gives editors a searchable
picker instead.

SalaryGrade's own salary_table pointer changes from passthrough
to a group relation so its title can be resolved and appended via
label_alt/label_alt_force, e.g. "A7, Grundgehaltssaetze Baden-
Wuerttemberg" wherever the record title is used (group field,
list module).

Add a formattedLabel_userFunc for the inline child header inside
SalaryTable's own edit form, where that same parent-table suffix
would just repeat information the editor already sees.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The label_alt-based title just concatenated the raw step_label and
amount ("3, 3421.84"), which reads more like a typo than a step
number. Prefix it with the step_label field's own label ("Step 3")
and format the amount for the current backend user's locale, joined
by a dash for readability.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Combine title and has_steps into one palette so they sit side by
side instead of stacking, saving vertical space in a form that can
already hold many inline levels.

Also show the "create new" button below the step list in addition
to above, and append the foreign table's own title to it, so long
lists of steps don't require scrolling back up just to add another
one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same treatment as the SalaryGrade -> SalaryStep relation: show the
"create new" button below the salary grade list in addition to
above, with the foreign table's title appended to it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Native <input type="number"> fields strip characters like "," while
typing or pasting, so a value copied from a German pay scale table
(e.g. "3.421,84") could never be entered correctly - editors had to
manually remove the thousands separator first.

Add a custom renderType, jobfair2LocalizedDecimal, backed by a
JavaScriptModuleInstruction that normalizes the typed value to the
machine format on blur, based on where the last "," or "." occurs
in the string rather than on the backend user's own UI language -
the source data's notation and the editor's language preference
are independent of each other. type=>number/format=>decimal is
kept unchanged, so the automatic DECIMAL(10,2) column derivation is
unaffected. Applied to flat_amount, amount, salary_min and
salary_max.

Require ext-intl for the NumberFormatter calls this and the
SalaryStep title formatter rely on.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Configuration/TCA/*, Configuration/TCA/Overrides/*, RequestMiddlewares.php,
Extbase/Persistence/Classes.php and ext_localconf.php referenced classes
by their fully qualified name in the code body. Add use imports and
reference the short class names instead, per project convention.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
sys_language_uid, l10n_parent, l10n_diffsource, hidden, starttime
and endtime were defined manually in all four tables' 'columns'
arrays, duplicating exactly what TYPO3 core's TcaEnrichment already
generates from the corresponding 'ctrl' settings (languageField,
transOrigPointerField, transOrigDiffSourceField, enablecolumns).

Move the only real customizations - the legal-notice description
on SalaryTable/SalaryGrade's starttime/endtime and the
allowLanguageSynchronization behaviour on all four tables - into
small Configuration/TCA/Overrides/*.php files, which core applies
after enrichment.

While at it, fix Job's l10n_source column: the ctrl entry and the
actual database column are both named l10n_diffsource, so that
'l10n_source' definition was dead configuration that never
matched anything - and add the two missing label translations
for SalaryGrade's starttime/endtime, which resolved to blank
because their LLL keys never existed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
'exclude' is documented as a boolean; the integer 1 worked by
coincidence (PHP truthiness) but did not match the type TCA
actually expects.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
'types' array keys must match the actual type of the value the
ctrl['type'] pointer field holds. has_steps and salary_mode are
int-valued (checkbox / select with integer item values), and tables
without a ctrl['type'] resolve to int 0 internally
(BackendUtility::getTCAtypeValue()) - so all of them get int keys
instead of numeric strings. Also reorder them ascending (SalaryGrade
had '1' before '0') and apply the same key typing/order to
typeicon_classes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reorder each table's 'columns' array to follow the same sequence
fields actually appear in via showitem/palettes, so the config file
reads top-to-bottom the same way the form does. Fields with no
showitem placement at all (SalaryGrade's salary_table, SalaryStep's
salary_grade - both exist only to resolve the parent title via
label_alt, plus Job's non-form fields like link/tender_file/
pdf_files/is_internal/salary_mode) are grouped at the end.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SalaryStep's salary_grade column just declared type=>passthrough,
duplicating exactly what DefaultTcaSchema already generates for a
foreign_field pointer column (int(11) NOT NULL DEFAULT 0) when the
child table leaves it undefined or passthrough. Drop it from both
the TCA columns array and ext_tables.sql.

SalaryGrade's own salary_table pointer (the analogous column for
SalaryTable's salary_grades relation) is intentionally left as-is:
it was changed from passthrough to type=>group earlier so its title
can be resolved for label_alt, and DefaultTcaSchema's passthrough
fallback does not apply to a group-typed column - its generic
default would be a nullable TEXT column instead of the NOT NULL
int the relation actually needs, so the manual ext_tables.sql
declaration for that one stays.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
type=>group on salary_table let editors jump straight from a
SalaryGrade child record into editing its parent SalaryTable via the
group field's built-in edit link - not something an inline relation
pointer column should ever offer. Revert it to passthrough (its
foreign_field default) and resolve the parent's title through a full
label_userFunc instead, using BackendUtility::getRecordWSOL() +
getRecordTitle() - both already handle workspace overlay internally,
so no manual QueryBuilder code is needed for this.

Rename InlineRecordTitleFormatter to SalaryGradeTitleFormatter and
add formatTitle() (ctrl-level label_userFunc, includes the parent
title) alongside the existing formatInlineChildTitle() (IRRE-only,
title alone - FormEngine's TcaRecordTitle prioritizes
formattedLabel_userFunc over label_userFunc for inline children, so
the two never conflict). label_alt/label_alt_force on SalaryGrade
are removed, since label_userFunc bypasses that mechanism entirely.

The salary_table column itself drops out of ext_tables.sql again -
passthrough is DefaultTcaSchema's foreign_field fallback case, so
the int(11) column is auto-generated same as SalaryStep's
salary_grade.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TcaPreparation::addSystemFieldsToShowitemTypes() only auto-adds the
access tab for tt_content, not for custom extension tables, so the
access palette/div still has to be declared explicitly in showitem.

However, starttime/endtime already have official core labels
(LLL:EXT:frontend...:starttime_formlabel / :endtime_formlabel) and the
access palette itself has an official title
(LLL:EXT:frontend...:pages.palettes.access), exactly as used by
EXT:maps2's poicollection TCA and already used in job.php here. Adopt
that native pattern for salarygrade, salarystep and salarytable
instead of maintaining redundant custom labels, and drop the
now-unused tx_jobfair2_domain_model_salarygrade/salarytable
.starttime/.endtime label translations (EN+DE) - the .description
keys with the domain-specific legal notice stay untouched.
This extension had no .editorconfig yet, unlike other jweiland/*
extensions. Add the standard jweiland.net configuration (4-space
indent for PHP, tabs for JSON/XLF/SQL, LF line endings, trimmed
trailing whitespace) so editors format new files consistently with
the rest of the ecosystem.
- Use the SPDX-preferred "GPL-2.0-or-later" identifier instead of the
  deprecated "GPL-2.0+" suffix notation.
- Prefix the description with "Job fair 2 -" to match ext_emconf.php's
  title and disambiguate from the original (non-2) jobfair extension.
- Alphabetize the require block (ext-intl before typo3/cms-core).
jobfair2 was extracted from Markus Kugler's original project into its
own jweiland.net-maintained package (see CLAUDE.md history section).
Update author/author_mail to reflect the new maintainer.
Continues the cleanup already applied to salarygrade/salarystep:
DefaultTcaSchema derives a matching DB column for every plain input,
text, check, datetime, file, select and single-table group field from
its TCA config alone, so declaring them again here is redundant
boilerplate that has to be kept in sync by hand.

Removed: all tx_jobfair2_domain_model_job columns except the two
decimal fields, the whole tx_jobfair2_domain_model_jobarea/-jobtype
blocks (each only had the auto-generated "title" column), and the
tt_address "import_key" column (defined via TCA in
Configuration/TCA/Overrides/tt_address.php).

Kept: salary_min/salary_max (job), flat_amount (salarygrade) and
amount (salarystep) - decimal precision still requires an explicit
declaration, TCA alone cannot express it (see
Documentation-internal note in the TCA decimal fields).

Also reformats the kept decimal(10,2) declarations to decimal(10, 2)
for consistent spacing.
Matches the composer.json convention of other jweiland/* extensions,
which ship their license text as LICENSE.txt.
Replace the stale require-dev (roave/security-advisories dev-latest,
phpunit/phpunit ~4.8.0 - far behind the PHPUnit version shipped by
typo3/testing-framework ^13.4) with the actual tooling used by
Build/Scripts/runTests.sh: typo3/testing-framework, typo3/coding-
standards and ergebnis/composer-normalize. Add autoload-dev for
Tests/, and the composer/TYPO3 "config"/"extra" block (.Build vendor-
dir, bin-dir, web-dir) the test runner and CI workflow expect.
Standard jweiland.net repo hygiene files: .gitignore excludes the
.Build/ test-tooling vendor dir, editor/OS cruft and composer.lock;
.gitattributes export-ignores CI/Build/Tests/dotfiles from
composer/TER package archives so they don't ship to end users.
Standard TYPO3 core .phpstorm.meta.php: teaches PhpStorm's code
completion the concrete return/argument types behind
Context::getAspect(), ServerRequestInterface::getAttribute() and
similar string-keyed APIs.
- Build/Scripts/runTests.sh: docker/podman-based test runner (cgl,
  lint, composer validate/normalize, unit/functional tests,
  phpstan), copied from TYPO3's testing-framework template - matches
  the suites invoked by the new CI workflow.
- Build/cgl/config.php: php-cs-fixer ruleset per PER-CS1.0/PSR-12.
- Build/phpunit/{Unit,Functional}Tests.xml + bootstrap files: minimal
  PHPUnit setup for Tests/Unit and Tests/Functional (both still to be
  created).

Fixed a leftover "jweiland/events2" package reference (copy-paste
from another jweiland extension) in the header comments of
UnitTests.xml, UnitTestsBootstrap.php and FunctionalTestsBootstrap.php
to correctly say "jweiland/jobfair2".
- ci.yml: runs Build/Scripts/runTests.sh (lint, composer
  validate/normalize, cgl) on PHP 8.2/8.3 for every pull request.
- backport.yml: opens backport PRs on merge, driven by a
  "Backport <branch>" label.
- ter-release.yml: publishes to the TYPO3 Extension Repository via
  typo3/tailor whenever a GitHub release is published.
The CI workflow added in b69e54e runs Build/Scripts/runTests.sh -s cgl
in dry-run mode, which failed on PR #1 (25 of 54 files) because the
existing codebase predates the CGL ruleset that ships with this same
branch (Build/cgl/config.php).

Applied `php-cs-fixer fix` with that exact config: missing package
header comments, trailing commas in multiline argument/parameter
lists, brace position and single-line empty body style. No behavioral
changes.
runTests.sh auto-detects podman before docker when both are
installed. GitHub's ubuntu-latest runners ship both, but their
current podman/crun combination fails on every container pull with
"OCI runtime error: crun: unknown version specified", so every step
(lint, composerUpdate, composerValidate, composerNormalize, cgl)
failed regardless of PHP version - reproduced twice, not a one-off
flake. Pass -b docker explicitly to bypass the broken podman path.
@sfroemkenjw
sfroemkenjw merged commit a3c1d8e into main Jul 30, 2026
4 checks passed
@sfroemkenjw
sfroemkenjw deleted the feature/salary-grades branch July 30, 2026 13:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant