FiveTech Support Forums

FiveWin / Harbour / xBase community
Board index mod_harbour Analizando : back-End (HIX) + front-End ( ... )
Posts: 410
Joined: Sun Jan 31, 2010 03:30 PM
Analizando : back-End (HIX) + front-End ( ... )
Posted: Fri Jan 23, 2026 05:13 PM
Buen dia.. 

No estoy fijando una solucion ideal, solo a manera de conocimiento y que se pueda obtener una realimentacion .

Pasada la etapa de reconocimiento del webinar : HIX + HTML + DBF 

Llegamos a la parte más compleja como lo es la creacion de la vista o front-End !!! hacerlo a pulso al estilo fivewin, usando templates, usando IA + template, etc..

Voy a insertar 4 archivos en bloques (cada bloque esta separado segĂşn tipo de archivo fĂ­sico)

Los bloques (ITEM 2, 3 Y 4), fueron generados con COPILOT

1. ejemplo tomado de :  HIX & AntiGravity apps gallery - foro de fivewin

// agenda.prg

function Main()

local cAction    := Upost( "action" )
local cId        := Upost( "id" )
local cFirstname := Upost( "firstname" )
local cLastname  := Upost( "lastname" )
local cPhone     := Upost( "phone" )
local cEmail     := Upost( "email" )

// Search Parameter (Post)
local cSearch    := Upost( "search" )

local cDbPath    := "c:/hix/testdir/agenda.dbf"
local cTmpDir    := "c:/hix/testdir"
local cMessage   := ""

// Ensure Directory Exists
if ! IsDirectory( cTmpDir )
    DirMake( cTmpDir )
endif

if ! File( cDbPath )
    DbCreate( cDbPath, { ;
        { "ID",        "C",  8, 0 }, ;
        { "FIRSTNAME", "C", 30, 0 }, ;
        { "LASTNAME",  "C", 30, 0 }, ;
        { "PHONE",     "C", 20, 0 }, ;
        { "EMAIL",     "C", 50, 0 } ;
        } )
endif

// Robust Opening Logic
if Select("contacts") > 0
    select contacts
else
    select 0
    use (cDbPath) shared alias "contacts"
    if NetErr()
        UWrite( "Error opening database.<br>" )
        return ""
    endif
endif

set deleted on

do case
    case cAction == "save"
        if !Empty(cFirstname) .and. !Empty(cLastname)
            select contacts
            if Empty( cId )
                append blank
                replace id with GenerateID()
                replace firstname with cFirstname
                replace lastname  with cLastname
                replace phone     with cPhone
                replace email     with cEmail
                commit
                unlock
                cMessage := "Contact saved successfully."
            else
                // Update existing
                go top
                locate for id == cId
                if found()
                    if Rlock()
                        replace firstname with cFirstname
                        replace lastname  with cLastname
                        replace phone     with cPhone
                        replace email     with cEmail
                        commit
                        unlock
                        cMessage := "Contact updated successfully."
                    endif
                else
                    // ID not found? Treat as new.
                    append blank
                    replace id with GenerateID()
                    replace firstname with cFirstname
                    replace lastname  with cLastname
                    replace phone     with cPhone
                    replace email     with cEmail
                    commit
                    unlock
                    cMessage := "Contact saved successfully."
                endif
            endif
        endif
        cId := ""
        cFirstname := ""
        cLastname := ""
        cPhone := ""
        cEmail := ""
        // Clear search on save
        cSearch := ""

case cAction == "delete"
    if ! Empty( cId )
        select contacts
        go top
        locate for id == cId
        if found()
            if Rlock()
                delete
                unlock
                commit
                cMessage := "Contact deleted successfully."
            else
                cMessage := "Error: Could not lock record (" + AllTrim(cId) + ") for deletion."
            endif
        else
            cMessage := "Error: Contact ID (" + AllTrim(cId) + ") not found."
        endif
    else
        cMessage := "Error: No ID provided for deletion."
    endif
    cId := ""
    cSearch := "" // Clear search context on delete

case cAction == "edit"
    select contacts
    go top
    locate for id == cId
    if found()
        cFirstname := contacts->firstname
        cLastname  := contacts->lastname
        cPhone     := contacts->phone
        cEmail     := contacts->email
    endif

case cAction == "cancel"
    cId := ""
    cFirstname := ""
    cLastname := ""
    cPhone := ""
    cEmail := ""
    cSearch := ""

case cAction == "search"
    // Handled by default read of cSearch parameter logic
    // No specific logic needed here, just don't clear it.

endcase

RenderView( cId, cFirstname, cLastname, cPhone, cEmail, cMessage, cSearch )

select contacts
use

return ""

//****************************

function RenderView( cId, cFirst, cLast, cPhone, cEmail, cMessage, cSearch )
    local cHtml := MemoRead( "c:/hix/agenda.view" )
    local cRows := ""
    local cFormAction := "agenda.prg"
    local cMsgHtml := ""
    local lMatch := .T.
    local cFullData := ""

if Empty( cHtml )
    UWrite( "Error: Could not read agenda.view template." )
    return ""
endif

select contacts
go top

do while ! Eof()
    lMatch := .T.

// Search Filter Logic
if !Empty( cSearch )
    cFullData := Upper(contacts->firstname + " " + contacts->lastname + " " + contacts->phone + " " + contacts->email)
    if ! ( Upper(AllTrim(cSearch)) $ cFullData )
        lMatch := .F.
    endif
endif

if lMatch
    cRows += [<tr>]
    cRows += [<td>] + AllTrim( contacts->firstname ) + [ ] + AllTrim( contacts->lastname ) + [</td>]
    cRows += [<td>] + AllTrim( contacts->phone ) + [</td>]
    cRows += [<td class="actions-cell">]

    // POST Form for Edit
    cRows += [<form method="POST" action="] + cFormAction + [" style="display:inline;">]
    cRows += [<input type="hidden" name="action" value="edit">]
    cRows += [<input type="hidden" name="id" value="] + AllTrim(contacts->id) + [">]
    cRows += [<input type="hidden" name="search" value="] + cSearch + [">]

    cRows += [<button type="submit" class="btn btn-sm btn-edit"><i class="fa-solid fa-pen"></i></button>]
    cRows += [</form> ]

    // POST Form for Delete
    cRows += [<form method="POST" action="] + cFormAction + [" style="display:inline;" onsubmit="return confirm('Delete contact?')">]
    cRows += [<input type="hidden" name="action" value="delete">]
    cRows += [<input type="hidden" name="id" value="] + AllTrim(contacts->id) + [">]
    cRows += [<input type="hidden" name="search" value="] + cSearch + [">]
    cRows += [<button type="submit" class="btn btn-sm btn-danger"><i class="fa-solid fa-trash"></i></button>]
    cRows += [</form>]

    cRows += [</td></tr>]
endif
skip
enddo

if Empty( cRows )
    if !Empty( cSearch )
        cRows := [<tr><td colspan="3" class="empty-state">No contacts found matching "] + cSearch + [".</td></tr>]
    else
        cRows := [<tr><td colspan="3" class="empty-state">No contacts found. Add one above!</td></tr>]
    endif
endif

// Process Message
if !Empty( cMessage )
    // Determine type of alert
    // If message starts with "Error", use alert-danger (red)
    if Left( cMessage, 5 ) == "Error"
        cMsgHtml := [<div class="alert alert-danger" style="color: #f87171; background-color: rgba(248, 113, 113, 0.2); border: 1px solid rgba(248, 113, 113, 0.3);">] + cMessage + [</div>]
    else
        cMsgHtml := [<div class="alert alert-success">] + cMessage + [</div>]
    endif
endif

cHtml := StrTran( cHtml, "{{ACTION}}", cFormAction )
cHtml := StrTran( cHtml, "{{VAL_ID}}", iif( Empty(cId), "", cId ) )
cHtml := StrTran( cHtml, "{{VAL_FIRSTNAME}}", iif( Empty(cFirst), "", cFirst ) )
cHtml := StrTran( cHtml, "{{VAL_LASTNAME}}", iif( Empty(cLast), "", cLast ) )
cHtml := StrTran( cHtml, "{{VAL_PHONE}}", iif( Empty(cPhone), "", cPhone ) )
cHtml := StrTran( cHtml, "{{VAL_EMAIL}}", iif( Empty(cEmail), "", cEmail ) )
cHtml := StrTran( cHtml, "{{CONTACTS_ROWS}}", cRows )
cHtml := StrTran( cHtml, "{{MESSAGE}}", cMsgHtml )
cHtml := StrTran( cHtml, "{{VAL_SEARCH}}", iif( Empty(cSearch), "", cSearch ) )

