PHPackages                             laboiteacode/filament-business-hours - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. laboiteacode/filament-business-hours

ActiveLibrary

laboiteacode/filament-business-hours
====================================

Filament plugin to manage the opening hours of a business: a weekly 24h grid, exceptional closures, vacation periods and timezones, backed by spatie/opening-hours.

v0.1.0(today)00MITPHP ^8.2

Since Jul 22Compare

[ Source](https://github.com/La-boite-a-code/Filament-Business-Hours)[ Packagist](https://packagist.org/packages/laboiteacode/filament-business-hours)[ Docs](https://github.com/la-boite-a-code/filament-business-hours)[ RSS](/packages/laboiteacode-filament-business-hours/feed)WikiDiscussions Synced today

READMEChangelogDependencies (9)Versions (2)Used By (0)

Filament Business Hours
=======================

[](#filament-business-hours)

[![Latest Version on Packagist](https://camo.githubusercontent.com/7c203193640e7801b7b1be16902b60048f32ff27f01ab21578fc470341190a44/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6c61626f69746561636f64652f66696c616d656e742d627573696e6573732d686f7572732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/laboiteacode/filament-business-hours)[![Tests](https://camo.githubusercontent.com/f66697e9a0af03e469e3d8c8fe4a72fc326df3e9115e5eba7a5a65848c98669a/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6c612d626f6974652d612d636f64652f66696c616d656e742d627573696e6573732d686f7572732f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/la-boite-a-code/filament-business-hours/actions/workflows/run-tests.yml)[![Total Downloads](https://camo.githubusercontent.com/7436a1f28e3a4a5dc5a2aaebbb1d648e6e3062f0ac3a24a63a6528bc74dbd524/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6c61626f69746561636f64652f66696c616d656e742d627573696e6573732d686f7572732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/laboiteacode/filament-business-hours)[![License](https://camo.githubusercontent.com/e48776ab0f85e7290635a8302e31bde5b16a3854edd751e268174cea9c03905c/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6c61626f69746561636f64652f66696c616d656e742d627573696e6573732d686f7572732e7376673f7374796c653d666c61742d737175617265)](LICENSE.md)

Manage the opening hours of a business from your Filament panel: a weekly 24-hour grid you drag ranges onto, exceptional closures, special-hours days, vacation periods and timezones. Everything is stored **exactly in the format consumed by [spatie/opening-hours](https://github.com/spatie/opening-hours)**, so your application can query the hours with the library you already know.

[![Filament Business Hours](art/banner.jpg)](art/banner.jpg)

Features
--------

[](#features)

- **A real weekly grid.** 7 days of stacked hour cells, planner style, themed with your panel's own Filament color palette. Drag on empty cells to create a range, drag a range to move it, stretch its edges to resize it. Everything snaps to a configurable step, and partially covered cells carry a small minute badge ("30" on the 05:00 cell means the range starts at 05:30).
- **Precise editing.** Click a range (or right-click it, or press Enter) to open an inline editor with real time inputs, down to the minute, plus a delete button. `Delete` on a focused range removes it.
- **Day tooling.** Copy one day onto any others in two clicks, clear a day, per-day totals, first day of the week configurable (Monday by default).
- **Exceptional dates.** Closures or special hours for a single date, with an optional label ("Christmas") and an optional *repeat every year*.
- **Vacation periods.** Date ranges during which the business is closed, with a label, stored as native spatie date-range exceptions.
- **Timezones.** Pick the timezone the hours are expressed in (all IANA identifiers, or a restricted list you provide).
- **spatie/opening-hours native.** What is stored is what `OpeningHours::create()` eats: no export step, no proprietary format. Data is validated by actually instantiating spatie's object before saving, so a stored record can always be queried.
- **Configurable twice over.** Every option is available both in a published config file and through a fluent, per-panel plugin API.
- **Authorization built in.** A single `canAccess()` hook drives both the navigation item and the route.
- **Translated.** Ships with English, French and Spanish.

Compatibility
-------------

[](#compatibility)

PackageSupported versionsPHP8.2, 8.3, 8.4, 8.5Laravel12, 13Filament4, 5Installation
------------

[](#installation)

```
composer require laboiteacode/filament-business-hours
```

Publish and run the migration (one `business_hours` table):

```
php artisan vendor:publish --tag=filament-business-hours-migrations
php artisan migrate
```

Register the plugin in your panel provider:

```
use LaBoiteACode\FilamentBusinessHours\FilamentBusinessHoursPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->plugin(FilamentBusinessHoursPlugin::make());
}
```

That's it: a "Business hours" page appears in your panel navigation.

Querying the hours with spatie/opening-hours
--------------------------------------------

[](#querying-the-hours-with-spatieopening-hours)

The page persists everything in one row of the package's `business_hours`table (the `default` row unless configured otherwise), and that row holds the **exact definition format of spatie/opening-hours**. There is no export or conversion step: turning a stored record into a queryable object is a plain `OpeningHours::create()` call, which the model wraps for you.

### The one-liner

[](#the-one-liner)

```
use LaBoiteACode\FilamentBusinessHours\Models\BusinessHours;

$openingHours = BusinessHours::openingHours();             // the "default" record
$openingHours = BusinessHours::openingHours('shop-paris'); // another record

// Null when no record has been saved yet under that key.
```

### Everything spatie can answer, your data can too

[](#everything-spatie-can-answer-your-data-can-too)

What you get back is a regular `Spatie\OpeningHours\OpeningHours` instance, so the [whole spatie query API](https://github.com/spatie/opening-hours#usage)applies to the hours managed from your panel:

```
$openingHours->isOpenAt(new DateTime('2026-12-24 11:00')); // special hours applied
$openingHours->isClosedAt(now());
$openingHours->isOpenOn('monday');

$openingHours->nextOpen(now());   // when do we open next?
$openingHours->nextClose(now());  // until when are we open?

$openingHours->forDay('monday');                    // the weekly ranges of a day
$openingHours->forDate(new DateTime('2026-12-25')); // a concrete date, exceptions applied
$openingHours->forWeek();

$openingHours->exceptionalClosingDates();           // vacations and closures, expanded
```

The timezone picked in the panel is part of the definition, so date and time queries are interpreted in that timezone automatically. Labels typed in the panel ("Christmas", "Summer break", ...) travel as spatie `data` and come back through `$openingHours->forDate(...)->data`.

### From a record instance

[](#from-a-record-instance)

```
$record = BusinessHours::resolve('default');

$record->toOpeningHours();      // Spatie\OpeningHours\OpeningHours
$record->toOpeningHoursArray(); // the raw definition array for OpeningHours::create()
```

`toOpeningHoursArray()` returns a plain spatie definition: weekly hours, exceptions (single dates, yearly recurring `m-d` dates, `Y-m-d to Y-m-d`vacation ranges), the `overflow` flag and the `timezone`:

```
[
    'monday' => ['09:00-12:00', '14:00-18:00'],
    // ...
    'exceptions' => [
        '2026-12-24' => ['10:00-16:00'],                                  // special hours
        '2026-12-25' => ['hours' => [], 'data' => 'Christmas'],           // closed, labelled
        '01-01'      => [],                                               // closed every year
        '2026-08-03 to 2026-08-16' => ['hours' => [], 'data' => 'Summer'], // vacation period
    ],
    'timezone' => 'Europe/Paris',
]
```

### Without the model

[](#without-the-model)

Because the columns themselves are stored in spatie shape, nothing forces you through the Eloquent model. Any code that can read the table can assemble the definition on its own, for instance from a raw query or another service:

```
use Illuminate\Support\Facades\DB;
use Spatie\OpeningHours\OpeningHours;

$row = DB::table('business_hours')->where('key', 'default')->first();

$data = json_decode($row->hours, true)
    + ['exceptions' => json_decode($row->exceptions, true)];

if ($row->overflow) {
    $data['overflow'] = true;
}

if ($row->timezone) {
    $data['timezone'] = $row->timezone;
}

$openingHours = OpeningHours::create($data);
```

### Caching busy paths

[](#caching-busy-paths)

`OpeningHours::create()` parses the whole definition each time. On a page hit on every request (a storefront header showing "open until 18:00", say), cache the definition array rather than rebuilding it:

```
use Spatie\OpeningHours\OpeningHours;

$data = cache()->remember(
    'business-hours:default',
    now()->addHour(),
    fn (): array => BusinessHours::resolve('default')->toOpeningHoursArray(),
);

$openingHours = OpeningHours::create($data);
```

Invalidate the cache key when the record is saved (a model observer on `BusinessHours`, or simply a short TTL as above).

### Seeding and writing from code

[](#seeding-and-writing-from-code)

The door swings both ways: seeders, imports or your own admin code can write rows directly, and the page will pick them up on next load:

```
BusinessHours::create([
    'key' => 'shop-paris',
    'timezone' => 'Europe/Paris',
    'hours' => [
        'monday' => ['09:00-12:00', '14:00-18:00'],
        'tuesday' => ['09:00-18:00'],
    ],
    'exceptions' => [
        '12-25' => ['hours' => [], 'data' => 'Christmas'],
    ],
]);
```

Exception entries the page cannot represent (recurring `m-d to m-d` periods, structured `data` payloads, ...) are preserved verbatim across edits, so advanced hand-written definitions survive the UI.

The grid
--------

[](#the-grid)

- **Create**: click and drag vertically on a day; a plain click creates a one-hour block.
- **Move / resize**: drag a range, or drag its top/bottom edge. Everything snaps to the configured step (15 minutes by default).
- **Edit precisely / delete**: click a range, right-click it, or focus it and press Enter. The inline editor accepts any minute, regardless of the snap step. Overlapping or touching ranges are merged automatically on save.
- **Copy / clear a day**: hover a day header for its actions.

Saving is explicit: the **Save** header action persists the whole page (grid, exceptional dates, vacation periods and settings) as one record, after validating it against spatie/opening-hours itself. A date covered twice (say, an exceptional closure inside a vacation period) is rejected with a readable message.

### Ranges past midnight

[](#ranges-past-midnight)

The grid works on a per-day, 00:00 to 24:00 basis. A night business closing at 2 AM is usually best modelled with two ranges (`20:00-24:00` on Friday and `00:00-02:00` on Saturday). If you prefer spatie's `overflow` mode, enable "Ranges past midnight" in the settings modal: overnight ranges such as `20:00-02:00` are then accepted, shown clamped to midnight in the grid, and editable through the inline editor.

Configuration
-------------

[](#configuration)

Publish the config file if you want application-wide defaults:

```
php artisan vendor:publish --tag=filament-business-hours-config
```

Every option can also be set per panel through the fluent API, which wins over the config file:

```
FilamentBusinessHoursPlugin::make()
    // Navigation
    ->navigationLabel('Opening hours')
    ->navigationIcon('heroicon-o-clock')
    ->navigationGroup('Shop')
    ->navigationSort(10)
    ->registerNavigation(true)
    ->slug('opening-hours')
    ->cluster(Settings::class)

    // Which row of the business_hours table this panel manages
    ->record('shop-paris')
    // Swap the model for your own (must extend the package model)
    ->model(App\Models\ShopHours::class)

    // Grid
    ->firstDayOfWeek('sunday')  // 'monday' ... 'sunday'
    ->slotStep(30)              // snapping, in minutes

    // Timezones
    ->timezones(['Europe/Paris', 'Europe/Brussels'])
    ->defaultTimezone('Europe/Paris')

    // Authorization
    ->canAccessUsing(fn (): bool => auth()->user()?->can('manageBusinessHours') ?? false);
```

Several panels can manage different records by giving each plugin instance its own `->record()` key (one page per panel).

### Authorization

[](#authorization)

By default the page is available to anyone who can access the panel. Restrict it with a Gate ability in the config:

```
'authorization' => [
    'gate' => 'manageBusinessHours',
],
```

or with the `->canAccessUsing()` closure shown above, which takes precedence.

### Validation and limits

[](#validation-and-limits)

Nothing reaches the database without being validated. On save, the page:

- normalizes every range (format check, chronological sort, merge of overlapping or touching ranges);
- refuses dates covered twice (an exceptional date inside a vacation period, two overlapping vacation periods, ...) with a readable message;
- validates the timezone;
- validates the whole payload by actually instantiating `Spatie\OpeningHours\OpeningHours`, so a stored record is queryable by definition.

Because the Livewire state is client-writable, hard caps also protect the server against crafted payloads: at most 100 ranges per day, 500 exceptional dates and vacation periods in total, 1100 days per vacation period, and 255 characters per label. Malformed state degrades gracefully instead of breaking the page render.

Database
--------

[](#database)

One table, one row per set of hours:

ColumnTypeContent`key`stringidentifies the set (`default`, `shop-paris`, ...), unique`timezone`string / nullIANA identifier, null = application timezone`overflow`boolspatie "overflow" flag (ranges past midnight)`hours`json`{"monday": ["09:00-12:00"], ...}`, spatie day format`exceptions`jsonspatie exceptions map (dates, `m-d`, `Y-m-d to Y-m-d` ranges)Testing
-------

[](#testing)

```
composer test
composer analyse
composer format
```

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

Credits
-------

[](#credits)

- [Alexandre Ribes](https://alexandre-ribes.fr)
- [La Boîte à Code](https://laboiteacode.fr)
- [Spatie](https://github.com/spatie) for [spatie/opening-hours](https://github.com/spatie/opening-hours)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

36

—

LowBetter than 79% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity35

Early-stage or recently created project

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/7fd1bf4d557c8949ae533b8e84a5b90bf966d491397089abc0fb6e97cb3569cb?d=identicon)[Alexandre Ribes](/maintainers/Alexandre%20Ribes)

---

Tags

spatielaravelfilamentfilamentphpopening-hoursbusiness hourslaboiteacode

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/laboiteacode-filament-business-hours/health.svg)

```
[![Health](https://phpackages.com/badges/laboiteacode-filament-business-hours/health.svg)](https://phpackages.com/packages/laboiteacode-filament-business-hours)
```

###  Alternatives

[rawilk/profile-filament-plugin

Profile &amp; MFA starter kit for filament.

3914.8k](/packages/rawilk-profile-filament-plugin)[spatie/laravel-pdf

Create PDFs in Laravel apps

1.0k4.8M48](/packages/spatie-laravel-pdf)[stephenjude/filament-jetstream

A Laravel starter kit built with Filament inspired by Jetstream.

17760.2k3](/packages/stephenjude-filament-jetstream)[croustibat/filament-jobs-monitor

Background Jobs monitoring like Horizon for all drivers for FilamentPHP

274333.4k9](/packages/croustibat-filament-jobs-monitor)[stephenjude/filament-debugger

About

104162.2k2](/packages/stephenjude-filament-debugger)[finity-labs/fin-mail

A powerful email template manager and composer for Filament with dynamic token replacement, template versioning, and inline email sending.

284.8k2](/packages/finity-labs-fin-mail)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
