How to Add a Checklist with Progress to Each Record in Oracle APEX

Copy template steps to each new record, tick them with checkboxes saved by a dynamic action, and show progress with a Percent Graph and a Complete badge.

Many records need a fixed set of steps before they are finished. An order must be paid, picked, packed and shipped; a new employee needs a laptop, an account and a badge. A checklist on each record shows what is done, who did it and when, and a progress bar shows at a glance how far each record is.

This guide builds such a checklist in Oracle APEX. Checklist templates hold the standard steps, and a new record gets its own copy of the steps. Users tick the steps with checkboxes that save at once through Ajax, and a report shows the progress as a bar with a Complete badge at 100%.

The guide uses orders as the example; the same steps work for any table. The post How to Build a Survey Form from Question Tables in Oracle APEX also builds rows from a set of template questions. This one copies steps to each record, saves each tick without a page submit, and tracks progress per record.

Step 1: Create the Tables

The example needs four tables and one view:

  • CHK_TEMPLATES holds the checklist templates, such as Standard Order and Express Order.
  • CHK_TEMPLATE_ITEMS holds the steps of each template with a sequence number.
  • CHK_ORDERS holds the records that get a checklist, with the template they were created from.
  • CHK_ORDER_STEPS holds the copied steps of each order, with IS_DONE, DONE_BY and DONE_ON.
  • CHK_ORDER_PROGRESS_V counts the done steps per order and calculates the percentage.

Create the tables, the view and the sample templates:

Example:

create table chk_templates (
  template_id    number generated by default on null as identity,
  template_name  varchar2(100) not null,
  is_active      varchar2(1)   default on null 'Y' not null,
  constraint chk_templates_pk primary key (template_id),
  constraint chk_templates_name_uk unique (template_name),
  constraint chk_templates_active_ck check (is_active in ('Y', 'N'))
);

create table chk_template_items (
  template_item_id  number generated by default on null as identity,
  template_id       number        not null,
  step_seq          number(3)     not null,
  step_label        varchar2(200) not null,
  constraint chk_template_items_pk primary key (template_item_id),
  constraint chk_template_items_tpl_fk foreign key (template_id)
    references chk_templates (template_id),
  constraint chk_template_items_seq_uk unique (template_id, step_seq)
);

create table chk_orders (
  order_id       number generated by default on null as identity,
  order_no       varchar2(20)  not null,
  customer_name  varchar2(100) not null,
  order_date     date          default on null sysdate not null,
  template_id    number        not null,
  constraint chk_orders_pk primary key (order_id),
  constraint chk_orders_no_uk unique (order_no),
  constraint chk_orders_tpl_fk foreign key (template_id)
    references chk_templates (template_id)
);

create index chk_orders_tpl_ix on chk_orders (template_id);

create table chk_order_steps (
  step_id     number generated by default on null as identity,
  order_id    number        not null,
  step_seq    number(3)     not null,
  step_label  varchar2(200) not null,
  is_done     varchar2(1)   default on null 'N' not null,
  done_by     varchar2(128),
  done_on     date,
  constraint chk_order_steps_pk primary key (step_id),
  constraint chk_order_steps_order_fk foreign key (order_id)
    references chk_orders (order_id) on delete cascade,
  constraint chk_order_steps_seq_uk unique (order_id, step_seq),
  constraint chk_order_steps_done_ck check (
    (is_done = 'Y' and done_by is not null and done_on is not null)
    or (is_done = 'N' and done_by is null and done_on is null))
);

create or replace view chk_order_progress_v as
select o.order_id,
       o.order_no,
       o.customer_name,
       o.order_date,
       count(s.step_id) as steps_total,
       count(case when s.is_done = 'Y' then 1 end) as steps_done,
       case when count(s.step_id) = 0 then 0
            else round(100 * count(case when s.is_done = 'Y' then 1 end) / count(s.step_id))
       end as pct_done
from   chk_orders o
       left join chk_order_steps s on s.order_id = o.order_id
group  by o.order_id, o.order_no, o.customer_name, o.order_date;

insert into chk_templates (template_id, template_name) values (1, 'Standard Order');
insert into chk_templates (template_id, template_name) values (2, 'Express Order');
alter table chk_templates modify template_id generated by default on null as identity (start with limit value);

