0% found this document useful (0 votes)
3 views21 pages

Process Flow

The document outlines the detailed process flow for the Willow Cafe Inventory Management System, including system architecture, authentication, dashboard management, point-of-sale operations, and reporting. It describes the roles of various modules and the step-by-step workflows for user login, registration, product management, and report generation. The system utilizes PHP, MySQL, and various JavaScript libraries for a responsive user experience and real-time data handling.

Uploaded by

kerkjanagcopra
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views21 pages

Process Flow

The document outlines the detailed process flow for the Willow Cafe Inventory Management System, including system architecture, authentication, dashboard management, point-of-sale operations, and reporting. It describes the roles of various modules and the step-by-step workflows for user login, registration, product management, and report generation. The system utilizes PHP, MySQL, and various JavaScript libraries for a responsive user experience and real-time data handling.

Uploaded by

kerkjanagcopra
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd

SYSTEM PROCESS FLOW

Willow Cafe Inventory Management System


Detailed End-to-End Process Documentation

Author Kerk Jan S. Agcopra

System Willow Cafe Inventory Management System

Stack PHP · Bootstrap 3 · jQuery 1.12.4 · AlertifyJS · [Link] · MySQL

Modules Login/Register · Dashboard · POS · Reports

Date March 2026

Table of Contents
1. System Architecture Overview
2. Authentication Flow ([Link])
2.1 Login Process
2.2 User Registration Process
3. Dashboard / Inventory Management Flow ([Link])
3.1 Page Load & Initial Data Fetch
3.2 Live Search & Pagination
3.3 Add New Product
3.4 Edit Product
3.5 Delete Product
3.6 Restock (Add Stock)
3.7 StockOut (Remove Stock)
3.8 Excel Bulk Import
3.9 Auto-Refresh
4. Point-of-Sale Flow ([Link])
4.1 POS Page Load
4.2 Product Search & Category Filtering
4.3 Cart Management
4.4 Discount Application
4.5 Payment & Change Calculation
4.6 Process Sale
4.7 Receipt Generation & Printing
5. Reports & Analytics Flow ([Link])
5.1 Report Generation
5.2 Stock Movements Report
5.3 Sales Report
5.4 Best Sellers Report
5.5 Charts
5.6 User Activity Modal
5.7 Print Report
5.8 Email Report
6. Cross-Cutting Concerns
6.1 Session Management & Security
6.2 Client-Side Validation States
6.3 AJAX Error Handling Pattern
6.4 Navigation
1. System Architecture Overview
The system is a server-rendered PHP application that communicates with the browser through a combination of
full-page loads (for navigation and report generation) and asynchronous AJAX calls (for real-time table updates,
form validation, and POS operations). The following table summarises the four primary modules and their roles.

Module File Responsibilities Key AJAX Endpoints


ajax/[Link]
Login, session creation, user
Authentication [Link] ajax/[Link]
registration
ajax/[Link]

ajax/get-products-
[Link] ajax/add-
[Link] ajax/update-
[Link] ajax/delete-
Product CRUD, stock movement
[Link] ajax/check-
Dashboard [Link] (restock/stockout), pagination,
[Link] ajax/get-
search
[Link] ajax/get-
[Link] ajax/get-
[Link]
ajax/[Link]

ajax/[Link]
Sales transactions, cart
ajax/[Link]
Point of Sale [Link] management, discount, receipt
ajax/[Link]
generation
ajax/[Link]

ajax/[Link]
Stock/sales/best-seller reports,
Reports [Link] [Link] (form
charts, user activity, print, email
POST)

All pages share a common design system: DM Sans + Playfair Display fonts, green CSS variable tokens, the
same sticky header with delegated nav-link click handling, AlertifyJS for all user notifications, and Bootstrap 3
modals styled with a green gradient header and a circular rotating close button.
2. Authentication Flow ([Link])

2.1 Login Process


The login page is the system entry point. Before rendering, PHP checks whether a valid session already exists. If
it does, the user is immediately redirected to [Link] without seeing the login form.

LOGIN — Step-by-Step Flow

# Actor Action Output / Decision


1 Browser User navigates to [Link] PHP checks is_logged_in()

Session exists → redirect to [Link]


2 PHP User never sees login form
immediately

No session → render login form. Check ? If timeout param present, display


3 PHP
timeout GET param session-expiry warning banner

JavaScript intercepts the form


4 User Enter username and password, click Log In
submit ([Link])

Empty username → [Link].


Username < 3 chars →
[Link]. Empty password →
5 JavaScript Client-side validation runs before any AJAX call
[Link]. Password < 6 chars
→ [Link]. Any fail → stop
here

All client validations pass → button disabled, [Link]() clears


6 JavaScript
spinner shown previous toasts

