/cotizador. Each block belongs to a category, carries a base price and default hours estimate, and can hold an arbitrary list of key-value extras stored in its config field.
Data model
Categories
Categories group related blocks in the builder’s sidebar. A category must exist before blocks can be assigned to it.| Field | Type | Notes |
|---|---|---|
id | bigint | Auto-incremented primary key |
name | string | Unique display name |
description | text | Optional longer description shown in the UI |
icon | string | Optional icon identifier displayed in the builder sidebar |
color | string | Optional colour (hex or CSS class) used to distinguish the category in the UI |
order | integer | Sort position (ascending). Auto-assigned on creation |
is_active | boolean | Defaults to true. Inactive categories are hidden |
created_at | timestamp | — |
updated_at | timestamp | — |
Blocks
Each block represents one billable service line. Blocks are nested under a category.| Field | Type | Notes |
|---|---|---|
id | bigint | Auto-incremented primary key |
name | string | Block display name |
description | text | Optional description shown in the builder card |
category_id | bigint (FK) | References quote_block_categories.id — cascades on delete |
base_price | decimal(10,2) | Base price for one unit of the block |
default_hours | integer | Estimated hours of work for this block |
config | json | Array of key-value extras (see below) |
formula | text | Reserved for future pricing formula support |
validation_rules | json | Reserved for future input validation |
is_active | boolean | Defaults to true. Only active blocks appear in the builder |
order | integer | Sort position within the category (ascending) |
created_at | timestamp | — |
updated_at | timestamp | — |
The config field
The config column stores an array of single-key objects. Each entry maps one string key to a string or numeric value.
processConfig() in QuoteBlockController before being stored.
How processConfig works
processConfig is a private method on QuoteBlockController that converts the raw form input into the structured array stored in config.
- Rows with an empty
keyare silently skipped. - Numeric values are automatically cast (
"3"→3,"1.5"→1.5). - String values are stored as-is.
- Passing no extras (or an empty array) returns
[], which stores as an empty JSON array.
Managing blocks via the admin UI
The admin panel for quote blocks is available at/bloques (authentication required).
Create a category
Submit the New Category form on the
/bloques index page. The name field must be unique across all categories. The order value is automatically set to max(order) + 1.Create a block
Click New Block and select a parent category. Fill in the name, base price, default hours, and any extra config rows. The block’s
order is automatically set to max(order) + 1 within that category.Edit a block
Navigate to
/bloques/{id}/edit. All fields including extras can be updated. The order field is not shown in the edit form — use drag-and-drop reordering instead.Reorder blocks
The index page supports drag-and-drop reordering via
POST /bloques/reorder (JSON body: {"blocks": [{"id": 1, "order": 0}, ...]}). Each block’s order is updated individually.How blocks appear in the builder
The quote builder SPA fetches blocks fromGET /api/quote-blocks. The controller applies two scopes before returning data:
active()— filters tois_active = trueon both categories and blocks.ordered()— sorts byorder ASCon both categories and blocks.
is_active = false are never sent to the client — they are invisible in the builder and cannot be added to quotes.
Eloquent relationships
Validation rules on store and update
TheQuoteBlockController applies the following rules when creating or updating a block:
| Field | Rule |
|---|---|
name | Required, string, max 255 characters |
category_id | Required, must exist in quote_block_categories |
base_price | Required, numeric, min 0 |
default_hours | Required, integer, min 0 |
description | Optional |
is_active | Optional boolean (defaults to true) |
| Field | Rule |
|---|---|
name | Required, string, max 255, unique in quote_block_categories |
description | Optional, max 500 characters |
Model scopes
BothQuoteBlock and QuoteBlockCategory define reusable local scopes: