This commit is contained in:
amazuecos
2025-05-05 00:47:51 +02:00
20 changed files with 2260 additions and 13 deletions

View File

@ -822,6 +822,7 @@ $routes->group('logistica', ['namespace' => 'App\Controllers\Logistica'], functi
$routes->get('panel', 'LogisticaController::panel', ['as' => 'LogisticaPanel']);
$routes->get('envios', 'LogisticaController::gestionEnvios', ['as' => 'gestionEnvios']);
$routes->get('enviosFerros', 'LogisticaController::gestionEnviosFerros', ['as' => 'gestionEnviosFerros']);
$routes->get('etiquetasLogistica', 'LogisticaController::etiquetasLogistica', ['as' => 'etiquetasLogistica']);
$routes->get('datatableEnvios', 'LogisticaController::datatable_envios');
$routes->get('datatableLineasEnvios/(:num)', 'LogisticaController::datatable_enviosEdit/$1');
$routes->get('envio/(:num)', 'LogisticaController::editEnvio/$1');
@ -845,6 +846,25 @@ $routes->group('logistica', ['namespace' => 'App\Controllers\Logistica'], functi
});
$routes->group('etiquetasTitulos', ['namespace' => 'App\Controllers\Logistica'], function ($routes) {
$routes->get('otList', 'EtiquetasTitulosController::findOTs');
$routes->get('addList', 'EtiquetasTitulosController::findAddresses');
$routes->post('newEtiquetaTitulos', 'EtiquetasTitulosController::addEtiqueta');
$routes->get('datatable', 'EtiquetasTitulosController::datatable');
$routes->post('delete', 'EtiquetasTitulosController::deleteEtiqueta');
$routes->get('edit/(:num)', 'EtiquetasTitulosController::edit/$1');
$routes->get('datatableLineas/(:num)', 'EtiquetasTitulosController::datatableLineasEtiquetas/$1');
$routes->get('findOts', 'EtiquetasTitulosController::findOtsWithAddress');
$routes->post('addLineas', 'EtiquetasTitulosController::addLineasEtiqueta');
$routes->post('deleteLineas', 'EtiquetasTitulosController::deleteLineasEtiqueta');
$routes->post('updateLineas', 'EtiquetasTitulosController::updateLineasEtiqueta');
$routes->post('updateComentarios', 'EtiquetasTitulosController::updateComentarios');
$routes->post('updateOrdenCajas', 'EtiquetasTitulosController::updateOrdenCajas');
$routes->post('renumber', 'EtiquetasTitulosController::renumberCajas');
$routes->post('imprimirEtiquetas', 'EtiquetasTitulosController::imprimirEtiquetas');
});
/*
* --------------------------------------------------------------------
* Translation

View File

@ -0,0 +1,467 @@
<?php
namespace App\Controllers\Logistica;
use App\Controllers\BaseController;
use App\Services\ImpresoraEtiquetaService;
use App\Services\EtiquetasTitulosService;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Psr\Log\LoggerInterface;
use Hermawan\DataTables\DataTable;
class EtiquetasTitulosController extends BaseController
{
protected ImpresoraEtiquetaService $impresoraEtiquetaService;
protected string $locale;
protected array $viewData;
protected static $controllerSlug = 'etiquetas_titulos';
protected static $viewPath = 'themes/vuexy/form/logistica/';
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
{
$this->impresoraEtiquetaService = service('impresora_etiqueta');
$this->locale = session()->get('lang');
$this->model = model('App\Models\Etiquetas\EtiquetasTitulosModel');
$this->viewData['pageTitle'] = lang('Logistica.logistica');
// Breadcrumbs
$this->viewData['breadcrumb'] = [
['title' => lang("App.menu_logistica"), 'route' => route_to("LogisticaPanel"), 'active' => false],
];
parent::initController($request, $response, $logger);
}
public function findOTs()
{
if ($this->request->isAJAX()) {
$query = EtiquetasTitulosService::getOtsWithTitulos();
if ($this->request->getGet("q")) {
$query->groupStart()
->orLike("ot.id", $this->request->getGet("q"))
->orLike("pr.titulo)", $this->request->getGet("q"))
->groupEnd();
}
$result = $query->orderBy("id", "DESC")->get()->getResultObject();
return $this->response->setJSON($result);
} else {
return $this->failUnauthorized('Invalid request', 403);
}
}
public function findAddresses()
{
if ($this->request->isAJAX()) {
$ot_id = $this->request->getGet("ot_id");
$query = EtiquetasTitulosService::getDireccionesOT($ot_id);
if ($this->request->getGet("q")) {
$query->groupStart()
->orLike("pd.direccion", $this->request->getGet("q"))
->groupEnd();
}
$result = $query->orderBy("pd.direccion", "ASC")->get()->getResultObject();
return $this->response->setJSON($result);
} else {
return $this->failUnauthorized('Invalid request', 403);
}
}
public function addEtiqueta()
{
if ($this->request->isAJAX()) {
$data = [];
$data['user_id'] = auth()->user()->id;
$data['ot_id'] = $this->request->getPost('ot_id') ?? null;
$data['direccion'] = $this->request->getPost('direccion') ?? null;
$data['unidades_caja'] = $this->request->getPost('unidades_caja') ?? null;
if (
$this->request->getPost('ot_id') == null ||
$this->request->getPost('direccion') == null ||
$this->request->getPost('unidades_caja') == null
) {
return [
'status' => false,
'message' => lang('Logistica.errorMissingData')
];
}
$result = EtiquetasTitulosService::addEtiqueta($data);
return $this->response->setJSON($result);
} else {
return $this->failUnauthorized('Invalid request', 403);
}
}
public function deleteEtiqueta($id = null)
{
if ($this->request->isAJAX()) {
$id = $this->request->getPost('id') ?? null;
if ($id == null) {
return [
'status' => false,
'message' => lang('Logistica.errorMissingData')
];
}
$modelLineas = model('App\Models\Etiquetas\EtiquetasTitulosLineasModel');
$ids = $modelLineas->where('etiqueta_titulos_id', $id)->findColumn('id');
if ($ids) {
$modelLineas->delete($ids);
}
$model = model('App\Models\Etiquetas\EtiquetasTitulosModel');
$id = $model->where('id', $id)->findColumn('id');
if ($id) {
$model->delete($id);
}
$result = [
'status' => true,
'message' => lang('Logistica.success.jhn<successDeleteEtiqueta'),
];
return $this->response->setJSON($result);
} else {
return $this->failUnauthorized('Invalid request', 403);
}
}
public function edit($id = null)
{
if (empty($id)) {
return redirect()->to(base_url('logistica/selectEnvios/simple'))->with('error', lang('Logistica.errors.noEnvio'));
}
$model = model('App\Models\Etiquetas\EtiquetasTitulosModel');
$etiquetaEntity = $model->select('etiquetas_titulos.*, clientes.nombre as cliente')
->join('clientes', 'clientes.id = etiquetas_titulos.cliente_id', 'left')
->where('etiquetas_titulos.id', $id)
->first();
if (empty($etiquetaEntity)) {
return redirect()->to(base_url('logistica/etiquetasLogistica'))->with('error', lang('Logistica.errors.noEnvio'));
}
$modelImpresora = model('App\Models\Configuracion\ImpresoraEtiquetaModel');
$impresoras = $modelImpresora->select('id, name')
->where('deleted_at', null)
->where('tipo', 1)
->orderBy('name', 'asc')
->findAll();
$etiquetaEntity->impresoras = $impresoras;
$viewData = [
'currentModule' => static::$controllerSlug,
'boxTitle' => '<i class="ti ti-ticket ti-xl"></i>' . ' ' . lang('Logistica.EtiquetasTitulos') . ' [' . $etiquetaEntity->id . ']: ' . $etiquetaEntity->direccion,
'usingServerSideDataTable' => true,
'etiquetaEntity' => $etiquetaEntity,
];
$viewData = array_merge($this->viewData, $viewData); // merge any possible values from the parent controller class
return view(static::$viewPath . 'viewEtiquetasTitulosEdit', $viewData);
}
public function datatable()
{
$q = $this->model->getEtiquetasTitulos();
if (!empty($otsFilter)) {
$q->groupStart();
$q->like('etl.ot_id', $otsFilter);
$q->groupEnd();
}
$result = DataTable::of($q)
->add("action", callback: function ($q) {
return '
<div class="btn-group btn-group-sm">
<a href="javascript:void(0);"><i class="ti ti-pencil ti-sm btn-edit mx-2" data-id="' . $q->id . '"></i></a>
<a href="javascript:void(0);"><i class="ti ti-trash ti-sm btn-delete" data-id="' . $q->id . '"></i></a>
</div>
';
});
return $result->toJson(returnAsObject: true);
}
public function findOtsWithAddress()
{
if ($this->request->isAJAX()) {
$id = $this->request->getGet('id') ?? null;
$query = EtiquetasTitulosService::findOTsWithAddress($id);
if ($this->request->getGet("q")) {
$query->groupStart()
->orLike("name", $this->request->getGet("q"))
->groupEnd();
}
$result = $query->orderBy("id", "DESC")->get()->getResultObject();
return $this->response->setJSON($result);
} else {
return $this->failUnauthorized('Invalid request', 403);
}
}
public function addLineasEtiqueta()
{
if ($this->request->isAJAX()) {
$etiqueta_id = $this->request->getPost('etiqueta_id') ?? null;
$ot_id = $this->request->getPost('ot_id') ?? null;
$unidades = $this->request->getPost('unidades') ?? null;
$cajas = $this->request->getPost('cajas') ?? null;
$result = EtiquetasTitulosService::addLineasEtiqueta($etiqueta_id, $ot_id, $unidades, $cajas);
return $this->response->setJSON($result);
} else {
return $this->failUnauthorized('Invalid request', 403);
}
}
public function datatableLineasEtiquetas($id = null)
{
$model = model('App\Models\Etiquetas\EtiquetasTitulosLineasModel');
$id = $this->request->getGet('id') ?? null;
$direccion = $this->request->getGet('direccion') ?? null;
$q = $model->getDatatableQuery($id, $direccion);
$result = DataTable::of($q)
->add(
"rowSelected",
callback: function ($q) {
return '<input type="checkbox" class="form-check-input checkbox-linea-envio" name="row_selected[]" value="' . $q->id . '">';
}
)
->edit(
"ot",
function ($row, $meta) {
return '<a href="' . base_url('produccion/ordentrabajo/edit/' . $row->ot) . '" target="_blank">' . $row->ot . '</a>';
}
)
->edit(
"unidades",
function ($row, $meta) {
return '<input type="number" class="form-control input-lineas input-unidades text-center"
data-id="' . $row->id . '" data-name="unidades" value="' . $row->unidades . '">';
}
)
->edit(
"numero_caja",
function ($row, $meta) {
return '<input type="number" class="form-control input-lineas input-cajas text-center"
data-id="' . $row->id . '" data-name="numero_caja" value="' . $row->numero_caja . '">';
}
)
->add("unidades_raw", fn($row) => $row->unidades)
->add("pesoUnidad_raw", fn($row) => $row->pesoUnidad)
->add(
"action",
callback: function ($q) {
return '
<div class="btn-group btn-group-sm">
<a href="javascript:void(0);"><i class="ti ti-trash ti-sm btn-delete" data-id="' . $q->id . '"></i></a>
</div>
';
}
);
return $result->toJson(returnAsObject: true);
}
public function deleteLineasEtiqueta()
{
if ($this->request->isAJAX()) {
$ids = $this->request->getPost('ids') ?? [];
if ($ids == [] || $ids == null) {
return [
'status' => false,
'message' => lang('Logistica.errors.errorMissingData')
];
}
$model = model('App\Models\Etiquetas\EtiquetasTitulosLineasModel');
for ($i = 0; $i < count($ids); $i++) {
$model->delete($ids[$i]);
}
$result = [
'status' => true,
'message' => lang('Logistica.success.successDeleteLines'),
];
return $this->response->setJSON($result);
} else {
return $this->failUnauthorized('Invalid request', 403);
}
}
public function updateLineasEtiqueta()
{
if ($this->request->isAJAX()) {
$id = $this->request->getPost('id') ?? null;
$name = $this->request->getPost('name') ?? null;
$value = $this->request->getPost('value') ?? null;
if ($id == null || $name == null || $value == null) {
return [
'status' => false,
'message' => lang('Logistica.errors.errorMissingData')
];
}
$model = model('App\Models\Etiquetas\EtiquetasTitulosLineasModel');
$model->update($id, [$name => $value]);
$result = [
'status' => true,
'message' => lang('Logistica.success.successUpdateLine'),
];
return $this->response->setJSON($result);
} else {
return $this->failUnauthorized('Invalid request', 403);
}
}
public function updateComentarios()
{
if ($this->request->isAJAX()) {
$id = $this->request->getPost('id') ?? null;
$comentarios = $this->request->getPost('comentarios') ?? null;
if ($id == null || $comentarios == null) {
return [
'status' => false,
'message' => lang('Logistica.errors.errorMissingData')
];
}
$model = model('App\Models\Etiquetas\EtiquetasTitulosModel');
$model->update($id, ['comentarios' => $comentarios]);
$result = [
'status' => true,
'message' => lang('Logistica.success.comentariosUpdated'),
];
return $this->response->setJSON($result);
} else {
return $this->failUnauthorized('Invalid request', 403);
}
}
public function updateOrdenCajas()
{
$rawInput = $this->request->getBody();
$data = json_decode($rawInput, true);
$orden = $data['orden'] ?? [];
if (!is_array($orden)) {
return $this->response->setJSON([
'status' => false,
'message' => 'Datos inválidos'
]);
}
$model = model('App\Models\Etiquetas\EtiquetasTitulosLineasModel');
foreach ($orden as $item) {
if (isset($item['id'], $item['numero_caja'])) {
$model->update($item['id'], [
'numero_caja' => $item['numero_caja'],
'updated_at' => date('Y-m-d H:i:s') // opcional
]);
}
}
return $this->response->setJSON([
'status' => true,
'message' => 'Orden de cajas actualizado correctamente'
]);
}
public function renumberCajas()
{
$id = $this->request->getPost('id') ?? null;
if ($id == null) {
return [
'status' => false,
'message' => lang('Logistica.errors.errorMissingData')
];
}
$result = EtiquetasTitulosService::reordenarCajas($id);
return $this->response->setJSON($result);
}
public function imprimirEtiquetas()
{
$etiqueta_id = $this->request->getPost('etiqueta_id') ?? null;
$ids = $this->request->getPost('ids') ?? [];
$impresora_id = $this->request->getPost('impresora_id') ?? null;
$modelImpresora = model('App\Models\Configuracion\ImpresoraEtiquetaModel');
$impresora = $modelImpresora->select('id, name, ip, port, user, pass')
->where('deleted_at', null)
->where('id', $impresora_id)
->orderBy('name', 'asc')
->first();
if ($impresora == null) {
return $this->response->setJSON([
'status' => false,
'message' => 'Impresora no válida'
]);
}
if ($etiqueta_id == null || $ids == []) {
return [
'status' => false,
'message' => lang('Logistica.errors.errorMissingData')
];
}
$result = EtiquetasTitulosService::imprimirEtiquetas($etiqueta_id, $ids, $impresora);
return $this->response->setJSON($result);
}
}

View File

@ -85,6 +85,19 @@ class LogisticaController extends BaseController
return view(static::$viewPath . 'viewLogisticaSelectEnvios', $viewData);
}
public function etiquetasLogistica()
{
$viewData = [
'currentModule' => static::$controllerSlug,
'boxTitle' => lang('Logistica.etiquetasTitulos'),
'usingServerSideDataTable' => true,
];
$viewData = array_merge($this->viewData, $viewData); // merge any possible values from the parent controller class
return view(static::$viewPath . 'viewImpresionEtiquetas', $viewData);
}
public function listAlbaranes(){
$viewData = [
'currentModule' => static::$controllerSlug,

View File

@ -0,0 +1,60 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateEtiquetasTitulos extends Migration
{
public function up()
{
// Tabla: etiquetas_titulos
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'comentarios' => ['type' => 'TEXT', 'null' => true],
'direccion' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => false],
'att' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => false],
'cliente_id' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
'deleted_at' => ['type' => 'DATETIME', 'null' => true],
'user_created_at' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
'user_updated_at' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
'user_deleted_at' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addForeignKey('user_created_at', 'users', 'id', 'SET NULL', 'CASCADE');
$this->forge->addForeignKey('user_updated_at', 'users', 'id', 'SET NULL', 'CASCADE');
$this->forge->addForeignKey('user_deleted_at', 'users', 'id', 'SET NULL', 'CASCADE');
$this->forge->addForeignKey('cliente_id', 'clientes', 'id', 'SET NULL', 'CASCADE');
$this->forge->createTable('etiquetas_titulos');
// Tabla: etiquetas_titulos_lineas
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'etiqueta_titulos_id' => ['type' => 'INT', 'unsigned' => true],
'ot_id' => ['type' => 'INT', 'unsigned' => true],
'unidades' => ['type' => 'INT', 'unsigned' => true],
'numero_caja' => ['type' => 'INT', 'unsigned' => true],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
'deleted_at' => ['type' => 'DATETIME', 'null' => true],
'user_created_at' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
'user_updated_at' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
'user_deleted_at' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addForeignKey('etiqueta_titulos_id', 'etiquetas_titulos', 'id', 'CASCADE', 'CASCADE');
$this->forge->addForeignKey('ot_id', 'ordenes_trabajo', 'id', 'CASCADE', 'CASCADE');
$this->forge->addForeignKey('user_created_at', 'users', 'id', 'SET NULL', 'CASCADE');
$this->forge->addForeignKey('user_updated_at', 'users', 'id', 'SET NULL', 'CASCADE');
$this->forge->addForeignKey('user_deleted_at', 'users', 'id', 'SET NULL', 'CASCADE');
$this->forge->createTable('etiquetas_titulos_lineas');
}
public function down()
{
$this->forge->dropTable('etiquetas_titulos_lineas', true);
$this->forge->dropTable('etiquetas_titulos', true);
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Entities\Etiquetas;
use App\Models\Etiquetas\EtiquetasTitulosLineasModel;
use CodeIgniter\Entity\Entity;
class EtiquetaTitulo extends Entity
{
protected $dates = ['created_at', 'updated_at', 'deleted_at'];
public function getLineas()
{
$model = new EtiquetasTitulosLineasModel();
return $model->where('etiqueta_titulos_id', $this->id)->findAll();
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Entities\Etiquetas;
use CodeIgniter\Entity\Entity;
class EtiquetaTituloLinea extends Entity
{
protected $dates = ['created_at', 'updated_at', 'deleted_at'];
}

View File

@ -753,7 +753,7 @@ return [
"menu_informes" => "Informes",
"menu_pedidos" => "Pedidos",
"menu_pedidos_validacion" => "Validación",
"menu_pedidos_validacion" => "Aprobación",
"menu_pedidos_activos" => "Producción",
"menu_pedidos_finalizados" => "Finalizados",
"menu_pedidos_cancelados" => "Cancelados",

View File

@ -65,16 +65,42 @@ return [
'finalizarEnvio' => 'Finalizar envío',
'finalizarEnvioYOTs' => 'Finalizar envío y OTS',
'EtiquetasTitulos' => 'Etiquetas de títulos',
'id' => 'ID',
'otId' => 'OT ID',
'num_caja' => 'Nº Caja',
'unidadesTotalesOt' => 'Unidades totales OT',
'numeroCajas' => 'Nº Cajas',
'unidadesEnCaja' => 'Unidades en caja',
'listadoEtiquetas' => 'Listado de etiquetas',
'nuevaEtiqueta' => 'Nueva etiqueta',
'cliente' => 'Cliente',
'comentariosEtiqueta' => 'Comentarios etiqueta',
'addLineaEtiqueta' => 'Añadir líneas a la etiqueta',
'renumber' => 'Renumerar etiquetas',
'errors' => [
'noEnvio' => 'No se ha encontrado el envio',
'noEnvioLineas' => 'No se han encontrado líneas de envío',
'noDataToFind' => 'No se ha introducido ningún dato para buscar',
'notFound' => 'No se encuentra el pedido o ISBN, el pedido no está en producción o finalizado o no tiene envíos pendientes',
'noAddresses' => 'El pedido no tiene direcciones de envío',
'errorMissingData' => 'Faltan datos para crear la etiqueta',
'errorInsertarEtiqueta' => 'Error al insertar la etiqueta',
'noEtiqueta' => 'No se ha encontrado la etiqueta',
'noEtiquetaLineas' => 'No se han encontrado líneas de etiqueta',
],
'success' => [
'finalizado' => 'El envío se ha finalizado correctamente',
'finalizadoOTs' => 'El envío se ha finalizado correctamente.\nSe han creado las OTs siguientes OTs: {ots}',
'successDeleteEtiqueta' => 'Etiqueta eliminada correctamente',
'successInsertLines' => 'Lineas de etiqueta insertadas correctamente',
'successDeleteLines' => 'Lineas de etiqueta eliminadas correctamente',
'successUpdateLine' => 'Linea de etiqueta actualizadas correctamente',
'comentariosUpdated' => 'Comentarios actualizados correctamente',
'successReordenarCajas' => 'Cajas reordenadas correctamente',
'imprimirEtiquetas' => 'Etiquetas impresas correctamente',
],
];

View File

@ -0,0 +1,108 @@
<?php
namespace App\Models\Etiquetas;
use CodeIgniter\Model;
use App\Entities\Etiquetas\EtiquetaTituloLinea;
class EtiquetasTitulosLineasModel extends Model
{
protected $table = 'etiquetas_titulos_lineas';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = EtiquetaTituloLinea::class;
protected $useSoftDeletes = true;
protected $allowedFields = [
'etiqueta_titulos_id',
'ot_id',
'unidades',
'numero_caja',
'user_created_at',
'user_updated_at',
'user_deleted_at',
];
protected $useTimestamps = true;
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $deletedField = 'deleted_at';
protected $validationRules = [];
protected $validationMessages = [];
protected $skipValidation = false;
protected $beforeDelete = ['addUserDeleted'];
protected $beforeUpdate = ['addUserUpdated'];
protected function addUserDeleted(array $data)
{
$userId = auth()->user()->id;
if (!isset($data['data'])) {
$data['data'] = [];
}
if (!empty($data['id'])) {
$builder = $this->builder();
$builder->whereIn($this->primaryKey, (array) $data['id'])
->update(['user_deleted_at' => $userId]);
}
return $data;
}
protected function addUserUpdated(array $data)
{
$userId = auth()->user()->id;
if (!isset($data['data'])) {
$data['data'] = [];
}
if (!empty($data['id'])) {
$builder = $this->builder();
$builder->whereIn($this->primaryKey, (array) $data['id'])
->update(['user_updated_at' => $userId]);
}
return $data;
}
public function getDatatableQuery($etiqueta_id, $direccion = null)
{
$direccionNormalizada = str_replace(' ', '', strtolower(trim($direccion)));
// Subconsulta: suma de pesos por presupuesto
$subPeso = $this->db->table('presupuesto_linea')
->select('presupuesto_id, ROUND(SUM(peso)/1000, 2) as pesoUnidad')
->groupBy('presupuesto_id');
$builder = $this->db->table('etiquetas_titulos_lineas etl')
->select('
etl.id as id,
etl.ot_id as ot,
pr.titulo as titulo,
etl.unidades as unidades,
etl.numero_caja as numero_caja,
etl.numero_caja as numero_caja_raw,
pd.cantidad as unidadesTotal,
etl.id as id,
etl.unidades as unidadesRaw,
peso_sub.pesoUnidad
')
->join('etiquetas_titulos et', 'et.id = etl.etiqueta_titulos_id')
->join('ordenes_trabajo ot', 'ot.id = etl.ot_id')
->join('pedidos p', 'p.id = ot.pedido_id')
->join('pedidos_linea pl', 'pl.pedido_id = p.id')
->join('presupuestos pr', 'pr.id = pl.presupuesto_id')
->join("({$subPeso->getCompiledSelect()}) as peso_sub", 'peso_sub.presupuesto_id = pr.id', 'left')
->join('presupuesto_direcciones pd', 'pd.presupuesto_id = pr.id')
->where('etl.deleted_at IS NULL')
->where('et.id', $etiqueta_id)
->where("REPLACE(LOWER(TRIM(pd.direccion)), ' ', '') = '{$direccionNormalizada}'", null, false)
->orderBy('etl.numero_caja');
return $builder;
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace App\Models\Etiquetas;
use CodeIgniter\Model;
use App\Entities\Etiquetas\EtiquetaTitulo;
class EtiquetasTitulosModel extends Model
{
protected $table = 'etiquetas_titulos';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = EtiquetaTitulo::class;
protected $useSoftDeletes = true;
protected $allowedFields = [
'comentarios',
'direccion',
'att',
'cliente_id',
'user_created_at',
'user_updated_at',
'user_deleted_at',
];
protected $useTimestamps = true;
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $deletedField = 'deleted_at';
protected $validationRules = [];
protected $validationMessages = [];
protected $skipValidation = false;
protected $beforeDelete = ['addUserDeleted'];
protected $beforeUpdate = ['addUserUpdated'];
protected function addUserDeleted(array $data)
{
$userId = auth()->user()->id;
if (!isset($data['data'])) {
$data['data'] = [];
}
if (!empty($data['id'])) {
$builder = $this->builder();
$builder->whereIn($this->primaryKey, (array) $data['id'])
->update(['user_deleted_at' => $userId]);
}
return $data;
}
protected function addUserUpdated(array $data)
{
$userId = auth()->user()->id;
if (!isset($data['data'])) {
$data['data'] = [];
}
if (!empty($data['id'])) {
$builder = $this->builder();
$builder->whereIn($this->primaryKey, (array) $data['id'])
->update(['user_updated_at' => $userId]);
}
return $data;
}
public function getEtiquetasTitulos()
{
return $this->db->table('etiquetas_titulos et')
->select('et.id, GROUP_CONCAT(DISTINCT etl.ot_id ORDER BY etl.ot_id ASC SEPARATOR ", ") as lista_ots')
->select('COUNT(DISTINCT etl.numero_caja) as cajas, et.att, et.direccion')
->join('etiquetas_titulos_lineas etl', 'etl.etiqueta_titulos_id = et.id')
->where('et.deleted_at IS NULL')
->where('etl.deleted_at IS NULL')
->groupBy('et.id, et.att, et.direccion');
}
}

View File

@ -0,0 +1,364 @@
<?php
namespace App\Services;
use Config\Services;
class EtiquetasTitulosService
{
public static function getOtsWithTitulos()
{
$db = \Config\Database::connect();
// 3. Subconsulta principal
$builder = $db->table('ordenes_trabajo ot')
->select("
ot.id AS id,
CONCAT('[', ot.id, '] - ', pr.titulo) AS name")
->join('pedidos p', 'p.id = ot.pedido_id')
->join('pedidos_linea pl', 'p.id = pl.pedido_id')
->join('presupuestos pr', 'pr.id = pl.presupuesto_id')
->join('orden_trabajo_dates ot_dates', 'ot_dates.orden_trabajo_id = ot.id')
->whereIn('p.estado', ['finalizado', 'produccion']);
return $builder;
}
public static function getDireccionesOT($ot_id)
{
$db = \Config\Database::connect();
// 3. Subconsulta principal
$builder = $db->table('presupuesto_direcciones pd')
->select("
pd.id AS id,
pd.direccion AS name")
->join('presupuestos pr', 'pr.id = pd.presupuesto_id')
->join('pedidos_linea pl', 'pr.id = pl.presupuesto_id')
->join('pedidos p', 'p.id = pl.pedido_id')
->join('ordenes_trabajo ot', 'ot.pedido_id = p.id')
->join('orden_trabajo_dates ot_dates', 'ot_dates.orden_trabajo_id = ot.id')
->whereIn('p.estado', ['finalizado', 'produccion'])
->where('ot.id', $ot_id);
return $builder;
}
public static function addEtiqueta($data)
{
$db = \Config\Database::connect();
$builder = $db->table('presupuesto_direcciones pd');
$builder->select('pd.att, pd.direccion, pd.cantidad, pr.cliente_id');
$builder->join('presupuestos pr', 'pr.id = pd.presupuesto_id');
$builder->join('pedidos_linea pl', 'pr.id = pl.presupuesto_id');
$builder->join('pedidos p', 'p.id = pl.pedido_id');
$builder->join('ordenes_trabajo ot', 'ot.pedido_id = p.id');
$builder->where('ot.id', $data['ot_id']);
$builder->where('pd.direccion', $data['direccion']);
$result = $builder->get()->getRow();
$data['att'] = $result->att;
$data['direccion'] = $result->direccion;
$data['cantidad'] = $result->cantidad;
$data['cliente_id'] = $result->cliente_id;
$modelEtiquetasTitulos = model('App\Models\Etiquetas\EtiquetasTitulosModel');
$modelEtiquetasTitulos->insert([
'direccion' => $data['direccion'],
'att' => $data['att'],
'cliente_id' => $data['cliente_id'],
'user_created_at' => $data['user_id'],
]);
$etiquetaId = $modelEtiquetasTitulos->getInsertID();
if ($etiquetaId == null) {
return [
'status' => false,
'message' => lang('Logistica.errorInsertarEtiqueta'),
];
}
$cantidad_restante = intval($data['cantidad']);
$numero_caja = 1;
while ($cantidad_restante > 0) {
$modelEtiquetasTitulosLineas = model('App\Models\Etiquetas\EtiquetasTitulosLineasModel');
$modelEtiquetasTitulosLineas->insert([
'etiqueta_titulos_id' => $etiquetaId,
'ot_id' => $data['ot_id'],
'unidades' => $cantidad_restante - intval($data['unidades_caja']) < 0 ? $cantidad_restante : intval($data['unidades_caja']),
'numero_caja' => $numero_caja,
'user_created_at' => $data['user_id'],
]);
$cantidad_restante -= $data['unidades_caja'];
$numero_caja++;
}
return [
'status' => true,
'etiqueta' => $etiquetaId,
];
}
public static function findOTsWithAddress(int $etiqueta_id)
{
$db = \Config\Database::connect();
// 1. Dirección del envío actual
$etiqueta = $db->table('etiquetas_titulos')->select('direccion')->where('id', $etiqueta_id)->get()->getRow();
if (!$etiqueta) {
return $db->table('(SELECT NULL AS id, NULL AS name, NULL as unidades) AS empty')->where('1 = 0');
}
$direccionNormalizada = str_replace(' ', '', strtolower(trim($etiqueta->direccion)));
$direccionSQL = $db->escape($direccionNormalizada);
// 2. Obtener presupuestos con esa dirección
$presupuestosConEsaDireccion = $db->table('presupuesto_direcciones')
->select('presupuesto_id')
->where("REPLACE(LOWER(TRIM(direccion)), ' ', '') = $direccionSQL", null, false)
->get()
->getResultArray();
$presupuestoIds = array_column($presupuestosConEsaDireccion, 'presupuesto_id');
if (empty($presupuestoIds)) {
return $db->table('(SELECT NULL AS id, NULL AS name, NULL as unidades) AS empty')->where('1 = 0');
}
// 3. Subconsulta principal
$subBuilder = $db->table('pedidos_linea pl')
->select("
ot.id AS id,
CONCAT('[', ot.id, '] - ', pr.titulo) AS name,
pd.cantidad AS description
")
->join('pedidos p', 'p.id = pl.pedido_id')
->join('presupuestos pr', 'pr.id = pl.presupuesto_id')
->join('presupuesto_direcciones pd', 'pd.presupuesto_id = pr.id')
->join('ordenes_trabajo ot', 'ot.pedido_id = p.id')
->join('orden_trabajo_dates ot_dates', 'ot_dates.orden_trabajo_id = ot.id')
->whereIn('pr.id', $presupuestoIds)
->whereIn('p.estado', ['finalizado', 'produccion'])
->groupBy('pl.id');
// 4. Envolver y filtrar por unidades pendientes
$builder = $db->table("({$subBuilder->getCompiledSelect(false)}) AS sub");
$builder->select('id, name, description');
return $builder;
}
public static function addLineasEtiqueta($etiqueta_id, $ot_id, $unidades, $cajas)
{
$unidades_caja = intdiv($unidades, $cajas);
$unidades_caja = $unidades_caja == 0 ? $unidades : $unidades_caja;
$unidades_restantes = $unidades;
$modelEtitquetaLinea = model('\App\Models\Etiquetas\EtiquetasTitulosLineasModel');
$next_caja = $modelEtitquetaLinea
->selectMax('numero_caja')
->where('etiqueta_titulos_id', $etiqueta_id)
->first();
$next_caja = $next_caja->numero_caja ?? 0;
$next_caja++;
$user_id = auth()->user()->id;
while ($unidades_restantes > 0) {
$modelEtitquetaLinea->insert([
'etiqueta_titulos_id' => $etiqueta_id,
'ot_id' => $ot_id,
'unidades' => $unidades_restantes - $unidades_caja < 0 ? $unidades_restantes : intval($unidades_caja),
'numero_caja' => $next_caja,
'user_created_at' => $user_id,
]);
$unidades_restantes -= $unidades_caja;
$next_caja++;
}
return [
'status' => true,
'message' => lang('Logistica.success.successInsertLines'),
];
}
public static function reordenarCajas($etiqueta_id)
{
$model = model('App\Models\Etiquetas\EtiquetasTitulosLineasModel');
// 1. Obtener todas las líneas ordenadas por numero_caja e id
$lineas = $model
->where('etiqueta_titulos_id', $etiqueta_id)
->orderBy('numero_caja ASC')
->orderBy('id ASC')
->findAll();
// 2. Agrupar por caja
$grupos = [];
foreach ($lineas as $linea) {
$grupos[$linea->numero_caja][] = $linea;
}
// 3. Reasignar números de caja de forma consecutiva
$nuevoNumeroCaja = 1;
foreach ($grupos as $grupo) {
foreach ($grupo as $linea) {
$model->update($linea->id, ['numero_caja' => $nuevoNumeroCaja]);
}
$nuevoNumeroCaja++;
}
return [
'status' => true,
'message' => lang('Logistica.success.successReordenarCajas'),
];
}
public static function imprimirEtiquetas($etiqueta_id = null, $ids = [], $printer = null){
$model = model('App\Models\Etiquetas\EtiquetasTitulosModel');
$modelLineas = model('App\Models\Etiquetas\EtiquetasTitulosLineasModel');
if ($etiqueta_id) {
$etiquetas = $model->where('id', $etiqueta_id)->first();
$etiquetas_lineas = $modelLineas->whereIn('id', $ids)->orderBy('numero_caja', 'ASC')->findAll();
}
// buscar el maximo numero_cajas en el array de objetos etiquetas_lineas
$max_num_cajas = 0;
foreach ($etiquetas_lineas as $linea) {
if ($linea->numero_caja > $max_num_cajas) {
$max_num_cajas = $linea->numero_caja;
}
}
// se obtienen los numero_caja diferentes en un array
$numero_cajas = [];
foreach ($etiquetas_lineas as $linea) {
if (!in_array($linea->numero_caja, $numero_cajas)) {
$numero_cajas[] = $linea->numero_caja;
}
}
if (empty($etiquetas)) {
return [
'status' => false,
'message' => lang('Logistica.errors.noEtiqueta'),
];
}
if (empty($etiquetas_lineas)) {
return [
'status' => false,
'message' => lang('Logistica.errors.noEtiquetaLineas'),
];
}
$data = [
"printer" => $printer->name,
"header" => [
"_FORMAT" => "E:MULTI.ZPL",
"_QUANTITY" => 1,
"_PRINBTERNAME" => $printer->name,
"_JOBNAME" => "LBL101"
],
'direccion' => $etiquetas->direccion,
'grupos' => [],
'notas' => $etiquetas->comentarios,
'nombre' => $etiquetas->att,
];
$index_etiqueta = 0;
foreach ($numero_cajas as $caja) {
array_push($data['grupos'], [
]);
$prefix = 1;
$lineas = array_filter($etiquetas_lineas, function ($linea) use ($caja) {
return $linea->numero_caja == $caja;
});
$lineaCounter = 0;
foreach($lineas as $linea){
if($lineaCounter >= 5){
$lineaCounter = 0;
$index_etiqueta++;
array_push($data['grupos'], []);
}
$modelPresupuestos = model('App\Models\OrdenTrabajo\OrdenTrabajoModel');
$datos_etiqueta = $modelPresupuestos->select('
pr.titulo as titulo,
pr.isbn as isbn,
pr.referencia_cliente as referencia_cliente,
p.id as id_pedido,
ordenes_trabajo.total_tirada as total_tirada,')
->join('pedidos p', 'p.id = ordenes_trabajo.pedido_id')
->join('pedidos_linea pl', 'pl.pedido_id = p.id')
->join('presupuestos pr', 'pr.id = pl.presupuesto_id')
->where('ordenes_trabajo.id',$linea->ot_id)->first();
$data['grupos'][$index_etiqueta][] = [
'prefix' => $caja,
'titulo' => mb_substr($datos_etiqueta->titulo, 0, 40),
'cantidad' => $linea->unidades,
'tirada' => $datos_etiqueta->total_tirada,
'ean' => str_replace('-', '', $datos_etiqueta->isbn),
'npedido' => $datos_etiqueta->id,
'refcliente' => $datos_etiqueta->referencia_cliente,
];
$lineaCounter++;
}
$index_etiqueta++;
}
$servicioImpresora = new ImpresoraEtiquetaService();
$xml = $servicioImpresora->createEtiquetaTitulos($data);
if ($xml == null) {
return [
'status' => false,
'message' => lang('Logistica.errors.noEtiquetas'),
];
}
$sk_environment = getenv('SK_ENVIRONMENT');
if ($sk_environment == 'production') {
$status = $servicioImpresora->sendToImpresoraEtiqueta("ETIQUETA", $xml, $printer);
if ($status) {
return [
'status' => true,
'message' => lang('Logistica.success.imprimirEtiquetas'),
];
} else {
return [
'status' => false,
'message' => lang('Logistica.errors.noEtiquetas'),
];
}
} else {
return [
'status' => true,
'message' => lang('Logistica.success.imprimirEtiquetas'),
'data' => $xml
];
}
}
}

View File

@ -24,9 +24,9 @@ class ImpresoraEtiquetaService extends BaseService
"labels" => [
[
"cliente" => "Cliente Potencial",
"titulo" => "[1234] TEST OLIVEROS",
"titulo" => "[1234] TEST OLIVEROS",
"cantidad" => 100,
"tirada" => 50,
"tirada" => 50,
"cajas" => 1,
"ean" => null,
"nombre" => "___Nombre___",
@ -76,6 +76,66 @@ class ImpresoraEtiquetaService extends BaseService
$xml->appendChild($labels);
return $xml->saveXML();
}
public function createEtiquetaTitulos(array $data_label = []): ?string
{
$xml = new DOMDocument('1.0', 'utf-8');
// Crear nodo raíz "labels"
$labels = $xml->createElement("labels");
// Establecer atributos de "labels" desde "header"
foreach ($data_label["header"] as $key => $value) {
$labels->setAttribute($key, $value);
}
// Recorrer grupos de etiquetas
foreach ($data_label["grupos"] as $grupo) {
$labelChild = $xml->createElement('label');
// Crear variables específicas del grupo
foreach ($grupo as $libro) {
$prefix = $libro['prefix'];
$variables = [
"titulo$prefix" => $libro['titulo'],
"tirada$prefix" => $libro['tirada'],
"ean$prefix" => $libro['ean'],
"npedido$prefix" => $libro['npedido'],
"refcliente$prefix" => $libro['refcliente'],
"textpedido$prefix" => 'Pedido',
"textcantidad$prefix" => 'Cantidad:',
"textref$prefix" => 'Ref:',
"textean$prefix" => 'ISBN',
];
foreach ($variables as $varName => $varValue) {
$variableChild = $xml->createElement('variable');
$variableChild->setAttribute("name", $varName);
$variableChild->appendChild($xml->createTextNode((string) $varValue));
$labelChild->appendChild($variableChild);
}
}
// Variables comunes del grupo
$nombreVariable = $xml->createElement('variable', htmlspecialchars($data_label['nombre']));
$nombreVariable->setAttribute('name', 'nombre');
$labelChild->appendChild($nombreVariable);
$direccionVariable = $xml->createElement('variable', htmlspecialchars($data_label['direccion']));
$direccionVariable->setAttribute('name', 'direccion');
$labelChild->appendChild($direccionVariable);
$labels->appendChild($labelChild);
}
$xml->appendChild($labels);
return $xml->saveXML();
}
public function sendToImpresoraEtiqueta(string $name, string $content, ImpresoraEtiquetaEntity $impresoraEtiqueta): bool
{
$tmpFile = tmpfile();
@ -84,8 +144,8 @@ class ImpresoraEtiquetaService extends BaseService
rewind($tmpFile);
$tmpMetaData = stream_get_meta_data($tmpFile);
$conn = @ftp_connect($impresoraEtiqueta->ip,$impresoraEtiqueta->port);
if(!$conn){
$conn = @ftp_connect($impresoraEtiqueta->ip, $impresoraEtiqueta->port);
if (!$conn) {
throw new Exception('Error al establecer conexión FTP');
}
$isLoginSuccess = @ftp_login($conn, $impresoraEtiqueta->user, $impresoraEtiqueta->pass);
@ -99,10 +159,10 @@ class ImpresoraEtiquetaService extends BaseService
if (ftp_put($conn, $name, $tmpMetaData['uri'], FTP_ASCII) === FALSE) {
$status = false;
ftp_close($conn);
}else{
} else {
$status = true;
}
return $status;
}
}

View File

@ -78,6 +78,7 @@ class LogisticaService
return $builder;
}
public static function findForNewEnvio()
{
$db = \Config\Database::connect();
@ -576,11 +577,11 @@ class LogisticaService
$data = [
"printer" => $printer->name,
"header" => [
"_FORMAT" => "E:PEDIDO.ZPL",
"_QUANTITY" => 1,
"_PRINBTERNAME" => $printer->name,
"_JOBNAME" => "LBL101"
],
"_FORMAT" => "E:PEDIDO.ZPL",
"_QUANTITY" => 1,
"_PRINBTERNAME" => $printer->name,
"_JOBNAME" => "LBL101"
],
];
foreach ($lineas as $linea) {

View File

@ -0,0 +1,249 @@
<?= $this->include("themes/_commonPartialsBs/sweetalert") ?>
<?= $this->include('themes/_commonPartialsBs/datatables') ?>
<?= $this->include("themes/_commonPartialsBs/select2bs5") ?>
<?= $this->extend('themes/vuexy/main/defaultlayout') ?>
<?= $this->section('content'); ?>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h4><?= $boxTitle ?>
</h4>
</div>
<div class="card-body">
<?= view("themes/_commonPartialsBs/_alertBoxes") ?>
<input type="hidden" id="id" name="id" value="<?= $etiquetaEntity->id ?>">
<div class="accordion accordion-bordered">
<div class="card accordion-item active mb-5">
<h4 class="accordion-header px-4 py-3">
<?= lang("Logistica.datosEnvio") ?>
</h4>
<div id="accordionDatosEnvioTip" class="accordion-collapse collapse show">
<div class="accordion-body px-4 py-3">
<div class="d-flex flex-row mb-3">
<div class="col-sm-3 px-3">
<label for="att" class="form-label">
<?= lang("Logistica.cliente") ?>
</label>
<input readonly id="cliente" name="cliente" tabindex="1" maxlength="50"
class="form-control" value="<?= old('att', $etiquetaEntity->cliente) ?>">
</div>
<div class="col-sm-4 px-3">
<label for="att" class="form-label">
<?= lang("Logistica.att") ?>
</label>
<input readonly id="att" name="att" tabindex="1" maxlength="50"
class="form-control" value="<?= old('att', $etiquetaEntity->att) ?>">
</div>
<div class="col-sm-5 px-3">
<label for="direccion" class="form-label">
<?= lang("Logistica.direccion") ?>
</label>
<input readonly id="direccion" name="direccion" tabindex="1" maxlength="50"
class="form-control"
value="<?= old('direccion', $etiquetaEntity->direccion) ?>">
</div>
</div>
<div class="d-flex flex-row mb-3">
<div class="col-sm-9 px-3">
<label for="comentarios" class="form-label">
<?= lang("Logistica.comentariosEtiqueta") ?>
</label>
<input id="comentarios" name="comentarios" tabindex="1" maxlength="50"
class="form-control"
value="<?= old('comentarios', $etiquetaEntity->comentarios) ?>">
</div>
<div class="col-sm-3 px-3">
<button id="guardarComentarios" name="guardar_comentarios" tabindex="1"
class="btn btn-primary mt-4 w-100">
<?= lang("Logistica.guardar") ?>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="accordion accordion-bordered">
<div class="card accordion-item active mb-5">
<h4 class="accordion-header px-4 py-3">
<?= lang("Logistica.addLineaEtiqueta") ?>
</h4>
<div id="accordionaddLineasEnvioTip" class="accordion-collapse collapse show">
<div class="d-flex flex-row mb-3">
<div class="col-sm-12 px-3">
<p><?= lang('Logistica.addLineasText') ?></p>
</div>
</div>
<div class="d-flex flex-row mb-3">
<div class="col-sm-6 px-3">
<label for="buscadorPedidos" class="form-label">
<?= lang("Logistica.buscadorPedidosTitle2") ?>
</label>
<select id="buscadorPedidos" name="buscador_pedidos" tabindex="1" maxlength="50"
class="form-control select2bs2" style="width: 100%;">
</select>
</div>
<div class="col-sm-2 px-3">
<button id="btnAddLinea" name="btnBuscar" tabindex="1"
class="btn btn-primary mt-4 w-100">
<?= lang("Logistica.add") ?>
<ti class="ti ti-circle-plus"></ti>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="accordion accordion-bordered">
<div class="card accordion-item active mb-5">
<h4 class="accordion-header px-4 py-3">
<?= lang("Logistica.lineasEnvio") ?>
</h4>
<div id="accordionDatosEnvioTip" class="accordion-collapse collapse show">
<div class="accordion-body px-4 py-3">
<div class="d-flex flex-row">
<p><?= lang('Logistica.buttonsActions') ?></p>
</div>
<div class="d-flex flex-row mb-3 align-items-end">
<div class="col-sm-2 px-3">
<button id="btnSelectAll" name="btnSelectAll" tabindex="1"
class="btn btn-primary w-100">
<?= lang("Logistica.selectAll") ?>
<i class="ti ti-select"></i>
</button>
</div>
<div class="col-sm-2 px-3">
<button id="btnEliminarLineas" name="btnEliminarLineas" tabindex="1"
class="btn btn-danger w-100">
<?= lang("Logistica.eliminar") ?>
<i class="ti ti-trash"></i>
</button>
</div>
<div class="col-sm-2 px-3">
<button id="btnRenumber" name="btnRenumber" tabindex="1"
class="btn btn-success w-100">
<?= lang("Logistica.renumber") ?>
<i class="ti ti-box"></i>
</button>
</div>
<div class="col-sm-2 px-3">
<button id="btnImprimirEtiquetas" name="btnImprimirEtiquetas" tabindex="1"
class="btn btn-info w-100">
<?= lang("Logistica.imprimirEtiquetas") ?>
<i class="ti ti-printer"></i>
</button>
</div>
<div class="col-sm-2 px-3 d-flex flex-column justify-content-end">
<div class="d-flex flex-column justify-content-end h-100">
<label for="impresoraEtiquetas" class="form-label">
<?= lang("Logistica.impresoraEtiquetas") ?>
</label>
<select id="impresoraEtiquetas" name="impresora_etiquetas" tabindex="1"
maxlength="50" class="form-control select2bs2" style="width: 100%;">
<?php foreach ($etiquetaEntity->impresoras as $impresora): ?>
<option value="<?= $impresora->id ?>">
<?= $impresora->name ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
<div class="row mb-3">
<table id="tableLineasEtiqueta" class="table table-striped table-hover w-100">
<thead>
<tr>
<th></th>
<th style="max-width: 10%;"><?= lang("Logistica.otId") ?></th>
<th style="max-width: 70%;"><?= lang("Logistica.titulo") ?></th>
<th style="max-width: 10%;"><?= lang("Logistica.num_caja") ?></th>
<th class="text-center" style="width: 10%;">
<?= lang("Logistica.unidadesEnCaja") ?>
</th>
<th class="text-center" style="width: 10%;">
<?= lang("Logistica.unidadesTotalesOt") ?>
</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<th colspan="11">
<div class="text-end">
<?= lang("Logistica.unidadesTotalesFooter") ?>
<span id="footer-unidades-envio"></span>
</div>
</th>
</tr>
<tr>
<th colspan="11">
<div class="text-end">
<?= lang("Logistica.peso") ?>
<span id="footer-peso"></span>
</div>
</th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('css') ?>
<link rel="stylesheet" href="<?= site_url('themes/vuexy/vendor/libs/sweetalert2/sweetalert2.css') ?>" />
<link rel="stylesheet" href="https://cdn.datatables.net/rowreorder/1.4.1/css/rowReorder.dataTables.min.css">
<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.2/themes/base/jquery-ui.css">
<link rel="stylesheet" href="<?= site_url("/themes/vuexy/vendor/libs/flatpickr/flatpickr.css") ?>">
<link rel="stylesheet" href="https://cdn.datatables.net/rowreorder/1.4.1/css/rowReorder.dataTables.min.css">
<?= $this->endSection() ?>
<?= $this->section('additionalExternalJs') ?>
<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.min.js"></script>
<script src="<?= site_url('themes/vuexy/vendor/libs/sweetalert2/sweetalert2.js') ?>"></script>
<script src="https://cdn.datatables.net/rowgroup/1.3.1/js/dataTables.rowGroup.min.js"></script>
<script src="https://cdn.datatables.net/rowreorder/1.4.1/js/dataTables.rowReorder.min.js"></script>
<script type="module" src="<?= site_url("assets/js/safekat/pages/logistica/etiquetaEdit.js") ?>"></script>
<?= $this->endSection() ?>

View File

@ -0,0 +1,119 @@
<?= $this->include("themes/_commonPartialsBs/sweetalert") ?>
<?= $this->include('themes/_commonPartialsBs/datatables') ?>
<?= $this->include("themes/_commonPartialsBs/select2bs5") ?>
<?= $this->extend('themes/vuexy/main/defaultlayout') ?>
<?= $this->section('content'); ?>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h4><?= $boxTitle ?></h4>
</div>
<div class="card-body">
<?= view("themes/_commonPartialsBs/_alertBoxes") ?>
<div class="card accordion-item active mb-5">
<h4 class="accordion-header px-4 py-3">
<?= lang("Logistica.nuevaEtiqueta") ?>
</h4>
<div id="accordionNuevaEtiquetaTip" class="accordion-collapse collapse show">
<div class="accordion-body px-4 py-3">
<div class="row">
<div class="mb-1 col-sm-6">
<label for="buscadorPedidos" class="form-label">
<?= lang("Logistica.buscadorPedidosTitle2") ?>
</label>
<select id="buscadorPedidos" name="buscador_pedidos" tabindex="1" maxlength="50"
class="form-control select2bs2">
</select>
</div>
</div>
<div class="row select-direcciones d-none">
<div class="col-sm-6 px-3">
<label for="selectDirecciones" class="form-label">
<?= lang("Logistica.selectDirecciones") ?>
</label>
<select id="selectDirecciones" name="select_direcciones" tabindex="1" maxlength="50"
class="form-control select2bs2" style="width: 100%;">
</select>
</div>
</div>
<div class="row add-etiqueta d-none">
<div class="col-sm-2 px-3">
<button id="btnAddEtiqueta" name="btn_add_etiqueta" tabindex="1"
class="btn btn-primary mt-4 w-100">
<?= lang("Logistica.add") ?>
<ti class="ti ti-circle-plus"></ti>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="card accordion-item active">
<h4 class="accordion-header px-4 py-3">
<?= lang("Logistica.listadoEtiquetas") ?>
</h4>
<div id="accordionListadoEnviosTip" class="accordion-collapse collapse show">
<div class="accordion-body px-4 py-3">
<div class="row">
<table id="tableOfEquiquetas" class="table table-striped table-hover w-100">
<thead>
<tr>
<th style="max-width: 8%;"><?= lang('Logistica.id') ?? 'ID Envío' ?></th>
<th style="max-width: 10%;"><?= lang('Logistica.numeroOts') ?? 'Nº OTs' ?></th>
<th style="max-width: 8%;"><?= lang('Logistica.numeroCajas') ?? 'Nº Cajas' ?></th>
<th><?= lang('Logistica.att') ?? 'Att' ?></th>
<th><?= lang('Logistica.direccion') ?? 'Dirección' ?></th>
<th style="max-width: 120px;"><?= lang('Logistica.acciones') ?? 'Acciones' ?></th>
</tr>
<tr>
<th><input type="text" class="form-control envio-filter" name="id"></th>
<th><input type="text" class="form-control envio-filter-ots" name="ots"></th>
<th></th>
<th><input type="text" class="form-control envio-filter" name="att"></th>
<th><input type="text" class="form-control envio-filter" name="direccion"></th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="mt-3">
<button type="button" class="btn btn-secondary" id="btnBackToPanel"
onclick="window.location.href='<?= route_to('LogisticaPanel') ?>'">
<?= lang('Logistica.backToPanel') ?>
</button>
</div>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('css') ?>
<link rel="stylesheet" href="<?= site_url('themes/vuexy/vendor/libs/sweetalert2/sweetalert2.css') ?>" />
<?= $this->endSection() ?>
<?= $this->section('additionalExternalJs') ?>
<script src="<?= site_url('themes/vuexy/vendor/libs/sweetalert2/sweetalert2.js') ?>"></script>
<script type="module" src="<?= site_url("assets/js/safekat/pages/logistica/impresionEtiquetas.js") ?>"></script>
<?= $this->endSection() ?>

View File

@ -21,7 +21,7 @@
<img src="<?= site_url("assets/img/logistica/envios_ferros.png") ?>" alt="Envío de Ferros/Prototipos">
<div><span><?= lang("Logistica.envioFerros"); ?></span></div>
</div>
<div class="item">
<div class="item" onclick="location.href='<?= route_to('etiquetasLogistica') ?>'">
<img src="<?= site_url("assets/img/logistica/impresionEtiquetas.jpg") ?>" alt="Etiquetas de títulos">
<div><span><?= lang("Logistica.etiquetasTitulos"); ?></span></div>
</div>

View File

@ -88,6 +88,9 @@ let ClassSelect = function (domItem, url, placeholder, allowClear = false, param
this.getText = () => {
return this.item.find(":selected").text();
};
this.getDesc = () => {
return this.item.find(":selected").data("desc");
};
this.onChange = function (callback) {
this.item.on('change', callback);
};

View File

@ -0,0 +1,467 @@
import ClassSelect from '../../components/select2.js';
import Ajax from '../../components/ajax.js';
import AlbaranComponent from '../../components/albaranComponent.js';
class EtiquetaEdit {
constructor() {
this.tableCols = [
{ data: "rowSelected" },
{ data: "ot" },
{ data: "titulo" },
{ data: "numero_caja" },
{ data: "unidades" },
{ data: "unidadesTotal" },
{ data: "id" },
{ data: "pesoUnidad" },
{ data: "unidadesRaw" },
{ data: "action" },
{ data: "numero_caja_raw" }
];
this.table = null;
this.direccion = $('#direccion');
this.addLineas = $('#btnAddLinea');
this.btnEliminarLineas = $('#btnEliminarLineas');
this.btbnGuardarComentarios = $('#guardarComentarios');
this.btnSelectAll = $('#btnSelectAll');
this.btnRenumber = $('#btnRenumber');
this.btnImprimirEtiquetas = $('#btnImprimirEtiquetas');
this.buscador = new ClassSelect($('#buscadorPedidos'), '/etiquetasTitulos/findOts', '', true, { 'id': $('#id').val() });
}
init() {
const self = this;
this._initDatatable();
this.buscador.init();
this.addLineas.on('click', this._addLineas.bind(this));
$(document).on('click', '.btn-delete', this._deleteLinea.bind(this));
$(document).on('change', '.input-lineas', this._updateLinea.bind(this));
this.btnEliminarLineas.on('click', this._deleteLinea.bind(this));
this.btbnGuardarComentarios.on('click', this._guardarComentarios.bind(this));
this.btnSelectAll.on('click', this._selectAll.bind(this));
this.btnRenumber.on('click', this._renumber.bind(this));
this.btnImprimirEtiquetas.on('click', this._imprimirEtiquetas.bind(this));
}
_imprimirEtiquetas() {
const self = this;
const ids = [];
this.table.rows().every(function (rowIdx, tableLoop, rowLoop) {
const node = this.node(); // DOM del tr
const checkbox = $(node).find('.checkbox-linea-envio');
if (checkbox.is(':checked')) {
const data = this.data();
ids.push(data.id);
}
});
$.post(
'/etiquetasTitulos/imprimirEtiquetas',
{
etiqueta_id: $('#id').val(),
ids: ids,
impresora_id: $('#impresoraEtiquetas').val()
},
function (response) {
if (response.status) {
popSuccessAlert(response.message);
if(response.data) {
// show xml in new tab
const blob = new Blob([response.data], { type: 'application/xml' });
const url = URL.createObjectURL(blob);
const newTab = window.open(url, '_blank');
if (newTab) {
newTab.onload = function () {
// Revoke the object URL after the new tab has loaded
URL.revokeObjectURL(url);
};
} else {
popErrorAlert('Error abriendo la pestaña');
}
}
} else {
popErrorAlert('Error imprimiendo las etiquetas');
}
}
).fail(function (xhr, status, error) {
popErrorAlert(error);
});
}
_renumber() {
const self = this;
$.post(
'/etiquetasTitulos/renumber',
{
id: $('#id').val()
},
function (response) {
if (response.status) {
self.table.ajax.reload();
popSuccessAlert(response.message);
} else {
popErrorAlert('Error renumerando las lineas');
}
}
).fail(function (xhr, status, error) {
popErrorAlert(error);
});
}
_selectAll() {
const checkboxes = this.table.$('input[type="checkbox"]');
const allChecked = checkboxes.length === checkboxes.filter(':checked').length;
checkboxes.prop('checked', !allChecked);
}
_initDatatable() {
const self = this;
this.table = $('#tableLineasEtiqueta').DataTable({
processing: true,
serverSide: true,
autoWidth: true,
responsive: true,
scrollX: true,
orderCellsTop: true,
rowId: 'id',
order: [[10, 'asc']],
rowGroup: {
dataSrc: 'numero_caja_raw',
startRender: function (rows, group) {
let totalUnidades = 0;
let totalPeso = 0;
rows.data().each(function (row) {
const unidades = parseFloat(row.unidadesRaw) || 0;
const pesoUnidad = parseFloat(row.pesoUnidad) || 0;
totalUnidades += unidades;
totalPeso += unidades * pesoUnidad;
});
const groupId = 'grupo-caja-' + group;
return $('<tr/>')
.attr('data-group', groupId)
.addClass('group-header bg-light fw-bold')
.css('cursor', 'pointer')
.append(
`<td colspan="11">
📦 CAJA ${group} UNIDADES: ${totalUnidades} PESO: ${totalPeso.toFixed(2)} kg
</td>`
);
}
},
rowReorder: {
dataSrc: 'numero_caja_raw',
update: false,
selector: 'td:not(.dt-no-reorder)' // evita inputs
},
lengthMenu: [5, 10, 25, 50, 100],
pageLength: 50,
ajax: {
url: "/etiquetasTitulos/datatableLineas/" + $('#id').val(),
data: function (d) {
d.direccion = $('#direccion').val();
d.id = $('#id').val();
},
},
columns: this.tableCols,
language: {
url: "/themes/vuexy/vendor/libs/datatables-sk/plugins/i18n/es-ES.json"
},
columnDefs: [
{
targets: [0, 9],
className: "text-center dt-no-reorder",
orderable: false,
searchable: false
},
{
targets: [3, 4],
className: "text-center dt-no-reorder"
},
{
targets: [1, 2, 4, 5, 6],
className: "text-center"
},
{
targets: [6, 7, 8, 10],
visible: false
}
]
});
$('#tableLineasEtiqueta tbody').on('click', 'tr.group-header', function () {
const group = $(this).data('group');
const table = $('#tableLineasEtiqueta').DataTable();
const icon = $(this).find('.group-toggle-icon');
let visible = true;
table.rows().every(function () {
const row = this.node();
const data = this.data();
if ('numero_caja' in data && ('grupo-caja-' + data.numero_caja) === group) {
if (!$(row).hasClass('group-header')) {
$(row).toggle();
visible = !$(row).is(':visible');
}
}
});
// Cambiar el icono
icon.text(visible ? '▼' : '►');
});
this.table.on('row-reorder', function (e, diff, edit) {
if (!diff.length || !edit.triggerRow) return;
const table = self.table;
const movedRowData = table.row(edit.triggerRow).data();
if (!movedRowData?.id) return;
const movedItem = diff.find(d => {
const rowData = table.row(d.node).data();
return rowData?.id === movedRowData.id;
});
if (!movedItem) return;
const payload = {
id: movedRowData.id,
numero_caja: movedItem.newData
};
$.ajax({
url: '/etiquetasTitulos/updateOrdenCajas',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ orden: [payload] }),
success: function (response) {
if (response.status) {
setTimeout(() => {
self.table.ajax.reload(null, false);
}, 100);
} else {
popErrorAlert('Error actualizando el orden');
}
},
error: function () {
popErrorAlert('Error en la solicitud AJAX');
}
});
});
}
_addLineas() {
if (this.buscador.item.select2('data').length > 0) {
let maxUnidades = 0;
if (this.buscador.item.select2('data')[0].desc) {
maxUnidades = parseInt(this.buscador.item.select2('data')[0].desc);
Swal.fire({
title: 'Unidades',
html: `
<div class="mb-2">
<label for="inputUnidades">Unidades:</label>
<input type="number" id="inputUnidades" class="swal2-input" value="${maxUnidades}">
</div>
<div>
<label for="inputCajas">Cajas:</label>
<input type="number" id="inputCajas" class="swal2-input" value="1">
</div>
`,
icon: 'info',
showCancelButton: true,
confirmButtonColor: '#3085d6',
confirmButtonText: 'Aceptar',
cancelButtonText: 'Cancelar',
customClass: {
confirmButton: 'btn btn-primary me-1',
cancelButton: 'btn btn-secondary'
},
buttonsStyling: false,
preConfirm: () => {
const unidades = parseInt(document.getElementById('inputUnidades').value);
const cajas = parseInt(document.getElementById('inputCajas').value);
if (isNaN(unidades) || isNaN(cajas)) {
Swal.showValidationMessage('Debe completar ambos campos');
}
return { unidades, cajas };
}
}).then((result) => {
if (result.isConfirmed) {
const { unidades, cajas } = result.value;
const url = '/etiquetasTitulos/addLineas';
const data = {
etiqueta_id: $('#id').val(),
ot_id: this.buscador.getVal(),
unidades: unidades,
cajas: cajas
};
$.post(
url,
data,
function (response) {
if (response.status) {
self.table.ajax.reload();
popSuccessAlert(response.message);
} else {
popErrorAlert('Error en la respuesta');
}
}
).fail(function (xhr, status, error) {
popErrorAlert(error);
});
}
});
}
}
}
_deleteLinea(e) {
e.preventDefault();
const self = this;
let ids = [];
if (e.currentTarget.id == "btnEliminarLineas") {
this.table.rows().every(function (rowIdx, tableLoop, rowLoop) {
const node = this.node(); // DOM del tr
const checkbox = $(node).find('.checkbox-linea-envio');
if (checkbox.is(':checked')) {
const data = this.data();
ids.push(data.id);
}
});
}
else {
ids.push($(e.currentTarget).attr('data-id'));
}
Swal.fire({
title: '¿Está seguro de eliminar la linea?',
text: "No podrá revertir esta acción",
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Sí',
cancelButtonText: 'Cancelar',
customClass: {
confirmButton: 'btn btn-danger me-1',
cancelButton: 'btn btn-secondary'
},
buttonsStyling: false
}).then((result) => {
if (result.isConfirmed) {
$.post(
'/etiquetasTitulos/deleteLineas',
{
ids: ids
},
function (response) {
if (response.status) {
self.table.ajax.reload();
popSuccessAlert(response.message);
} else {
popErrorAlert('Error borrando la etiqueta');
}
}
).fail(function (xhr, status, error) {
popErrorAlert(error);
});
}
})
}
_updateLinea(e) {
e.preventDefault();
const self = this;
const id = $(e.currentTarget).attr('data-id');
const name = $(e.currentTarget).attr('data-name');
const value = $(e.currentTarget).val();
const url = '/etiquetasTitulos/updateLineas';
const data = {
id: id,
name: name,
value: value
};
$.post(
url,
data,
function (response) {
if (!response.status) {
popErrorAlert('Error actualizando la linea');
}
else {
self.table.ajax.reload();
}
}
).fail(function (xhr, status, error) {
popErrorAlert(error);
});
}
_guardarComentarios() {
const self = this;
const id = $('#id').val();
const comentarios = $('#comentarios').val();
const url = '/etiquetasTitulos/updateComentarios';
const data = {
id: id,
comentarios: comentarios
};
$.post(
url,
data,
function (response) {
if (!response.status) {
popErrorAlert('Error actualizando comentarios');
}
else {
popSuccessAlert(response.message);
}
}
).fail(function (xhr, status, error) {
popErrorAlert(error);
});
}
}
document.addEventListener('DOMContentLoaded', function () {
/*const dropdown = document.querySelector(".dropdown-language");
const activeItem = dropdown.querySelector(".dropdown-menu .dropdown-item");
let locale = 'es';
if (activeItem) {
locale = activeItem.getAttribute("data-language");
}
new Ajax('/translate/getTranslation', { locale: locale, translationFile: [] }, {},
function (translations) {
window.language = JSON.parse(translations);
new EtiquetaEdit().init();
},
function (error) {
console.log("Error getting translations:", error);
}
).post();
*/
new EtiquetaEdit().init();
});
export default EtiquetaEdit;