insert into chk_template_items (template_id, step_seq, step_label) values (1, 10, 'Confirm payment received');
insert into chk_template_items (template_id, step_seq, step_label) values (1, 20, 'Pick items from the warehouse');
insert into chk_template_items (template_id, step_seq, step_label) values (1, 30, 'Pack and label the parcel');
insert into chk_template_items (template_id, step_seq, step_label) values (1, 40, 'Hand over to the courier');
insert into chk_template_items (template_id, step_seq, step_label) values (1, 50, 'Send the tracking email');
insert into chk_template_items (template_id, step_seq, step_label) values (2, 10, 'Confirm payment received');
insert into chk_template_items (template_id, step_seq, step_label) values (2, 20, 'Pack the parcel');
insert into chk_template_items (template_id, step_seq, step_label) values (2, 30, 'Book the same-day courier');
commit;

select t.template_name, i.step_seq, i.step_label
from   chk_templates t
       join chk_template_items i on i.template_id = t.template_id
order  by t.template_id, i.step_seq;

Output:

Table CHK_TEMPLATES created.

Table CHK_TEMPLATE_ITEMS created.

Table CHK_ORDERS created.

Index CHK_ORDERS_TPL_IX created.

Table CHK_ORDER_STEPS created.

View CHK_ORDER_PROGRESS_V created.

1 row inserted.

1 row inserted.

Table CHK_TEMPLATES altered.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

1 row inserted.

Commit complete.

TEMPLATE_NAME        STEP_SEQ STEP_LABEL
_________________ ___________ ________________________________
Standard Order             10 Confirm payment received
Standard Order             20 Pick items from the warehouse
Standard Order             30 Pack and label the parcel
Standard Order             40 Hand over to the courier
Standard Order             50 Send the tracking email
Express Order              10 Confirm payment received
Express Order              20 Pack the parcel
Express Order              30 Book the same-day courier

8 rows selected.
  • CHK_TEMPLATE_ITEMS_SEQ_UK and CHK_ORDER_STEPS_SEQ_UK allow each sequence number only once per template and per order.
  • CHK_ORDER_STEPS_DONE_CK keeps the columns in step: a done step has a user and a date, an open step has neither.
  • CHK_ORDER_STEPS_ORDER_FK has ON DELETE CASCADE, so deleting an order also deletes its steps.
  • IS_ACTIVE, IS_DONE and ORDER_DATE use DEFAULT ON NULL, so no trigger is needed to fill them.

The view uses a LEFT JOIN, so an order without steps still appears with 0%. The CASE expression avoids a division by zero in that case. The two templates were inserted with fixed IDs, so the ALTER TABLE statement moves the identity past the highest ID.

Step 2: Create the Checklist Package

The package has two procedures. CREATE_ORDER inserts an order and copies the steps of its template. SET_STEP ticks or unticks one step:

Example:

create or replace package chk_pkg as
  procedure create_order (
    p_order_no      in  chk_orders.order_no%type,
    p_customer_name in  chk_orders.customer_name%type,
    p_template_id   in  chk_orders.template_id%type,
    p_order_id      out chk_orders.order_id%type);

  procedure set_step (
    p_order_id in chk_order_steps.order_id%type,
    p_step_id  in chk_order_steps.step_id%type,
    p_done     in varchar2,
    p_user     in varchar2);
end chk_pkg;
/

create or replace package body chk_pkg as
  procedure create_order (
    p_order_no      in  chk_orders.order_no%type,
    p_customer_name in  chk_orders.customer_name%type,
    p_template_id   in  chk_orders.template_id%type,
    p_order_id      out chk_orders.order_id%type)
  is
  begin
    insert into chk_orders (order_no, customer_name, template_id)
    values (p_order_no, p_customer_name, p_template_id)
    returning order_id into p_order_id;

    insert into chk_order_steps (order_id, step_seq, step_label)
    select p_order_id, i.step_seq, i.step_label
    from   chk_template_items i
           join chk_templates t on t.template_id = i.template_id
    where  i.template_id = p_template_id
    and    t.is_active = 'Y';

    if sql%rowcount = 0 then
      raise_application_error(-20003,
        'The checklist template is inactive or has no steps.');
    end if;
  exception
    when dup_val_on_index then
      raise_application_error(-20002,
        'Order number ' || p_order_no || ' already exists.');
  end create_order;

  procedure set_step (
    p_order_id in chk_order_steps.order_id%type,
    p_step_id  in chk_order_steps.step_id%type,
    p_done     in varchar2,
    p_user     in varchar2)
  is
  begin
    update chk_order_steps
    set    is_done = p_done,
           done_by = case when p_done = 'Y' then p_user end,
           done_on = case when p_done = 'Y' then sysdate end
    where  step_id  = p_step_id
    and    order_id = p_order_id;

    if sql%rowcount = 0 then
      raise_application_error(-20001,
        'This step does not belong to the selected order.');
    end if;
  end set_step;
