Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Timesheets Laravel Package

timwassenburg/filament-timesheets

Filament Timesheets is a plugin for managing employee timesheets inside Filament panels. Track hours, log work against projects or tasks, and review entries with ready-made resources and widgets.

View on GitHub
Deep Wiki
Context7

Getting Started

Start by installing the package and publishing its assets:

composer require vendor/laravel-projects
php artisan vendor:publish --tag=projects-config
php artisan vendor:publish --tag=projects-migrations
php artisan migrate

Look at these three places first, in order:

  1. config/projects.php — table names, model bindings, and any grouping/feature flags. This is where you override defaults rather than editing vendor code.
  2. The published migrations — confirm the client_id foreign key on the projects table (added or formalized in v1.0.3), its nullability, and the onDelete behaviour before you build anything on top of it.
  3. The Project and Client models — check the relationship names (client(), projects()) and whether the package exposes query scopes you should reuse.

Your first use case with v1.0.3 is almost always a client-centric project index — a page or endpoint that shows each client alongside their projects:

$clients = Client::query()
    ->with(['projects' => fn ($q) => $q->latest()->limit(5)])
    ->withCount('projects')
    ->orderBy('name')
    ->paginate(25);

That single query gives you the client name, a project count, and a preview list — the shape most "group by client" screens need.

Implementation Patterns

Group on the query, not in the view. The tempting shortcut is Project::all()->groupBy('client_id') in a Blade template. It works in a demo and falls over at scale. Prefer querying from the Client side with withCount/with, or use a dedicated scope if the package ships one.

Keep unassigned projects as an explicit bucket. Any project without a client will silently disappear from a groupBy('client_id') collection or an inner join. Handle it deliberately:

$groups = Project::with('client')
    ->orderBy('client_id')
    ->get()
    ->groupBy(fn (Project $p) => $p->client?->name ?? 'Unassigned');

Shape the API response around the group, not the row. For JSON consumers, transform to a client-keyed structure in a Resource rather than leaking client_id and letting the frontend regroup:

return ClientResource::collection(
    Client::withCount('projects')->with('projects')->paginate()
);

Use route model binding with nested scoping for per-client project lists. If the package's Project supports it, Route::get('/clients/{client}/projects', ...) plus $client->projects() keeps authorisation checks in one place and avoids trusting a client_id from the request body.

Aggregate in SQL when you only need numbers. withCount('projects'), withSum, and withMax('projects.updated_at') are far cheaper than loading relations and counting in PHP. Reach for with(...) only when you actually render the child rows.

Treat the grouping as a read-side concern. Don't add a client_group column or denormalize the relationship because a screen needs grouping; the client_id FK already expresses it. Denormalizing here creates a second source of truth you'll have to backfill on every client rename or reassignment.

Gotchas and Tips

Paginating grouped results is subtly wrong. paginate() counts rows, not groups. If you paginate projects and then group in PHP, page 2 can contain a partial group for a client that already appeared on page 1. Paginate clients (the parent) and eager-load their projects, or accept that the count is "projects per page" and label it as such in the UI.

groupBy on a collection resets keys — use mapWithKeys if you need stable client IDs as array keys for a JS payload. Also be aware that groupBy sorts by the grouping value by default; pass $preserveKeys = true on the second argument if you've already ordered deliberately.

Watch the N+1 on the client name. Rendering $project->client->name inside a loop over projects without with('client') is the single most common performance regression in client-grouped screens. Laravel's Model::preventLazyLoading() in AppServiceProvider (non-production) will catch this for you.

Null and soft-deleted clients. Decide now what a project belonging to a soft-deleted client looks like. with('client') returns null, and withCount may or may not include soft-deleted parents depending on the FK setup. Test both cases explicitly; the "Unassigned" bucket is where these quietly accumulate.

Check the FK behaviour before deleting a client. If the migration uses cascadeOnDelete, deleting a client wipes their projects — usually not what a business wants. nullOnDelete orphans them into the unassigned group, which is safer but requires client_id to be nullable. Verify which one shipped rather than assuming.

Backfilling existing data. If v1.0.3 introduced or populated client_id, run the migration on a copy of production first and count orphaned rows afterwards:

Project::whereNull('client_id')->count();

Any non-zero number is a data-quality conversation, not a bug.

Duplicate clients from free-text entry. If clients are created from a form without normalisation, you'll get "Acme", "acme", and "Acme Ltd" as three groups. Add a unique index on a normalised slug or lowercase name, and merge existing duplicates before the grouping UI goes live — otherwise the feature looks broken on day one.

Cache the grouped summary if it's expensive. Client-grouped dashboards tend to grow heavy joins. Cache the aggregate with a short TTL and forget it on Project::saved/deleted and Client::saved/deleted via model events or observers, rather than reaching for a full-page cache.

Testing. Use factories to cover the three states — client with projects, client with none, project with no client. A test asserting "the index returns 4 groups" is cheap insurance against a future change that turns an outer join into an inner one.

Extension point. Resist editing published migrations or vendor models. If you need extra behaviour on grouping, add your own query scope or a repository/service class that wraps the package models; that survives the next patch release.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator