Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/tutosrive/factus_challenge/llms.txt

Use this file to discover all available pages before exploring further.

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

ColumnFieldNotes
(delete button)Renders deleteRowButton; only invoices with status === 0 can actually be deleted
IDidNumeric, sorted descending by default
ESTADOstatusFormatted: 0'En espera', 1'Enviada'
DOCUMENTOdocument.nameNested field access
NÚMERO FACTURAnumberInvoice number string
CLIENTE APIapi_client_nameThe Factus API client name
ID CLIENTEidentificationCustomer tax ID
NOMBRE CLIENTEgraphic_representation_nameCustomer display name
FORMA DE PAGOpayment_form.nameNested field
HORA CREACIÓNcreated_atFormatted with Luxon (see below)

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:
const dt = DateTime.fromFormat(value, 'dd-MM-yyyy hh:mm:ss a').setLocale('es-419')
return dt.toFormat("hh:mm a, 'del' cccc dd 'de' LLLL 'de' yyyy")
// Example output: "09:16 PM, del jueves 23 de enero de 2025"

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:
{
  observation: "...",
  payment_method_code: 1,
  customer: {
    identification: "...", // remapped from customer.id
    identification_document_id: "...", // remapped from customer.type_id
    legal_organization_id: "...", // remapped from customer.id_org
    // ...other customer fields
  },
  items: [ /* rows from the embedded products table */ ]
}
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:
// DELETE /factura/:reference_code
if (data.status === 0) {
  const req = await Helpers.fetchJSON(`${urlAPI}/factura/${data.reference_code}`, {
    method: 'DELETE',
  })
  // ...
} else {
  Toast.show({
    message: 'La factura ha sido enviada a la DIAN, <span class="text-danger">no se puede eliminar</span>',
    mode: 'warning',
  })
}
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:
Lookup tableEndpointUsed for
municipalityGET /get-data/municipalityCity/municipality select
identification_documentGET /get-data/identification_documentID type select (NIT, Cédula, etc.)
customer_tributeGET /get-data/customer_tributeTribute type select
legal_organizationGET /get-data/legal_organizationOrg type select (Natural / Jurídica)
Customer data is then loaded from GET ${urlAPI}/get-data/customer.

Table Columns

ColumnFieldNotes
(edit button)Opens edit modal
(delete button)Opens delete confirmation modal
IDidSorted ascending by default
NOMBREnames
E-MAILemail
TIPO IDtype_idResolved to name via identification_document lookup
COMPAÑÍAcompanyNull values shown as '----'
DIRECCIÓNaddress
TELÉFONOphone
NOMBRE COMERCIALtrade_nameNull values shown as '----'
MUNICIPIOmunicipality_idResolved to name via municipality lookup
DVverification_digitDigit for NIT verification; '----' if null
ORGid_orgResolved to name via legal_organization lookup
TRIBUTOtribute_idResolved to name via customer_tribute lookup

CRUD Operations

// POST /add-data/customer
const response = await Helpers.fetchJSON(`${urlAPI}/add-data/customer`, {
  method: 'POST',
  body: getFormData(),
})
// On success: Clientes.#table.addRow(response.data)

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

ColumnField
IDbill.id
ESTADObill.status (0=En espera, 1=Enviada)
DOCUMENTObill.document.name
NÚMERO FACTURAbill.number
CUFEbill.cufe
ID CLIENTEcustomer.identification
NOMBRE CLIENTEcustomer.graphic_representation_name
FORMA DE PAGObill.payment_form.name
PREFIJO RANGOnumbering_range.prefix
HORA CREACIÓNbill.created_at (Luxon formatted)
HORA VALIDACIÓNbill.validated (Luxon formatted)

PDF Download

A PDF download button (rendered with icons.pdf_icon1) triggers FactuSearch.download_pdf():
// GET /factura-download/:number
const req_pdf64 = await Helpers.fetchJSON(`${urlAPI}/factura-download/${number}`)
// Response contains: { pdf_base_64_encoded, file_name }
// A temporary <a> element is created and clicked to trigger the browser download.
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:
// Button HTML strings (contain SVG icons from window.icons)
window.addRowButton    // "Nuevo registro" — appears in table footers
window.addProductButton // "Agregar producto" — appears in the products sub-table footer
window.editRowButton   // Per-row edit icon (function, returns HTML string)
window.deleteRowButton // Per-row delete icon (function, returns HTML string)

window.addButton       // Form submit: "Agregar"
window.editButton      // Form submit: "Actualizar"
window.deleteButton    // Form submit: "Eliminar"
window.cancelButton    // Form cancel: "Cancelar"

window.tableHeight     // 'calc(100vh - 190px)' — applied to every Tabulator instance
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.

Build docs developers (and LLMs) love