Skip to main content
Quote management covers everything that happens after a client submits a quote: reviewing it in the admin panel, moving it through the status lifecycle, and responding with a scheduled meeting and a PDF by email.

Status lifecycle

Every Quote has a status column that moves through these values:
1

draft

Set when a client calls POST /api/quotes/save-draft. The quote is stored but not yet sent to DMI for review. No PDF is generated at this point.
2

sent

Set when a client calls POST /api/quotes/submit. The sent_at timestamp is recorded and a PDF is generated and stored. The quote is now visible and actionable in the admin list.
3

accepted / rejected / expired

Set by an admin via PATCH /admin/cotizaciones/{quote}/status. These terminal states signal the final outcome of the quote.
Valid status values stored in the database are: draft, sent, accepted, rejected, expired. Use these exact strings when filtering via the status query parameter or setting status via PATCH.

Admin quote list

The quote list is available at GET /admin/cotizaciones and requires the auth and verified middleware. It renders the admin.quotes.index view. Two query parameters are accepted:
ParameterBehaviour
statusFilters by exact status value (draft, sent, accepted, rejected, or expired)
qFull-text search across client_name, client_email, and reference using LIKE %query%
Parameters can be combined. The query string is preserved across paginated pages via withQueryString(). Example URL
/admin/cotizaciones?status=sent&q=garcia

Pagination

Results are paginated at 15 records per page, ordered by created_at descending (newest first). Each row includes the count of associated QuoteItem records via withCount('items').

Quote detail view

GET /admin/cotizaciones/{quote} renders admin.quotes.show with the quote and its related data eager-loaded:
  • items.block — each line item with its originating QuoteBlock
  • replies — all QuoteReply records for this quote, ordered by sent_at descending
The detail view shows client information, the full list of service blocks selected, financial totals, and the reply history.

Updating quote status

Send a PATCH request to update a quote’s status:
PATCH /admin/cotizaciones/{quote}/status
This route is named admin.quotes.status and is protected by auth + verified middleware.
There is no server-side enforcement of status transition order. An admin can set any quote to any of the five valid status values directly.

Replying and scheduling a meeting

The reply action sends a Google Meet appointment message to the client by email and attaches the quote PDF.
POST /admin/cotizaciones/{quote}/reply
Required field
FieldTypeValidation
meeting_datestringrequired, valid date

What happens on reply

1

Format meeting message

The meeting_date value is parsed and formatted as dd/mm/yyyy HH:mm. The message stored and sent is:
Cita Virtual en GoogleMeet: 25/03/2026 10:00
2

Save QuoteReply record

A QuoteReply is created with:
  • quote_id — the parent quote
  • message — the formatted meeting message
  • sent_at — the exact meeting_date value provided in the request
3

Regenerate PDF

The quote PDF is regenerated from the current quote data and written to storage/app/public/quotes/{reference}.pdf, overwriting any previously stored version.
4

Send email

QuoteReplyMail is dispatched to client_email via Mail::to(). The email carries the formatted meeting message and the PDF as an attachment.
5

Redirect

On success, the admin is redirected back to the quote detail view (admin.quotes.show) with a success flash message.

Data models

Key columns relevant to management:
ColumnTypeNotes
referencestringAuto-generated: COT- + strtoupper(uniqid())
client_namestring
client_emailstring
client_companystringNullable
client_phonestringNullable
additional_requirementstextNullable
statusstringdraft, sent, accepted, rejected, or expired
sent_atdatetimeSet on submit
subtotaldecimal(2)
taxdecimal(2)
totaldecimal(2)
total_hoursinteger
pdf_pathstringRelative path under storage/public/
dataJSONFull raw request payload from the builder
One row per selected block, linked to the parent Quote:
ColumnTypeNotes
quote_idintegerForeign key to quotes
quote_block_idintegerForeign key to quote_blocks (not nullable per migration)
namestringBlock name at time of quote
descriptiontextNullable
typestringBlock type
quantityintegerDefault 1
hoursinteger
unit_pricedecimal(2)
total_pricedecimal(2)
dataJSONExtra config from the block
Represents one admin reply / meeting schedule action:
ColumnTypeNotes
quote_idintegerForeign key to quotes
sent_atdatetimeThe scheduled meeting date/time
The message field is formatted by the controller and stored, but QuoteReply::$fillable only lists quote_id and sent_at. Review your migration if you need the message column to be queryable independently.

Route reference

MethodURINameDescription
GET/admin/cotizacionesadmin.quotes.indexPaginated list with filters
GET/admin/cotizaciones/{quote}admin.quotes.showQuote detail
PATCH/admin/cotizaciones/{quote}/statusadmin.quotes.statusUpdate status
POST/admin/cotizaciones/{quote}/replyadmin.quotes.replySend meeting reply
GET/admin/cotizaciones/{quote}/pdfadmin.quotes.pdfDownload PDF
All routes require auth + verified middleware.

Build docs developers (and LLMs) love