================================================================================
MPUMALANGA PVC - QUOTES MODULE
================================================================================
Last Updated: 2026-03-26
Path: /app/quotes/
================================================================================

WHAT THIS SECTION DOES
-----------------------
Manages price quotations sent to clients before an invoice is raised.
A quote has a header (client, order type, notes, terms) and line items
(stock items with qty, size, panels, and price).
Quotes can be converted to invoices with one click.

DATABASE TABLES
---------------
quotes:
  record_id              int
  client_id              int   - FK to clients.record_id
  order_type             text  - SUPPLY / SUPPLY & INSTALL / SUPPLY & DELIVERY
  user_id                int   - FK to users.record_id
  status                 text  - OPENED
  subject                text  - Text block above line items on PDF
  notes                  text  - Important notes section
  terms                  text  - Terms and conditions
  area                   text  - Installation area description
  quote_number           int   - Incrementing quote number
  additional_delivery_details text
  date_time_created      varchar(50) - Auto timestamp

quote_list:
  record_id    int
  stock_id     int   - FK to stock.record_id
  qty          text
  price        text  - Stored as currency string (e.g. "R 1.234.56")
  quote_id     int   - FK to quotes.record_id
  size_m       text
  pannels      text

================================================================================
FILES
================================================================================

home.php
--------
What it does:
  Lists all quotes with an Edit button per row.
  Uses table class: new table("quotes") + add_action_button("edit_quotes.php")

Note: Page title incorrectly says "JOB CARDS".

--------------------------------------------------------------------------------
add_quotes.php
--------------
What it does:
  Form to create a new quote. Includes client selection, order type, subject,
  notes, terms, area, and a dynamic line items table.

How it works:
  On load:
    - Queries quotes for the last record to calculate the next quote_number.
    - Calls get_stock_datalist("stock_list") to build the stock datalist.
    - Calls get_clients_datalist("clients_list") to build the client datalist.

  Quote number logic:
    SELECT last quote ORDER BY record_id DESC LIMIT 1, then +1.
    If no quotes exist, starts at 1.

  Order type change:
    When SUPPLY or SUPPLY & DELIVERY is selected, a text area for
    "Additional Delivery Details" is shown. For SUPPLY & INSTALL it is hidden.
    The notes text also updates to change the payment terms wording
    (70% upfront vs 100% upfront).

JS Functions:
  get_client_name(el)
    - Fires when the client datalist input changes.
    - Splits the selected value on ":" to extract client_id and client name.
    - Sets the hidden client_id input and cleans the display name.
    - Alerts and clears if the selection is not a valid datalist option.

  change_terms(val)
    - Shows/hides the additional delivery details textarea based on order type.
    - Updates the notes field payment terms wording.

  add_row()
    - Adds a new line item row to the table.
    - Increments the global index counter.
    - New row has Code and Description inputs both linked to stock_list datalist.

  delete_row(el)
    - Removes the parent <tr> of the clicked button from the table.
    - Calls calculateTotals() after removal.

  get_unit_of_measure(input, i)
    - Fires when a Code or Description stock input changes.
    - Splits the datalist value on ~ to get: code, description, UOM, price.
    - Populates unit_of_measure_{i}, stock_code_{i}, stock_{i}, retail_price_{i}.
    - Special case: if code is "DISCOUNT", sets price to "-R 0.00" and qty to 1.

  change_to_currency(input)
    - Formats a price field as South African Rand (R 1.234.56).
    - Strips R, commas, spaces before parsing, then re-formats.

  calculateRow(input, i)
    - Multiplies qty_{i} * price_{i} and writes result to total_{i}.
    - Calls calculateTotals().

  calculateTotals()
    - Sums all total_{i} inputs.
    - NOTE: VAT is set to 0 on quotes (subtotal = net total, no VAT added).
    - Writes subtotal, vat, net_total fields.

  save(url)
    - Validates client_id and order_type are filled. Alerts if not.
    - Collects all inputs (including [] arrays for line items).
    - Creates and submits a hidden POST form.

--------------------------------------------------------------------------------
save_quotes.php
---------------
What it does:
  Receives POST from add_quotes.php, inserts quote header and all line items.