// Add title dynamic
if !Empty(cId)
    cHtml := StrTran( cHtml, "{{FORM_TITLE}}", "Edit Contact" )
    cHtml := StrTran( cHtml, "{{CANCEL_BUTTON}}", [<a href="] + cFormAction + [?action=cancel" class="btn" style="background-color: #475569; color: white; margin-top: 0.5rem; justify-content:center;">Cancel Update</a>] )
else
    cHtml := StrTran( cHtml, "{{FORM_TITLE}}", "Add Contact" )
    cHtml := StrTran( cHtml, "{{CANCEL_BUTTON}}", "" )
endif

UWrite( cHtml )

return ""

function GenerateID()
    local cChars := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    local cId := ""
    local i
    for i := 1 to 8
        cId += SubStr( cChars, Int( hb_Random() * Len(cChars) ) + 1, 1 )
    next
return cId


//**************************************************************************************************


//   "c:/hix/agenda.view"

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HIX Agenda</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <style>
        :root {
            --primary-color: #6366f1;
            --primary-hover: #4f46e5;
            --danger-color: #ef4444;
            --bg-color: #0f172a;
            --card-bg: #1e293b;
            --text-color: #f8fafc;
            --text-muted: #94a3b8;
            --input-bg: #334155;
            --border-color: #475569;
        }

body {
    font-family: 'Outfit', sans-serif;
    background-color: var(--bg-color);
    color: var(--text-color);
    margin: 0;
    padding: 2rem;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
}

.container {
    width: 100%;
    max-width: 900px;
    display: grid;
    grid-template-columns: 1fr;
    gap: 2rem;
}

@media (min-width: 768px) {
    .container {
        grid-template-columns: 350px 1fr;
    }
}

