Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
f94a351
feat(Component): añade BaseComponent como clase base abstracta del si…
abdedarghal111 Jun 19, 2026
bc025d7
feat(Component): añade ActionResult como valor de retorno para maneja…
abdedarghal111 Jun 19, 2026
5cbf2f0
feat(Component): añade ComponentText para inputs de texto de una sola…
abdedarghal111 Jun 19, 2026
fd10e6f
feat(Component): añade ComponentTextarea para áreas de texto multilínea
abdedarghal111 Jun 19, 2026
e937513
feat(Component): añade ComponentNumber para inputs numéricos con prec…
abdedarghal111 Jun 19, 2026
57db044
feat(Component): añade ComponentSelect con soporte para opciones está…
abdedarghal111 Jun 19, 2026
f59d0fe
feat(Component): añade ComponentCheckbox para campos booleanos como f…
abdedarghal111 Jun 19, 2026
90cffb3
feat(Component): añade ComponentBlock para agrupar componentes en pes…
abdedarghal111 Jun 19, 2026
b1bb92c
feat(Component): añade UIController como controlador base para vistas…
abdedarghal111 Jun 19, 2026
67fef20
feat(PanelController): integra bloques de componentes como pestañas m…
abdedarghal111 Jun 19, 2026
01b643d
feat(Controller): añade DashboardComponents como controlador de demos…
abdedarghal111 Jun 19, 2026
a067410
feat(Controller): añade NewFormaPago como gestor de formas de pago co…
abdedarghal111 Jun 19, 2026
2d86b8e
chore: eliminar errata
abdedarghal111 Jun 19, 2026
316ae68
chore: eliminar errata
abdedarghal111 Jun 19, 2026
796e4b9
docs(Component): documenta todas las funciones de BaseComponent
abdedarghal111 Jun 21, 2026
e303724
feat(Component): añade ComponentBlock como bloque agrupador de compon…
abdedarghal111 Jun 21, 2026
e06ee39
feat(Component): añade ComponentCheckbox para campos booleanos
abdedarghal111 Jun 21, 2026
b8e46c2
fix(Component): castea FS_NF0 a int en ComponentNumber igual que Widg…
abdedarghal111 Jun 21, 2026
472417b
feat(Component): añade ComponentSelect para campos de selección
abdedarghal111 Jun 21, 2026
8a664e2
feat(Component): añade ComponentText para campos de texto
abdedarghal111 Jun 21, 2026
09e22f5
feat(Component): añade ComponentTextarea para campos de texto multilínea
abdedarghal111 Jun 21, 2026
3856131
fix(Component): reemplaza request->get() deprecado por queryOrInput()…
abdedarghal111 Jun 21, 2026
6d85e9c
feat(Component): añade FieldComponent como capa de presentación sobre…
abdedarghal111 Jun 21, 2026
cee4272
feat(Component): añade ComponentModalPicker como selector emergente m…
abdedarghal111 Jun 21, 2026
427e87c
feat(Component): añade ComponentSubcuenta como picker de subcuentas c…
abdedarghal111 Jun 21, 2026
240292e
feat(Component): añade ComponentVariante como picker de variantes de …
abdedarghal111 Jun 21, 2026
705f0be
feat(Component): añade ComponentModelPicker como selector modal genér…
abdedarghal111 Jun 21, 2026
abf9979
feat(Component): añade skipFormProcessing() para omitir el procesado …
abdedarghal111 Jun 21, 2026
255a986
feat(UIComponents): añade controladores maestros UIEditController, UI…
abdedarghal111 Jun 22, 2026
c254f06
feat(View): añade plantillas Twig maestras para UIEditController y UI…
abdedarghal111 Jun 22, 2026
c0da07a
refactor(Component): añade setExtraWhere() y mejora ComponentModelPic…
abdedarghal111 Jun 22, 2026
c5943c8
chore(Component): elimina comentarios separadores de sección en Compo…
abdedarghal111 Jun 22, 2026
6764c16
chore: elimina UIRelatedList y plantillas Twig de componentes sin ref…
abdedarghal111 Jun 22, 2026
fa510ab
refactor(UIComponents): paridad visual con sistema XML y sistema de f…
abdedarghal111 Jun 22, 2026
55d7e5e
refactor(Component): paridad visual de celdas con widgets XML
abdedarghal111 Jun 22, 2026
7cb297d
fix(PDFCore): compatibilidad con columnas sin widget en exportación
abdedarghal111 Jun 23, 2026
2804ab3
feat(UIListController): implementa exportación PDF/XLS/CSV/MAIL
abdedarghal111 Jun 23, 2026
45e613d
chore: elimina vistas NewFormaPago y componentes sin uso del nuevo si…
abdedarghal111 Jun 23, 2026
318c37b
feat(UIComponents): orden de columnas, nivel de seguridad y paridad c…
abdedarghal111 Jun 23, 2026
1879a40
refactor(UIListController): abstracción declarativa de botones del to…
abdedarghal111 Jun 23, 2026
de0bdc3
feat(UIListController): highlight y toggle de columna de ordenación a…
abdedarghal111 Jun 23, 2026
f2586d7
feat(UIComponents): añade UIPanelController y refactoriza NewEditAsie…
abdedarghal111 Jun 23, 2026
1b202eb
fix(ComponentNumber): elimina alineación text-end forzada en inputHtml
abdedarghal111 Jun 26, 2026
c4894a4
fix(UIListTab): establece resolvedOrderIndex al orden por defecto en …
abdedarghal111 Jun 26, 2026
88667e5
feat(ListView): permite sobreescribir btnNewUrl mediante settings['bt…
abdedarghal111 Jun 26, 2026
c2f8672
feat(UIEditController): añade soporte a EditListView con acciones ins…
abdedarghal111 Jun 26, 2026
759b0b3
feat(UIEditController.twig): renderiza listas como nav-pills igual qu…
abdedarghal111 Jun 26, 2026
d34ffc8
feat(NewEdit/ListAtributo,Fabricante): nuevos controladores UI con Ed…
abdedarghal111 Jun 26, 2026
111ce2e
feat(UIEditController): añade HtmlView, panelListViews e inlineListVi…
abdedarghal111 Jun 26, 2026
353b074
feat(ComponentDate): nuevo componente date/datetime nativo del navega…
abdedarghal111 Jun 26, 2026
ddddf96
fix(ComponentSelect): añade fallback DB cuando el valor no está en op…
abdedarghal111 Jun 26, 2026
5a566bd
feat(NewListProducto): listado de productos, variantes y stock sobre …
abdedarghal111 Jun 26, 2026
648b1b1
feat(NewEditProducto): controlador de edición de productos sobre UIEd…
abdedarghal111 Jun 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions Core/Component/ActionResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php
/**
* This file is part of FacturaScripts
* Copyright (C) 2023-2025 Carlos Garcia Gomez <carlos@facturascripts.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

namespace FacturaScripts\Core\Component;

/**
* Valor de retorno para los manejadores de eventos de componentes (p. ej. 'save' o 'delete').
*
* Cuando exit es true el controlador detiene el renderizado y, o bien redirige (si redirect
* está definido), o bien suprime la plantilla por completo. withRedirect() establece ambos
* campos en una sola llamada. Los manejadores que solo necesitan registrar un aviso y
* permanecer en la misma página deben devolver ActionResult::make() sin ningún encadenamiento.
*
* @author Abderrahim Darghal Belkacemi <abdedarghal111@gmail.com>
*/
class ActionResult
{
public bool $exit = false;
public bool $stop = false;
public string $redirect = '';
public string $message = '';

public static function make(): static
{
return new static();
}

public function exit(): static
{
$this->exit = true;
return $this;
}

public function stop(): static
{
$this->stop = true;
return $this;
}

public function withRedirect(string $url): static
{
$this->redirect = $url;
$this->exit = true;
return $this;
}

public function withMessage(string $message): static
{
$this->message = $message;
return $this;
}
}
246 changes: 246 additions & 0 deletions Core/Component/BaseComponent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
<?php
/**
* This file is part of FacturaScripts
* Copyright (C) 2023-2025 Carlos Garcia Gomez <carlos@facturascripts.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

namespace FacturaScripts\Core\Component;

use FacturaScripts\Core\Request;
use FacturaScripts\Core\Tools;

/**
* Contrato mínimo de un componente de interfaz.
*
* Responsabilidades exclusivas de esta clase:
* - Identidad: expone el fieldname que lo identifica dentro de un formulario.
* - Valor: almacena y recupera el valor actual del componente.
* - Resolución: extrae el valor de la petición HTTP o del modelo, con soporte
* para un resolver personalizado que cortocircuita la lógica por defecto.
* - Validación: motor de reglas con nombre ('email', 'numeric', 'max:N'…) y
* closures arbitrarios con firma fn(mixed $valor, Translator $lang): ?string.
* - Procesamiento de petición: recibe el POST, valida y escribe en el modelo.
*
* Todo lo relacionado con presentación visual (etiqueta, columnas, icono,
* renderizado HTML) vive en FieldComponent, que extiende esta clase.
*
* @author Abderrahim Darghal Belkacemi <abdedarghal111@gmail.com>
*/
abstract class BaseComponent
{
protected string $fieldname;
protected mixed $value = null;

protected array $validationErrors = [];
protected array $validationRules = [];
protected array $customRules = [];

/** @var callable|null Resolver personalizado: fn(Request, ?object): mixed */
protected $resolver = null;

public function __construct(string $fieldname)
{
$this->fieldname = $fieldname;
$this->addDefaultRules();
}

/** Crea una instancia con API fluida. */
public static function make(string $fieldname): static
{
return new static($fieldname);
}

public function fieldname(): string
{
return $this->fieldname;
}

public function setValue(mixed $value): static
{
$this->value = $value;
return $this;
}

public function value(): mixed
{
return $this->value;
}

/**
* Asigna un resolver personalizado.
*
* El callable recibe (Request, ?object) y devuelve el valor extraído.
* Cuando está presente, sustituye completamente la lógica por defecto.
*/
public function setResolver(callable $fn): static
{
$this->resolver = $fn;
return $this;
}

/**
* Extrae el valor del componente de la petición o del modelo.
*
* Orden: resolver personalizado → cuerpo POST → propiedad del modelo → null.
*/
public function resolve(Request $request, ?object $model = null): mixed
{
if ($this->resolver !== null) {
return ($this->resolver)($request, $model);
}

return $request->request->get($this->fieldname)
?? ($model !== null && property_exists($model, $this->fieldname)
? $model->{$this->fieldname}
: null);
}

/**
* Añade una regla con nombre o un closure validador.
*
* Reglas con nombre disponibles: 'email', 'numeric', 'max:N', 'min:N',
* 'min_val:N', 'max_val:N'.
*
* Firma del closure: fn(mixed $valor, Translator $lang): ?string
* Devuelve null para pasar, o un string con el mensaje de error para fallar.
*/
public function addRule(string|callable $rule, mixed ...$params): static
{
if (is_callable($rule)) {
$this->customRules[] = $rule;
} else {
$this->validationRules[] = ['rule' => $rule, 'params' => $params];
}
return $this;
}

/**
* Ejecuta todas las reglas sobre el valor dado y devuelve los mensajes de error.
*
* Devuelve un array vacío si el valor es válido.
*/
public function validate(mixed $value): array
{
$errors = [];
$lang = Tools::lang();

if ($this->isRequired() && ($value === null || $value === '')) {
$errors[] = $lang->trans('field-required', ['%field%' => $this->fieldname]);
}

foreach ($this->validationRules as $item) {
$error = $this->applyRule($item['rule'], $value, $item['params']);
if ($error !== null) {
$errors[] = $error;
}
}

foreach ($this->customRules as $fn) {
$error = $fn($value, $lang);
if ($error !== null) {
$errors[] = $error;
}
}

return $errors;
}

/**
* Inyecta los errores de validación desde el controlador al componente
* para que el renderizador pueda mostrar el feedback en línea.
*/
public function setValidationErrors(array $errors): static
{
$this->validationErrors = $errors;
return $this;
}

public function validationErrors(): array
{
return $this->validationErrors;
}

public function hasValidationErrors(): bool
{
return !empty($this->validationErrors);
}

/**
* Extrae el valor del POST, lo valida y lo escribe en el modelo si no hay errores.
*
* Devuelve ['success' => bool, 'errors' => string[], 'value' => mixed].
*/
public function processRequest(Request $request, ?object $model = null): array
{
$value = $this->resolve($request, $model);
$errors = $this->validate($value);

if (empty($errors) && $model !== null) {
$model->{$this->fieldname} = $value;
}

return ['success' => empty($errors), 'errors' => $errors, 'value' => $value];
}

/** Sobreescribe para registrar reglas por defecto al construir el componente. */
protected function addDefaultRules(): void
{
}

/**
* Indica si el campo es obligatorio.
*
* La implementación base devuelve false. FieldComponent sobreescribe este
* método con la propiedad $required que el usuario configura con setRequired().
*/
protected function isRequired(): bool
{
return false;
}

private function applyRule(string $rule, mixed $value, array $params): ?string
{
$colonPos = strpos($rule, ':');
$ruleName = $colonPos !== false ? substr($rule, 0, $colonPos) : $rule;
$ruleParam = $colonPos !== false ? substr($rule, $colonPos + 1) : ($params[0] ?? '');

if ($value === null || $value === '') {
return null;
}

return match ($ruleName) {
'max' => mb_strlen((string) $value) > (int) $ruleParam
? Tools::lang()->trans('value-too-long', ['%field%' => $this->fieldname, '%max%' => $ruleParam])
: null,
'min' => mb_strlen((string) $value) < (int) $ruleParam
? Tools::lang()->trans('value-too-short', ['%field%' => $this->fieldname, '%min%' => $ruleParam])
: null,
'numeric' => !is_numeric($value)
? Tools::lang()->trans('value-must-be-numeric', ['%field%' => $this->fieldname])
: null,
'email' => !filter_var($value, FILTER_VALIDATE_EMAIL)
? Tools::lang()->trans('invalid-email')
: null,
'min_val' => (float) $value < (float) $ruleParam
? Tools::lang()->trans('value-too-low', ['%field%' => $this->fieldname, '%min%' => $ruleParam])
: null,
'max_val' => (float) $value > (float) $ruleParam
? Tools::lang()->trans('value-too-high', ['%field%' => $this->fieldname, '%max%' => $ruleParam])
: null,
default => null,
};
}
}
Loading
Loading