Multiple Selection

Add the option to select multiple rows and take an action.

Multiple Selection adds row checkboxes to a KAS table and lets a developer run one or more actions on the selected rows. Configure it on the page that sets $dbtable, $idfield, and $query, before including core/table_view.php.

1. Basic Multiple Selection

$dbtable = 'INVOICES';
$idfield = 'INID';
$multiselect = true;
// $query must select INID before including core/table_view.php.

This displays a checkbox for each rendered row, a select-all checkbox, and the default bulk delete action. The selection covers rendered rows, not every result on other pages. The query must include $idfield: the table uses that result as each row's selected ID. The single-row delete control is separate.

2. Custom actions and properties

Set $multiselect to an associative array. Each key names an action; its value supplies the action's properties. The common properties are:

  • icon (required): HTML for the action icon.
  • id (required): a distinct control ID for this action on the page.
  • type (required): 'form' or 'ajax'.
  • title (optional): control tooltip; defaults to an empty string.
  • confirm (optional): confirmation text; omit or use false to skip confirmation.
  • message (optional): success text; KAS supplies a default when omitted.
  • dbtable and idfield (optional): default to the page's $dbtable and $idfield.

Form action: With type => 'form' and form_class => 'KAS_FORM', supply dbaction. nexturl defaults to $self_url. The examples below use this standard KAS_FORM route.

AJAX action: With type => 'ajax', supply url and ajax_action. Optional success is JavaScript run after a successful JSON response; its default is location.reload(). AJAX uses its own endpoint and response contract.

The special COPY_FIRST option also requires columns_to_copy and column_headers. The default DEL option is described below.

3. KAS_FORM Multiple Selection

The standard flow is table_view.php → generated KAS_FORM → db_actions.php → pre_actions.php → query_builder.php (when a generic case exists) → domain handler → nexturl redirect. A custom action can own its work in a domain handler without a generic query-builder case.

Each checkbox contains an individually encrypted row ID. The click handler joins the selected values into a comma-separated idvalue. Separately, encdata encrypts the action name, table, and key field; decrypting encdata does not decrypt the selected IDs. A custom handler must do that itself:

$selectedIds = array();
foreach (explode(',', $idvalue) as $encryptedId) {
    $id = $encryption->decrypt($encryptedId);
    if (!ctype_digit((string)$id) || (int)$id <= 0) {
        echo 'dberror:Invalid selection.';
        die();
    }
    $selectedIds[] = (int)$id;
}
$selectedIds = array_values(array_unique($selectedIds));
// Reload and authorize every selected row before writing.

The decrypted ID is still untrusted. Check that every row exists and belongs to the expected parent or user before changing data.

4. Default DELETE_SELECTION

$multiselect = true adds the DEL option automatically. An action array also gets DEL unless it explicitly defines or suppresses it. Use 'DEL' => false when the table should expose only your custom actions:

// After defining your custom action array:
$multiselect['DEL'] = false;

The default uses KAS_FORM and dbaction => 'DELETE_SELECTION'. pre_actions.php runs before the generic delete SQL in query_builder.php. For example, KAS checks all selected invoices or invoice items for protected STOCK_HISTORY references before deleting any of them. Add whole-selection validation there when a new table needs an integrity guard. Generic bulk deletion is not automatically a transaction for every table.

5. Multiple custom actions

An array can expose several controls. CLIENT_SUMMARY.php uses PAID and MERGE on invoices; because it does not suppress DEL, the default delete control also appears. This structural example assumes both action variables contain their complete required properties:

$multiselect = array(
    'PAID'  => $paidAction,
    'MERGE' => $mergeAction,
    'DEL'   => false // Optional: omit this line to keep bulk delete.
);

Give each action a unique id and its own server-side handler or endpoint.

6. BILLING example: MARKED_AS_PAID

The invoice table in CLIENT_SUMMARY.php uses INVOICES.INID and configures a KAS_FORM action like this:

