Merge branch 'feat/importador_rama_2' into 'main'

Feat/importador rama 2

See merge request jjimenez/safekat!754
This commit is contained in:
Ignacio Martinez Navajas
2025-04-29 15:10:55 +00:00
6 changed files with 413 additions and 241 deletions

View File

@ -18,6 +18,7 @@ $routes->group('importador', ['namespace' => 'App\Controllers\Importadores'], fu
* AJAX
*========================**/
$routes->post('validar-fila', 'ImportadorCatalogo::validarFila');
$routes->post('importar-fila', 'ImportadorCatalogo::importarFila');
});

View File

@ -5,22 +5,13 @@ namespace App\Controllers\API;
use App\Controllers\Presupuestos\Presupuestocliente;
use CodeIgniter\RESTful\ResourceController;
use CodeIgniter\API\ResponseTrait;
use App\Models\API\ItemModel;
class ImprimelibrosApi extends ResourceController
{
use ResponseTrait;
public function index()
{
$model = new ItemModel();
$data = $model->findAll();
return $this->respond($data);
}
public function calcular()
{
helper(['form']);

View File

@ -4,8 +4,7 @@ namespace App\Controllers\Importadores;
use App\Controllers\BaseResourceController;
use App\Entities\Catalogo\CatalogoLibroEntity;
use App\Models\Catalogo\CatalogoLibroModel;
use App\Models\Clientes\ClienteModel;
use Hermawan\DataTables\DataTable;
use App\Controllers\Presupuestos\Presupuestocliente;
class ImportadorCatalogo extends BaseResourceController
{
@ -57,134 +56,6 @@ class ImportadorCatalogo extends BaseResourceController
}
public function add()
{
if ($this->request->getPost()):
$postData = $this->request->getPost();
$sanitizedData = $this->sanitized($postData, true);
$sanitizedData['user_created_id'] = auth()->user()->id;
unset($sanitizedData['isk']);
$noException = true;
if ($successfulResult = $this->canValidate()):
if ($this->canValidate()):
try {
$successfulResult = $this->model->skipValidation(true)->save($sanitizedData);
} catch (\Exception $e) {
$noException = false;
$this->dealWithException($e);
}
else:
$this->viewData['errorMessage'] = lang('Basic.global.formErr1', [lang('Basic.global.record')]);
$this->session->setFlashdata('formErrors', $this->model->errors());
endif;
$thenRedirect = true; // Change this to false if you want your user to stay on the form after submission
endif;
if ($noException && $successfulResult):
$id = $this->model->db->insertID();
$message = lang('Basic.global.saveSuccess', [lang('Basic.global.record')]) . '.';
if ($thenRedirect):
if (!empty($this->indexRoute)):
return redirect()->to(route_to($this->indexRoute))->with('sweet-success', $message);
else:
return $this->redirect2listView('sweet-success', $message);
endif;
else:
$this->session->setFlashData('sweet-success', $message);
endif;
endif; // $noException && $successfulResult
endif; // ($requestMethod === 'post')
$this->viewData['catalogoLibrosEntity'] = isset($sanitizedData) ? new CatalogoLibroEntity($sanitizedData) : new CatalogoLibroEntity();
$this->viewData['formAction'] = route_to('catalogoLibrosAdd');
$this->viewData['boxTitle'] = lang('Basic.global.addNew') . ' ' . lang('Catalogo.moduleTitle') . ' ' . lang('Basic.global.addNewSuffix');
return $this->displayForm(__METHOD__);
} // end function add()
public function edit($requestedId = null)
{
if ($requestedId == null):
return $this->redirect2listView();
endif;
$id = filter_var($requestedId, FILTER_SANITIZE_URL);
$catalogoLibrosEntity = $this->model->find($id);
if ($catalogoLibrosEntity == false):
$message = lang('Basic.global.notFoundWithIdErr', [mb_strtolower(lang('Catalogo.pais')), $id]);
return $this->redirect2listView('sweet-error', $message);
endif;
if ($this->request->getPost()):
$postData = $this->request->getPost();
$sanitizedData = $this->sanitized($postData, true);
unset($sanitizedData['isk']);
$sanitizedData['user_update_id'] = auth()->user()->id;
$noException = true;
if ($successfulResult = $this->canValidate()): // if ($successfulResult = $this->validate($this->formValidationRules) ) :
if ($this->canValidate()):
try {
$successfulResult = $this->model->skipValidation(true)->update($id, $sanitizedData);
} catch (\Exception $e) {
$noException = false;
$this->dealWithException($e);
}
else:
$this->viewData['warningMessage'] = lang('Basic.global.formErr1', [mb_strtolower(lang('Catalogo.catalogo'))]);
$this->session->setFlashdata('formErrors', $this->model->errors());
endif;
$catalogoLibrosEntity->fill($sanitizedData);
$thenRedirect = false;
endif;
if ($noException && $successfulResult):
$id = $catalogoLibrosEntity->id ?? $id;
$message = lang('Basic.global.updateSuccess', [lang('Basic.global.record')]) . '.';
if ($thenRedirect):
if (!empty($this->indexRoute)):
return redirect()->to(route_to($this->indexRoute))->with('sweet-success', $message);
else:
return $this->redirect2listView('sweet-success', $message);
endif;
else:
$this->session->setFlashData('sweet-success', $message);
endif;
endif; // $noException && $successfulResult
endif; // ($requestMethod === 'post')
$this->viewData['catalogoLibrosEntity'] = $catalogoLibrosEntity;
$this->viewData['formAction'] = route_to('catalogoLibrosEdit', $id);
$this->viewData['boxTitle'] = lang('Basic.global.edit2') . ' ' . lang('Catalogo.moduleTitle') . ' ' . lang('Basic.global.edit3');
return $this->displayForm(__METHOD__, $id);
} // end function edit(...)
public function validarFila()
{
$json = $this->request->getJSON();
@ -234,4 +105,210 @@ class ImportadorCatalogo extends BaseResourceController
]);
}
public function importarFila()
{
$json = $this->request->getJSON();
if (!$json || !isset($json->fila[0])) {
return $this->response->setJSON([
'success' => false,
'message' => 'Datos inválidos.'
]);
}
// Mapear cada columna a una variable
$isbn = isset($json->fila[0]) ? trim($json->fila[0]) : null;
$refCliente = isset($json->fila[1]) ? trim($json->fila[1]) : null;
//$descripcion = isset($json->fila[2]) ? trim($json->fila[2]) : null;
$tirada = isset($json->fila[3]) ? (float) $json->fila[3] : null;
$precio_compra = isset($json->fila[4]) ? (float) $json->fila[4] : null;
if (empty($isbn)) {
return $this->response->setJSON([
'success' => false,
'message' => 'Input vacío o no proporcionado.'
]);
}
$catalogoModel = new CatalogoLibroModel();
// 1. Buscar por ISBN exacto
$libro = $catalogoModel->where('isbn', $isbn)->first();
// 2. Si no, buscar por EAN sin guiones
if (!$libro) {
$eanLimpio = str_replace('-', '', $isbn);
$libro = $catalogoModel->where('REPLACE(ean, "-", "")', $eanLimpio)->first();
}
if (!$libro) {
return $this->response->setJSON([
'success' => false,
'message' => 'No se encontró el libro en el catálogo.'
]);
}
// Aquí ya tenemos el libro correcto.
// Ahora se prepara la "inserción" o el "registro" a importar
// Variables intermedias
$colorPaginas = (int) ($libro->color_paginas ?? 0);
$negroPaginas = (int) ($libro->negro_paginas ?? 0);
$papelInteriorDiferente = ($colorPaginas > 0 && $negroPaginas > 0) ? 1 : 0;
// --- Interior (lo que cambiamos ahora)
if ($papelInteriorDiferente) {
// Mixto: páginas en negro + color
$interior = [
'papelInterior' => [
'negro' => $libro->negro_papel_id,
'color' => $libro->color_papel_id,
],
'gramajeInterior' => [
'negro' => $libro->negro_gramaje,
'color' => $libro->color_gramaje,
]
];
} else {
// SOLO un tipo: negro O color
$colorPaginas = (int) ($libro->color_paginas ?? 0);
$negroPaginas = (int) ($libro->negro_paginas ?? 0);
if ($colorPaginas > 0 && $negroPaginas == 0) {
// Libro completamente en color
$interior = [
'papelInterior' => $libro->color_papel_id,
'gramajeInterior' => $libro->color_gramaje,
];
} else {
// Libro completamente en blanco y negro
$interior = [
'papelInterior' => $libro->negro_papel_id,
'gramajeInterior' => $libro->negro_gramaje,
];
}
}
// Sobrecubierta
$sobrecubierta = [];
if (!is_null($libro->sobrecubierta_paginas)) {
$sobrecubierta['papel'] = $libro->sobrecubierta_papel_id;
$sobrecubierta['gramaje'] = $libro->sobrecubierta_gramaje;
$sobrecubierta['solapas'] = $libro->sobrecubierta_solapas;
$sobrecubierta['acabado'] = $libro->sobrecubierta_acabado_id;
}
$dataToImport = [
'selectedTirada' => $tirada,
'datosCabecera' => [
'titulo' => $libro->titulo,
'autor' => $libro->autor,
'isbn' => $isbn,
'coleccion' => $libro->coleccion,
'referenciaCliente' => $refCliente
],
'tirada' => array_values(array_filter([
$tirada,
null,
null,
null,
])),
'tamanio' => [
'ancho' => $libro->ancho,
'alto' => $libro->alto
],
'tipo' => "",
'tipo_presupuesto_id' => $libro->encuadernacion_id,
'clienteId' => 251,
'isColor' => (in_array(strtolower($libro->tipo_impresion), ['color', 'colorhq']) ? 1 : 0),
'isHq' => (in_array(strtolower($libro->tipo_impresion), ['negrohq', 'colorhq']) ? 1 : 0),
'paginas' => $libro->paginas,
'paginasColor' => $colorPaginas,
'papelInteriorDiferente' => $papelInteriorDiferente,
'paginasCuadernillo' => 32,
'interior' => $interior,
'cubierta' => [
'papelCubierta' => $libro->cubierta_papel_id,
'gramajeCubierta' => $libro->cubierta_gramaje,
'solapas' => $libro->cubierta_ancho_solapas,
'acabado' => $libro->cubierta_acabado_id,
'cabezada' => 'WHI',
'lomoRedondo' => 0
],
'guardas' => [],
'sobrecubierta' => $sobrecubierta,
'faja' => [],
'entrega_taller' => 1,
];
/*return $this->response->setJSON([
'success' => true,
'message' => 'Libro encontrado y preparado para importar.',
'data' => $dataToImport
]);*/
// Procedemos a intentar guardar el presupuesto
// Instancia de presupuesto cliente
$presupuestocliente = new Presupuestocliente();
try {
$response = $presupuestocliente->guardar($dataToImport);
// DEBUG LINE
//return $this->respond($response);
if (!isset($response['sk_id'])) {
return $this->respond([
'status' => 400,
'error' => 'Missing sk_id',
'message' => 'El identificador sk_id es requerido pero no se recibió.'
], 400);
}
$response = [
'status' => 200,
'error' => null,
'data' => [
'sk_id' => $response['sk_id'],
'sk_url' => $response['sk_url'] ?? null
]
];
// Ajuste del precio a RAMA
$dataToUpdate = [
'total_aceptado' => ($tirada * $precio_compra),
'total_aceptado_revisado' => ($tirada * $precio_compra),
'total_precio_unidad' => $precio_compra
];
$presupuestoModel = model('App\Models\Presupuestos\Presupuestomodel');
$presupuestoModel->update($response['data']['sk_id'], $dataToUpdate);
return $this->respond($response);
} catch (\Exception $e) {
return $this->respond([
'status' => 500,
'error' => 'Server error',
'message' => 'Error inesperado durante el procesado',
'debug' => $e->getMessage()
]);
}
}
}

View File

@ -208,7 +208,7 @@ class LogisticaController extends BaseController
}
$modelImpresora = model('App\Models\Configuracion\ImpresoraEtiquetaModel');
$impresora = $modelImpresora->select('id, name')
$impresora = $modelImpresora->select('id, name, ip, port, user, pass')
->where('deleted_at', null)
->where('id', $printer_id)
->orderBy('name', 'asc')

View File

@ -35,40 +35,33 @@
<div class="col-md-12 mb-3">
<div class="table-responsive">
<table id="excelTable" class="table table-striped table-hover" style="width: 100%;">
<thead>
<tr>
<th>
<input type="checkbox" id="select-all" class="form-check-input">
</th>
<th><?= lang('Importador.input') ?></th>
<th><?= lang('Importador.idlinea') ?></th>
<th><?= lang('Importador.descripcion') ?></th>
<th><?= lang('Importador.cnt_pedida') ?></th>
<th><?= lang('Importador.precio_compra') ?></th>
<th class="text-nowrap" style="min-width: 120px;">
<?= lang('Basic.global.Action') ?></th>
</tr>
<tr>
<th></th>
<th><input type="text" class="form-control form-control-sm"
placeholder="Filtrar..." /></th>
<th><input type="text" class="form-control form-control-sm"
placeholder="Filtrar..." /></th>
<th><input type="text" class="form-control form-control-sm"
placeholder="Filtrar..." /></th>
<th><input type="text" class="form-control form-control-sm"
placeholder="Filtrar..." /></th>
<th><input type="text" class="form-control form-control-sm"
placeholder="Filtrar..." /></th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<table id="excelTable" class="table table-striped table-hover" style="width: 100%;">
<thead>
<tr>
<th><input type="checkbox" id="selectAll"></th> <!-- Checkbox general -->
<th><?= lang('Importador.input') ?></th>
<th><?= lang('Importador.idlinea') ?></th>
<th><?= lang('Importador.descripcion') ?></th>
<th><?= lang('Importador.cnt_pedida') ?></th>
<th><?= lang('Importador.precio_compra') ?></th>
<th>Notas</th> <!-- Comentarios -->
<th><?= lang('Basic.global.Action') ?></th>
<!-- Acciones (importar/eliminar) -->
</tr>
<tr> <!-- Segunda fila para filtros -->
<th></th> <!-- No filtro en checkbox -->
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th> <!-- No filtro en notas -->
<th></th> <!-- No filtro en acciones -->
</tr>
</thead>
<tbody></tbody>
</table>
</div>

