Skip to main content
The Factus Challenge frontend has four dedicated page views plus a home screen, all wired together by the router in index.mjs. Each view is implemented as an ES module that exports a default class with only static methods — you never call new Factus() or new Clientes(). Calling the constructor throws an explicit error. The entry point for every view is a single .init() static method that fetches data, injects HTML into <main>, and initialises a Tabulator table.
The Factus.init() / Clientes.init() / FactuSearch.init() pattern keeps all state as private static class fields (#table, #modal, #form, etc.). This means each module is effectively a singleton — call .init() once and the class manages its own table lifecycle, modal dialogs, and event listeners without any external coordination.

Views

The Factus class manages the invoice list and invoice creation form. It is loaded when the user navigates to pages/factus.html.

Initialisation

Factus.init() performs the following steps before rendering the table:
  1. Fetches the invoice form HTML partial from /resources/html/factus.html via Helpers.fetchText.
  2. Loads payment methods from GET ${urlAPI}/get-data/payment_method and pre-builds the <option> list string.
  3. Fetches all invoices from GET ${urlAPI}/factura.
  4. Injects a <div id="table-container"> into <main> and initialises a Tabulator instance.

Table Columns

Date Formatting

Dates from the API arrive as dd-MM-yyyy hh:mm:ss a (e.g. 23-01-2025 09:16:27 PM) and are formatted for display using Luxon with the es-419 locale:

Creating an Invoice

Clicking the “Nuevo registro” button in the table footer opens a Modal containing the factus.html form partial. The form includes:
  • A customer <select> populated from GET ${urlAPI}/get-data/customer
  • A payment method <select> pre-loaded during init()
  • An observation <textarea>
  • An embedded Tabulator products table (see below)
  • A “Nuevo cliente” button that opens pages/cliente.html in a new tab via Customs.new_client()
On submission, Factus.#getFormData() assembles the request body. It queries the customer record via POST ${urlAPI}/get-join/ with a raw SQL SELECT to retrieve full customer details, then maps fields to the shape expected by the API:
The assembled body is sent to POST ${urlAPI}/factura. On success the page reloads after 2 seconds to reflect the new invoice in the table.

Embedded Products Table

Inside the invoice creation modal, a second Tabulator instance (#products_table) allows building the line items. Product selection happens via a popover (not a modal):
  1. Clicking “Agregar producto” calls Customs.popover() to render a <select> of products and a quantity stepper (- / + buttons).
  2. Product options are fetched from GET ${urlAPI}/get-data/products and rendered with Helpers.toOptionList.
  3. When the user confirms, the full product detail is fetched via POST ${urlAPI}/get-join with SELECT * FROM products WHERE code_reference = '...'.
  4. The row is added to #products_table with quantity set by the stepper and price multiplied by quantity.
Products columns in the embedded table: code_reference, name, price (money formatter), quantity, tax_rate, discount_rate, unit_measure_id, tribute_id, standard_code_id, is_excluded, withholding_taxes (JSON formatter).

Deleting an Invoice

Clicking the delete icon on a row opens a confirmation Modal. The actual DELETE request is only sent if data.status === 0:
The Clientes class manages the full customer lifecycle. It is loaded when the user navigates to pages/cliente.html.

Initialisation

Clientes.init() pre-fetches four lookup tables before building the main table, so all dropdown options are ready when a form opens:Customer data is then loaded from GET ${urlAPI}/get-data/customer.

Table Columns

CRUD Operations

Form Fields

The clientes.html partial contains inputs for: id (tax identification number), type_id (ID document type), names, address, phone, email, company, id_org (legal organization), tribute_id, trade_name, and municipality_id.The DV (verification digit) field container is hidden by default and only revealed — with required set to true — when the user selects NIT (type ID 6) from the identification type dropdown.
The FactuSearch class provides a search-by-number interface for inspecting a single invoice in detail. It is loaded when the user navigates to pages/factusget.html.

How it works

FactuSearch.init() loads the search_factus.html partial into <main> and attaches a click listener to the search button (#buscar-envio).When the user enters an invoice number and submits:
  1. GET ${urlAPI}/factura/:number is called.
  2. On success, a detail panel and a Tabulator table are rendered in #container-info.

Detail Panel Fields

The detail panel displays (from response[0]):
  • Document type: bill.document.name
  • Observation: bill.observation
  • API client company: company.name, company.nit, company.municipality
  • Reference code: bill.reference_code
  • Customer: customer.names, customer.legal_organization.name
  • Any API errors from bill.errors[]

Search Result Table Columns

PDF Download

A PDF download button (rendered with icons.pdf_icon1) triggers FactuSearch.download_pdf():
The About class renders a static profile and project information page. It is loaded when the user navigates to pages/about.html.About.init() fetches the /resources/html/about.html partial and injects it into <main>, then calls #listenLinks() to wire up interactive elements.

Interactive elements

All external links on the page are intercepted and replaced with a redirect confirmation popover (via Customs.popover) before the user is taken off-site. This includes:
  • GitHub, Instagram, YouTube, and email icons in #social-links
  • Project links in #projects
  • Links in the #about-me and #laboral-expreience sections
The “Contáctame” button renders a popover containing a QR code image (/resources/assets/images/qrw.webp).Email links open a mailto: URL after confirmation via a dedicated popover.

Global Button & Table Conventions

index.mjs registers a set of standardised button HTML strings and a table height constant as window globals. Every page controller uses these, ensuring a consistent look:
The generic database endpoints (GET /get-data/:table, POST /add-data/:table, PATCH /update-data/:table/:property/:value, DELETE /delete/:table/:property/:value) accept arbitrary table names from the frontend. In development this is convenient, but in a production deployment these endpoints must be protected (authentication, allowlisting, or removal) to prevent unauthorised data access or modification.