h1 {
    text-align: center;
    margin-bottom: 2rem;
    font-weight: 600;
    background: linear-gradient(to right, #818cf8, #c4b5fd);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    width: 100%;
}

.card {
    background-color: var(--card-bg);
    padding: 1.5rem;
    border-radius: 1rem;
    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
    border: 1px solid rgba(255, 255, 255, 0.05);
    height: fit-content;
}

.card-header {
    font-size: 1.25rem;
    font-weight: 600;
    margin-bottom: 1.5rem;
    color: var(--text-color);
    border-bottom: 1px solid var(--border-color);
    padding-bottom: 0.5rem;
}

.form-group {
    margin-bottom: 1rem;
}

label {
    display: block;
    margin-bottom: 0.5rem;
    font-size: 0.875rem;
    color: var(--text-muted);
}

input {
    width: 100%;
    padding: 0.75rem;
    border-radius: 0.5rem;
    border: 1px solid var(--border-color);
    background-color: var(--input-bg);
    color: white;
    font-family: inherit;
    box-sizing: border-box;
    transition: all 0.2s;
}

input:focus {
    outline: none;
    border-color: var(--primary-color);
    box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
}

.btn {
    width: 100%;
    padding: 0.75rem;
    border: none;
    border-radius: 0.5rem;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.2s;
    display: inline-flex;
    justify-content: center;
    align-items: center;
    gap: 0.5rem;
    text-decoration: none;
    font-size: 0.9rem;
}

.btn-primary {
    background-color: var(--primary-color);
    color: white;
}

.btn-primary:hover {
    background-color: var(--primary-hover);
}

.btn-sm {
    padding: 0.4rem 0.8rem;
    font-size: 0.8rem;
    width: auto;
}

.btn-danger {
    background-color: transparent;
    color: var(--danger-color);
    border: 1px solid var(--danger-color);
}

.btn-danger:hover {
    background-color: var(--danger-color);
    color: white;
}

.btn-edit {
    background-color: transparent;
    color: var(--primary-color);
    border: 1px solid var(--primary-color);
}

.btn-edit:hover {
    background-color: var(--primary-color);
    color: white;
}

.btn-search {
    width: auto;
    align-self: flex-end;
    margin-bottom: 2px;
}


table {
    width: 100%;
    border-collapse: collapse;
    color: var(--text-color);
}

th, td {
    text-align: left;
    padding: 1rem;
    border-bottom: 1px solid var(--border-color);
}

th {
    color: var(--text-muted);
    font-weight: 600;
    font-size: 0.85rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

tr:last-child td {
    border-bottom: none;
}

.empty-state {
    text-align: center;
    padding: 2rem;
    color: var(--text-muted);
}

.actions-cell {
    display: flex;
    gap: 0.5rem;
    justify-content: flex-end;
}

.alert {
    padding: 1rem;
    margin-bottom: 1rem;
    border-radius: 0.5rem;
    font-weight: 500;
    text-align: center;
}

.alert-success {
    background-color: rgba(16, 185, 129, 0.2);
    color: #34d399;
    border: 1px solid rgba(16, 185, 129, 0.3);
}

.search-bar {
    display: flex;
    gap: 0.5rem;
    margin-bottom: 1rem;
}

</style>
</head>
<body>

<h1><i class="fa-solid fa-address-book"></i> HIX Agenda</h1>

<div class="container">
    <!-- Form Section -->
    <div class="card">
        <div class="card-header">
            {{FORM_TITLE}}
        </div>

    {{MESSAGE}}

    <form method="POST" action="{{ACTION}}">
        <input type="hidden" name="action" value="save">
        <input type="hidden" name="id" value="{{VAL_ID}}">

        <div class="form-group">
            <label>First Name</label>
            <input type="text" name="firstname" value="{{VAL_FIRSTNAME}}" required placeholder="John">
        </div>

        <div class="form-group">
            <label>Last Name</label>
            <input type="text" name="lastname" value="{{VAL_LASTNAME}}" required placeholder="Doe">
        </div>

        <div class="form-group">
            <label>Phone</label>
            <input type="tel" name="phone" value="{{VAL_PHONE}}" placeholder="+1 234 567 890">
        </div>

        <div class="form-group">
            <label>Email</label>
            <input type="email" name="email" value="{{VAL_EMAIL}}" placeholder="john@example.com">
        </div>

        <button type="submit" class="btn btn-primary">
            <i class="fa-solid fa-floppy-disk"></i> Save Contact
        </button>

        {{CANCEL_BUTTON}}
    </form>
</div>

<!-- List Section -->
<div class="card">
    <div class="card-header">
        Contacts
    </div>

    <form method="POST" action="{{ACTION}}" class="search-bar">
        <input type="hidden" name="action" value="search">
        <input type="text" name="search" value="{{VAL_SEARCH}}" placeholder="Search contacts..." style="flex-grow:1;">
        <button type="submit" class="btn btn-primary btn-search"><i class="fa-solid fa-magnifying-glass"></i></button>
        <a href="{{ACTION}}" class="btn btn-search" style="background: #334155;"><i class="fa-solid fa-xmark"></i></a>
    </form>

    <table>
        <thead>
            <tr>
                <th>Name</th>
                <th>Phone / Email</th>
                <th style="text-align: right;">Actions</th>
            </tr>
        </thead>
        <tbody>
            {{CONTACTS_ROWS}}
        </tbody>
    </table>
</div>
</div>

</body>
</html>

*************************************************************************************
*************************************************************************************

2. BACKEND EN HIX (USADO EN LOS BLOQUE O ITEMS 3 Y 4) )


#include "hbjson.ch"

function Main()
   local cMethod := GetEnv("REQUEST_METHOD")
   local cPath   := GetEnv("PATH_INFO")

   do case
   case cMethod == "GET"
      return GetRecords()

   case cMethod == "POST"
      return CreateRecord()

   case cMethod == "PUT"
      return UpdateRecord()

   case cMethod == "DELETE"
      return DeleteRecord()

   otherwise
      ? "{ \"error\": \"Método no soportado\" }"
   endcase
return nil


//************************

function GetRecords()
   local aData := {}
   USE people NEW SHARED
   DO WHILE !EOF()
      AAdd(aData, { ;
         "ID"        => AllTrim(people->ID), ;
         "FIRSTNAME" => AllTrim(people->FIRSTNAME), ;
         "LASTNAME"  => AllTrim(people->LASTNAME), ;
         "PHONE"     => AllTrim(people->PHONE), ;
         "EMAIL"     => AllTrim(people->EMAIL) ;
      })
      SKIP
   ENDDO
   USE
   ? hb_jsonEncode(aData)
return nil


//********************************
function CreateRecord()
   local hData := hb_jsonDecode( ReadStdIn() )
   USE people NEW EXCLUSIVE
   APPEND BLANK
   REPLACE people->ID        WITH hData["ID"]
   REPLACE people->FIRSTNAME WITH hData["FIRSTNAME"]
   REPLACE people->LASTNAME  WITH hData["LASTNAME"]
   REPLACE people->PHONE     WITH hData["PHONE"]
   REPLACE people->EMAIL     WITH hData["EMAIL"]
   USE
   ? "{ \"status\": \"created\" }"
return nil

//***************************

function UpdateRecord()
   local hData := hb_jsonDecode( ReadStdIn() )
   USE people NEW EXCLUSIVE
   LOCATE FOR people->ID == hData["ID"]
   if Found()
      REPLACE people->FIRSTNAME WITH hData["FIRSTNAME"]
      REPLACE people->LASTNAME  WITH hData["LASTNAME"]
      REPLACE people->PHONE     WITH hData["PHONE"]
      REPLACE people->EMAIL     WITH hData["EMAIL"]
      ? "{ \"status\": \"updated\" }"
   else
      ? "{ \"error\": \"ID no encontrado\" }"
   endif
   USE
return nil

//*********************************

function DeleteRecord()
   local hData := hb_jsonDecode( ReadStdIn() )
   USE people NEW EXCLUSIVE
   LOCATE FOR people->ID == hData["ID"]
   if Found()
      DELETE
      ? "{ \"status\": \"deleted\" }"
   else
      ? "{ \"error\": \"ID no encontrado\" }"
   endif
   USE
return nil

*************************************************************************************
*************************************************************************************

3. front-End : html + js + css .. puro

/* .html */

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <title>GestiĂłn de Personas</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <header>
    <h1>GestiĂłn de Personas</h1>
  </header>

  <main>
    <section class="table-section">
      <button id="btnAdd">âž• Nuevo Registro</button>
      <table id="peopleTable">
        <thead>
          <tr>
            <th>ID</th>
            <th>Nombre</th>
            <th>Apellido</th>
            <th>Teléfono</th>
            <th>Email</th>
            <th>Acciones</th>
          </tr>
        </thead>
        <tbody>
          <!-- Filas dinámicas -->
        </tbody>
      </table>
    </section>

<!-- Modal -->
<div id="modal" class="modal">
  <div class="modal-content">
    <span id="closeModal" class="close">&times;</span>
    <h2 id="modalTitle">Nuevo Registro</h2>
    <form id="personForm">
      <label for="ID">ID</label>
      <input type="text" id="ID" required>
      <label for="FIRSTNAME">Nombre</label>
      <input type="text" id="FIRSTNAME" required>
      <label for="LASTNAME">Apellido</label>
      <input type="text" id="LASTNAME" required>
      <label for="PHONE">Teléfono</label>
      <input type="text" id="PHONE">
      <label for="EMAIL">Email</label>
      <input type="email" id="EMAIL">
      <button type="submit" id="saveBtn">Guardar</button>
    </form>
  </div>
</div>
  </main>

  <footer>
    <p>© 2026 Empresa XYZ - Gestión de Personas</p>
  </footer>

  <!-- Capa JS separada -->
  <script src="app.js"></script>
</body>
</html>


////////////////////////////
// .css
body {
  font-family: 'Segoe UI', Tahoma, sans-serif;
  margin: 0;
  background: #f4f6f9;
  color: #333;
}

header {
  background: #004080;
  color: white;
  padding: 1rem;
  text-align: center;
}

.table-section {
  padding: 2rem;
}

button {
  background: #004080;
  color: white;
  border: none;
  padding: 0.5rem 1rem;
  margin-bottom: 1rem;
  cursor: pointer;
  border-radius: 4px;
}

button:hover {
  background: #0066cc;
}

table {
  width: 100%;
  border-collapse: collapse;
  background: white;
  box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}

th, td {
  padding: 0.75rem;
  border-bottom: 1px solid #ddd;
}

th {
  background: #e6e6e6;
}

footer {
  background: #004080;
  color: white;
  text-align: center;
  padding: 1rem;
}

/* Modal */
.modal {
  display: none;
  position: fixed;
  z-index: 10;
  left: 0; top: 0;
  width: 100%; height: 100%;
  background: rgba(0,0,0,0.5);
}

.modal-content {
  background: white;
  margin: 10% auto;
  padding: 2rem;
  width: 400px;
  border-radius: 8px;
}

.close {
  float: right;
  font-size: 1.5rem;
  cursor: pointer;
}


//*******************************
// .js logica de vista

const apiUrl = '/api/people';
const tableBody = document.querySelector('#peopleTable tbody');
const modal = document.getElementById('modal');
const closeModal = document.getElementById('closeModal');
const btnAdd = document.getElementById('btnAdd');
const form = document.getElementById('personForm');
let editing = false;

// Cargar registros
async function loadPeople() {
  const res = await fetch(apiUrl);
  const data = await res.json();
  tableBody.innerHTML = '';
  data.forEach(person => {
    const row = document.createElement('tr');
    row.innerHTML = `
      <td>${person.ID}</td>
      <td>${person.FIRSTNAME}</td>
      <td>${person.LASTNAME}</td>
      <td>${person.PHONE}</td>
      <td>${person.EMAIL}</td>
      <td>
        <button class="editBtn" data-id="${person.ID}">✏️</button>
        <button class="deleteBtn" data-id="${person.ID}">🗑️</button>
      </td>
    `;
    tableBody.appendChild(row);
  });

  // Asignar eventos dinámicos
  document.querySelectorAll('.editBtn').forEach(btn =>
    btn.addEventListener('click', () => editPerson(btn.dataset.id))
  );
  document.querySelectorAll('.deleteBtn').forEach(btn =>
    btn.addEventListener('click', () => deletePerson(btn.dataset.id))
  );
}

// Crear o actualizar
form.addEventListener('submit', async e => {
  e.preventDefault();
  const person = {
    ID: document.getElementById('ID').value,
    FIRSTNAME: document.getElementById('FIRSTNAME').value,
    LASTNAME: document.getElementById('LASTNAME').value,
    PHONE: document.getElementById('PHONE').value,
    EMAIL: document.getElementById('EMAIL').value
  };

  const method = editing ? 'PUT' : 'POST';
  await fetch(apiUrl, {
    method,
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(person)
  });

  modal.style.display = 'none';
  loadPeople();
});

// Editar
async function editPerson(id) {
  const res = await fetch(apiUrl);
  const data = await res.json();
  const person = data.find(p => p.ID === id);
  if (person) {
    document.getElementById('ID').value = person.ID;
    document.getElementById('FIRSTNAME').value = person.FIRSTNAME;
    document.getElementById('LASTNAME').value = person.LASTNAME;
    document.getElementById('PHONE').value = person.PHONE;
    document.getElementById('EMAIL').value = person.EMAIL;
    editing = true;
    modal.style.display = 'block';
  }
}

// Eliminar
async function deletePerson(id) {
  await fetch(apiUrl, {
    method: 'DELETE',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ ID: id })
  });
  loadPeople();
}

// Modal control
btnAdd.onclick = () => {
  form.reset();
  editing = false;
  modal.style.display = 'block';
};
closeModal.onclick = () => modal.style.display = 'none';
window.onclick = e => { if (e.target === modal) modal.style.display = 'none'; };

// Inicializar
loadPeople();

*************************************************************************************
*************************************************************************************

4. front-End : frameWork angular 

// .html
<p-table [value]="people" [(selection)]="selectedPerson" dataKey="ID">
  <ng-template pTemplate="header">
    <tr>
      <th>ID</th>
      <th>Nombre</th>
      <th>Apellido</th>
      <th>Teléfono</th>
      <th>Email</th>
      <th>Acciones</th>
    </tr>
  </ng-template>
  <ng-template pTemplate="body" let-person>
    <tr>
      <td>{{person.ID}}</td>
      <td>{{person.FIRSTNAME}}</td>
      <td>{{person.LASTNAME}}</td>
      <td>{{person.PHONE}}</td>
      <td>{{person.EMAIL}}</td>
      <td>
        <button pButton type="button" icon="pi pi-pencil" (click)="editPerson(person)"></button>
        <button pButton type="button" icon="pi pi-trash" (click)="delete(person)"></button>
      </td>
    </tr>
  </ng-template>
</p-table>

