Skip to content

feat: worklog admin page - #349

Merged
tuj merged 3 commits into
developfrom
feature/worklog-admin-page
Sep 7, 2026
Merged

feat: worklog admin page#349
tuj merged 3 commits into
developfrom
feature/worklog-admin-page

Conversation

@tuj

@tuj tuj commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Link to ticket

https://leantime.itkdev.dk/#/tickets/showTicket/8000

Description

Adds a standalone worklog page at /admin/worklog, so worklogs can be searched and
filtered across the whole system.

Until now a worklog could only be reached through something else: the invoice-entry
worklog picker (scoped to one project and one invoice entry) or the workload-report
drill-down modal from PR-346 (scoped to one worker and one period). There was no way to
answer "where did these hours go?" without first knowing which invoice or which worker
to look under.

The page follows the established list-page pattern in this repo — FilterData DTO →
FilterType form → Repository::getFilteredPagination() → thin controller → Twig table
— modelled on ProductController / IssueController. The row markup is deliberately the
one from PR-346 (templates/reports/workload_report_worklogs.html.twig) so the two views
read the same, plus Worker, Data provider and Invoice columns.

Filters: free-text search (worklog description, issue name, tracker issue id), date
range, worker, project, data provider, billed status.
Access: ROLE_ADMIN, in the existing Admin submenu. Read-only — no export, no editing.
Paging: 25 per page, sortable columns.

Notes for the reviewer

Three decisions worth a second pair of eyes:

  • CSRF is disabled on the filter form. Symfony returned 422 for any hand-built query
    string, and a token riding along in the URL makes a filtered link unshareable. The
    existing filter forms (ProductFilterType, IssueFilterType) don't do this and carry a
    _token query param as a result. WorkloadReportController already disables it the
    same way.
  • Two latent bugs in WorklogRepository::createFilterDataQueryBuilder() were
    deliberately not inherited.
    That method passes
    'worklog.isBilled = FALSE OR worklog.isBilled is NULL' to andWhere() unparenthesised
    — DQL binds AND tighter, so with any other predicate the whole query widens — and it
    calls $filterData->periodTo->modify('tomorrow'), mutating the DTO in place. The new
    method wraps the OR in expr()->orX() and clones the date. The originals are untouched
    here because fixing them changes invoice-entry behaviour; that belongs on its own branch.
  • Two deliberate deviations from the house pattern. sortFieldAllowList is set (the
    sort key comes straight from the query string, and no existing repository allow-lists
    it), and the page size is 25 rather than the 10 hardcoded everywhere else — ten rows is
    too few for a page whose whole point is scanning.
Considered and dropped

Client and Account filters were both scoped out because neither would match anything:

  • Account has no Doctrine association to Worklog, Issue or Client.
    Issue::$accountKey / $accountId are strings written only by AppFixtures and never
    queried.
  • Project::$clients is a ManyToMany, but nothing writes to the join table — the only
    call to addClient() is inside Client::addProject(), which has no callers. The one
    real worklog→client path is Worklog → InvoiceEntry → Invoice → Client, which exists
    only for invoiced worklogs and would silently hide every unbilled hour.

Both are worth adding once those associations are actually populated.

Screenshot of the result

Screenshot 2026-08-27 at 12 52 51

Checklist

  • My code is covered by test cases.
  • My code passes our test (all our tests).
  • My code passes our static analysis suite.
  • My code passes our continuous integration process.

@tuj tuj changed the title Feature/worklog admin page feat: worklog admin page Aug 27, 2026
@tuj tuj self-assigned this Aug 27, 2026
@tuj tuj added the enhancement New feature or request label Aug 27, 2026
@tuj
tuj requested a review from jeppekroghitk September 2, 2026 18:19
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

#[Route('/admin/worklog', name: 'app_worklog_')]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the underscore intended? "app_worklog_"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. It's a prefix for the name for each route in the controller

@tuj
tuj merged commit 0f8c6c3 into develop Sep 7, 2026
8 checks passed
@tuj
tuj deleted the feature/worklog-admin-page branch September 7, 2026 08:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants