jguapin/approval-mapping

Standalone approval mapping backend and UI package for Laravel.

Maintainers

Package info

github.com/jagwarthegreat/approval-mapping

pkg:composer/jguapin/approval-mapping

Transparency log

Statistics

Installs: 26

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.11 2026-08-05 02:44 UTC

This package is auto-updated.

Last update: 2026-08-05 02:46:21 UTC


README

Standalone Laravel package for versioned approval matrices, runtime approval requests, and an admin UI.

Configure who approves what (by company / business unit / branch / department / module), version those rules, then let host models create ApprovalRequest records via HasApprovalMapping. Microservices (PO / INV / ITMS / AP) use the same matrix over HTTP — see Remote services.

A working example here: https://github.com/jagwarthegreat/map-test

Features

  • Versioned approval mapping (AMVPMAMPMAAMLPM)
  • Runtime requests + audit logs (APPROARLPE)
  • Sanctum JSON API for versions, lookups, matrix saves, and remote approval requests
  • Blade + Alpine admin UI at /approval-mapping (zero frontend build)
  • Optional Vue 3 embeddable UI (publish assets, mount in your Vite app)
  • Configurable org dimensions via feature flags + host model bindings

Architecture

flowchart LR
  Host["Host model + HasApprovalMapping"] --> Svc["ApprovalMappingService"]
  Ms["Microservice HTTP"] --> ReqApi["Request API"]
  ReqApi --> Svc
  UI["Blade or Vue admin"] --> API["Version / Lookup API"]
  API --> VSvc["ApprovalMappingVersionService"]
  Svc --> AMVPM["AMVPM active version"]
  AMVPM --> AMPMA["AMPMA mapping row"]
  AMPMA --> AMLPM["AMLPM level groups"]
  Svc --> APPRO["APPRO request"]
  APPRO --> ARLPE["ARLPE logs"]
Loading
Table Model Role
AMVPM ApprovalMappingVersion Versioned matrix scope (BU, company, module, dates, active)
AMPMA ApprovalMapping One mapping row (branch, department, type, …)
AMLPM ApprovalMappingLevel JSON level → approver group IDs
APPRO ApprovalRequest Runtime request created from a host model or remote HTTP submit
ARLPE ApprovalRequestLog Approval actions audit trail

Layers follow Controller → Service → Model. Host apps never hardcode package model class names for org entities — bind them in config and resolve via ModelResolver.

Installation

composer require jguapin/approval-mapping
php artisan approval-mapping:install --migrate

Optional Vue UI assets:

php artisan approval-mapping:install --with-assets

What gets published