$dbtable = 'INVOICES';
$idfield = 'INID';
$multiselect = array(
    'PAID' => array(
        'icon' => '<i class="fas fa-thumbs-up"></i>',
        'id' => 'mark_as_paid',
        'type' => 'form',
        'title' => translate(array('en:Mark as Paid', 'es:Marcar como pagado', 'fr:Marquer comme payé')),
        'confirm' => translate(array('en:Mark selected invoices as paid?', 'es:¿Marcar como pagadas las facturas seleccionadas?', 'fr:Marquer les factures sélectionnées comme payées ?')),
        'message' => translate(array('en:Invoices marked as paid', 'es:Facturas marcadas como pagadas', 'fr:Factures marquées comme payées')),
        'form_class' => 'KAS_FORM',
        'dbaction' => 'MARKED_AS_PAID'
    )
);

kas/core/db_actions/billing.php handles the action by decrypting the selected INIDs and updating invoice status, balance, and payment date. It is a compact existing example; use stronger whole-selection validation and a transaction when your own operation requires all-or-nothing behavior.

7. Advanced KAS_FORM example: MOVE_TO_NEW_INVOICE

EDIT_INVOICE.php configures MOVE_TO_NEW_INVOICE for INVOICE_ITEMS.IIID:

$dbtable = 'INVOICE_ITEMS';
$idfield = 'IIID';
$multiselect = array(
    'MOVE_TO_NEW_INVOICE' => array(
        'icon' => '<i class="fas fa-file-export"></i>',
        'id' => 'move_to_new_invoice',
        'type' => 'form',
        'title' => translate(array('en:Move to New Invoice', 'es:Mover a una factura nueva', 'fr:Déplacer vers une nouvelle facture')),
        'confirm' => translate(array('en:Move the selected items to a new invoice?', 'es:¿Mover los artículos seleccionados a una factura nueva?', 'fr:Déplacer les articles sélectionnés vers une nouvelle facture ?')),
        'message' => translate(array('en:Selected items moved to a new invoice', 'es:Artículos seleccionados movidos a una factura nueva', 'fr:Articles sélectionnés déplacés vers une nouvelle facture')),
        'form_class' => 'KAS_FORM',
        'dbaction' => 'MOVE_TO_NEW_INVOICE'
    )
);

Its billing.php handler validates the encrypted IIIDs, deduplicates them, reloads the items, and requires every item to belong to the same source invoice. It then owns a transaction: create the destination invoice, assign its number, change INVOICE_ITEMS.INID for selected rows without changing IIID, change STOCK_HISTORY.INID for those IIIDs without changing STOCK_HISTORY.IIID, recalculate both invoices, and commit. An error after the transaction starts rolls it back and returns dberror:. Payments are not copied; selected invoice metadata is copied or reset by the handler.

This pattern is appropriate when a selection changes several related tables. Do not put an early generic query in query_builder.php for an action whose handler must validate and commit the complete operation.

8. AJAX Multiple Selection

AJAX actions are supported and production-tested. They use the same checkboxes but POST to url instead of entering the KAS_FORM database-action pipeline:

$multiselect = array(
    'MY_AJAX_ACTION' => array(
        'icon' => '<i class="fas fa-check"></i>',
        'id' => 'my_ajax_action',
        'type' => 'ajax',
        'url' => 'ajax/myAction.php',
        'ajax_action' => 'MY_AJAX_ACTION',
        'confirm' => translate(array('en:Process selected rows?', 'es:¿Procesar las filas seleccionadas?', 'fr:Traiter les lignes sélectionnées ?')),
        'message' => translate(array('en:Rows processed.', 'es:Filas procesadas.', 'fr:Lignes traitées.')),
        'success' => 'location.reload()'
    )
);

The browser sends a POST with dbtable, idfield, action (from ajax_action), and idvalue. The last field is a comma-separated list of encrypted row IDs; there is no KAS_FORM encdata field. If nothing is selected, the browser displays an error and sends no request. The endpoint must still reject an empty selection itself.

Return JSON such as {"success":true,"message":"Rows processed."} on success or {"success":false,"message":"Invalid selection."} with an appropriate error HTTP status on failure. The configured success JavaScript runs only when the parsed response has success === true. AJAX is useful when an endpoint should report its outcome without the KAS_FORM redirect flow.