end chk_pkg;
/

select object_name, object_type, status
from   user_objects
where  object_name = 'CHK_PKG';

Output:

Package CHK_PKG compiled

Package Body CHK_PKG compiled

OBJECT_NAME    OBJECT_TYPE     STATUS
______________ _______________ _________
CHK_PKG        PACKAGE         VALID
CHK_PKG        PACKAGE BODY    VALID

CREATE_ORDER copies the template steps with one INSERT ... SELECT. It copies only from an active template. If no row was copied, it raises ORA-20003, and a duplicate order number becomes the friendly ORA-20002 message.

The steps are copied, not linked. A later change to a template does not change the checklists of existing orders, which keeps the history correct.

SET_STEP updates the step only when it belongs to the given order. It sets DONE_BY and DONE_ON when the step is ticked and clears them when it is unticked. The user name comes in as a parameter; in APEX it is :APP_USER.

A trigger could also copy the steps after each insert into CHK_ORDERS. The package was chosen because the template is a choice of the user, and one call creates the order and its steps together. Neither procedure commits; APEX commits at the end of the request.

Step 3: Test the Package in SQL

Create two orders from the two templates, then tick two steps of the first order:

Example:

set serveroutput on
declare
  l_id number;
begin
  chk_pkg.create_order('SO-1001', 'Northwind Traders', 1, l_id);
  dbms_output.put_line('SO-1001 created with id ' || l_id);
  chk_pkg.create_order('SO-1002', 'Blue Harbor Cafe', 2, l_id);
  dbms_output.put_line('SO-1002 created with id ' || l_id);
  commit;
end;
/

select order_id, step_id, step_seq, step_label, is_done
from   chk_order_steps
order  by order_id, step_seq;

begin
  chk_pkg.set_step(1, 1, 'Y', 'DEMO_DEV');
  chk_pkg.set_step(1, 2, 'Y', 'DEMO_DEV');
  commit;
end;
/

select order_no, steps_done, steps_total, pct_done
from   chk_order_progress_v
order  by order_no;

Output:

SO-1001 created with id 1
SO-1002 created with id 2

PL/SQL procedure successfully completed.

   ORDER_ID    STEP_ID    STEP_SEQ STEP_LABEL                       IS_DONE
___________ __________ ___________ ________________________________ __________
          1          1          10 Confirm payment received         N
          1          2          20 Pick items from the warehouse    N
          1          3          30 Pack and label the parcel        N
          1          4          40 Hand over to the courier         N
          1          5          50 Send the tracking email          N
          2          6          10 Confirm payment received         N
          2          7          20 Pack the parcel                  N
          2          8          30 Book the same-day courier        N

8 rows selected.

PL/SQL procedure successfully completed.

ORDER_NO       STEPS_DONE    STEPS_TOTAL    PCT_DONE
___________ _____________ ______________ ___________
SO-1001                 2              5          40
SO-1002                 0              3           0

SO-1001 got the five steps of Standard Order and SO-1002 the three steps of Express Order. After two ticks, the view shows SO-1001 at 40% and SO-1002 at 0%.

Now test the error cases: a duplicate order number, a step of another order, a template without steps, and a direct update that skips the package:

Example:

declare
  l_id number;
begin
  chk_pkg.create_order('SO-1001', 'Another Customer', 1, l_id);
end;
/

begin
  chk_pkg.set_step(2, 1, 'Y', 'DEMO_DEV');
end;
/

insert into chk_templates (template_name, is_active) values ('Retired Process', 'N');