POST to ajax/[Link] with { username,


7 AJAX Async call, button stays disabled
password }

Query users table: SELECT * WHERE


8 PHP (AJAX) PDO prepared statement
username = ?

If user found: password_verify(input,


9 PHP (AJAX) Decision point
stored_hash)

Passwords match → set $_SESSION:


10 PHP (AJAX) logged_in, user_id, username, fullname. Return Session established
{ success: true }

Passwords do NOT match OR user not found →


11 PHP (AJAX) Error response
return { success: false, message }

success: true → [Link] =


12 JavaScript Full redirect to dashboard
"[Link]"

success: false → Toast shown, form stays on


13 JavaScript
[Link]([Link]) screen

complete: handler always restores button text Prevents permanently disabled


14 JavaScript
and re-enables it button on error

2.2 User Registration Process


Registration is handled inside a Bootstrap 3 modal overlay on the same page. The modal design matches the
dashboard/POS/report modal style. All validation runs in real time as the user types.
REGISTRATION — Step-by-Step Flow

# Actor Action Output / Decision


Modal opens. Form resets. All
1 User Click "Create New Account" button field-valid/invalid/checking
classes cleared.

validateRegUsername() runs
2 User Type in Username field (keyup event fires)
synchronously on every keystroke

field-invalid (red border) if any fail,


Username validation checks: not empty → min 3
3 JavaScript field-valid (green border) if all
chars → ^[a-zA-Z0-9_]+$ regex
pass

If format validates:
Click away from Username field (blur event
4 User checkUsernameAvailability()
fires)
AJAX call triggered

Field switches to field-checking state (amber Visual feedback while AJAX is in


5 JavaScript
border + spinner text) flight

POST to ajax/[Link] with


6 AJAX Async call
{ username }

Query users table: SELECT COUNT(*) WHERE


7 PHP (AJAX) Returns { exists: true/false }
username = ?

exists: true → field-invalid, "Username is already


8 JavaScript User must choose different name
taken"

9 JavaScript exists: false → field-valid, error cleared Username available

AJAX error: → field-valid, [Link] (non- complete: handler restores state


10 JavaScript
blocking) even on network failure

validateRegFullname(): required
→ min 2 chars → no digits →
11 User Type in Full Name field (keyup/blur) letters/spaces/dots/hyphens/apos
trophes only → at least 2 space-
separated parts each ≥ 2 chars

validateRegPassword(): required
12 User Type in Password field (keyup/blur)
→ min 6 chars

validateRegConfirmPassword():
13 User Type in Confirm Password field (keyup/blur) required → must match Password
field exactly

Password field changes also re-validate Confirm Keeps both fields in sync as user
14 JavaScript
if it already has a value edits

All four validators run


15 User Click Register button
synchronously first

Any validator returns false → [Link],


16 JavaScript User fixes highlighted fields
submit blocked

Username field has field-invalid class → Prevents submitting a taken


17 JavaScript
[Link], submit blocked username

POST to ajax/[Link] with


18 JavaScript All pass → button disabled, spinner shown
serialised form data

19 PHP (AJAX) Server-side re-validation of all fields Returns { success, message,


errors }

success: true → [Link], modal hidden,


20 JavaScript User can now log in
form reset, classes cleared

success: false with errors object →


21 JavaScript Server errors shown inline
showFieldError per field + [Link]

complete: handler restores Register button Button never permanently


22 JavaScript
regardless of outcome disabled
3. Dashboard / Inventory Management Flow ([Link])

3.1 Page Load & Initial Data Fetch


require_login() runs first. If no valid session exists the user is redirected back to [Link]. Flash messages
(success/error/errors) are read from the session then immediately unset so they only show once.

# Actor Action Output / Decision


require_login() — check session, redirect if not Unauthenticated users sent to
1 PHP
authenticated [Link]

Read and unset flash messages from


2 PHP $success, $error, $errors
$_SESSION

Page renders. Preloader overlay shown (full Only shown if no flash messages
3 Browser
screen green gradient) present and not a page reload

Preloader simulates loading progress (random Messages cycle: "Brewing your


4 JavaScript
5–15% increments, 200ms interval) inventory experience…" etc.

Progress reaches 100% → preloader fades out


5 JavaScript Main content animates in
(0.5s), then removed from DOM

loadProducts(1, 10, "") called immediately inside Initial AJAX call to populate the
6 JavaScript
[Link] table

GET ajax/[Link]? Returns { success, products[],


7 AJAX
page=1&limit=10&search= pagination{} }

displayProducts() renders table rows. Table visible, pagination controls


8 JavaScript
updatePaginationInfo() updates page counters updated

Auto-refresh timer set: loadProducts() every 30 Timer skipped if any modal is