9. INVENTORY example: Sold Out

The production-tested reference is kas/includes/INVENTORY/VIEW_PRODUCTS.php with kas/ajax/inventory.php. The table uses PRODUCTS.PRID and configures the action as follows (shown with only its relevant properties):

$dbtable = 'PRODUCTS';
$idfield = 'PRID';
$multiselect = array(
    'SOLD_OUT' => array(
        'icon' => '<i class="fa-solid fa-empty-set"></i>',
        'id' => 'sold_out',
        'type' => 'ajax',
        'title' => translate(array('en:Sold Out', 'es:Agotado', 'fr:Épuisé')),
        'confirm' => translate(array('en:Mark selected products as sold out and remove their remaining stock?', 'es:¿Marcar los productos seleccionados como agotados y retirar las existencias restantes?', 'fr:Marquer les produits sélectionnés comme épuisés et retirer le stock restant ?')),
        'message' => translate(array('en:Selected products marked as sold out.', 'es:Los productos seleccionados se marcaron como agotados.', 'fr:Les produits sélectionnés ont été marqués comme épuisés.')),
        'url' => 'ajax/inventory.php',
        'ajax_action' => 'SOLD_OUT',
        'success' => 'setTimeout(function(){ location.reload(); }, 1200)'
    )
);

The endpoint checks the action and table/key pair, decrypts and deduplicates selected PRIDs, reloads and locks the product rows, and verifies their remaining restock batches. Inside one transaction it records negative STOCK_HISTORY.STOCKCHANGE movements with TYPE='removed' for the stock removed from each batch, then sets PRODUCTS.global_inventory = 0 and sold_out = 1. The movements appear as Stock Removed and have notes in the form Set as sold out by USER, using the current KAS user's name. A product already at zero gets no fake stock movement. A failed validation or write rolls the whole selection back.

10. AJAX endpoint rules

Follow the Sold Out endpoint's pattern:

  1. Include KAS settings/bootstrap and call generate_clean_requests('POST').
  2. Require POST, explicitly allow the expected action, and check the current KAS user.
  3. Check the expected dbtable and idfield; never use posted names as permission to update an arbitrary table.
  4. Split and decrypt idvalue, reject invalid IDs, deduplicate, and reload every record.
  5. Validate ownership, parent relationships, and the complete selection before writes.
  6. For related writes, start a transaction, check each query, and roll back on failure.
  7. Respond with JSON containing a boolean success and a user-facing message; use an error HTTP status for failure.

11. Confirmation, success, and errors

confirm opens the browser confirmation dialog before either action route; omit it or set it to false to skip the dialog. For KAS_FORM, message becomes the success message on the nexturl path, while a dberror: response stops success handling. For AJAX, the endpoint's JSON message takes precedence over the configured message. A success response shows the message and then executes success; a false result, HTTP error, or invalid JSON shows an error and never executes success. The AJAX renderer escapes server message text before showing it.

12. Translations

For new KAS code, use the language-tagged array form:

translate(array(
    'en:Select at least one row.',
    'es:Seleccione al menos una fila.',
    'fr:Sélectionnez au moins une ligne.'
));

The positional signature remains supported for legacy code; do not use it in new examples.

13. Security and data integrity

Encrypted input is still untrusted input. Validate every selected ID, reload records, check authorization and parent ownership, and validate the whole selection before writing. Use a transaction for multi-record or multi-table changes that must succeed together. Never rely only on hidden controls, disabled buttons, or foreign keys to enforce these rules.

14. Implementation checklist

  1. Ensure $query selects $idfield; enable $multiselect and decide whether to keep DEL.
  2. Give every action a unique id and the required form or AJAX properties.
  3. For KAS_FORM, inspect the generic query builder and choose the owning handler. For AJAX, implement a strict endpoint and JSON response.
  4. Decrypt and validate all selected IDs; reload and authorize every record before writes.
  5. Make integrity-sensitive work transactional and return the correct route-specific error.
  6. Test single, multiple, empty, invalid, missing, and mixed-parent selections as applicable; verify the success callback runs only on AJAX success.