declare
  l_id number;
begin
  chk_pkg.create_order('SO-9001', 'Test Customer', 3, l_id);
end;
/

select count(*) as orders_named_so_9001
from   chk_orders
where  order_no = 'SO-9001';

rollback;

update chk_order_steps
set    is_done = 'Y'
where  step_id = 3;

select step_id, is_done, done_by
from   chk_order_steps
where  order_id = 1
order  by step_seq;

Output:

declare
*
ERROR at line 1:
ORA-20002: Order number SO-1001 already exists.
ORA-06512: at "APXSOL.CHK_PKG", line 26
ORA-06512: at line 4
Help: https://docs.oracle.com/error-help/db/ora-20002/

begin
*
ERROR at line 1:
ORA-20001: This step does not belong to the selected order.
ORA-06512: at "APXSOL.CHK_PKG", line 45
ORA-06512: at line 2
Help: https://docs.oracle.com/error-help/db/ora-20001/

1 row inserted.

declare
*
ERROR at line 1:
ORA-20003: The checklist template is inactive or has no steps.
ORA-06512: at "APXSOL.CHK_PKG", line 21
ORA-06512: at line 4
Help: https://docs.oracle.com/error-help/db/ora-20003/

   ORDERS_NAMED_SO_9001
_______________________
                      0

Rollback complete.

Error starting at line : 28 File @ b73/31-errors.sql
In command -
update chk_order_steps
set    is_done = 'Y'
where  step_id = 3
Error report -
ORA-02290: check constraint (APXSOL.CHK_ORDER_STEPS_DONE_CK) violated
Help: https://docs.oracle.com/error-help/db/ora-02290/

   STEP_ID IS_DONE    DONE_BY
__________ __________ ___________
         1 Y          DEMO_DEV
         2 Y          DEMO_DEV
         3 N
         4 N
         5 N

The inactive template raised ORA-20003 after the order row had been inserted. The count shows that SO-9001 was not saved: Oracle rolled back the whole failed call, not only the last statement. The direct update failed on CHK_ORDER_STEPS_DONE_CK, because it set IS_DONE without a user and a date.

Finally, check that a template change does not reach existing orders. The test is rolled back at the end:

Example:

update chk_template_items
set    step_label = 'Send the tracking email and SMS'
where  template_id = 1
and    step_seq = 50;

select 'template' as source, step_label
from   chk_template_items
where  template_id = 1 and step_seq = 50
union all
select o.order_no, s.step_label
from   chk_order_steps s
       join chk_orders o on o.order_id = s.order_id
where  s.step_seq = 50;

rollback;

Output:

1 row updated.

SOURCE      STEP_LABEL
___________ __________________________________
template    Send the tracking email and SMS
SO-1001     Send the tracking email
SO-1003     Send the tracking email

Rollback complete.

Step 4: Create the Order Checklist Page

In App Builder, open the application, click Create Page and select Blank Page:

Create a Page wizard with the Blank Page tile marked with a red box
Select the Blank Page type.

Enter page number 700 and the name Order Checklist. Turn Use Breadcrumb off, keep Use Navigation on and enter the icon fa-check-square-o. Click Create Page:

Create Blank Page screen with Page Number 700, Name Order Checklist, Use Breadcrumb off, Use Navigation on and Icon fa-check-square-o marked with red boxes
The page number, name and navigation entry.

Step 5: Set the Page Security and CSS

Only users who work on orders should tick steps. In Page Designer, select the page node and set Security, Authorization Scheme to Can Edit. In this example, Can Edit passes for editors and administrators:

Page 700 properties with Authorization Scheme Can Edit marked with a red box
The page requires the Can Edit scheme.

The wizard also created an Order Checklist entry in the navigation menu. In Shared Components, open Navigation Menu, edit the Order Checklist entry and set Authorization Scheme to Can Edit, then click Apply Changes:

List Entry Order Checklist with Authorization Scheme Can Edit marked with a red box
Readers do not see the menu entry.

Back on the page node, enter this CSS in CSS, Inline. It styles the status badge and puts each checkbox next to its label:

Example:

.chk-badge { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 12px; }
.chk-step { display: flex; align-items: center; gap: 8px; }
.chk-step input { width: 18px; height: 18px; }
Page 700 CSS Inline property with the badge and checkbox styles marked with a red box
The page CSS.