How it works:
  - Sanitises subject, note, terms, area by stripping quotes and double-quotes.
  - Recalculates quote_number from DB to prevent race conditions.
  - Inserts into quotes table, gets back the new quote_id.
  - Loops over $_POST['stock_code'] array:
    - For each code, looks up the stock record_id by code.
    - Inserts a row into quote_list with that stock_id, qty, price (cleaned
      via number_to_save()), size_m, pannels.
  - Redirects to home.php.

Note: If a stock code is not found in the stock table, $item_id will be null/0
and a quote_list row with stock_id=0 will be saved. No validation is done.

--------------------------------------------------------------------------------
edit_quotes.php
---------------
What it does:
  Pre-filled edit form for an existing quote. Same layout as add, but with
  existing data loaded from DB.

How it works:
  - Loads quote by $_GET['record_id'].
  - Loads quote_list rows and for each row loads the matching stock record.
  - Renders each line item row with existing values pre-filled.
  - PHP $index counter is passed to JS as `let index = <?php echo $index; ?>`
    so that add_row() continues from the correct number.
  - calculateRow() is called on page load for all existing rows to calculate
    their totals.
  - Has a USER CREATED dropdown (pre-selected to original user).
  - Shows DATE CREATED as read-only.
  - Saves to update_quotes.php.

JS Functions: Same as add_quotes.php plus:
  calculateRow() (edit version)
    - Loops from i=1 to index-1 and recalculates all rows at once.
    - Called on page load.

--------------------------------------------------------------------------------
update_quotes.php
-----------------
What it does:
  Receives POST from edit_quotes.php, updates the quote header and rebuilds
  all line items.

How it works:
  - Sanitises subject, note, terms, area.
  - Updates the quotes row.
  - DELETEs all existing quote_list rows for this quote_id.
  - Re-inserts all line items from the POST arrays.
    Loops over $_POST['stock_id'] (which contains stock codes on the edit page).
    For each code, looks up the stock record_id and inserts into quote_list.
  - Redirects to home.php.

Note: Empty stock_id values are skipped with `if (empty($item_name)) continue`.

--------------------------------------------------------------------------------
get_unit_of_measure.ajax.php
----------------------------
What it does:
  AJAX endpoint. Receives a stock record_id via POST and returns the
  unit_of_measure and retail price as a comma-separated string.

Response format:  "UNIT_OF_MEASURE,retail_price"
Called by:        get_unit_of_measure() JS in edit pages that use stock_id
                  (not stock code). The invoices edit page uses this approach.

--------------------------------------------------------------------------------
quote.pdf.php
-------------
What it does:
  Generates a PDF quotation for the given quote (?record_id=X).
  Uses the FPDF library.

PDF contents:
  - Company and client details side by side at top.
  - Quote number, date, order type.
  - Delivery details block (if filled in).
  - Subject block.
  - Area.
  - Line items table: Code, Description, UOM, Qty, Size, Panels, Amount, Total.
  - Totals (subtotal and net total - VAT is 0 on quotes).
  - Notes and Terms blocks.

  sa_str_to_float($str)  - Local helper function.
    Cleans a South African currency string (handles R, commas, dots) and
    returns a PHP float. Used to clean price strings before arithmetic.

--------------------------------------------------------------------------------
convert_to_invoice.php
----------------------
What it does:
  Converts a quote into an invoice. Called via a button on the quote list/edit.
  URL: convert_to_invoice.php?record_id={quote_id}

How it works:
  Step 1 - Check if already converted:
    Queries invoices WHERE quote_id = $quote_id.
    If found, redirects straight to the existing invoice's edit page.

  Step 2 - Create new invoice:
    Gets the last invoice_number and adds 1.
    Gets all quote data.
    Inserts a new row into invoices with data from the quote.
    The invoice subject is replaced with a standard MPPVC acceptance text.
    The notes are replaced with a payment options message.

  Step 3 - Copy line items:
    Loops over quote_list rows and inserts each into invoice_list.
    Prices are cleaned via number_to_save() before saving.

  Step 4 - Redirect:
    Goes to edit_invoices.php?record_id={new_invoice_id}

================================================================================
KNOWN ISSUES SUMMARY
================================================================================
1. home.php title says "JOB CARDS".
2. No VAT on quotes (intentional - but VAT row is hidden, not removed from code).
3. save_quotes.php does not validate that stock codes exist before inserting.
4. Quote number can theoretically collide under concurrent usage (recalculated
   from DB at save time, not locked).
5. SQL injection throughout.

================================================================================