The sidebar gains a new section that lists each currently-activated module
as a direct shortcut to its settings page. This is the always-visible
companion to the company-context Active Modules index — both surface the
same set of modules, but the index is the catalog landing page and the
sidebar group is the per-module quick access.
- BootstrapController returns module_menu populated from
\InvoiceShelf\Modules\Registry::allMenu(), but only on the company-context
branch — not on the super-admin branch (lines 53-69), since super admins
don't see the dynamic group. Because nwidart only boots service providers
for currently-activated modules, the registry naturally contains only
active modules at request time, no extra filtering needed.
- bootstrap.service.ts BootstrapResponse type extended with
module_menu?: ModuleMenuItem[]; new ModuleMenuItem interface
(title/link/icon) — shaped distinctly from MenuItem because module entries
use namespaced i18n keys and don't carry group/ability metadata.
- global.store.ts exposes a moduleMenu ref + a hasActiveModules computed.
- SiteSidebar.vue appends a new "Modules" section after the existing
menuGroups output, in both the mobile (Dialog) and desktop branches. The
section is hidden when hasActiveModules is false. Uses the
modules.sidebar.section_title i18n key added in the previous commit.
Adds the read-only company "Active Modules" index page (lists every
instance-activated module with a Settings shortcut) and the schema-driven
settings framework (generic BaseSchemaForm.vue renderer + per-company
persistence in CompanySetting). Bundled because they share the same
routes/api.php edit and the index page's Settings button targets the
settings page.
Backend:
- CompanyModulesController::index() returns every Module::enabled = true row
with a kebab-case slug (via Str::kebab()) and a has_settings flag computed
from \InvoiceShelf\Modules\Registry::settingsFor(). nwidart stores module
names in PascalCase ("HelloWorld") but URLs and registry keys use kebab
("hello-world") — the controller normalizes so module authors can call
Registry::registerSettings('hello-world') naturally without thinking
about the storage format.
- ModuleSettingsController::show(\$slug) returns the registered Schema +
per-company values from CompanySetting (defaults flow through when nothing
has been saved yet). update(\$slug) builds Laravel validator rules from
the Schema's per-field rules arrays — with type-rule fallbacks for
switch -> boolean, number -> numeric, multiselect -> array — silently
drops unknown keys, and persists via CompanySetting::setSettings() under
the module.{slug}.{key} prefix. Activation is instance-global, but
settings are per-company: two companies on the same instance can
configure the same activated module differently.
- routes/api.php mounts GET /api/v1/company-modules at the root of the
company API group and GET/PUT /api/v1/modules/{slug}/settings inside the
existing modules prefix.
Frontend:
- BaseSchemaForm.vue is the central new component — a generic schema-driven
form renderer that maps schema fields to BaseInput / BaseTextarea /
BaseSwitch / BaseMultiselect by type, and builds Vuelidate rules
dynamically from each field's rules array (supports required, email, url,
numeric, min:N, max:N). New fields are added by extending the type ->
component map.
- CompanyModulesIndexView.vue fetches /company-modules and renders a card
grid (with empty/loading states); CompanyModuleCard.vue is the per-row
component with the Settings button. ModuleSettingsView.vue fetches
/modules/{slug}/settings, hands {schema, values} to BaseSchemaForm, and
posts back on submit.
- Company-context routes.ts is rebuilt after the previous commit relocated
the marketplace browser away. It now declares modules.index +
modules.settings, both gated by manage-module ability.
- New api/services/{companyModules,moduleSettings}.service.ts thin clients.
- lang/en.json adds modules.index.{description,empty_title,empty_description},
modules.settings.{title,open,saved,not_found,none}, and
modules.sidebar.section_title. The sidebar key is added here even though
the dynamic sidebar rendering lands in the next commit — keeping all i18n
additions in one file edit avoids hunk-splitting lang/en.json.
Now that the legacy v1 frontend (commit 064bdf53) is gone, the v2 directory is the only frontend and the v2 suffix is just noise. Renames resources/scripts-v2 to resources/scripts via git mv (so git records the move as renames, preserving blame and log --follow), then bulk-rewrites the 152 files that imported via @v2/... to use @/scripts/... instead. The existing @ alias (resources/) covers the new path with no extra config needed.
Drops the now-unused @v2 alias from vite.config.js and points the laravel-vite-plugin entry at resources/scripts/main.ts. Updates the only blade reference (resources/views/app.blade.php) to match. The package.json test script (eslint ./resources/scripts) automatically targets the right place after the rename without any edit.
Verified: npm run build exits clean and the Vite warning lines now reference resources/scripts/plugins/i18n.ts, confirming every import resolved through the new path. git log --follow on any moved file walks back through its scripts-v2 history.