9 JavaScript
seconds currently open (.[Link] check)

3.2 Live Search & Pagination


# Actor Action Output / Decision
1 User Type in the search box (#liveSearch) Input event fires

Previous debounce timer cleared. New 300ms Prevents a request on every


2 JavaScript
timer started keystroke

After 300ms idle: currentSearch updated, AJAX call loadProducts(1,


3 JavaScript
currentPage reset to 1 pageLimit, searchTerm)

SQL LIKE query searches prod_ID, prod_name, Returns matching products for
4 PHP
supplier columns page 1

currentPage
5 User Click Next / Previous pagination button incremented/decremented,
loadProducts() called

pageLimit updated, currentPage


6 User Change the per-page dropdown (#page-limit)
reset to 1, loadProducts() called

complete: handler always re-enables pagination Controls never permanently


7 JavaScript
controls and hides spinner disabled
3.3 Add New Product
All fields are validated in real time. The product name is also checked for uniqueness against the database via
AJAX before the save call is made.

# Actor Action Output / Decision


Modal opens. Form reset. Error
1 User Click "Add New Product" button feedback divs cleared. Field
classes cleared.

validateRequired(): not empty,


2 User Type in Product Name (keyup fires)
min 2 chars → field-valid/invalid

If format valid:
3 User Click away from Product Name (blur fires) checkProductNameUnique(name,
null, ...) AJAX triggered

Field turns field-checking (amber). "Checking ajax/[Link]?


4 JavaScript
name…" shown name=...&exclude_id=

SELECT COUNT(*) FROM inventoryproducts


5 PHP (AJAX) Returns { exists: true/false }
WHERE prod_name = ?

exists: true → field-invalid, "A product with this


6 JavaScript User must change name
name already exists"

validateSelect(): must not be


7 User Select Category (change/blur fires)
empty → field-valid/invalid

validateNumber(min=0, integer):
8 User Enter Quantity (keyup/blur fires)
must be ≥ 0

validateSelect(): must not be


9 User Select Unit Measure (change/blur fires)
empty

validateNumber(min=0.01,
10 User Enter Price Per Unit (keyup/blur fires)
decimal): must be > 0

validateNumber(min=0, integer):
11 User Enter Min Stock Level (keyup/blur fires)
must be ≥ 0

validateRequired(): not empty,


12 User Enter Supplier (keyup/blur fires)
min 2 chars

All 7 validators run synchronously


13 User Click "Add Product" button
in sequence

14 JavaScript Any fails → [Link], blocked User corrects highlighted fields

All pass → button disabled, spinner →


Callback pattern — proceeds only
15 JavaScript checkProductNameUnique() called one final
if unique
time

16 JavaScript Not unique → [Link], button restored Blocked

Unique → POST to ajax/[Link] with


17 JavaScript Button shows "Saving…"
serialised form data

Server-side validation, INSERT INTO Returns { success, message } or {


18 PHP (AJAX)
inventoryproducts success: false, errors }

success: true → [Link], modal hidden,


19 JavaScript New product visible
form reset, loadProducts() reloads table

success: false → per-field errors shown,


20 JavaScript Form stays open for correction
[Link]
21 JavaScript complete: restores button regardless of outcome Never permanently disabled

3.4 Edit Product


The Edit modal is pre-filled by an AJAX call. A snapshot of the original values is stored; saving is blocked if
nothing was changed. Quantity is read-only — it must be changed through Restock or StockOut.

# Actor Action Output / Decision


delegated .on("click", ".edit-
1 User Click the Edit (pencil) button on a product row
product-btn") fires

Modal opens. Form hidden. Loading spinner


2 JavaScript Clean state
shown. originalEditValues = {} reset

Fetches latest product data from


3 AJAX GET ajax/[Link]?id={prod_ID}
DB

Form fields populated: name, category, quantity complete: handler hides spinner,
4 JavaScript
(readonly), unit, price, min stock, supplier shows form

originalEditValues snapshot taken: {prod_name,


category, unit_measure, price_per_unit,
5 JavaScript Change detection baseline set
minimum_stock_level, supplier}. Quantity
excluded (readonly).

Modify any field (keyup/blur validation same as field-valid/invalid borders update


6 User
Add form) in real time

Product name blur →


exclude_id = prodID so own
7 User checkProductNameUnique(name, prodID, ...)
name does not flag as duplicate
called

All validators run synchronously


8 User Click "Update Product"
(quantity skipped)

Change detection: compare currentValues vs Shallow string comparison of all 6


9 JavaScript
originalEditValues editable fields

No changes detected → [Link]("No


10 JavaScript Prevents empty update calls
changes detected"), submit blocked

Name changed → run


11 JavaScript checkProductNameUnique() first, then Async chain via callback
proceedWithSave()

Name unchanged → proceedWithSave() directly Efficiency: uniqueness check only


12 JavaScript
(skips unnecessary AJAX) if name changed

POST ajax/[Link] with serialised Server validates, UPDATE


13 AJAX
form data inventoryproducts

success → [Link], modal hidden,


14 JavaScript Updated values visible
loadProducts() refreshes table

15 JavaScript complete: always restores button Never stuck in loading state

3.5 Delete Product


# Actor Action Output / Decision
delegated .on("click", ".delete-
1 User Click the Delete (trash) button on a product row
product") fires, [Link]()
[Link]() dialog shown: "Are you sure
2 JavaScript User must explicitly confirm
you want to delete {name}?"

Clicks Cancel → [Link]("Deletion


3 User No action taken
cancelled")

Clicks OK → delete button disabled, row opacity


4 User Visual feedback before AJAX
set to 0.6, spinner appended to row

Row reference captured BEFORE


5 AJAX POST ajax/[Link] with { prod_id }
confirm dialog opens (closure)

DELETE FROM inventoryproducts WHERE


6 PHP (AJAX) Returns { success, message }
prod_ID = ?

success: true → $[Link](500) → Smooth 500ms fade before DOM


7 JavaScript
[Link]() removal

totalRecords decremented. If no rows remain on Handles last-item-on-page edge


8 JavaScript
page → loadProducts() reloads case

success: false → row restored to full opacity,


9 JavaScript No silent failures
delete button re-enabled, [Link]

3.6 Restock (Add Stock)


Opens a two-column modal: left sidebar shows the product info card and quick stats; right main panel has the Add
Stock form and a tabbed activity history.

# Actor Action Output / Decision


delegated .on("click", ".btn-
Click the Restock (stack) button on a product
1 User restock") fires. Product data read
row
from data- attributes.

Build product info card HTML (name, current Injected into #restock-product-
2 JavaScript
stock, price, supplier, min stock, status) summary sidebar

restock-prod-id hidden field set. Quantity and


3 JavaScript Modal state fresh
Note fields cleared.

loadRestockHistory(prodId) → GET ajax/get-


4 AJAX (×2) Populates "Restock Only" tab
[Link]?prod_id=

loadAllActivities(prodId, "restock") → GET


5 AJAX Populates "All Activities" tab
ajax/[Link]?prod_id=

Both history tables rendered with date, type


6 JavaScript badge, user avatar, qty changed, notes. Tabs switch without new AJAX
complete: handlers on both calls.

Enter quantity to add (must be > 0). Optionally


7 User Form submit validation runs
add a note.

Click "Save Restock" button (type=submit,


8 User restockForm submit event fires
form=restockForm)

[Link](). Validate: qty must be integer [Link] and return false if


9 JavaScript
>0 invalid

Button disabled, spinner shown. Loading overlay


10 JavaScript Visual feedback
appended to sidebar.

POST [Link] with serialised form


11 AJAX Expects JSON response
(prod_id, quantity_added, note)
Record previous_quantity. INSERT INTO
Returns { success, message,
12 PHP restock_history. UPDATE inventoryproducts
new_quantity }
SET quantity = quantity + quantity_added

success: true → [Link], modal hidden,


13 JavaScript Updated stock visible immediately
loadProducts() refreshes table

complete: restores button and removes loading


14 JavaScript Never stuck in loading
overlay regardless of outcome

3.7 StockOut (Remove Stock)


Identical modal pattern to Restock but uses an orange colour theme and validates that quantity removed does not
exceed current stock.

# Actor Action Output / Decision


delegated .on("click", ".btn-
Click the StockOut (open box) button on a
1 User unstock") fires. Orange product
product row
card built.

Orange product info card injected into #unstock-


2 JavaScript Modal state fresh
product-summary. unstock-prod-id set.

loadUnstockHistory and loadAllActivities called


3 AJAX (×2) Populates both history tabs
with prodId

4 User Enter quantity to remove. Optionally add a note. Form submit fires

[Link](). Read current qty from


5 JavaScript Parsed from sidebar display value
#modal-unstock-current-qty text

6 JavaScript Validate: qty must be integer > 0 [Link] if invalid

[Link] "Cannot remove N


7 JavaScript Validate: qty must not exceed current stock
units. Only M available."

POST [Link] with { prod_id,


8 AJAX Expects JSON response
quantity_removed, note }

Record previous_quantity. INSERT INTO


unstock_history. UPDATE inventoryproducts Returns { success, message,
9 PHP
SET quantity = quantity − quantity_removed. new_quantity }
Reject if result < 0.

success: true → [Link], modal hidden,


10 JavaScript Updated stock visible
loadProducts() refreshes table

complete: restores button and removes loading


11 JavaScript Always runs
overlay

3.8 Excel Bulk Import


# Actor Action Output / Decision
Click "Choose Excel File" label (hidden file input
1 User File picker opens
trick)

change event: file name shown,


2 User Select a .xlsx / .xls / .csv file
Import button enabled

Form submits (multipart/form-data


3 User Click Import button
POST) to [Link]
4 PHP Validate file extension and MIME type Reject unsupported formats

Parse rows. For each row: validate required


5 PHP Skip/flag duplicates
fields, check for duplicate prod_name

Batch INSERT INTO inventoryproducts for valid Redirect back with $_SESSION
6 PHP
rows success/error flash message

Page reloads. Flash message displayed. Table Import count shown in success
7 Browser
reloads with new products message

3.9 Auto-Refresh
A setInterval timer fires every 30,000ms (30 seconds). Before calling loadProducts() it checks whether any
Bootstrap modal is currently open using $(".[Link]").length. If a modal is open, the refresh is skipped entirely to
prevent table data from changing under a modal form the user is actively filling in. The timer is cleared on window
beforeunload.
4. Point-of-Sale Flow ([Link])

4.1 POS Page Load


# Actor Action Output / Decision
1 PHP require_login() — redirect if no session Session must exist

Populates today's transaction


get_todays_sales_summary($pdo) called
2 PHP count and revenue in the header
server-side
stat badges

[Link]: loadCategories() +
Discount "No Disc." button set to
3 JavaScript loadProducts() + setupEventListeners() called in
active by default
sequence

Returns distinct categories from


4 AJAX GET ajax/[Link]
inventoryproducts

Category tabs rendered dynamically. "All Items" complete: handler (non-critical, no


5 JavaScript
always first. spinner)

Returns ALL in-stock and low-


6 AJAX GET ajax/[Link] stock products (full list, no
pagination)

All subsequent filtering is client-


products[] array populated in memory.
7 JavaScript side — no further product AJAX
filterAndDisplayProducts() called.
needed

4.2 Product Search & Category Filtering


# Actor Action Output / Decision
delegated .on("click", ".category-
1 User Click a category tab tab") sets currentCategory, calls
filterAndDisplayProducts()

Input event with 300ms debounce


2 User Type in the search box (#productSearch)
timer

After 300ms: currentSearch updated, No AJAX — pure in-memory filter


3 JavaScript
filterAndDisplayProducts() called on products[]

Filter by category (if not "all") then filter by displayProducts(filtered) renders


4 JavaScript
search term (prod_name OR supplier contains) matching product cards

Each card shows: name, price, stock badge (In Out of Stock cards have reduced
5 JavaScript
Stock / Low Stock / Out of Stock) opacity and cursor:not-allowed

Out of Stock cards do NOT get a click handler data-clickable="false" attribute


6 JavaScript
— cannot be added to cart controls this

4.3 Cart Management


# Actor Action Output / Decision
.on("click", ".product-card[data-
clickable=true]") fires.
1 User Click an In Stock or Low Stock product card
addToCart({ prod_id, name, price,
stock }) called.
2 JavaScript Check if product already in cart[] by prod_id Decision: existing vs new item

Exceeds stock →
Existing: check [Link] + 1 ≤
3 JavaScript [Link]("Not enough stock
max_stock
available!")

Existing and within stock: [Link]++,


4 JavaScript Cart updated
[Link] = qty × price

New item: push { prod_id, name, price,


5 JavaScript Cart grows
quantity:1, subtotal:price, max_stock } to cart[]

If discount is active: calculateDiscount() called. Discount recalculates on every


6 JavaScript
Otherwise: renderCart() + updateTotals() add

7 JavaScript [Link]("Added to cart") Toast confirmation

delegated .on("click", ".qty-btn")


8 User Click − button on a cart item reads data-prod-id and data-
change="-1"

If new qty < 1: item spliced from


updateCartItemQuantity(prodId, -1). New qty =
9 JavaScript cart[], [Link]("Item
current − 1.
removed")

data-change="+1". If new qty >


10 User Click + button on a cart item
max_stock → [Link]

delegated .on("click", ".cart-item-


11 User Click × remove button on a cart item remove") reads data-prod-id.
removeFromCart(prodId)

cart filtered to exclude prodId. If cart empty:


12 JavaScript resetDiscount(). Else if discount active: [Link]("Item removed")
recalculate.

13 User Click "Clear" button [Link] dialog

Confirms clear → cart = [], resetDiscount(),


14 User [Link]("Cart cleared")
renderCart(), amountTendered cleared

renderCart(): builds HTML string with data


Empty cart shows empty-cart
15 JavaScript attributes (no inline onclick). Injects into
placeholder
#cartItems

updateTotals(): sums all [Link] values →


16 JavaScript Always reflects current cart state
updates #cartSubtotal and #cartTotal

4.4 Discount Application


# Actor Action Output / Decision
Click a discount button: No Disc. | Senior 20% | .on("click", ".discount-btn") reads
1 User
PWD 20% | Employee 10% data-type and data-percent

If cart is empty → [Link]("Add items to Cannot apply discount to empty


2 JavaScript
cart first"), stop cart

[Link] and .percentage updated. Active states: grey (none), amber


3 JavaScript All discount buttons lose "active" class. Clicked (senior/PWD), blue (employee),
button gains "active" class green (default)

Re-runs on every cart change


4 JavaScript calculateDiscount() called
while a discount is active

5 JavaScript If type is "none" or percentage is 0: reset all Clean state for no-discount path
[Link] = qty × price. Hide discount
summary and details panels.

If discount active: subtotal = sum of all (qty ×


6 JavaScript price). discountAmount = subtotal × pct/100. Calculation
newTotal = subtotal − discountAmount.

discountFactor = newTotal / subtotal. Each Proportional distribution across


7 JavaScript
[Link] = qty × price × discountFactor items

Discount summary box shown: Original


8 JavaScript Customer name / ID fields shown
Subtotal, Discount Amount, New Total

Enter Customer Name (optional). Enter ID [Link]


9 User
Number (optional). and idNumber updated on input

4.5 Payment & Change Calculation


# Actor Action Output / Decision
1 User Enter cash amount in #amountTendered input input event fires on every change

Amount added to current


Click a quick-cash button (₱20 / ₱50 / ₱100 /
2 User tendered value. input event
₱200 / ₱500 / ₱1000)
triggered.

If tendered ≥ total AND total > 0:


calculateChange(): parse #cartTotal (strip ₱),
3 JavaScript changeAmount shown in green
parse tendered
box

No change shown until sufficient


4 JavaScript If tendered < total: change display hidden
payment

validatePayment(): cart not empty AND


Button disabled by default until
5 JavaScript tendered ≥ total AND total > 0 → Process Sale
conditions met
button enabled. Otherwise disabled.

4.6 Process Sale


# Actor Action Output / Decision
Click "Process Sale" button (only enabled when
1 User #processSaleBtn click event fires
payment sufficient)

Guard checks: cart not empty, tendered ≥ total.


2 JavaScript Prevents double-submission
Button disabled, spinner shown.

saleData object built: { cart[], tendered, change,


discount_type, discount_percentage,
3 JavaScript Full transaction snapshot
discount_amount, subtotal, customer_name,
id_number }

POST ajax/[Link] with


4 AJAX [Link](saleData), contentType: Server processes entire payload
application/json

Validate payload. Generate unique Receipt # format defined server-


5 PHP (AJAX)
receipt_number. side

BEGIN TRANSACTION: INSERT INTO sales.


For each cart item: INSERT INTO sale_items,
6 PHP (AJAX) Atomic — all or nothing
UPDATE inventoryproducts SET quantity =
quantity − [Link]

7 PHP (AJAX) COMMIT. Return { success: true, receipt_no, Full data for receipt rendering
cashier, cart[], total, tendered, change,
discount{} }

Any error → ROLLBACK. Return { success:


8 PHP (AJAX) Stock not deducted on failure
false, message }

success: true → showReceipt(response), clear


9 JavaScript cart, resetDiscount, clearAmountTendered, Multiple post-sale actions
loadProducts(), updateHeaderStats()

Sale not recorded, stock


10 JavaScript success: false → [Link](message)
unchanged

complete: if button still shows spinner → restore


11 JavaScript button (failure path). Success path button is Handles both paths
never restored — cart is cleared instead.

4.7 Receipt Generation & Printing


# Actor Action Output / Decision
showReceipt(data): build receipt HTML string
Injected into #receiptContent,
1 JavaScript (Courier New monospace, thermal-receipt
modal shown
layout)

Receipt includes: cafe name/address, receipt #,


date/time, cashier name, customer info (if
2 JavaScript All from server response data
named), itemised list with qty × price, discount
box (if applied), total/cash/change

#printReceiptBtn .on("click") →
3 User Click "Print Receipt" button
[Link]()

@media print hides all body content Only receipt prints, not the POS
4 Browser
except .receipt-modal UI

#newSaleBtn hides modal and


5 User Click "New Sale"
calls clearCart()
5. Reports & Analytics Flow ([Link])

5.1 Report Generation


Unlike the dashboard and POS which rely heavily on AJAX, [Link] uses full-page GET requests for report
data. SQL queries run server-side on page load. AJAX is only used for the User Activity modal.

# Actor Action Output / Decision


require_login(). Read GET params: report_type, Defaults: report_type=stock, last
1 PHP
start_date, end_date, movement_type 30 days, all movements

Validate date range: if start > end →


2 PHP Error displayed on next render
$_SESSION error set, no query runs

Results stored in
Based on report_type: run appropriate SQL
3 PHP $all_movements, $sales_data,
queries (up to 6 queries per page load)
$best_sellers, $sales_summary

Always run: category totals query + 7-day


4 PHP 7 queries for chart data
sales/discount chart data (loop, 1 query per day)

Compute $report_type_label and PHP-computed strings used


5 PHP
$movement_type_label for the print header in .print-header div

delegated .on("click", ".report-


6 User Click a report tab (Stock / Sales / Best Sellers) tab") updates hidden
reportTypeInput, submits form

Full page GET reload with new report_type PHP re-runs all queries for new
7 Browser
param type

Change date range or movement type, click GET form submission. PHP re-
8 User
"Generate Report" queries with new params.

5.2 Stock Movements Report


Displayed when report_type = "stock". Queries restock_history and unstock_history tables joined with
inventoryproducts. The movement_type filter (all / restock / unstock) determines which tables are queried — all
uses UNION ALL.
• Columns shown: Type badge (RESTOCK/STOCKOUT), Product Name, Category, Unit, Previous Stock, New
Stock, Qty Changed, Date & Time, Modified By (avatar + name), Notes
• The "Items Summary" eye icon on Sales rows uses data-items attribute — click triggers [Link]() (no inline
onclick).

5.3 Sales Report


Displayed when report_type = "sales". Queries the sales table joined with users, sale_items, and
inventoryproducts using GROUP_CONCAT for the items summary.
• Summary cards shown: Total Transactions, Total Revenue, Total Discounts (with discounted transaction
count), Items Sold (across all products)
• Table columns: Receipt #, Date & Time, Cashier (avatar + name), Customer, Items count + eye icon, Discount
(badge + amount), Total, Cash, Change
5.4 Best Sellers Report
Displayed when report_type = "products". Queries sale_items joined with inventoryproducts and sales, grouped
by product, ordered by total_quantity_sold DESC, limited to top 20.
• Columns: Rank (medal emoji for top 3, # for rest), Product, Category, Unit, Times Sold, Qty Sold, Avg Price,
Total Revenue

5.5 Charts
Two [Link] charts are initialised inline using PHP-encoded JSON data — no AJAX needed for chart data.
# Actor Action Output / Decision
Category totals: loop through $products array, $summary_labels[] and
1 PHP
sum quantity per category $summary_data[] built in PHP

7-day sales trend: loop i from 6 to 0, one SQL $sales_chart_labels[],


2 PHP query per day for SUM(total_amount) and $sales_chart_data[],
SUM(discount_amount) $discount_chart_data[]

Stock Levels chart: horizontal bar chart using


Rendered on canvas
3 JavaScript $summary_labels and $summary_data (green
#stockLevelsChart
palette)

Sales Trend chart: line chart with two datasets Rendered on canvas
4 JavaScript — Sales Revenue (green fill) and Discounts #salesTrendChart. Only shown
(orange dashed) for sales/products report types.

5.6 User Activity Modal


# Actor Action Output / Decision
Modal opens.
1 User Click "View User Activity" button
loadUserActivity(30) called.

Loading spinner shown, Refresh button


2 JavaScript Clean state
disabled, previous userChart destroyed if exists

Returns { success, users[],


3 AJAX GET ajax/[Link]?days=30
summary{} }

Query: join sales + restock_history +


unstock_history grouped by user. Count sales, Complex multi-source
4 PHP (AJAX)
restocks, stockouts, items_sold, total_revenue, aggregation
last_activity per user

success: false → [Link] + error state in


5 JavaScript complete: restores Refresh button
modal

Empty results → [Link] + "No user


6 JavaScript complete: restores Refresh button
activity" message

Has results: displayUserActivity(response) Summary pills, top performer


7 JavaScript
called card, user table rendered

updateUserChart(users): top 5 users by total


Chart rendered on canvas
8 JavaScript actions → grouped bar chart
#userActivityChart
(Sales/Restocks/Stockouts)

Change time period dropdown (7 / 30 / 90 / 365 loadUserActivity(days) called with


9 User
days) new value

10 User Click Refresh button loadUserActivity() called with


current dropdown value.
complete: always restores button.

exportUserActivityToCSV(): reads
11 User Click Export to CSV rendered table rows → builds
CSV string → Blob → download

5.7 Print Report


# Actor Action Output / Decision
#printReportBtn .on("click") fires.
1 User Click "Print Current Report" button
No inline onclick.

Stamp current timestamp into #printTimestamp Date/time accurate to the moment


2 JavaScript
inside .print-header div of printing

[Link]() called → @media print CSS


3 Browser All UI hidden
activates

Elements hidden: .header, .page-title, .report-


tabs, .report-actions, .filter-form, .summary-
cards, .charts-section, .active-users-
4 @media print Only data remains
card, .email-form-card, all buttons, all
modals, .table-header bar, avatar circles, eye
icons

Elements shown: .print-header (cafe name,


report type, date range, filter, record count,
5 @media print Print-only header always first
printed-by, timestamp). Report section h3. Data
tables.

Tables: full border grid, alternating row shading


6 @media print (#f7f7f7 even rows), 8.5pt Arial, repeated thead Paper-friendly format
on every page, flat outlined badges

@page: A4 landscape, 15mm/12mm margins.


7 @media print Page footer via @page @bottom-center with Professional layout
page numbers.

Modified By column: avatar circle hidden, user


8 @media print No decorative elements on paper
name text only

5.8 Email Report


# Actor Action Output / Decision
Fill in: Recipient Email, Subject, Message, Standard HTML form,
1 User
optional attachment file multipart/form-data

2 User Click "Send Email" button Form POST to [Link]

Read hidden fields: start_date, end_date, Context passed automatically


3 PHP
movement_type, report_type from current report view

Validate email. Process attachment if provided. Returns redirect with flash


4 PHP
Send email. message

5 Browser Page reloads. Flash message shown. Success or error banner


6. Cross-Cutting Concerns

6.1 Session Management & Security


• require_login() is called at the top of [Link], [Link], and [Link]. All AJAX endpoints also call
require_login() before any DB access.
• Passwords are stored using PHP password_hash() (bcrypt). password_verify() is used for login.
• All user-supplied values displayed in HTML pass through an escapeHtml() function (JS) or escape() helper
(PHP) to prevent XSS injection.
• All database queries use PDO prepared statements with parameter binding to prevent SQL injection.
• When a session expires, pages redirect to [Link]?timeout=1 which displays the "session expired" warning
banner.

6.2 Client-Side Validation States


A consistent three-state visual system is applied to all form inputs across the system:

CSS Class Border Colour Meaning


Validation passed. Field value is acceptable. Shown after user
field-valid #059669 (green)
successfully fills in a field.

Validation failed. Error message shown below the field. Save is


field-invalid #ef4444 (red)
blocked until resolved.

AJAX uniqueness check is in flight. Field cannot be saved until


field-checking #f59e0b (amber)
check completes. Spinner + "Checking…" text shown.

When a modal is closed, all field states are always reset: [Link](), error divs cleared, all three CSS classes removed
from all form-control elements.

6.3 AJAX Error Handling Pattern


All AJAX calls across the system follow the same three-handler pattern:
• success: — Process the response. Show [Link]/error/warning based on [Link] boolean.
• error: — Show [Link] with the error message. Restore any loading state. Never silently swallow failures.
• complete: — Always runs after success or error. Restores button text and disabled state. Hides loading
spinners. This ensures buttons are never permanently disabled even if an unexpected error occurs.

6.4 Navigation
All navigation links across [Link], [Link], and [Link] use the same pattern:
• HTML: <div class="nav-link" data-href="[Link]"> — no inline onclick attribute
• JavaScript: a single delegated event handler — $(document).on("click", ".header-nav .nav-link", function()
{ [Link] = $(this).data("href"); })
• This satisfies Requirement #5 (no inline event handlers) and makes the nav behaviour easily testable and
maintainable.
6.5 Keyboard Shortcuts
Shortcut Page Action
Ctrl + Shift + A Dashboard Open Add New Product modal

Open Edit modal for the currently selected (highlighted)


Ctrl + Shift + E Dashboard
row

Ctrl + Shift + R Dashboard Open Restock modal for the currently selected row

Ctrl + Shift + U Dashboard Open StockOut modal for the currently selected row

Ctrl + Shift + F Dashboard Focus and select the live search input

↑ / ↓ Arrow Keys Dashboard Move row selection up or down in the product table

Escape Dashboard Clear the search input and reload all products

F1 POS Focus the product search input

F2 POS Clear cart (triggers confirm dialog)

Process sale (only fires if Process Sale button is


F3 POS
enabled)

Ctrl + Shift + S POS Apply Senior 20% discount

Ctrl + Shift + P POS Apply PWD 20% discount

Ctrl + Shift + E POS Apply Employee 10% discount

Ctrl + Shift + N POS Remove discount (No Discount)

Add quick cash amount (₱20, ₱50, ₱100, ₱200, ₱500,


Ctrl + 1 to 6 POS
₱1000)

Escape POS Clear product search input and show all products

You might also like