Step 6: Add the Orders Report with Progress

Right-click Body and select Create Region. Set Name to Orders, Type to Classic Report and enter this SQL Query:

Example:

select order_id,
       order_no,
       customer_name,
       steps_done || ' of ' || steps_total as steps,
       pct_done,
       case when pct_done = 100 then 'Complete' else 'In Progress' end as status_label,
       case when pct_done = 100 then 'u-success' else 'u-warning' end as status_css,
       'Open' as open_link
from   chk_order_progress_v
order  by order_no
Region Orders with Name, Type Classic Report and the SQL Query marked with red boxes
The orders with their progress.

The query reads the view from Step 1. STATUS_LABEL is Complete at 100% and In Progress otherwise. STATUS_CSS holds the Universal Theme class u-success or u-warning, which colors the badge green or orange.

Expand Columns, select ORDER_ID and set Type to Hidden:

Column ORDER_ID with Type Hidden marked with a red box
ORDER_ID is needed only for the link.

Select PCT_DONE. Set Type to Percent Graph and Heading to Progress. This built-in column type draws a bar for a value from 0 to 100:

Column PCT_DONE with Type Percent Graph and Heading Progress marked with red boxes
The progress bar column.

Select STATUS_LABEL and enter this HTML Expression under Column Formatting:

Example:

<span class="chk-badge #STATUS_CSS#">#STATUS_LABEL#</span>
Column STATUS_LABEL with the HTML Expression marked with a red box
The status shows as a colored badge.

Select STATUS_CSS and set Type to Hidden. A hidden column can still be used in the HTML Expression of another column:

Column STATUS_CSS with Type Hidden marked with a red box
The CSS class column is hidden.

Select OPEN_LINK. Set Type to Link, Heading to Checklist and Link Text to #OPEN_LINK#:

Column OPEN_LINK with Type Link, the Target Page 700 and Link Text #OPEN_LINK# marked with red boxes
The Open link of each order.

Click Target. Set Type to Page in this application and Page to 700. Under Set Items, set Name to P700_ORDER_ID and Value to #ORDER_ID#, then click OK:

Link Builder Target dialog with Type Page in this application, Page 700 and the item P700_ORDER_ID set to ORDER_ID marked with red boxes
The link reloads the page for the selected order.

Set the other headings to Order, Customer and Steps Done, and turn Sorting, Sortable off for the columns so the report keeps the order of the query.

Step 7: Add the Hidden Items

Right-click Body, select Create Page Item and create P700_ORDER_ID with Type Hidden. Keep Value Protected on and the default storage Per Session. The Open link sets it, and the link has a checksum, so users cannot change it in the URL. A test with a hand-edited URL ended with a session state protection violation.

Create P700_ORDER_LABEL the same way. It holds the order number and customer for the region title.

Then create P700_STEP_ID with these properties:

  • Type: Hidden
  • Value Protected: off
  • Session State, Storage: Per Request (Memory Only)
Page item P700_STEP_ID with Name, Type Hidden, Value Protected off and Storage Per Request Memory Only marked with red boxes
The hidden item that receives the ticked step.

Create P700_STEP_DONE with the same properties. Value Protected must be off for both, because JavaScript sets their values in the browser. That is safe here, because SET_STEP checks that the step belongs to the selected order.

Step 8: Add the Order Label Computation

Right-click P700_ORDER_LABEL and select Create Computation. Set Point to Before Regions, Type to SQL Query (return single value) and enter this query:

Example:

select order_no || ' - ' || customer_name
from   chk_orders
where  order_id = :P700_ORDER_ID

Under Server-side Condition, set Type to Item is NOT NULL and Item to P700_ORDER_ID:

Computation for P700_ORDER_LABEL with Point Before Regions, Type SQL Query, the query and the condition Item is NOT NULL P700_ORDER_ID marked with red boxes
The computation fills the region title.

Step 9: Add the Checklist Region

Create a second region below Orders. Set Title to Checklist: &P700_ORDER_LABEL., Type to Classic Report and enter this SQL Query:

Example:

select step_id,
       step_seq,
       step_label,
       case when is_done = 'Y' then 'checked' end as checked_attr,
       done_by,
       done_on