View File

@ -19,6 +19,18 @@ document.addEventListener('DOMContentLoaded', function () {
order: [[1, 'asc']]
});
// Crear filtros por columna en el segundo <tr>
$('#excelTable thead tr:eq(1) th').each(function (i) {
if (![0, 6, 7].includes(i)) { // No poner input en Checkbox, Notas ni Acciones
$(this).html('<input type="text" class="form-control form-control-sm" placeholder="Filtrar..." />');
$('input', this).on('keyup change', function () {
if (dataTable.column(i).search() !== this.value) {
dataTable.column(i).search(this.value).draw();
}
});
}
});
document.getElementById('excelFile').addEventListener('change', function (e) {
const file = e.target.files[0];
if (!file) return;
@ -64,38 +76,39 @@ document.addEventListener('DOMContentLoaded', function () {
const rowData = TABLE_COLUMNS.map(col => data[i][headerMap[col]] ?? '');
// Llamar backend para validar la fila
const isValid = await validarFila(rowData);
const result = await validarFila(rowData);
let checkboxHtml = '';
let actionBtnsHtml = '';
let notaHtml = '';
if (isValid) {
if (result.apto) {
checkboxHtml = `<input type="checkbox" class="select-row form-check-input" checked>`;
notaHtml = '';
actionBtnsHtml = `
<div class="d-flex flex-column align-items-start">
<button type="button" class="btn btn-outline-success btn-sm mb-1 importRow">
<i class="fas fa-file-import me-1"></i> Importar
</button>
<button type="button" class="btn btn-outline-danger btn-sm deleteRow">
<i class="fas fa-trash-alt me-1"></i> Eliminar
</button>
</div>
`;
<div class="d-flex flex-column align-items-start">
<button type="button" class="btn btn-outline-success btn-sm mb-1 importRow">
<i class="fas fa-file-import me-1"></i> Importar
</button>
<button type="button" class="btn btn-outline-danger btn-sm deleteRow">
<i class="fas fa-trash-alt me-1"></i> Eliminar
</button>
</div>
`;
} else {
checkboxHtml = `<input type="checkbox" class="select-row form-check-input" disabled>`;
notaHtml = `<span class="badge bg-danger">${result.reason}</span>`;
actionBtnsHtml = `
<div class="d-flex flex-column align-items-start">
<span class="badge rounded-pill bg-danger mb-2">No Apto</span>
<button type="button" class="btn btn-outline-danger btn-sm deleteRow">
<i class="fas fa-trash-alt me-1"></i> Eliminar
</button>
</div>
`;
<div class="d-flex flex-column align-items-start">
<span class="badge rounded-pill bg-danger mb-2">No Apto</span>
<button type="button" class="btn btn-outline-danger btn-sm deleteRow">
<i class="fas fa-trash-alt me-1"></i> Eliminar
</button>
</div>
`;
}
rows.push([checkboxHtml, ...rowData, actionBtnsHtml]);
rows.push([checkboxHtml, ...rowData, notaHtml, actionBtnsHtml]);
}
dataTable.clear().rows.add(rows).draw();
@ -114,11 +127,10 @@ document.addEventListener('DOMContentLoaded', function () {
body: JSON.stringify({ fila: rowData })
});
const result = await response.json();
return result.apto === true;
return await response.json();
} catch (error) {
console.error('Error validando fila', error);
return false;
return { apto: false, reason: 'Error conexión' };
}
}
@ -127,25 +139,92 @@ document.addEventListener('DOMContentLoaded', function () {
dataTable.row($(this).parents('tr')).remove().draw();
});
$('#excelTable tbody').off('click', '.importRow').on('click', '.importRow', function () {
const rowData = dataTable.row($(this).parents('tr')).data();
console.log('Importar esta fila:', rowData);
// Aquí podrías enviar sólo esta fila al servidor si quieres importar individualmente
});
}
$('#excelTable tbody').off('click', '.importRow').on('click', '.importRow', async function () {
const $row = $(this).closest('tr');
const rowData = dataTable.row($row).data();
if (!rowData) return;
document.getElementById('importBtn').addEventListener('click', function () {
const selectedRows = [];
const fila = rowData.slice(1, 6); // solo datos de negocio
dataTable.rows().every(function () {
const data = this.data();
const checkboxHtml = $(data[0]).find('input.select-row');
if (checkboxHtml.length > 0 && checkboxHtml.is(':checked') && !checkboxHtml.is(':disabled')) {
selectedRows.push(data.slice(1, -1)); // sin checkbox ni botones
try {
const response = await fetch('/importador/catalogo/importar-fila', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '<?= csrf_hash() ?>'
},
body: JSON.stringify({ fila: fila })
});
const result = await response.json();
if (response.ok && result.status === 200) {
const skUrl = result.data?.sk_url?.replace('presupuestocliente', 'presupuestoadmin') ?? null;
// Actualizar campo "Notas" con el enlace
if (skUrl) {
const notasHtml = `<a href="${skUrl}" target="_blank" class="btn btn-sm btn-secondary">Ver presupuesto</a>`;
// La columna de notas es la posición 6 (índice 6)
rowData[6] = notasHtml;
dataTable.row($row).data(rowData).draw(false); // Redibujar la fila SIN perder scroll ni filtros
}
Swal.fire({
title: 'Importado correctamente',
html: skUrl
? `La fila se importó exitosamente.<br><br><a href="${skUrl}" target="_blank" class="btn btn-primary mt-2">Ver presupuesto</a>`
: 'La fila se importó exitosamente.',
icon: 'success',
confirmButtonText: 'Aceptar',
buttonsStyling: true,
customClass: { confirmButton: 'btn btn-success' }
});
} else {
Swal.fire({
title: 'Error',
text: result.message ?? 'Hubo un error al importar esta fila.',
icon: 'error',
confirmButtonText: 'Aceptar',
buttonsStyling: true,
customClass: { confirmButton: 'btn btn-danger' }
});
}
} catch (error) {
console.error('Error en la llamada a importar-fila:', error);
Swal.fire({
title: 'Error',
text: 'Error de comunicación con el servidor.',
icon: 'error',
confirmButtonText: 'Aceptar',
buttonsStyling: true,
customClass: { confirmButton: 'btn btn-danger' }
});
}
});
if (selectedRows.length === 0) {
// Select All funcional
$('#selectAll').off('change').on('change', function () {
const checked = $(this).is(':checked');
$('#excelTable tbody input.select-row:enabled').prop('checked', checked);
});
}
/* Importacion */
document.getElementById('importBtn').addEventListener('click', function () {
const filasAptas = [];
dataTable.rows().every(function () {
const rowNode = this.node();
const checkbox = $(rowNode).find('input.select-row');
if (checkbox.length > 0 && checkbox.is(':checked') && !checkbox.is(':disabled')) {
const rowData = this.data();
filasAptas.push({ fila: rowData.slice(1, 6), rowNode: rowNode, rowData: rowData });
}
});
if (filasAptas.length === 0) {
Swal.fire({
title: 'Atención',
text: 'No hay filas aptas seleccionadas para importar.',
@ -157,35 +236,66 @@ document.addEventListener('DOMContentLoaded', function () {
return;
}
fetch('/importar', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '<?= csrf_hash() ?>'
},
body: JSON.stringify({ data: selectedRows })
}).then(res => res.json())
.then(response => {
Swal.fire({
title: 'Importación exitosa',
text: response.message,
icon: 'success',
confirmButtonText: 'Aceptar',
buttonsStyling: true,
customClass: { confirmButton: 'btn btn-success' }
});
})
.catch(error => {
console.error(error);
Swal.fire({
title: 'Error',
text: 'Error importando datos.',
icon: 'error',
confirmButtonText: 'Aceptar',
buttonsStyling: true,
customClass: { confirmButton: 'btn btn-danger' }
});
Swal.fire({
title: '¿Confirmar importación?',
text: `Se van a importar ${filasAptas.length} filas.`,
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Sí, importar',
cancelButtonText: 'Cancelar',
reverseButtons: true,
buttonsStyling: true,
customClass: {
confirmButton: 'btn btn-primary',
cancelButton: 'btn btn-secondary'
}
}).then(async (result) => {
if (!result.isConfirmed) return;
for (const fila of filasAptas) {
try {
const response = await fetch('/importador/catalogo/importar-fila', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '<?= csrf_hash() ?>'
},
body: JSON.stringify({ fila: fila.fila })
});
const result = await response.json();
if (response.ok && result.status === 200) {
const skUrl = result.data?.sk_url?.replace('presupuestocliente', 'presupuestoadmin') ?? null;
if (skUrl) {
fila.rowData[6] = `<a href="${skUrl}" target="_blank" class="btn btn-sm btn-primary">Ver presupuesto</a>`;
} else {
fila.rowData[6] = `<span class="badge bg-success">Importado</span>`;
}
} else {
fila.rowData[6] = `<span class="badge bg-danger">${result.message ?? 'Error desconocido'}</span>`;
}
dataTable.row(fila.rowNode).data(fila.rowData).draw(false);
} catch (error) {
console.error('Error importando fila:', error);
fila.rowData[6] = `<span class="badge bg-danger">Error de comunicación</span>`;
dataTable.row(fila.rowNode).data(fila.rowData).draw(false);
}
}
Swal.fire({
title: 'Importación finalizada',
text: 'Se han procesado todas las filas seleccionadas.',
icon: 'success',
confirmButtonText: 'Aceptar',
buttonsStyling: true,
customClass: { confirmButton: 'btn btn-success' }
});
});
});
});