<button pButton type="button" label="Nuevo" icon="pi pi-plus" (click)="addPerson()"></button>

<p-dialog header="Persona" [(visible)]="displayDialog" [modal]="true">
  <div class="p-fluid">
    <div class="p-field">
      <label for="firstname">Nombre</label>
      <input id="firstname" type="text" pInputText [(ngModel)]="selectedPerson.FIRSTNAME"/>
    </div>
    <div class="p-field">
      <label for="lastname">Apellido</label>
      <input id="lastname" type="text" pInputText [(ngModel)]="selectedPerson.LASTNAME"/>
    </div>
    <div class="p-field">
      <label for="phone">Teléfono</label>
      <input id="phone" type="text" pInputText [(ngModel)]="selectedPerson.PHONE"/>
    </div>
    <div class="p-field">
      <label for="email">Email</label>
      <input id="email" type="text" pInputText [(ngModel)]="selectedPerson.EMAIL"/>
    </div>
  </div>
  <p-footer>
    <button pButton type="button" label="Guardar" (click)="save()"></button>
  </p-footer>
</p-dialog>


//*******************************************************************
// . logica en vista .js o ts

import { Component, OnInit } from '@angular/core';
import { PeopleService, Person } from './people.service';

@Component({
  selector: 'app-people',
  templateUrl: './people.component.html'
})
export class PeopleComponent implements OnInit {
  people: Person[] = [];
  selectedPerson: Person | null = null;
  displayDialog: boolean = false;
  newPerson: boolean = false;

