laboiteacode/filament-business-hours
Filament field for managing business opening hours on a drag-and-drop weekly 24-hour grid, with exceptional closures, special hours, vacation periods, and timezones. Stores data natively in spatie/opening-hours format.
spatie/opening-hours definition (weekly map + exceptions + overflow + timezone) in one JSON row. That is the same artefact the app queries with OpeningHours::create(), so it is a reversible, low-lock-in choice — a real architectural decision point, not just a UI purchase.composer require plus one migration and a plugin registration.isOpenAt, nextOpen, nextClose, exceptionalClosingDates().->record('shop-paris') gives one hours set per key, and multiple panels can each manage a different row. Supports a "one page per location" or "one page per brand" model without schema changes.canAccess() hook / Gate ability drives both nav item and route — supports the "who is allowed to change that we're closed?" question (ops manager vs. any panel user).Consider it when all of these hold:
spatie/opening-hours as the query layer, or already use it. If you need a different query semantic (per-service hours, resource calendars), the storage format will fight you.key.Look elsewhere when:
m-d to m-d ranges are preserved verbatim across edits, but not editable in the grid.)overflow mode as an opt-in that shows clamped ranges. Doable, but a nightly venue should validate this in a spike before committing.spatie/opening-hours and build a thin editor, or use a dedicated scheduling product.key is the model; bulk management of dozens of locations is not what this is.For executives (30 seconds). This is a small, MIT-licensed building block that gives our admin panel a proper opening-hours editor — a 24-hour weekly grid, holiday closures, vacation periods and timezones — for roughly the cost of one Composer dependency and one database table. It does not invent a data format: everything it saves is the exact input format of spatie/opening-hours, an established library we would likely standardise on anyway. That means the strategic risk is bounded — if we ever drop the plugin, we keep our data and only lose the editor. It unlocks "open now", holiday notices and hours-gated checkout copy without an engineering project, and it explicitly does not drag us into booking, capacity or staffing, which stay separate roadmap decisions.
For engineering. One table, one row per hours set (key, timezone, overflow, hours JSON, exceptions JSON). The page saves the whole state atomically after normalising ranges and validating by actually instantiating Spatie\OpeningHours\OpeningHours, so a stored record is queryable by construction. Reads are a one-liner (BusinessHours::openingHours('shop-paris')) or toOpeningHoursArray() if we want to cache the parsed definition (recommended for per-request header badges: cache()->remember(...), invalidated on save). Configuration is available both as a published config file and a fluent per-panel API. Authorization is a single canAccessUsing() closure or Gate ability covering nav and route. Livewire state is treated as untrusted: caps of 100 ranges/day, 500 exceptions, 1100 days per vacation, 255-char labels, with graceful degradation on malformed state. Ships EN/FR/ES, has tests and static analysis in CI.
For risk and procurement. MIT licence, v1.x with a changelog and docs, and — notably for a package this size — a supply-chain hardening release: CI actions pinned to full commit SHAs, Dependabot with a cooldown window, and a published SECURITY.md with disclosure channels. That is better hygiene than most small packages ship. Counterweights to state openly: 8 GitHub stars, 0 dependents, one maintainer, single-agency origin, no track record at scale. Mitigations are cheap and should be part of the proposal: pin the exact version, isolate usage behind our own model/service (the package supports ->model() subclassing and raw table reads), keep the format standard so a fork or hand-written definitions remain viable, and treat the UI as replaceable.
Recommendation and the ask. Timebox a one-day spike on a single non-critical panel: install, migrate, model our real hours (including an overnight case if we have one), and confirm the query paths we need (isOpenAt, nextOpen, exceptionalClosingDates) plus a cached read on a hot page. Fund the spike, not the commitment. Decision criteria to come back with: does the overnight/exceptions model match our venues, is the per-request read cheap enough with caching, and are we comfortable owning a fork. If all three pass, adopt for presentation-layer hours now and defer the "hours as checkout policy" decision until that roadmap item is prioritised.
How can I help you explore Laravel packages today?