from   chk_order_steps
where  order_id = :P700_ORDER_ID
order  by step_seq
Region Checklist with Title, Type Classic Report and the SQL Query marked with red boxes
The steps of the selected order.

CHECKED_ATTR returns the word checked for a done step. The HTML of the checkbox uses it, so done steps show ticked when the region renders.

Under Server-side Condition, set Type to Item is NOT NULL and Item to P700_ORDER_ID. The region shows only after the user opens an order:

Checklist region Server-side Condition with Type Item is NOT NULL and Item P700_ORDER_ID marked with red boxes
The region needs a selected order.

Expand Columns and set Type to Hidden for STEP_ID, STEP_SEQ and CHECKED_ATTR. Then select STEP_LABEL, set Heading to Step and enter this HTML Expression:

Example:

<label class="chk-step"><input type="checkbox" class="js-step" data-id="#STEP_ID#" #CHECKED_ATTR#>#STEP_LABEL#</label>
Column STEP_LABEL with the HTML Expression of the checkbox marked with a red box
Each step becomes a checkbox with its label.

The class js-step lets the dynamic action find the checkboxes, and data-id carries the step ID. The label wraps the checkbox, so a click on the text also ticks it.

Select DONE_ON and set Format Mask to DD-MON-YYYY HH24:MI. Set the headings Done By and Done On, and turn Sortable off for the columns:

Column DONE_ON with Format Mask DD-MON-YYYY HH24:MI marked with a red box
The date and time format.

Step 10: Add the New Order Region

Create a third region. Set Name to New Order and Type to Static Content. Right-click it and create the page item P700_NEW_ORDER_NO with Type Text Field and Label Order Number:

Page item P700_NEW_ORDER_NO with Name, Type Text Field, Label Order Number and Region New Order marked with red boxes
The order number item.

Turn Validation, Value Required on and set Session State, Storage to Per Request (Memory Only). The form starts empty again after each new order:

Page item P700_NEW_ORDER_NO with Value Required on and Storage Per Request Memory Only marked with red boxes
The item is required and not kept in the session.

Create P700_NEW_CUSTOMER with Type Text Field, Label Customer, Value Required on and Storage Per Request. Turn Layout, Start New Row off so it sits next to the order number:

Page item P700_NEW_CUSTOMER with Start New Row off marked with a red box
The customer item on the same row.

Create P700_NEW_TEMPLATE_ID with Type Select List and Label Checklist Template. Set List of Values, Type to SQL Query and enter this query:

Example:

select template_name, template_id
from   chk_templates
where  is_active = 'Y'
order  by template_name

Keep Display Null Value on and set Null Display Value to "- Select a template -". Also set Value Required on, Storage Per Request and Start New Row off:

Page item P700_NEW_TEMPLATE_ID with Type Select List, LOV Type SQL Query, the query, Display Null Value and Null Display Value marked with red boxes
The template select list shows only active templates.

Right-click the region New Order and select Create Button. Set Button Name to CREATE_ORDER, Label to Create Order, Slot to Region Body, Start New Row off and Hot on:

Button CREATE_ORDER with Button Name, Label, Region New Order, Slot Region Body, Start New Row off and Hot marked with red boxes
The button sits after the three items.

Step 11: Add the Process and the Branch

Open the Processing tab, right-click Processing and select Create Process. Set Name to Create Order with Checklist, Type to Execute Code and enter this PL/SQL Code:

Example:

begin
    chk_pkg.create_order(
        p_order_no      => :P700_NEW_ORDER_NO,
        p_customer_name => :P700_NEW_CUSTOMER,
        p_template_id   => :P700_NEW_TEMPLATE_ID,
        p_order_id      => :P700_ORDER_ID);
end;
Process Create Order with Checklist with Name, Type Execute Code, Language PL/SQL and the PL/SQL Code marked with red boxes
The process calls CREATE_ORDER.

The procedure returns the new order ID into P700_ORDER_ID, so the page shows the checklist of the new order after the reload.

Set Success Message to "Order &P700_NEW_ORDER_NO. created with its checklist.", Error Message to #SQLERRM_TEXT#, and When Button Pressed to CREATE_ORDER:

Process with Success Message, Error Message SQLERRM_TEXT and When Button Pressed CREATE_ORDER marked with red boxes
The messages and the button condition.

#SQLERRM_TEXT# shows the text of the package error without the ORA- number. Right-click After Processing and select Create Branch. Set Name to Back to Order Checklist, Type to Page or URL (Redirect) and Target to page 700:

Branch Back to Order Checklist with Name, Type Page or URL Redirect and Target Page 700 marked with red boxes
The page reloads after a new order.

Step 12: Save Each Tick with a Dynamic Action

Open the Dynamic Actions tab, right-click Change and select Create Dynamic Action. Set Name to Save Checklist Step, Selection Type to jQuery Selector and jQuery Selector to .js-step.

Set Execution, Event Scope to Dynamic and Static Container to body. The checklist region is refreshed after each tick, and Dynamic scope keeps the event working for the new checkboxes:

Dynamic action Save Checklist Step with Name, Event Scope Dynamic, Static Container body, Event Change, Selection Type jQuery Selector and jQuery Selector .js-step marked with red boxes
The dynamic action reacts to every checkbox change.

Select the True action and set Action to Set Value, Set Type to JavaScript Expression, JavaScript Expression to this.triggeringElement.dataset.id, and Item(s) to P700_STEP_ID:

Set Value action with Set Type JavaScript Expression, the expression this.triggeringElement.dataset.id and Item P700_STEP_ID marked with red boxes
The step ID comes from the data-id attribute.

Add a second Set Value action with this JavaScript Expression and Item(s) P700_STEP_DONE:

Example:

this.triggeringElement.checked ? 'Y' : 'N'
Second Set Value action with the checked expression and Item P700_STEP_DONE marked with red boxes
Y for a ticked box, N for an unticked one.

Add a third action. Set Action to Execute Server-side Code, Language to PL/SQL, Items to Submit to P700_STEP_ID,P700_STEP_DONE, and enter this PL/SQL Code:

Example:

begin
    chk_pkg.set_step(
        p_order_id => :P700_ORDER_ID,
        p_step_id  => :P700_STEP_ID,
        p_done     => :P700_STEP_DONE,
        p_user     => :APP_USER);
end;
Execute Server-side Code action with Language PL/SQL, the PL/SQL Code and Items to Submit marked with red boxes
The Ajax call saves the step.

P700_ORDER_ID is not in Items to Submit. The server reads it from session state, where the Open link put it. Wait For Result stays on, so the next actions run after the save.

Add a Refresh action with Selection Type Region and Region Orders. It updates the progress bar:

Refresh action with Selection Type Region and Region Orders marked with red boxes
Refresh the Orders report.

Add a last Refresh action for the Checklist region. It shows the new Done By and Done On values. Save the page:

Refresh action with Selection Type Region and the Checklist region marked with red boxes
Refresh the Checklist report.

Step 13: Test Ticking Steps

Run the page as DEMO_DEV and click Open in the row of SO-1001. The checklist shows the two steps ticked in the SQL test. Tick Hand over to the courier:

Order Checklist page with SO-1001 at 2 of 5 steps and the step Hand over to the courier marked with a red box
The checklist of SO-1001 before the tick.

The page did not reload. The step shows DEMO_DEV and the time, and the Orders report changed to 4 of 5 with a longer bar:

Order Checklist page with SO-1001 at 4 of 5 steps and the ticked step Hand over to the courier marked with red boxes
The tick was saved through Ajax.

In this test, Pack and label the parcel had also been ticked earlier. Unticking it changed the row to 3 of 5 and cleared Done By and Done On; ticking it again brought back 4 of 5.

The times are the clock of the database server, which runs on UTC in this example. Use TIMESTAMP WITH LOCAL TIME ZONE if users should see their own time zone.

Step 14: Test a New Order and 100% Progress

Enter the order number SO-1003, the customer Green Valley Market and the template Standard Order, then click Create Order:

New Order region with Order Number SO-1003, Customer Green Valley Market and Checklist Template Standard Order marked with a red box
A new order from the Standard Order template.

The page shows the success message, SO-1003 at 0 of 5, and its checklist with the five copied steps:

Order Checklist page with the success message, the new row SO-1003 at 0 of 5 and the checklist title of SO-1003 marked with red boxes
The new order got its own checklist.