View File

@ -0,0 +1,177 @@
import ClassSelect from '../../components/select2.js';
$(() => {
let otsFilter = '';
const selectOts = new ClassSelect($('#buscadorPedidos'), '/etiquetasTitulos/otList', "", true);
selectOts.init();
const selectDirecciones = new ClassSelect($('#selectDirecciones'), '/etiquetasTitulos/addList', "", true, {
ot_id: () => selectOts.getVal()
});
selectDirecciones.init();
selectOts.item.on('select2:open', () => {
$('.select-direcciones').addClass('d-none');
$('.add-etiqueta').addClass('d-none');
})
selectOts.item.on('change', () => {
selectDirecciones.empty();
$('.add-etiqueta').addClass('d-none');
$('.select-direcciones').removeClass('d-none');
})
selectDirecciones.item.on('select2:open', () => {
$('.add-etiqueta').addClass('d-none');
})
selectDirecciones.item.on('change', () => {
$('.add-etiqueta').removeClass('d-none');
})
$('#btnAddEtiqueta').on('click', () => {
Swal.fire({
title: 'Unidades por caja',
text: 'Seleccione la cantidad de unidades por caja',
icon: 'info',
input: 'text',
inputLabel: 'Unidades/caja',
inputValue: 0,
showCancelButton: true,
confirmButtonColor: '#3085d6',
confirmButtonText: 'Aceptar',
cancelButtonText: 'Cancelar',
customClass: {
confirmButton: 'btn btn-primary me-1',
cancelButton: 'btn btn-secondary'
},
buttonsStyling: false
}).then((result) => {
if (result.isConfirmed) {
const unidades_caja = result.value;
const url = '/etiquetasTitulos/newEtiquetaTitulos';
const data = {
ot_id: selectOts.getVal(),
direccion: selectDirecciones.getText(),
unidades_caja: unidades_caja
};
$.post(
url,
data,
function (response) {
if (response.status) {
tableEtiquetas.ajax.reload();
// open the new etiqueta in a new tab
window.open(`${window.location.origin}/etiquetasTitulos/edit/${response.etiqueta}`, '_blank');
} else {
popErrorAlert('Error en la respuesta');
}
}
).fail(function (xhr, status, error) {
popErrorAlert(error);
});
}
});
})
const tableEtiquetas = $('#tableOfEquiquetas').DataTable({
processing: true,
serverSide: true,
autoWidth: true,
responsive: true,
scrollX: true,
orderCellsTop: true,
lengthMenu: [5, 10, 25, 50, 75, 100, 250, 500, 1000, 2500],
pageLength: 50,
"dom": 'lBrtip',
"ajax": {
"url": "/etiquetasTitulos/datatable",
"data": function (d) {
d.otsFilter = otsFilter;
}
},
"columns": [
{ "data": "id" },
{ "data": "lista_ots" },
{ "data": "cajas" },
{ "data": "att" },
{ "data": "direccion" },
{ "data": "action" }
],
"language": {
url: "/themes/vuexy/vendor/libs/datatables-sk/plugins/i18n/es-ES.json"
},
"columnDefs": [
{
orderable: false,
searchable: false,
targets: [5]
},
],
"order": [[0, "desc"]],
});
$(document).on('click', '.btn-edit', function (e) {
window.location.href = '/etiquetasTitulos/edit/' + $(this).attr('data-id');
});
$(document).on('click', '.btn-delete', function (e) {
Swal.fire({
title: '¿Está seguro de eliminar la etiqueta?',
text: "No podrá revertir esta acción",
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Sí',
cancelButtonText: 'Cancelar',
customClass: {
confirmButton: 'btn btn-danger me-1',
cancelButton: 'btn btn-secondary'
},
buttonsStyling: false
}).then((result) => {
if (result.isConfirmed) {
$.post(
'/etiquetasTitulos/delete',
{
id: $(this).attr('data-id')
},
function (response) {
if (response.status) {
tableEtiquetas.ajax.reload();
popSuccessAlert(response.message);
} else {
popErrorAlert('Error borrando la etiqueta');
}
}
).fail(function (xhr, status, error) {
popErrorAlert(error);
});
}
})
});
$(document).on("keyup", ".envio-filter", (event) => {
let columnName = $(event.currentTarget).attr("name");
let columnIndex = $('#tableOfEquiquetas').DataTable().columns().eq(0).filter(function (index) {
return $('#tableOfEquiquetas').DataTable().column(index).dataSrc() === columnName;
})[0];
$('#tableOfEquiquetas').DataTable().column(columnIndex).search($(event.currentTarget).val()).draw()
})
$(document).on("keyup", ".envio-filter-ots", (event) => {
otsFilter = $(event.currentTarget).val();
$('#tableOfEquiquetas').DataTable().ajax.reload();
})
$(document).on("change", ".envio-filter-select", (event) => {
let columnName = $(event.currentTarget).attr("name");
let columnIndex = $('#tableOfEquiquetas').DataTable().columns().eq(0).filter(function (index) {
return $('#tableOfEquiquetas').DataTable().column(index).dataSrc() === columnName;
})[0];
$('#tableOfEquiquetas').DataTable().column(columnIndex).search($(event.currentTarget).val()).draw();
});
})

View File

@ -139,4 +139,8 @@
.dtrg-group.ui-droppable-hover {
background-color: #d1ecf1 !important;
cursor: move;
}
.dt-no-reorder {
cursor: auto !important;
}