Tag / flag Destination
Config config/approval-mapping.php
Migrations database/migrations/*approval_mapping*
Views resources/views/vendor/approval-mapping
--with-assets resources/js/vendor/approval-mapping + public/vendor/approval-mapping (CSS)

Configuration

After install, open config/approval-mapping.php.

Feature flags

Toggle each organizational dimension. Disabled dimensions disappear from the UI and lookups return []. Nullable DB columns stay as-is.

'features' => [
    'company'       => true,
    'business_unit' => true,
    'branch'        => true,
    'department'    => true,  // false = free-text department in matrix
    'module'        => true,
],

Model bindings

Only user is required. Bind the rest to your Eloquent models (or leave null).

'models' => [
    'user'                      => \App\Models\User::class,
    'user_assign_group'         => \App\Models\UserGroup::class,
    'sidebar_menu'              => \App\Models\Menu::class,
    'module'                    => null,
    'company'                   => \App\Models\Company::class,
    'business_unit'             => \App\Models\BusinessUnit::class,
    'branch'                    => \App\Models\Branch::class,
    'department'                => \App\Models\Department::class,
    'company_branch_department' => \App\Models\CompanyBranchDepartment::class,
],

Field maps

Override column names when your schema differs from package defaults:

'field_maps' => [
    'company'           => ['label' => 'company_name', 'code' => 'code'],
    'business_unit'     => ['label' => 'name', 'code' => 'unit_code'],
    'branch'            => ['label' => 'name', 'code' => 'branch_code'],
    'department'        => ['label' => 'dept_name', 'code' => 'dept_code'],
    'module'            => ['label' => 'name', 'code' => 'code', 'reference' => 'reference',
                            'status_col' => 'status', 'status_active' => 0],
    'user_assign_group' => ['label' => 'group_name'],
],

Minimal setup (no org hierarchy)

'features' => [
    'company' => false,
    'business_unit' => false,
    'branch' => false,
    'department' => false,
    'module' => false,
],
'models' => [
    'user' => \App\Models\User::class,
    'user_assign_group' => \App\Models\ApproverGroup::class,
],

Gotcha: runtime resolveMapping() still requires business_unit_id in approvalContext() even if the feature flag is off. Pass a real BU id (or a sentinel your versions use).

Blade UI (Alpine + package CSS)

This is the built-in zero-build admin. Stack: Blade + Alpine.js 3 (CDN) + custom am-* CSS — not Tailwind.

Setup

  1. Run approval-mapping:install --migrate and configure models/features.
  2. Ensure users can authenticate via the web + auth middleware (defaults).
  3. Visit /approval-mapping (override with route.web_prefix).

No Vite/npm required. CSS is served at /approval-mapping/assets/approval-mapping.css. The page talks to session-auth JSON under /approval-mapping/api/....

User walkthrough

  1. Versions list — search, filter by company / BU / module (when enabled), paginate.
  2. + Add new — create a version (name, org scope, effective dates, active flag, notes).
  3. Edit / Delete — update metadata or remove a version.
  4. View details — open the mapping matrix:
    • Rows grouped by department (when enabled)
    • Sub-rows: branch, type (direct / agency)
    • Level N columns: one or more approver groups per level (+ / -)
    • Add level columns with the header +
  5. Update — saves mappings/levels for the current version.
  6. Save as new version — closes/copies into a new version name + effective_from.
  7. Sync to module — shown when supports_sync is true (stub response today).

Feature flags control which filters, columns, and lookups appear.

Vue UI (embeddable)

Optional Vue 3 admin with the same flows as Blade. Host apps mount it; the package does not register a separate Vue route.

Setup

php artisan approval-mapping:install --with-assets

Published paths:

  • JS/Vue: resources/js/vendor/approval-mapping/approval-mapping/
  • CSS: public/vendor/approval-mapping/approval-mapping.css

Host requirements:

  • vue ^3.4 (peer dependency)
  • @vitejs/plugin-vue so .vue SFCs compile
  • Sanctum auth against /api/v1/approval-mapping (cookie session or token)

Mount

import { mountApprovalMapping } from './vendor/approval-mapping/approval-mapping';

mountApprovalMapping('#approval-mapping', {
    apiBase: '/api/v1/approval-mapping',
    csrfToken: document.querySelector('meta[name="csrf-token"]')?.content,
    features: {
        company: true,
        business_unit: true,
        branch: true,
        department: true,
        module: true,
    },
});
<div id="approval-mapping"></div>
<link rel="stylesheet" href="/vendor/approval-mapping/approval-mapping.css">

Pass the same feature flags you use in config so the Vue UI matches Blade.

Auth note

  • Blade UI → session (auth middleware on web routes)
  • Vue UI → Sanctum API (auth:sanctum). Use cookie-based SPA auth or bearer tokens consistently with your host app.

Runtime integration (HasApprovalMapping)

Use the trait on any Eloquent model that should enter an approval flow:

use Jguapin\ApprovalMapping\Concerns\HasApprovalMapping;

class PurchaseRequest extends Model
{
    use HasApprovalMapping;

    protected $fillable = [
        // ...
        'approval_request_id', // optional: auto-filled after submit
    ];

    protected string $approvalModuleCode = 'PR';

    public function approvalContext(): array
    {
        return [
            'company_id'       => $this->company_id,
            'business_unit_id' => $this->business_unit_id, // required for resolve
            'branch_id'        => $this->branch_id,
            'type'             => 'direct', // or 'agency'
        ];
    }

    // optional
    public function approvalReferenceType(): string
    {
        return 'purchase_request';
    }
}

Submit:

$request = $purchaseRequest->submitForApproval(['priority' => 'high']);
// ApprovalRequest|null

Flow

sequenceDiagram
  participant Host as HostModel
  participant Trait as HasApprovalMapping
  participant Svc as ApprovalMappingService
  participant AMVPM as ActiveVersion
  participant AMPMA as MappingPlusLevels
  participant APPRO as ApprovalRequest

  Host->>Trait: submitForApproval(extraMetadata)
  Trait->>Svc: createRequestFor(model, moduleCode)
  Svc->>Host: approvalContext()
  Svc->>AMVPM: active version by BU (+ company)
  Svc->>AMPMA: mapping by module (+ branch/type)
  Svc->>APPRO: create pending + level_groups_snapshot
  Svc-->>Host: ApprovalRequest or null
Loading

Example ApprovalRequest output

{
  "id": 42,
  "requester_id": 7,
  "business_unit": "3",
  "module": "PR",
  "amount": 0,
  "status": "pending",
  "current_level": 1,
  "mapping_version_id": 5,
  "approval_mapping_id": 18,
  "metadata": {
    "priority": "high",
    "reference_type": "PurchaseRequest",
    "reference_id": 1001,
    "level_groups_snapshot": {
      "1": [2, 9],
      "2": [4]
    }
  }
}

When it returns null

Condition Result
Host has no approvalContext() null
No active version for BU (+ company) null
No mapping row for module / branch / type null
No requester (requested_by, created_by, or Auth::id()) null

Module code comes from $approvalModuleCode if set, otherwise 'DEFAULT'.

Relationship helper:

$purchaseRequest->approval_request; // BelongsTo ApprovalRequest

Remote services (HTTP)

Use the trait only when the Eloquent model lives in the same app / DB as this package.

If PO / INV / ITMS / AP records live in another service, call the host request API instead. Full walkthrough: Microservice user guide.

Short version:

  1. Host keeps model bindings + sidebar/menus + the matrix.
  2. Microservice authenticates with Sanctum (Authorization: Bearer …).
  3. Submit with module, reference_type, reference_id, requester_id, and org context.
  4. Store approval_request_id locally on the remote record if you need it. There is no cross-service Eloquent relation.

API

Prefix defaults to api/v1/approval-mapping with middleware api + auth:sanctum.

Method Path Purpose
GET /lookup/{type} companies, business-units, branches, departments, modules, user-assign-groups
GET /versions Paginated versions (search, filters, per_page)
POST /versions Create version
GET /versions/{version} Show version
PUT /versions/{version} Update version
DELETE /versions/{version} Delete version
GET /versions/{version}/details Matrix rows + level columns
PUT /versions/{version}/activate Activate (deactivates siblings in scope)
PUT /versions/{version}/mappings-levels Save matrix { rows: [...] }
POST /versions/save-as-new Copy mappings into a new version
POST /versions/{version}/sync-to-module Stub sync (returns synced_count: 0 when allowed)
POST /requests/resolve Check active mapping for module + org context
POST /requests Create request (idempotent while pending for the same reference)
GET /requests/by-reference Latest request by module + reference_type + reference_id
GET /requests/{id} Show request by id

Web UI also exposes the same version/lookup actions under {web_prefix}/api/* with session auth. Request endpoints are Sanctum API only.

Dual UI summary

Blade Vue
Enable Always (package routes) --with-assets + host mount
Stack Alpine CDN + am-* CSS Vue 3 SFCs + same CSS
Auth Session auth Sanctum API
URL /approval-mapping Wherever you mount the component

There is no config flag that switches Blade vs Vue — use Blade out of the box, or mount Vue in your SPA when you need it.