Open SO-1002 and tick its three steps. The bar fills up and the badge changes to Complete. SO-1001 and SO-1003 are not affected:

Order Checklist page with SO-1002 at 3 of 3 steps, a full bar and the Complete badge marked with a red box
All steps done: 100% and the Complete badge.

As ORDER_EDITOR, who has the Editor role, two steps of SO-1003 were ticked. The final state in SQL:

Example:

select order_no, steps_done, steps_total, pct_done
from   chk_order_progress_v
order  by order_no;

select o.order_no, s.step_label, s.is_done, s.done_by
from   chk_order_steps s
       join chk_orders o on o.order_id = s.order_id
where  o.order_no = 'SO-1003'
order  by s.step_seq;

Output:

ORDER_NO       STEPS_DONE    STEPS_TOTAL    PCT_DONE
___________ _____________ ______________ ___________
SO-1001                 4              5          80
SO-1002                 3              3         100
SO-1003                 2              5          40

ORDER_NO    STEP_LABEL                       IS_DONE    DONE_BY
___________ ________________________________ __________ _______________
SO-1003     Confirm payment received         Y          ORDER_EDITOR
SO-1003     Pick items from the warehouse    Y          ORDER_EDITOR
SO-1003     Pack and label the parcel        N
SO-1003     Hand over to the courier         N
SO-1003     Send the tracking email          N

Step 15: Test the Error Cases

Create an order with the existing number SO-1001. The process shows the package message, and no order is created:

Order Checklist page with the error Order number SO-1001 already exists
A duplicate order number is refused.

Clicking Create Order with empty fields showed three Value Required errors: Order Number, Customer and Checklist Template must have some value.

Next, a forged request. With SO-1003 open, the browser console changed the data-id of the first checkbox to 5, a step of SO-1001, and the box was ticked. The Ajax call returned the package error:

Order Checklist page with an error dialog ORA-20001 This step does not belong to the selected order
A step of another order is refused.

The checkbox stayed ticked in the browser, because the dynamic action stops at the error and does not refresh the region. Nothing was saved:

Example:

select o.order_no, s.step_id, s.step_label, s.is_done, s.done_by
from   chk_order_steps s
       join chk_orders o on o.order_id = s.order_id
where  s.step_id = 5
or     o.order_no = 'SO-1003'
order  by o.order_no, s.step_seq;

Output:

ORDER_NO       STEP_ID STEP_LABEL                       IS_DONE    DONE_BY
___________ __________ ________________________________ __________ __________
SO-1001              5 Send the tracking email          N
SO-1003             31 Confirm payment received         N
SO-1003             32 Pick items from the warehouse    N
SO-1003             33 Pack and label the parcel        N
SO-1003             34 Hand over to the courier         N
SO-1003             35 Send the tracking email          N

6 rows selected.

Last, SALES_USER, who has only the Reader role, opened the page URL. APEX blocked the page, and the Order Checklist menu entry was not shown to this user:

Access denied page with the message You are not allowed to change this data
Readers cannot open the checklist page.

Things to Know

  • Copy the template steps to each record. If records link to template rows instead, a template change rewrites the history of finished records.
  • Keep the progress in a view. A stored percentage column would need a trigger or extra code to stay correct after each tick.
  • Values set by JavaScript need Value Protected off. Check them on the server, as SET_STEP does with the order ID from session state.
  • A region refresh replaces the checkboxes with new elements. Event Scope Dynamic with the body as container keeps the event working for them.
  • The Percent Graph column type expects a number from 0 to 100.

Related Guides

Conclusion

A checklist with progress in Oracle APEX needs template tables, a steps table per record, a view for the progress and a small package. Each new record gets its own copy of the template steps, and constraints keep the done flag, user and date consistent.

On the page, checkboxes in a classic report save each tick through a dynamic action with Ajax, and a Percent Graph column with a Complete badge shows the progress. Duplicates, forged step IDs and readers were tested and refused.

Vinish Kapoor
Vinish Kapoor

An Oracle ACE, author of four books on Oracle APEX, SQL and PL/SQL, and Oracle Forms, and a software developer building Oracle database applications since 2001.

guest

0 Comments
Oldest
Newest Most Voted