  constructor(private peopleService: PeopleService) {}

  ngOnInit() {
    this.loadPeople();
  }

  loadPeople() {
    this.peopleService.getAll().subscribe(data => this.people = data);
  }

  addPerson() {
    this.newPerson = true;
    this.selectedPerson = { ID:'', FIRSTNAME:'', LASTNAME:'', PHONE:'', EMAIL:'' };
    this.displayDialog = true;
  }

  editPerson(person: Person) {
    this.newPerson = false;
    this.selectedPerson = { ...person };
    this.displayDialog = true;
  }

  save() {
    if (this.newPerson) {
      this.peopleService.create(this.selectedPerson!).subscribe(() => this.loadPeople());
    } else {
      this.peopleService.update(this.selectedPerson!).subscribe(() => this.loadPeople());
    }
    this.displayDialog = false;
  }

  delete(person: Person) {
    this.peopleService.delete(person.ID).subscribe(() => this.loadPeople());
  }
}


//****************************************
// .service 

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

export interface Person {
  ID: string;
  FIRSTNAME: string;
  LASTNAME: string;
  PHONE: string;
  EMAIL: string;
}

@Injectable({
  providedIn: 'root'
})
export class PeopleService {
  private apiUrl = '/api/people'; // tu endpoint Hix

  constructor(private http: HttpClient) {}

  getAll(): Observable<Person[]> {
    return this.http.get<Person[]>(this.apiUrl);
  }

  create(person: Person): Observable<any> {
    return this.http.post(this.apiUrl, person);
  }

  update(person: Person): Observable<any> {
    return this.http.put(this.apiUrl, person);
  }

  delete(id: string): Observable<any> {
    return this.http.request('delete', this.apiUrl, { body: { ID: id } });
  }
}

//*****************************************************************
Posts: 6983
Joined: Fri Oct 07, 2005 07:07 PM
Re: Analizando : back-End (HIX) + front-End ( ??)
Posted: Fri Jan 23, 2026 08:15 PM

Russimicro,

I’m actually using exactly the style you describe in point 2, but working against my own microservice.
The source code is not kept as one large file; instead, it is split into small, logical units, each in its own file. The big advantage of this approach is that every file has proper syntax support in the editor (HTML, CSS, etc.), and then, with a single assembly step, everything is merged back into one final HTML file, exactly like the one you showed.

The composition is fully explicit and can be extended quite freely, even up to 8 levels deep if needed. For example:

This is exactly what is meant by “explicit UI composition”:

  • HEAD_META_RESOURCES
    → document metadata and external dependencies

  • STYLE
    → inline or page-specific styles

  • HEADER
    → static UI header

  • FORM_SECTION_FORM
    → a block with a clear responsibility

  • LIST_SECTION_*
    → the list is split logically, not visually

    Each block has a single responsibility.
    Each block is replaceable.
    Each block is editor-friendly.

The key idea is that the UI structure is visible and explicit, without runtime magic or mandatory frameworks. The microservice only provides data; the HTML composition itself is resolved beforehand in a deterministic way.

Best regards,
Otto

Continue the discussion