Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27cbc9f449 | |||
| 4c09a0dcb4 | |||
| 8bb23ee563 | |||
| 20571c9e4b | |||
| fdde16b113 | |||
| 33b627f328 | |||
| d96b4be9e0 | |||
| 088e518db1 | |||
| 789c547bc7 | |||
| e5bf546ae7 | |||
| 6dd13e5d7d | |||
| b1f2bb60e3 | |||
| f7e97f55e9 | |||
| 70b712ff3b | |||
| fdc3af01f3 | |||
| 3d54140280 | |||
| bfdbbbfc8f | |||
| 40a5771a4b | |||
| 9f5a585717 | |||
| 7cbd74111d | |||
| cb221a8039 | |||
| ece1beb87f | |||
| d155d1cbab |
@@ -46,6 +46,7 @@ public/userarea/last_url.txt
|
|||||||
public/userarea/class/curl_auth_debug.log
|
public/userarea/class/curl_auth_debug.log
|
||||||
public/userarea/class/curl_request_debug.log
|
public/userarea/class/curl_request_debug.log
|
||||||
|
|
||||||
|
public/userarea/uploads/cad_area/originals/*
|
||||||
# Ignora tutti i log
|
# Ignora tutti i log
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class CreateJobSubRolesTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
if (!$this->hasTable('job_roles')) {
|
||||||
|
$rolesTable = $this->table('job_roles', [
|
||||||
|
'id' => false,
|
||||||
|
'primary_key' => ['id'],
|
||||||
|
'collation' => 'utf8mb4_unicode_ci',
|
||||||
|
'encoding' => 'utf8mb4',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$rolesTable
|
||||||
|
->addColumn('id', 'integer', [
|
||||||
|
'identity' => true,
|
||||||
|
'signed' => false,
|
||||||
|
])
|
||||||
|
->addColumn('name', 'string', [
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('description', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
])
|
||||||
|
->addColumn('sort_order', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
'default' => 999,
|
||||||
|
])
|
||||||
|
->addColumn('is_active', 'boolean', [
|
||||||
|
'null' => false,
|
||||||
|
'default' => 1,
|
||||||
|
])
|
||||||
|
->addColumn('created_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addColumn('updated_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
'update' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addIndex(['is_active'], [
|
||||||
|
'name' => 'idx_job_roles_is_active',
|
||||||
|
])
|
||||||
|
->addIndex(['sort_order'], [
|
||||||
|
'name' => 'idx_job_roles_sort_order',
|
||||||
|
])
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->hasTable('job_sub_roles')) {
|
||||||
|
$table = $this->table('job_sub_roles', [
|
||||||
|
'id' => false,
|
||||||
|
'primary_key' => ['id'],
|
||||||
|
'collation' => 'utf8mb4_unicode_ci',
|
||||||
|
'encoding' => 'utf8mb4',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$table
|
||||||
|
->addColumn('id', 'integer', [
|
||||||
|
'identity' => true,
|
||||||
|
'signed' => false,
|
||||||
|
])
|
||||||
|
->addColumn('job_role_id', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('name', 'string', [
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('description', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
])
|
||||||
|
->addColumn('sort_order', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
'default' => 999,
|
||||||
|
])
|
||||||
|
->addColumn('is_active', 'boolean', [
|
||||||
|
'null' => false,
|
||||||
|
'default' => 1,
|
||||||
|
])
|
||||||
|
->addColumn('created_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addColumn('updated_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
'update' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addIndex(['job_role_id'], [
|
||||||
|
'name' => 'idx_job_sub_roles_job_role_id',
|
||||||
|
])
|
||||||
|
->addIndex(['is_active'], [
|
||||||
|
'name' => 'idx_job_sub_roles_is_active',
|
||||||
|
])
|
||||||
|
->addIndex(['sort_order'], [
|
||||||
|
'name' => 'idx_job_sub_roles_sort_order',
|
||||||
|
])
|
||||||
|
->addForeignKey(
|
||||||
|
'job_role_id',
|
||||||
|
'job_roles',
|
||||||
|
'id',
|
||||||
|
[
|
||||||
|
'delete' => 'CASCADE',
|
||||||
|
'update' => 'CASCADE',
|
||||||
|
'constraint' => 'fk_job_sub_roles_job_role',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class CreatePpeItemsTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('ppe_items', [
|
||||||
|
'id' => false,
|
||||||
|
'primary_key' => ['id'],
|
||||||
|
'collation' => 'utf8mb4_unicode_ci',
|
||||||
|
'encoding' => 'utf8mb4',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$table
|
||||||
|
->addColumn('id', 'integer', [
|
||||||
|
'identity' => true,
|
||||||
|
'signed' => false,
|
||||||
|
])
|
||||||
|
->addColumn('name', 'string', [
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('description', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
])
|
||||||
|
->addColumn('category', 'string', [
|
||||||
|
'limit' => 100,
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'comment' => 'PPE category, for example Head, Hands, Eyes, Feet, Respiratory',
|
||||||
|
])
|
||||||
|
->addColumn('photo', 'string', [
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'comment' => 'PPE image path or filename',
|
||||||
|
])
|
||||||
|
->addColumn('standard_reference', 'string', [
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'comment' => 'Reference standard, for example EN ISO 20345',
|
||||||
|
])
|
||||||
|
->addColumn('validity_months', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'comment' => 'Default validity in months after assignment',
|
||||||
|
])
|
||||||
|
->addColumn('sort_order', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
'default' => 999,
|
||||||
|
])
|
||||||
|
->addColumn('is_active', 'boolean', [
|
||||||
|
'null' => false,
|
||||||
|
'default' => 1,
|
||||||
|
])
|
||||||
|
->addColumn('created_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addColumn('updated_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
'update' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addIndex(['category'], [
|
||||||
|
'name' => 'idx_ppe_items_category',
|
||||||
|
])
|
||||||
|
->addIndex(['is_active'], [
|
||||||
|
'name' => 'idx_ppe_items_is_active',
|
||||||
|
])
|
||||||
|
->addIndex(['sort_order'], [
|
||||||
|
'name' => 'idx_ppe_items_sort_order',
|
||||||
|
])
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class CreateEmployeePpeItemsTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('employee_ppe_items', [
|
||||||
|
'id' => false,
|
||||||
|
'primary_key' => ['id'],
|
||||||
|
'collation' => 'utf8mb4_unicode_ci',
|
||||||
|
'encoding' => 'utf8mb4',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$table
|
||||||
|
->addColumn('id', 'integer', [
|
||||||
|
'identity' => true,
|
||||||
|
'signed' => false,
|
||||||
|
])
|
||||||
|
->addColumn('employee_id', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('ppe_item_id', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('assigned_date', 'date', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
])
|
||||||
|
->addColumn('expiry_date', 'date', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
])
|
||||||
|
->addColumn('quantity', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
'default' => 1,
|
||||||
|
])
|
||||||
|
->addColumn('status', 'enum', [
|
||||||
|
'values' => [
|
||||||
|
'assigned',
|
||||||
|
'returned',
|
||||||
|
'expired',
|
||||||
|
'lost',
|
||||||
|
'damaged',
|
||||||
|
],
|
||||||
|
'null' => false,
|
||||||
|
'default' => 'assigned',
|
||||||
|
])
|
||||||
|
->addColumn('notes', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
])
|
||||||
|
->addColumn('created_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addColumn('updated_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
'update' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addIndex(['employee_id'], [
|
||||||
|
'name' => 'idx_employee_ppe_items_employee_id',
|
||||||
|
])
|
||||||
|
->addIndex(['ppe_item_id'], [
|
||||||
|
'name' => 'idx_employee_ppe_items_ppe_item_id',
|
||||||
|
])
|
||||||
|
->addIndex(['status'], [
|
||||||
|
'name' => 'idx_employee_ppe_items_status',
|
||||||
|
])
|
||||||
|
->addIndex(['expiry_date'], [
|
||||||
|
'name' => 'idx_employee_ppe_items_expiry_date',
|
||||||
|
])
|
||||||
|
->addForeignKey(
|
||||||
|
'employee_id',
|
||||||
|
'employees',
|
||||||
|
'id',
|
||||||
|
[
|
||||||
|
'delete' => 'CASCADE',
|
||||||
|
'update' => 'CASCADE',
|
||||||
|
'constraint' => 'fk_employee_ppe_items_employee',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
->addForeignKey(
|
||||||
|
'ppe_item_id',
|
||||||
|
'ppe_items',
|
||||||
|
'id',
|
||||||
|
[
|
||||||
|
'delete' => 'RESTRICT',
|
||||||
|
'update' => 'CASCADE',
|
||||||
|
'constraint' => 'fk_employee_ppe_items_ppe_item',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class CreateJobSubRolePpeItemsTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('job_sub_role_ppe_items', [
|
||||||
|
'id' => false,
|
||||||
|
'primary_key' => ['id'],
|
||||||
|
'collation' => 'utf8mb4_unicode_ci',
|
||||||
|
'encoding' => 'utf8mb4',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$table
|
||||||
|
->addColumn('id', 'integer', [
|
||||||
|
'identity' => true,
|
||||||
|
'signed' => false,
|
||||||
|
])
|
||||||
|
->addColumn('job_sub_role_id', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('ppe_item_id', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('requirement_type', 'enum', [
|
||||||
|
'values' => [
|
||||||
|
'mandatory',
|
||||||
|
'recommended',
|
||||||
|
'optional',
|
||||||
|
],
|
||||||
|
'null' => false,
|
||||||
|
'default' => 'mandatory',
|
||||||
|
'comment' => 'Defines if the PPE is mandatory, recommended or optional for the sub role',
|
||||||
|
])
|
||||||
|
->addColumn('notes', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
])
|
||||||
|
->addColumn('sort_order', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
'default' => 999,
|
||||||
|
])
|
||||||
|
->addColumn('is_active', 'boolean', [
|
||||||
|
'null' => false,
|
||||||
|
'default' => 1,
|
||||||
|
])
|
||||||
|
->addColumn('created_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addColumn('updated_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
'update' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addIndex(['job_sub_role_id'], [
|
||||||
|
'name' => 'idx_job_sub_role_ppe_items_sub_role_id',
|
||||||
|
])
|
||||||
|
->addIndex(['ppe_item_id'], [
|
||||||
|
'name' => 'idx_job_sub_role_ppe_items_ppe_item_id',
|
||||||
|
])
|
||||||
|
->addIndex(['requirement_type'], [
|
||||||
|
'name' => 'idx_job_sub_role_ppe_items_requirement_type',
|
||||||
|
])
|
||||||
|
->addIndex(['is_active'], [
|
||||||
|
'name' => 'idx_job_sub_role_ppe_items_is_active',
|
||||||
|
])
|
||||||
|
->addIndex(['job_sub_role_id', 'ppe_item_id'], [
|
||||||
|
'unique' => true,
|
||||||
|
'name' => 'uq_job_sub_role_ppe_item',
|
||||||
|
])
|
||||||
|
->addForeignKey(
|
||||||
|
'job_sub_role_id',
|
||||||
|
'job_sub_roles',
|
||||||
|
'id',
|
||||||
|
[
|
||||||
|
'delete' => 'CASCADE',
|
||||||
|
'update' => 'CASCADE',
|
||||||
|
'constraint' => 'fk_job_sub_role_ppe_items_sub_role',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
->addForeignKey(
|
||||||
|
'ppe_item_id',
|
||||||
|
'ppe_items',
|
||||||
|
'id',
|
||||||
|
[
|
||||||
|
'delete' => 'CASCADE',
|
||||||
|
'update' => 'CASCADE',
|
||||||
|
'constraint' => 'fk_job_sub_role_ppe_items_ppe_item',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddJobSubRoleIdToEmployeesTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (!$this->hasTable('employees')) {
|
||||||
|
throw new RuntimeException('Table employees does not exist.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = $this->table('employees');
|
||||||
|
|
||||||
|
if (!$table->hasColumn('job_role_id')) {
|
||||||
|
$table
|
||||||
|
->addColumn('job_role_id', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => true,
|
||||||
|
'after' => 'department_id',
|
||||||
|
])
|
||||||
|
->addIndex(['job_role_id'], [
|
||||||
|
'name' => 'idx_employees_job_role_id',
|
||||||
|
])
|
||||||
|
->addForeignKey(
|
||||||
|
'job_role_id',
|
||||||
|
'job_roles',
|
||||||
|
'id',
|
||||||
|
[
|
||||||
|
'delete' => 'SET_NULL',
|
||||||
|
'update' => 'CASCADE',
|
||||||
|
'constraint' => 'fk_employees_job_role',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = $this->table('employees');
|
||||||
|
|
||||||
|
if (!$table->hasColumn('job_sub_role_id')) {
|
||||||
|
$afterColumn = $table->hasColumn('job_role_id') ? 'job_role_id' : 'department_id';
|
||||||
|
|
||||||
|
$table
|
||||||
|
->addColumn('job_sub_role_id', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => true,
|
||||||
|
'after' => $afterColumn,
|
||||||
|
])
|
||||||
|
->addIndex(['job_sub_role_id'], [
|
||||||
|
'name' => 'idx_employees_job_sub_role_id',
|
||||||
|
])
|
||||||
|
->addForeignKey(
|
||||||
|
'job_sub_role_id',
|
||||||
|
'job_sub_roles',
|
||||||
|
'id',
|
||||||
|
[
|
||||||
|
'delete' => 'SET_NULL',
|
||||||
|
'update' => 'CASCADE',
|
||||||
|
'constraint' => 'fk_employees_job_sub_role',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (!$this->hasTable('employees')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = $this->table('employees');
|
||||||
|
|
||||||
|
if ($table->hasForeignKey('job_sub_role_id')) {
|
||||||
|
$table->dropForeignKey('job_sub_role_id')->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($table->hasForeignKey('job_role_id')) {
|
||||||
|
$table->dropForeignKey('job_role_id')->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = $this->table('employees');
|
||||||
|
|
||||||
|
if ($table->hasIndexByName('idx_employees_job_sub_role_id')) {
|
||||||
|
$table->removeIndexByName('idx_employees_job_sub_role_id')->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($table->hasIndexByName('idx_employees_job_role_id')) {
|
||||||
|
$table->removeIndexByName('idx_employees_job_role_id')->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = $this->table('employees');
|
||||||
|
|
||||||
|
if ($table->hasColumn('job_sub_role_id')) {
|
||||||
|
$table->removeColumn('job_sub_role_id')->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($table->hasColumn('job_role_id')) {
|
||||||
|
$table->removeColumn('job_role_id')->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddDeliveryFieldsToEmployeePpeItemsTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('employee_ppe_items');
|
||||||
|
|
||||||
|
$table
|
||||||
|
->addColumn('delivered_by', 'string', [
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'after' => 'expiry_date',
|
||||||
|
])
|
||||||
|
->addColumn('created_by', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'after' => 'notes',
|
||||||
|
])
|
||||||
|
->addIndex(['created_by'], [
|
||||||
|
'name' => 'idx_employee_ppe_items_created_by',
|
||||||
|
])
|
||||||
|
->addForeignKey(
|
||||||
|
'created_by',
|
||||||
|
'auth_users',
|
||||||
|
'id',
|
||||||
|
[
|
||||||
|
'delete' => 'SET_NULL',
|
||||||
|
'update' => 'CASCADE',
|
||||||
|
'constraint' => 'fk_employee_ppe_items_created_by',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class CreateEmployeeJobSubRolesTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (!$this->hasTable('employee_job_sub_roles')) {
|
||||||
|
$table = $this->table('employee_job_sub_roles', [
|
||||||
|
'id' => false,
|
||||||
|
'primary_key' => ['id'],
|
||||||
|
'signed' => false,
|
||||||
|
'collation' => 'utf8mb4_general_ci',
|
||||||
|
'encoding' => 'utf8mb4',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$table
|
||||||
|
->addColumn('id', 'integer', [
|
||||||
|
'identity' => true,
|
||||||
|
'signed' => false,
|
||||||
|
])
|
||||||
|
->addColumn('employee_id', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('job_sub_role_id', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('is_primary', 'boolean', [
|
||||||
|
'null' => false,
|
||||||
|
'default' => false,
|
||||||
|
])
|
||||||
|
->addColumn('created_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addIndex(['employee_id', 'job_sub_role_id'], [
|
||||||
|
'unique' => true,
|
||||||
|
'name' => 'uq_employee_subrole',
|
||||||
|
])
|
||||||
|
->addIndex(['employee_id'], [
|
||||||
|
'name' => 'idx_employee_job_sub_roles_employee',
|
||||||
|
])
|
||||||
|
->addIndex(['job_sub_role_id'], [
|
||||||
|
'name' => 'idx_employee_job_sub_roles_subrole',
|
||||||
|
])
|
||||||
|
->addForeignKey(
|
||||||
|
'employee_id',
|
||||||
|
'employees',
|
||||||
|
'id',
|
||||||
|
[
|
||||||
|
'delete' => 'CASCADE',
|
||||||
|
'update' => 'CASCADE',
|
||||||
|
'constraint' => 'fk_employee_job_sub_roles_employee',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
->addForeignKey(
|
||||||
|
'job_sub_role_id',
|
||||||
|
'job_sub_roles',
|
||||||
|
'id',
|
||||||
|
[
|
||||||
|
'delete' => 'CASCADE',
|
||||||
|
'update' => 'CASCADE',
|
||||||
|
'constraint' => 'fk_employee_job_sub_roles_subrole',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import existing single sub-role assignments from employees.job_sub_role_id
|
||||||
|
// into the new bridge table.
|
||||||
|
$this->execute("
|
||||||
|
INSERT IGNORE INTO employee_job_sub_roles
|
||||||
|
(employee_id, job_sub_role_id, is_primary, created_at)
|
||||||
|
SELECT
|
||||||
|
e.id,
|
||||||
|
e.job_sub_role_id,
|
||||||
|
1,
|
||||||
|
NOW()
|
||||||
|
FROM employees e
|
||||||
|
WHERE e.job_sub_role_id IS NOT NULL
|
||||||
|
AND e.job_sub_role_id > 0
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if ($this->hasTable('employee_job_sub_roles')) {
|
||||||
|
$this->table('employee_job_sub_roles')->drop()->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class CreateCompanyFunctionsTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (!$this->hasTable('company_functions')) {
|
||||||
|
$table = $this->table('company_functions', [
|
||||||
|
'id' => false,
|
||||||
|
'primary_key' => ['id'],
|
||||||
|
'signed' => false,
|
||||||
|
'collation' => 'utf8mb4_general_ci',
|
||||||
|
'encoding' => 'utf8mb4',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$table
|
||||||
|
->addColumn('id', 'integer', [
|
||||||
|
'identity' => true,
|
||||||
|
'signed' => false,
|
||||||
|
])
|
||||||
|
->addColumn('function_name', 'string', [
|
||||||
|
'limit' => 150,
|
||||||
|
'null' => false,
|
||||||
|
'comment' => 'Function name, for example RSPP, Medico del lavoro, RLS',
|
||||||
|
])
|
||||||
|
->addColumn('person_full_name', 'string', [
|
||||||
|
'limit' => 200,
|
||||||
|
'null' => false,
|
||||||
|
'comment' => 'Full name and surname of the person assigned to the function',
|
||||||
|
])
|
||||||
|
->addColumn('phone', 'string', [
|
||||||
|
'limit' => 80,
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('email', 'string', [
|
||||||
|
'limit' => 190,
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('notes', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('sort_order', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
'default' => 0,
|
||||||
|
])
|
||||||
|
->addColumn('is_active', 'boolean', [
|
||||||
|
'null' => false,
|
||||||
|
'default' => true,
|
||||||
|
])
|
||||||
|
->addColumn('created_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addColumn('updated_at', 'timestamp', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => null,
|
||||||
|
'update' => 'CURRENT_TIMESTAMP',
|
||||||
|
])
|
||||||
|
->addIndex(['function_name'], [
|
||||||
|
'name' => 'idx_company_functions_function_name',
|
||||||
|
])
|
||||||
|
->addIndex(['person_full_name'], [
|
||||||
|
'name' => 'idx_company_functions_person_full_name',
|
||||||
|
])
|
||||||
|
->addIndex(['email'], [
|
||||||
|
'name' => 'idx_company_functions_email',
|
||||||
|
])
|
||||||
|
->addIndex(['is_active', 'sort_order'], [
|
||||||
|
'name' => 'idx_company_functions_active_sort',
|
||||||
|
])
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->execute("
|
||||||
|
INSERT INTO company_functions
|
||||||
|
(function_name, person_full_name, phone, email, notes, sort_order, is_active, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
('RSPP', '', NULL, NULL, NULL, 10, 1, NOW(), NOW()),
|
||||||
|
('Medico del lavoro', '', NULL, NULL, NULL, 20, 1, NOW(), NOW()),
|
||||||
|
('RLS', '', NULL, NULL, NULL, 30, 1, NOW(), NOW())
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if ($this->hasTable('company_functions')) {
|
||||||
|
$this->table('company_functions')->drop()->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AlterScadFunctionsAddContactFields extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (!$this->hasTable('scad_functions')) {
|
||||||
|
throw new RuntimeException('Table scad_functions does not exist.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = $this->table('scad_functions');
|
||||||
|
|
||||||
|
if (!$table->hasColumn('person_full_name')) {
|
||||||
|
$table->addColumn('person_full_name', 'string', [
|
||||||
|
'limit' => 200,
|
||||||
|
'null' => true,
|
||||||
|
'after' => 'description',
|
||||||
|
'comment' => 'Full name and surname of the person assigned to the function',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('phone')) {
|
||||||
|
$table->addColumn('phone', 'string', [
|
||||||
|
'limit' => 80,
|
||||||
|
'null' => true,
|
||||||
|
'after' => 'person_full_name',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('email')) {
|
||||||
|
$table->addColumn('email', 'string', [
|
||||||
|
'limit' => 190,
|
||||||
|
'null' => true,
|
||||||
|
'after' => 'phone',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('notes')) {
|
||||||
|
$table->addColumn('notes', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
'after' => 'email',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('sort_order')) {
|
||||||
|
$table->addColumn('sort_order', 'integer', [
|
||||||
|
'signed' => false,
|
||||||
|
'null' => false,
|
||||||
|
'default' => 0,
|
||||||
|
'after' => 'status',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasIndexByName('idx_scad_functions_name')) {
|
||||||
|
$table->addIndex(['name'], [
|
||||||
|
'name' => 'idx_scad_functions_name',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasIndexByName('idx_scad_functions_person_full_name')) {
|
||||||
|
$table->addIndex(['person_full_name'], [
|
||||||
|
'name' => 'idx_scad_functions_person_full_name',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasIndexByName('idx_scad_functions_email')) {
|
||||||
|
$table->addIndex(['email'], [
|
||||||
|
'name' => 'idx_scad_functions_email',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasIndexByName('idx_scad_functions_status_sort')) {
|
||||||
|
$table->addIndex(['status', 'sort_order'], [
|
||||||
|
'name' => 'idx_scad_functions_status_sort',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table->update();
|
||||||
|
|
||||||
|
// Set a default order for existing rows without changing their names.
|
||||||
|
$this->execute("
|
||||||
|
UPDATE scad_functions
|
||||||
|
SET sort_order = id * 10
|
||||||
|
WHERE sort_order = 0
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (!$this->hasTable('scad_functions')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = $this->table('scad_functions');
|
||||||
|
|
||||||
|
if ($table->hasIndexByName('idx_scad_functions_status_sort')) {
|
||||||
|
$table->removeIndexByName('idx_scad_functions_status_sort');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($table->hasIndexByName('idx_scad_functions_email')) {
|
||||||
|
$table->removeIndexByName('idx_scad_functions_email');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($table->hasIndexByName('idx_scad_functions_person_full_name')) {
|
||||||
|
$table->removeIndexByName('idx_scad_functions_person_full_name');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($table->hasIndexByName('idx_scad_functions_name')) {
|
||||||
|
$table->removeIndexByName('idx_scad_functions_name');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($table->hasColumn('sort_order')) {
|
||||||
|
$table->removeColumn('sort_order');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($table->hasColumn('notes')) {
|
||||||
|
$table->removeColumn('notes');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($table->hasColumn('email')) {
|
||||||
|
$table->removeColumn('email');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($table->hasColumn('phone')) {
|
||||||
|
$table->removeColumn('phone');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($table->hasColumn('person_full_name')) {
|
||||||
|
$table->removeColumn('person_full_name');
|
||||||
|
}
|
||||||
|
|
||||||
|
$table->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class CreateCadAreaJobsTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('cad_area_jobs');
|
||||||
|
|
||||||
|
$table
|
||||||
|
->addColumn('iduser', 'integer', [
|
||||||
|
'null' => true,
|
||||||
|
'signed' => false,
|
||||||
|
'limit' => 10,
|
||||||
|
])
|
||||||
|
->addColumn('original_filename', 'string', [
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('stored_filename', 'string', [
|
||||||
|
'limit' => 255,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('file_path', 'string', [
|
||||||
|
'limit' => 500,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('file_url', 'string', [
|
||||||
|
'limit' => 500,
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('file_size', 'integer', [
|
||||||
|
'null' => true,
|
||||||
|
'signed' => false,
|
||||||
|
])
|
||||||
|
->addColumn('status', 'enum', [
|
||||||
|
'values' => [
|
||||||
|
'uploaded',
|
||||||
|
'processing',
|
||||||
|
'completed',
|
||||||
|
'error',
|
||||||
|
],
|
||||||
|
'default' => 'uploaded',
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->addColumn('area_mm2', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('area_cm2', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('area_m2', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 9,
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('scale_detected', 'string', [
|
||||||
|
'limit' => 50,
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('confidence', 'string', [
|
||||||
|
'limit' => 50,
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('message', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('python_response', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('created_at', 'timestamp', [
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addColumn('updated_at', 'timestamp', [
|
||||||
|
'default' => 'CURRENT_TIMESTAMP',
|
||||||
|
'update' => 'CURRENT_TIMESTAMP',
|
||||||
|
'null' => true,
|
||||||
|
])
|
||||||
|
->addIndex(['iduser'], [
|
||||||
|
'name' => 'idx_cad_area_jobs_iduser',
|
||||||
|
])
|
||||||
|
->addIndex(['status'], [
|
||||||
|
'name' => 'idx_cad_area_jobs_status',
|
||||||
|
])
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddNotifyFunctionToScadDeadlines extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (!$this->hasTable('scad_deadlines')) {
|
||||||
|
throw new RuntimeException('Table scad_deadlines does not exist.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = $this->table('scad_deadlines');
|
||||||
|
|
||||||
|
if (!$table->hasColumn('notify_function')) {
|
||||||
|
$table
|
||||||
|
->addColumn('notify_function', 'boolean', [
|
||||||
|
'null' => false,
|
||||||
|
'default' => false,
|
||||||
|
'after' => 'function_id',
|
||||||
|
'comment' => 'Send deadline reminder also to the linked function email',
|
||||||
|
])
|
||||||
|
->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
if (!$this->hasTable('scad_deadlines')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = $this->table('scad_deadlines');
|
||||||
|
|
||||||
|
if ($table->hasColumn('notify_function')) {
|
||||||
|
$table
|
||||||
|
->removeColumn('notify_function')
|
||||||
|
->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddRoiFieldsToCadAreaJobsTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('cad_area_jobs');
|
||||||
|
|
||||||
|
if (!$table->hasColumn('roi_x')) {
|
||||||
|
$table->addColumn('roi_x', 'decimal', [
|
||||||
|
'precision' => 12,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
'after' => 'file_size',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('roi_y')) {
|
||||||
|
$table->addColumn('roi_y', 'decimal', [
|
||||||
|
'precision' => 12,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
'after' => 'roi_x',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('roi_width')) {
|
||||||
|
$table->addColumn('roi_width', 'decimal', [
|
||||||
|
'precision' => 12,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
'after' => 'roi_y',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('roi_height')) {
|
||||||
|
$table->addColumn('roi_height', 'decimal', [
|
||||||
|
'precision' => 12,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
'after' => 'roi_width',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('roi_page')) {
|
||||||
|
$table->addColumn('roi_page', 'integer', [
|
||||||
|
'null' => true,
|
||||||
|
'default' => 1,
|
||||||
|
'after' => 'roi_height',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('calculation_mode')) {
|
||||||
|
$table->addColumn('calculation_mode', 'string', [
|
||||||
|
'limit' => 50,
|
||||||
|
'null' => true,
|
||||||
|
'default' => 'auto_roi',
|
||||||
|
'after' => 'roi_page',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddResultDetailFieldsToCadAreaJobsTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('cad_area_jobs');
|
||||||
|
|
||||||
|
if (!$table->hasColumn('width_mm')) {
|
||||||
|
$table->addColumn('width_mm', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('height_mm')) {
|
||||||
|
$table->addColumn('height_mm', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('scale_used')) {
|
||||||
|
$table->addColumn('scale_used', 'decimal', [
|
||||||
|
'precision' => 12,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('strategy_used')) {
|
||||||
|
$table->addColumn('strategy_used', 'string', [
|
||||||
|
'limit' => 100,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddManualTracingFieldsToCadAreaJobsTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('cad_area_jobs');
|
||||||
|
|
||||||
|
if (!$table->hasColumn('width_mm')) {
|
||||||
|
$table->addColumn('width_mm', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('height_mm')) {
|
||||||
|
$table->addColumn('height_mm', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('scale_used')) {
|
||||||
|
$table->addColumn('scale_used', 'decimal', [
|
||||||
|
'precision' => 12,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('strategy_used')) {
|
||||||
|
$table->addColumn('strategy_used', 'string', [
|
||||||
|
'limit' => 100,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('manual_calibration_px')) {
|
||||||
|
$table->addColumn('manual_calibration_px', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('manual_calibration_mm')) {
|
||||||
|
$table->addColumn('manual_calibration_mm', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('manual_mm_per_px')) {
|
||||||
|
$table->addColumn('manual_mm_per_px', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 10,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('manual_polygon_json')) {
|
||||||
|
$table->addColumn('manual_polygon_json', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('manual_area_mm2')) {
|
||||||
|
$table->addColumn('manual_area_mm2', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('manual_area_cm2')) {
|
||||||
|
$table->addColumn('manual_area_cm2', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('manual_width_mm')) {
|
||||||
|
$table->addColumn('manual_width_mm', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('manual_height_mm')) {
|
||||||
|
$table->addColumn('manual_height_mm', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('manual_status')) {
|
||||||
|
$table->addColumn('manual_status', 'string', [
|
||||||
|
'limit' => 50,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddManualHoleFieldsToCadAreaJobsTable extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change(): void
|
||||||
|
{
|
||||||
|
$table = $this->table('cad_area_jobs');
|
||||||
|
|
||||||
|
if (!$table->hasColumn('manual_outer_area_mm2')) {
|
||||||
|
$table->addColumn('manual_outer_area_mm2', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('manual_holes_area_mm2')) {
|
||||||
|
$table->addColumn('manual_holes_area_mm2', 'decimal', [
|
||||||
|
'precision' => 18,
|
||||||
|
'scale' => 6,
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$table->hasColumn('manual_holes_json')) {
|
||||||
|
$table->addColumn('manual_holes_json', 'text', [
|
||||||
|
'null' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
# 1. Database migration
|
||||||
|
|
||||||
|
```mysql
|
||||||
|
ALTER TABLE employees
|
||||||
|
ADD COLUMN address varchar(500) DEFAULT NULL AFTER last_name,
|
||||||
|
ADD COLUMN phone varchar(255) DEFAULT NULL AFTER address,
|
||||||
|
ADD COLUMN email varchar(255) DEFAULT NULL AFTER phone,
|
||||||
|
ADD COLUMN job_role_id int(10) UNSIGNED DEFAULT NULL AFTER department_id;
|
||||||
|
|
||||||
|
-- Replace ENUM status with plain VARCHAR for easier maintenance.
|
||||||
|
ALTER TABLE employees
|
||||||
|
MODIFY status varchar(255) NOT NULL DEFAULT 'active';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS job_roles (
|
||||||
|
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
name varchar(255) NOT NULL,
|
||||||
|
description text DEFAULT NULL,
|
||||||
|
sort_order int(10) UNSIGNED NOT NULL DEFAULT 999,
|
||||||
|
is_active tinyint(1) NOT NULL DEFAULT 1,
|
||||||
|
created_at timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uniq_job_roles_name (name),
|
||||||
|
KEY idx_job_roles_active (is_active),
|
||||||
|
KEY idx_job_roles_sort_order (sort_order)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
ALTER TABLE employees
|
||||||
|
ADD KEY idx_employees_job_role_id (job_role_id);
|
||||||
|
|
||||||
|
ALTER TABLE employees
|
||||||
|
ADD CONSTRAINT fk_employees_job_role
|
||||||
|
FOREIGN KEY (job_role_id) REFERENCES job_roles (id)
|
||||||
|
ON DELETE SET NULL
|
||||||
|
ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- 1) Seed job_roles with every distinct non-empty value of employees.position.
|
||||||
|
INSERT IGNORE INTO job_roles (name, is_active, sort_order, created_at, updated_at)
|
||||||
|
SELECT DISTINCT TRIM(position), 1, 999, NOW(), NOW()
|
||||||
|
FROM employees
|
||||||
|
WHERE position IS NOT NULL AND TRIM(position) <> '';
|
||||||
|
|
||||||
|
-- 2) Backfill employees.job_role_id by matching position text to job_roles.name.
|
||||||
|
UPDATE employees e
|
||||||
|
JOIN job_roles jr ON jr.name = TRIM(e.position)
|
||||||
|
SET e.job_role_id = jr.id
|
||||||
|
WHERE e.position IS NOT NULL AND TRIM(e.position) <> '';
|
||||||
|
|
||||||
|
-- 3) Drop the legacy column.
|
||||||
|
ALTER TABLE employees DROP COLUMN position;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS training_topics (
|
||||||
|
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
name varchar(255) NOT NULL,
|
||||||
|
description text DEFAULT NULL,
|
||||||
|
default_frequency_months int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
default_reminder_days int(10) UNSIGNED NOT NULL DEFAULT 30,
|
||||||
|
sort_order int(10) UNSIGNED NOT NULL DEFAULT 999,
|
||||||
|
is_active tinyint(1) NOT NULL DEFAULT 1,
|
||||||
|
is_mandatory tinyint(1) NOT NULL DEFAULT 0,
|
||||||
|
created_at timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uniq_training_topics_name (name),
|
||||||
|
KEY idx_training_topics_active (is_active),
|
||||||
|
KEY idx_training_topics_mandatory (is_mandatory),
|
||||||
|
KEY idx_training_topics_sort_order (sort_order)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS employee_documents (
|
||||||
|
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
employee_id int(10) UNSIGNED NOT NULL,
|
||||||
|
category varchar(255) NOT NULL DEFAULT 'other',
|
||||||
|
original_name varchar(500) NOT NULL,
|
||||||
|
stored_name varchar(500) NOT NULL,
|
||||||
|
mime_type varchar(255) DEFAULT NULL,
|
||||||
|
size int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
notes text DEFAULT NULL,
|
||||||
|
uploaded_by int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
created_at timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_employee_documents_employee (employee_id),
|
||||||
|
KEY idx_employee_documents_category (category),
|
||||||
|
KEY idx_employee_documents_uploaded_by (uploaded_by),
|
||||||
|
CONSTRAINT fk_employee_documents_employee
|
||||||
|
FOREIGN KEY (employee_id) REFERENCES employees (id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT fk_employee_documents_uploaded_by
|
||||||
|
FOREIGN KEY (uploaded_by) REFERENCES auth_users (id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS employee_ppe (
|
||||||
|
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
employee_id int(10) UNSIGNED NOT NULL,
|
||||||
|
item_name varchar(255) NOT NULL,
|
||||||
|
delivery_date date DEFAULT NULL,
|
||||||
|
delivered_by varchar(255) DEFAULT NULL,
|
||||||
|
notes text DEFAULT NULL,
|
||||||
|
created_by int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
created_at timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_employee_ppe_employee (employee_id),
|
||||||
|
KEY idx_employee_ppe_delivery_date (delivery_date),
|
||||||
|
CONSTRAINT fk_employee_ppe_employee
|
||||||
|
FOREIGN KEY (employee_id) REFERENCES employees (id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT fk_employee_ppe_created_by
|
||||||
|
FOREIGN KEY (created_by) REFERENCES auth_users (id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS employee_trainings (
|
||||||
|
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
employee_id int(10) UNSIGNED NOT NULL,
|
||||||
|
training_topic_id int(10) UNSIGNED NOT NULL,
|
||||||
|
completed_date date NOT NULL,
|
||||||
|
delivered_by varchar(255) DEFAULT NULL,
|
||||||
|
description text DEFAULT NULL,
|
||||||
|
training_type varchar(255) NOT NULL DEFAULT 'initial',
|
||||||
|
update_frequency_months int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
reminder_days int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
next_due_date date DEFAULT NULL,
|
||||||
|
created_by int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
created_at timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
updated_at timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_employee_trainings_employee (employee_id),
|
||||||
|
KEY idx_employee_trainings_topic (training_topic_id),
|
||||||
|
KEY idx_employee_trainings_next_due (next_due_date),
|
||||||
|
KEY idx_employee_trainings_employee_topic (employee_id, training_topic_id),
|
||||||
|
KEY idx_employee_trainings_created_by (created_by),
|
||||||
|
CONSTRAINT fk_employee_trainings_employee
|
||||||
|
FOREIGN KEY (employee_id) REFERENCES employees (id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT fk_employee_trainings_topic
|
||||||
|
FOREIGN KEY (training_topic_id) REFERENCES training_topics (id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT fk_employee_trainings_created_by
|
||||||
|
FOREIGN KEY (created_by) REFERENCES auth_users (id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS employee_training_attachments (
|
||||||
|
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
training_id int(10) UNSIGNED NOT NULL,
|
||||||
|
original_name varchar(500) NOT NULL,
|
||||||
|
stored_name varchar(500) NOT NULL,
|
||||||
|
mime_type varchar(255) DEFAULT NULL,
|
||||||
|
size int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
uploaded_by int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
created_at timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_employee_training_attachments_training (training_id),
|
||||||
|
KEY idx_employee_training_attachments_uploaded_by (uploaded_by),
|
||||||
|
CONSTRAINT fk_employee_training_attachments_training
|
||||||
|
FOREIGN KEY (training_id) REFERENCES employee_trainings (id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT fk_employee_training_attachments_uploaded_by
|
||||||
|
FOREIGN KEY (uploaded_by) REFERENCES auth_users (id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS employee_training_log (
|
||||||
|
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
employee_id int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
training_id int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
action varchar(255) NOT NULL,
|
||||||
|
field varchar(255) DEFAULT NULL,
|
||||||
|
old_value text DEFAULT NULL,
|
||||||
|
new_value text DEFAULT NULL,
|
||||||
|
changed_by int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
changed_at timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_employee_training_log_employee (employee_id),
|
||||||
|
KEY idx_employee_training_log_training (training_id),
|
||||||
|
KEY idx_employee_training_log_changed_at (changed_at),
|
||||||
|
CONSTRAINT fk_employee_training_log_employee
|
||||||
|
FOREIGN KEY (employee_id) REFERENCES employees (id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT fk_employee_training_log_training
|
||||||
|
FOREIGN KEY (training_id) REFERENCES employee_trainings (id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT fk_employee_training_log_changed_by
|
||||||
|
FOREIGN KEY (changed_by) REFERENCES auth_users (id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
INSERT INTO auth_roles (name, display_name, description, removable, created_at, updated_at) VALUES
|
||||||
|
('employee', 'Employee', 'Read-only access to own employee profile.', 1, NOW(), NOW()),
|
||||||
|
('employee-hr', 'HR Manager', 'Can manage employee profiles, documents, PPE and training records.', 1, NOW(), NOW()),
|
||||||
|
('manager', 'Manager', 'Same permissions as HR Manager.', 1, NOW(), NOW())
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
display_name = VALUES(display_name),
|
||||||
|
description = VALUES(description),
|
||||||
|
updated_at = NOW();
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS training_reminder_log (
|
||||||
|
id int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
training_id int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
employee_id int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
training_topic_id int(10) UNSIGNED DEFAULT NULL,
|
||||||
|
addressee_email varchar(255) NOT NULL,
|
||||||
|
next_due_date date DEFAULT NULL,
|
||||||
|
status_at_send varchar(255) NOT NULL,
|
||||||
|
sent_at timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_training_reminder_log_dedup (training_id, addressee_email, next_due_date),
|
||||||
|
KEY idx_training_reminder_log_dedup_missing (employee_id, training_topic_id, addressee_email),
|
||||||
|
KEY idx_training_reminder_log_sent_at (sent_at),
|
||||||
|
CONSTRAINT fk_training_reminder_log_training
|
||||||
|
FOREIGN KEY (training_id) REFERENCES employee_trainings (id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT fk_training_reminder_log_employee
|
||||||
|
FOREIGN KEY (employee_id) REFERENCES employees (id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT fk_training_reminder_log_topic
|
||||||
|
FOREIGN KEY (training_topic_id) REFERENCES training_topics (id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
```
|
||||||
|
|
||||||
|
# 2. Upload storage folder
|
||||||
|
|
||||||
|
Create the storage directory with the correct permissions for the web server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /var/www/zibo-dashboard/public/userarea/files/employees
|
||||||
|
chown -R www-data:www-data /var/www/zibo-dashboard/public/userarea/files
|
||||||
|
chmod -R 775 /var/www/zibo-dashboard/public/userarea/files
|
||||||
|
```
|
||||||
|
|
||||||
|
Uploaded files will be organized as:
|
||||||
|
|
||||||
|
```
|
||||||
|
files/employees/{employee_id}/documents/ # File Repository (HR)
|
||||||
|
files/employees/{employee_id}/trainings/ # Training certificates
|
||||||
|
```
|
||||||
|
|
||||||
|
# 3. Cron for automated emails
|
||||||
|
|
||||||
|
```cron
|
||||||
|
0 7 * * * /usr/bin/php /var/www/zibo-dashboard/public/userarea/cron/send_training_reminders.php \
|
||||||
|
>> /var/www/zibo-dashboard/storage/logs/training_reminders.log 2>&1
|
||||||
|
```
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Auth check for AJAX endpoints under /userarea/ajax/.
|
||||||
|
* Include this at the top of every ajax handler.
|
||||||
|
* Sets $currentUserId from session or returns 401 JSON.
|
||||||
|
*/
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($_SESSION['iduserlogin'])) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Non autorizzato. Effettua il login.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentUserId = (int)$_SESSION['iduserlogin'];
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID documento non valido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT employee_id, stored_name FROM employee_documents WHERE id = :id LIMIT 1");
|
||||||
|
$stmt->execute(['id' => $id]);
|
||||||
|
$doc = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$doc) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Documento non trovato.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$del = $pdo->prepare("DELETE FROM employee_documents WHERE id = :id");
|
||||||
|
$del->execute(['id' => $id]);
|
||||||
|
|
||||||
|
$path = __DIR__ . '/../../files/employees/' . (int)$doc['employee_id'] . '/documents/' . $doc['stored_name'];
|
||||||
|
if (is_file($path)) {
|
||||||
|
@unlink($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
include('../../include/headscript.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'ID DPI non valido.'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE employee_ppe_items
|
||||||
|
SET status = 'returned',
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'DPI rimosso correttamente.'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
$row = $pdo->prepare("SELECT employee_id FROM employee_trainings WHERE id = :id");
|
||||||
|
$row->execute(['id' => $id]);
|
||||||
|
$tr = $row->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$tr) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect attached files BEFORE deletion so we can unlink them after
|
||||||
|
$files = $pdo->prepare("SELECT stored_name FROM employee_training_attachments WHERE training_id = :id");
|
||||||
|
$files->execute(['id' => $id]);
|
||||||
|
$stored = $files->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
// Log BEFORE delete (FK on log allows SET NULL on training delete but we want a clean record)
|
||||||
|
$pdo->prepare("
|
||||||
|
INSERT INTO employee_training_log
|
||||||
|
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
|
||||||
|
VALUES
|
||||||
|
(:eid, NULL, 'deleted', NULL, NULL, NULL, :cb, NOW())
|
||||||
|
")->execute(['eid' => $tr['employee_id'], 'cb' => $currentUserId]);
|
||||||
|
|
||||||
|
$pdo->prepare("DELETE FROM employee_trainings WHERE id = :id")->execute(['id' => $id]);
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
foreach ($stored as $name) {
|
||||||
|
$path = __DIR__ . '/../../files/employees/' . (int)$tr['employee_id'] . '/trainings/' . $name;
|
||||||
|
if (is_file($path)) {
|
||||||
|
@unlink($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID allegato non valido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = $pdo->prepare("
|
||||||
|
SELECT a.stored_name, a.original_name, a.training_id, t.employee_id
|
||||||
|
FROM employee_training_attachments a
|
||||||
|
JOIN employee_trainings t ON t.id = a.training_id
|
||||||
|
WHERE a.id = :id
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$row->execute(['id' => $id]);
|
||||||
|
$att = $row->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$att) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Allegato non trovato.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
$pdo->prepare("DELETE FROM employee_training_attachments WHERE id = :id")->execute(['id' => $id]);
|
||||||
|
$pdo->prepare("
|
||||||
|
INSERT INTO employee_training_log
|
||||||
|
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
|
||||||
|
VALUES
|
||||||
|
(:eid, :tid, 'attachment_deleted', 'attachment', :name, NULL, :cb, NOW())
|
||||||
|
")->execute([
|
||||||
|
'eid' => $att['employee_id'],
|
||||||
|
'tid' => $att['training_id'],
|
||||||
|
'name' => $att['original_name'],
|
||||||
|
'cb' => $currentUserId,
|
||||||
|
]);
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
$path = __DIR__ . '/../../files/employees/' . (int)$att['employee_id'] . '/trainings/' . $att['stored_name'];
|
||||||
|
if (is_file($path)) {
|
||||||
|
@unlink($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../auth_check.php');
|
||||||
|
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||||
|
|
||||||
|
$id = (int)($_GET['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
http_response_code(400);
|
||||||
|
exit('ID non valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT d.*, e.auth_user_id
|
||||||
|
FROM employee_documents d
|
||||||
|
JOIN employees e ON e.id = d.employee_id
|
||||||
|
WHERE d.id = :id
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$stmt->execute(['id' => $id]);
|
||||||
|
$doc = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$doc) {
|
||||||
|
http_response_code(404);
|
||||||
|
exit('Documento non trovato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Access check: HR roles can download any; otherwise only own employee */
|
||||||
|
$roleStmt = $pdo->prepare("
|
||||||
|
SELECT r.name
|
||||||
|
FROM auth_users u
|
||||||
|
LEFT JOIN auth_roles r ON r.id = u.role_id
|
||||||
|
WHERE u.id = :id LIMIT 1
|
||||||
|
");
|
||||||
|
$roleStmt->execute(['id' => $currentUserId]);
|
||||||
|
$role = (string)$roleStmt->fetchColumn();
|
||||||
|
$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
|
||||||
|
$isHr = in_array($role, $hrRoles, true);
|
||||||
|
|
||||||
|
if (!$isHr && (int)$doc['auth_user_id'] !== $currentUserId) {
|
||||||
|
http_response_code(403);
|
||||||
|
exit('Accesso negato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = __DIR__ . '/../../files/employees/' . (int)$doc['employee_id'] . '/documents/' . $doc['stored_name'];
|
||||||
|
if (!is_file($path)) {
|
||||||
|
http_response_code(404);
|
||||||
|
exit('File non trovato sul server.');
|
||||||
|
}
|
||||||
|
|
||||||
|
while (ob_get_level() > 0) { ob_end_clean(); }
|
||||||
|
header('Content-Type: ' . (!empty($doc['mime_type']) ? $doc['mime_type'] : 'application/octet-stream'));
|
||||||
|
header('Content-Disposition: attachment; filename="' . rawurlencode($doc['original_name']) . '"');
|
||||||
|
header('Content-Length: ' . filesize($path));
|
||||||
|
header('Cache-Control: private, max-age=0, must-revalidate');
|
||||||
|
readfile($path);
|
||||||
|
exit;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../auth_check.php');
|
||||||
|
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||||
|
|
||||||
|
$id = (int)($_GET['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
http_response_code(400);
|
||||||
|
exit('ID non valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT a.*, t.employee_id, e.auth_user_id
|
||||||
|
FROM employee_training_attachments a
|
||||||
|
JOIN employee_trainings t ON t.id = a.training_id
|
||||||
|
JOIN employees e ON e.id = t.employee_id
|
||||||
|
WHERE a.id = :id
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$stmt->execute(['id' => $id]);
|
||||||
|
$att = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$att) {
|
||||||
|
http_response_code(404);
|
||||||
|
exit('Allegato non trovato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Access: HR or owning employee */
|
||||||
|
$roleStmt = $pdo->prepare("
|
||||||
|
SELECT r.name FROM auth_users u
|
||||||
|
LEFT JOIN auth_roles r ON r.id = u.role_id
|
||||||
|
WHERE u.id = :id LIMIT 1
|
||||||
|
");
|
||||||
|
$roleStmt->execute(['id' => $currentUserId]);
|
||||||
|
$role = (string)$roleStmt->fetchColumn();
|
||||||
|
$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
|
||||||
|
$isHr = in_array($role, $hrRoles, true);
|
||||||
|
|
||||||
|
if (!$isHr && (int)$att['auth_user_id'] !== $currentUserId) {
|
||||||
|
http_response_code(403);
|
||||||
|
exit('Accesso negato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = __DIR__ . '/../../files/employees/' . (int)$att['employee_id'] . '/trainings/' . $att['stored_name'];
|
||||||
|
if (!is_file($path)) {
|
||||||
|
http_response_code(404);
|
||||||
|
exit('File non trovato sul server.');
|
||||||
|
}
|
||||||
|
|
||||||
|
while (ob_get_level() > 0) { ob_end_clean(); }
|
||||||
|
header('Content-Type: ' . (!empty($att['mime_type']) ? $att['mime_type'] : 'application/octet-stream'));
|
||||||
|
header('Content-Disposition: attachment; filename="' . rawurlencode($att['original_name']) . '"');
|
||||||
|
header('Content-Length: ' . filesize($path));
|
||||||
|
header('Cache-Control: private, max-age=0, must-revalidate');
|
||||||
|
readfile($path);
|
||||||
|
exit;
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../auth_check.php');
|
||||||
|
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$trainingId = (int)($_GET['training_id'] ?? 0);
|
||||||
|
if ($trainingId <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
/* Access: HR or owner */
|
||||||
|
$ownerStmt = $pdo->prepare("
|
||||||
|
SELECT e.auth_user_id
|
||||||
|
FROM employee_trainings t
|
||||||
|
JOIN employees e ON e.id = t.employee_id
|
||||||
|
WHERE t.id = :id LIMIT 1
|
||||||
|
");
|
||||||
|
$ownerStmt->execute(['id' => $trainingId]);
|
||||||
|
$ownerAuthUserId = $ownerStmt->fetchColumn();
|
||||||
|
if ($ownerAuthUserId === false) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$roleStmt = $pdo->prepare("
|
||||||
|
SELECT r.name FROM auth_users u
|
||||||
|
LEFT JOIN auth_roles r ON r.id = u.role_id
|
||||||
|
WHERE u.id = :id LIMIT 1
|
||||||
|
");
|
||||||
|
$roleStmt->execute(['id' => $currentUserId]);
|
||||||
|
$role = (string)$roleStmt->fetchColumn();
|
||||||
|
$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
|
||||||
|
$isHr = in_array($role, $hrRoles, true);
|
||||||
|
|
||||||
|
if (!$isHr && (int)$ownerAuthUserId !== $currentUserId) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Accesso negato.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT id, original_name, mime_type, size, created_at
|
||||||
|
FROM employee_training_attachments
|
||||||
|
WHERE training_id = :tid
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
");
|
||||||
|
$stmt->execute(['tid' => $trainingId]);
|
||||||
|
$attachments = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'attachments' => $attachments,
|
||||||
|
'can_edit' => $isHr,
|
||||||
|
]);
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../auth_check.php');
|
||||||
|
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$trainingId = (int)($_GET['training_id'] ?? 0);
|
||||||
|
if ($trainingId <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
/* Access: HR or owner */
|
||||||
|
$ownerStmt = $pdo->prepare("
|
||||||
|
SELECT e.auth_user_id
|
||||||
|
FROM employee_trainings t
|
||||||
|
JOIN employees e ON e.id = t.employee_id
|
||||||
|
WHERE t.id = :id LIMIT 1
|
||||||
|
");
|
||||||
|
$ownerStmt->execute(['id' => $trainingId]);
|
||||||
|
$ownerAuthUserId = $ownerStmt->fetchColumn();
|
||||||
|
if ($ownerAuthUserId === false) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$roleStmt = $pdo->prepare("
|
||||||
|
SELECT r.name FROM auth_users u
|
||||||
|
LEFT JOIN auth_roles r ON r.id = u.role_id
|
||||||
|
WHERE u.id = :id LIMIT 1
|
||||||
|
");
|
||||||
|
$roleStmt->execute(['id' => $currentUserId]);
|
||||||
|
$role = (string)$roleStmt->fetchColumn();
|
||||||
|
$hrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
|
||||||
|
$isHr = in_array($role, $hrRoles, true);
|
||||||
|
|
||||||
|
if (!$isHr && (int)$ownerAuthUserId !== $currentUserId) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Accesso negato.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT l.id, l.action, l.field, l.old_value, l.new_value, l.changed_at,
|
||||||
|
TRIM(CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,''))) AS changed_by_name,
|
||||||
|
u.email AS changed_by_email
|
||||||
|
FROM employee_training_log l
|
||||||
|
LEFT JOIN auth_users u ON u.id = l.changed_by
|
||||||
|
WHERE l.training_id = :tid
|
||||||
|
ORDER BY l.changed_at DESC, l.id DESC
|
||||||
|
");
|
||||||
|
$stmt->execute(['tid' => $trainingId]);
|
||||||
|
$entries = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'entries' => $entries]);
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Bulk-assign a single DPI (PPE) item to several employees at once:
|
||||||
|
* one employee_ppe row per selected employee, all sharing the same
|
||||||
|
* item name / delivery date / delivered-by / notes.
|
||||||
|
* Mirrors ajax/trainings/save_bulk_training.php. HR-only.
|
||||||
|
*/
|
||||||
|
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// $pdo and $currentUserId from hr_auth_check.php
|
||||||
|
|
||||||
|
$itemName = trim($_POST['item_name'] ?? '');
|
||||||
|
$deliveryDate = trim($_POST['delivery_date'] ?? '');
|
||||||
|
$deliveredBy = trim($_POST['delivered_by'] ?? '');
|
||||||
|
$notes = trim($_POST['notes'] ?? '');
|
||||||
|
$employeeIds = $_POST['employee_ids'] ?? [];
|
||||||
|
|
||||||
|
if (!is_array($employeeIds)) {
|
||||||
|
$employeeIds = [];
|
||||||
|
}
|
||||||
|
$employeeIds = array_values(array_unique(array_filter(array_map('intval', $employeeIds), fn($v) => $v > 0)));
|
||||||
|
|
||||||
|
if ($itemName === '') {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Il nome del DPI è obbligatorio.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($deliveryDate !== '' && !DateTime::createFromFormat('Y-m-d', $deliveryDate)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Data di consegna non valida.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if (empty($employeeIds)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Selezionare almeno un dipendente.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$deliveryDate = $deliveryDate === '' ? null : $deliveryDate;
|
||||||
|
$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null;
|
||||||
|
$notes = $notes !== '' ? $notes : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
// Only insert for employees that actually exist
|
||||||
|
$checkEmp = $pdo->prepare("SELECT id FROM employees WHERE id = :id");
|
||||||
|
|
||||||
|
$ins = $pdo->prepare("
|
||||||
|
INSERT INTO employee_ppe
|
||||||
|
(employee_id, item_name, delivery_date, delivered_by, notes, created_by, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(:employee_id, :item_name, :delivery_date, :delivered_by, :notes, :created_by, NOW(), NOW())
|
||||||
|
");
|
||||||
|
|
||||||
|
$created = 0;
|
||||||
|
foreach ($employeeIds as $eid) {
|
||||||
|
$checkEmp->execute(['id' => $eid]);
|
||||||
|
if (!$checkEmp->fetchColumn()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$ins->execute([
|
||||||
|
'employee_id' => $eid,
|
||||||
|
'item_name' => $itemName,
|
||||||
|
'delivery_date' => $deliveryDate,
|
||||||
|
'delivered_by' => $deliveredBy,
|
||||||
|
'notes' => $notes,
|
||||||
|
'created_by' => $currentUserId,
|
||||||
|
]);
|
||||||
|
$created++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'created' => $created,
|
||||||
|
'message' => 'DPI assegnato a ' . $created . ' dipendent' . ($created === 1 ? 'e' : 'i') . '.',
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$employeeId = (int)($_POST['employee_id'] ?? 0);
|
||||||
|
$firstName = trim($_POST['first_name'] ?? '');
|
||||||
|
$lastName = trim($_POST['last_name'] ?? '');
|
||||||
|
$employeeCode = trim($_POST['employee_code'] ?? '');
|
||||||
|
$address = trim($_POST['address'] ?? '');
|
||||||
|
$phone = trim($_POST['phone'] ?? '');
|
||||||
|
$email = trim($_POST['email'] ?? '');
|
||||||
|
$hireDate = trim($_POST['hire_date'] ?? '');
|
||||||
|
$departmentId = $_POST['department_id'] ?? '';
|
||||||
|
$jobRoleId = $_POST['job_role_id'] ?? '';
|
||||||
|
$status = trim($_POST['status'] ?? '');
|
||||||
|
$authUserId = $_POST['auth_user_id'] ?? '';
|
||||||
|
$roleId = $_POST['role_id'] ?? '';
|
||||||
|
|
||||||
|
if ($employeeId <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($firstName === '' || $lastName === '') {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Nome e cognome sono obbligatori.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedStatus = ['active', 'inactive', 'suspended'];
|
||||||
|
if (!in_array($status, $allowedStatus, true)) {
|
||||||
|
$status = 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
$departmentId = ($departmentId === '' || $departmentId === null) ? null : (int)$departmentId;
|
||||||
|
$jobRoleId = ($jobRoleId === '' || $jobRoleId === null) ? null : (int)$jobRoleId;
|
||||||
|
$authUserId = ($authUserId === '' || $authUserId === null) ? null : (int)$authUserId;
|
||||||
|
$roleId = ($roleId === '' || $roleId === null) ? null : (int)$roleId;
|
||||||
|
$hireDate = $hireDate === '' ? null : $hireDate;
|
||||||
|
|
||||||
|
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Email non valida.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($employeeCode !== '') {
|
||||||
|
$check = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE employee_code = :code AND id <> :id");
|
||||||
|
$check->execute(['code' => $employeeCode, 'id' => $employeeId]);
|
||||||
|
if ((int)$check->fetchColumn() > 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Codice dipendente già in uso.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($authUserId !== null) {
|
||||||
|
$check = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE auth_user_id = :uid AND id <> :id");
|
||||||
|
$check->execute(['uid' => $authUserId, 'id' => $employeeId]);
|
||||||
|
if ((int)$check->fetchColumn() > 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Questo utente è già associato ad un altro dipendente.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE employees
|
||||||
|
SET first_name = :first_name,
|
||||||
|
last_name = :last_name,
|
||||||
|
employee_code = :employee_code,
|
||||||
|
address = :address,
|
||||||
|
phone = :phone,
|
||||||
|
email = :email,
|
||||||
|
hire_date = :hire_date,
|
||||||
|
department_id = :department_id,
|
||||||
|
job_role_id = :job_role_id,
|
||||||
|
status = :status,
|
||||||
|
auth_user_id = :auth_user_id,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = :id
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
'first_name' => $firstName,
|
||||||
|
'last_name' => $lastName,
|
||||||
|
'employee_code' => $employeeCode !== '' ? $employeeCode : null,
|
||||||
|
'address' => $address !== '' ? $address : null,
|
||||||
|
'phone' => $phone !== '' ? $phone : null,
|
||||||
|
'email' => $email !== '' ? $email : null,
|
||||||
|
'hire_date' => $hireDate,
|
||||||
|
'department_id' => $departmentId,
|
||||||
|
'job_role_id' => $jobRoleId,
|
||||||
|
'status' => $status,
|
||||||
|
'auth_user_id' => $authUserId,
|
||||||
|
'id' => $employeeId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Optionally update Vanguard role for the linked auth_user
|
||||||
|
if ($authUserId !== null && $roleId !== null) {
|
||||||
|
$check = $pdo->prepare("SELECT COUNT(*) FROM auth_roles WHERE id = ?");
|
||||||
|
$check->execute([$roleId]);
|
||||||
|
if ((int)$check->fetchColumn() > 0) {
|
||||||
|
$upd = $pdo->prepare("UPDATE auth_users SET role_id = :role_id, updated_at = NOW() WHERE id = :uid");
|
||||||
|
$upd->execute(['role_id' => $roleId, 'uid' => $authUserId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
<?php
|
||||||
|
include('../../include/headscript.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$id = isset($_POST['id']) && $_POST['id'] !== '' ? (int)$_POST['id'] : null;
|
||||||
|
$employeeId = (int)($_POST['employee_id'] ?? 0);
|
||||||
|
$ppeItemId = (int)($_POST['ppe_item_id'] ?? 0);
|
||||||
|
$assignedDate = trim($_POST['assigned_date'] ?? '');
|
||||||
|
$expiryDate = trim($_POST['expiry_date'] ?? '');
|
||||||
|
$deliveredBy = trim($_POST['delivered_by'] ?? '');
|
||||||
|
$status = trim($_POST['status'] ?? 'assigned');
|
||||||
|
$notes = trim($_POST['notes'] ?? '');
|
||||||
|
|
||||||
|
$allowedStatuses = [
|
||||||
|
'assigned',
|
||||||
|
'returned',
|
||||||
|
'expired',
|
||||||
|
'lost',
|
||||||
|
'damaged',
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($employeeId <= 0) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Dipendente non valido.'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ppeItemId <= 0) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Selezionare un DPI.'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($status, $allowedStatuses, true)) {
|
||||||
|
$status = 'assigned';
|
||||||
|
}
|
||||||
|
|
||||||
|
$checkEmployee = $pdo->prepare("SELECT id FROM employees WHERE id = ? LIMIT 1");
|
||||||
|
$checkEmployee->execute([$employeeId]);
|
||||||
|
|
||||||
|
if (!$checkEmployee->fetchColumn()) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Dipendente non trovato.'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$checkPpe = $pdo->prepare("SELECT id FROM ppe_items WHERE id = ? LIMIT 1");
|
||||||
|
$checkPpe->execute([$ppeItemId]);
|
||||||
|
|
||||||
|
if (!$checkPpe->fetchColumn()) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'DPI non trovato.'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($id) {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE employee_ppe_items
|
||||||
|
SET ppe_item_id = :ppe_item_id,
|
||||||
|
assigned_date = :assigned_date,
|
||||||
|
expiry_date = :expiry_date,
|
||||||
|
delivered_by = :delivered_by,
|
||||||
|
status = :status,
|
||||||
|
notes = :notes,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = :id
|
||||||
|
AND employee_id = :employee_id
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
'ppe_item_id' => $ppeItemId,
|
||||||
|
'assigned_date' => $assignedDate !== '' ? $assignedDate : null,
|
||||||
|
'expiry_date' => $expiryDate !== '' ? $expiryDate : null,
|
||||||
|
'delivered_by' => $deliveredBy !== '' ? $deliveredBy : null,
|
||||||
|
'status' => $status,
|
||||||
|
'notes' => $notes !== '' ? $notes : null,
|
||||||
|
'id' => $id,
|
||||||
|
'employee_id' => $employeeId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'DPI aggiornato.'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
INSERT INTO employee_ppe_items
|
||||||
|
(
|
||||||
|
employee_id,
|
||||||
|
ppe_item_id,
|
||||||
|
assigned_date,
|
||||||
|
expiry_date,
|
||||||
|
delivered_by,
|
||||||
|
quantity,
|
||||||
|
status,
|
||||||
|
notes,
|
||||||
|
created_by,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
:employee_id,
|
||||||
|
:ppe_item_id,
|
||||||
|
:assigned_date,
|
||||||
|
:expiry_date,
|
||||||
|
:delivered_by,
|
||||||
|
1,
|
||||||
|
:status,
|
||||||
|
:notes,
|
||||||
|
:created_by,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
)
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
'employee_id' => $employeeId,
|
||||||
|
'ppe_item_id' => $ppeItemId,
|
||||||
|
'assigned_date' => $assignedDate !== '' ? $assignedDate : null,
|
||||||
|
'expiry_date' => $expiryDate !== '' ? $expiryDate : null,
|
||||||
|
'delivered_by' => $deliveredBy !== '' ? $deliveredBy : null,
|
||||||
|
'status' => $status,
|
||||||
|
'notes' => $notes !== '' ? $notes : null,
|
||||||
|
'created_by' => isset($iduserlogin) ? (int)$iduserlogin : null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'DPI assegnato.'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$employeeId = (int)($_POST['employee_id'] ?? 0);
|
||||||
|
$topicId = (int)($_POST['training_topic_id'] ?? 0);
|
||||||
|
$completedDate = trim($_POST['completed_date'] ?? '');
|
||||||
|
$deliveredBy = trim($_POST['delivered_by'] ?? '');
|
||||||
|
$description = trim($_POST['description'] ?? '');
|
||||||
|
$trainingType = trim($_POST['training_type'] ?? 'initial');
|
||||||
|
$freqRaw = $_POST['update_frequency_months'] ?? '';
|
||||||
|
$remRaw = $_POST['reminder_days'] ?? '';
|
||||||
|
|
||||||
|
if ($employeeId <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($topicId <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Selezionare un corso.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($completedDate === '') {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'La data di completamento è obbligatoria.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if (!in_array($trainingType, ['initial', 'refresher'], true)) {
|
||||||
|
$trainingType = 'initial';
|
||||||
|
}
|
||||||
|
|
||||||
|
$topicStmt = $pdo->prepare("SELECT default_frequency_months, default_reminder_days FROM training_topics WHERE id = :id");
|
||||||
|
$topicStmt->execute(['id' => $topicId]);
|
||||||
|
$topic = $topicStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$topic) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Corso non trovato.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$freq = ($freqRaw === '' || $freqRaw === null) ? null : max(0, (int)$freqRaw);
|
||||||
|
$rem = ($remRaw === '' || $remRaw === null) ? null : max(0, (int)$remRaw);
|
||||||
|
|
||||||
|
/* Effective frequency for next_due_date: explicit override or topic default */
|
||||||
|
$effFreq = $freq !== null ? $freq : ($topic['default_frequency_months'] !== null ? (int)$topic['default_frequency_months'] : null);
|
||||||
|
|
||||||
|
$nextDue = null;
|
||||||
|
if ($effFreq !== null && $effFreq > 0) {
|
||||||
|
$d = DateTime::createFromFormat('Y-m-d', $completedDate);
|
||||||
|
if ($d) {
|
||||||
|
$d->modify('+' . (int)$effFreq . ' months');
|
||||||
|
$nextDue = $d->format('Y-m-d');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null;
|
||||||
|
$description = $description !== '' ? $description : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
if ($id > 0) {
|
||||||
|
$old = $pdo->prepare("SELECT * FROM employee_trainings WHERE id = :id");
|
||||||
|
$old->execute(['id' => $id]);
|
||||||
|
$oldRow = $old->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$oldRow) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$upd = $pdo->prepare("
|
||||||
|
UPDATE employee_trainings
|
||||||
|
SET training_topic_id = :topic_id,
|
||||||
|
completed_date = :completed_date,
|
||||||
|
delivered_by = :delivered_by,
|
||||||
|
description = :description,
|
||||||
|
training_type = :training_type,
|
||||||
|
update_frequency_months = :freq,
|
||||||
|
reminder_days = :rem,
|
||||||
|
next_due_date = :next_due,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = :id
|
||||||
|
");
|
||||||
|
$upd->execute([
|
||||||
|
'topic_id' => $topicId,
|
||||||
|
'completed_date' => $completedDate,
|
||||||
|
'delivered_by' => $deliveredBy,
|
||||||
|
'description' => $description,
|
||||||
|
'training_type' => $trainingType,
|
||||||
|
'freq' => $freq,
|
||||||
|
'rem' => $rem,
|
||||||
|
'next_due' => $nextDue,
|
||||||
|
'id' => $id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$fields = [
|
||||||
|
'training_topic_id' => $topicId,
|
||||||
|
'completed_date' => $completedDate,
|
||||||
|
'delivered_by' => $deliveredBy,
|
||||||
|
'description' => $description,
|
||||||
|
'training_type' => $trainingType,
|
||||||
|
'update_frequency_months' => $freq,
|
||||||
|
'reminder_days' => $rem,
|
||||||
|
'next_due_date' => $nextDue,
|
||||||
|
];
|
||||||
|
$logStmt = $pdo->prepare("
|
||||||
|
INSERT INTO employee_training_log
|
||||||
|
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
|
||||||
|
VALUES
|
||||||
|
(:eid, :tid, 'updated', :field, :old_v, :new_v, :cb, NOW())
|
||||||
|
");
|
||||||
|
foreach ($fields as $f => $newV) {
|
||||||
|
$oldV = $oldRow[$f] ?? null;
|
||||||
|
if ((string)$oldV !== (string)$newV) {
|
||||||
|
$logStmt->execute([
|
||||||
|
'eid' => $employeeId,
|
||||||
|
'tid' => $id,
|
||||||
|
'field' => $f,
|
||||||
|
'old_v' => $oldV,
|
||||||
|
'new_v' => $newV,
|
||||||
|
'cb' => $currentUserId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
echo json_encode(['success' => true, 'id' => $id]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ins = $pdo->prepare("
|
||||||
|
INSERT INTO employee_trainings
|
||||||
|
(employee_id, training_topic_id, completed_date,
|
||||||
|
delivered_by, description,
|
||||||
|
training_type, update_frequency_months, reminder_days, next_due_date,
|
||||||
|
created_by, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(:eid, :tid, :completed_date,
|
||||||
|
:delivered_by, :description,
|
||||||
|
:training_type, :freq, :rem, :next_due,
|
||||||
|
:cb, NOW(), NOW())
|
||||||
|
");
|
||||||
|
$ins->execute([
|
||||||
|
'eid' => $employeeId,
|
||||||
|
'tid' => $topicId,
|
||||||
|
'completed_date' => $completedDate,
|
||||||
|
'delivered_by' => $deliveredBy,
|
||||||
|
'description' => $description,
|
||||||
|
'training_type' => $trainingType,
|
||||||
|
'freq' => $freq,
|
||||||
|
'rem' => $rem,
|
||||||
|
'next_due' => $nextDue,
|
||||||
|
'cb' => $currentUserId,
|
||||||
|
]);
|
||||||
|
$newId = (int)$pdo->lastInsertId();
|
||||||
|
|
||||||
|
$pdo->prepare("
|
||||||
|
INSERT INTO employee_training_log
|
||||||
|
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
|
||||||
|
VALUES
|
||||||
|
(:eid, :tid, 'created', NULL, NULL, NULL, :cb, NOW())
|
||||||
|
")->execute(['eid' => $employeeId, 'tid' => $newId, 'cb' => $currentUserId]);
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
echo json_encode(['success' => true, 'id' => $newId]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$employeeId = (int)($_POST['employee_id'] ?? 0);
|
||||||
|
$category = trim($_POST['category'] ?? 'other');
|
||||||
|
$notes = trim($_POST['notes'] ?? '');
|
||||||
|
|
||||||
|
$allowedCategories = ['job_description', 'contract', 'rules', 'other'];
|
||||||
|
if (!in_array($category, $allowedCategories, true)) {
|
||||||
|
$category = 'other';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($employeeId <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$check = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE id = :id");
|
||||||
|
$check->execute(['id' => $employeeId]);
|
||||||
|
if ((int)$check->fetchColumn() === 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Dipendente non trovato.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
$errCode = $_FILES['file']['error'] ?? -1;
|
||||||
|
$msg = 'Errore nel caricamento del file.';
|
||||||
|
if ($errCode === UPLOAD_ERR_INI_SIZE || $errCode === UPLOAD_ERR_FORM_SIZE) {
|
||||||
|
$msg = 'Il file supera la dimensione massima consentita.';
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => false, 'message' => $msg]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$originalName = $_FILES['file']['name'];
|
||||||
|
$tmpPath = $_FILES['file']['tmp_name'];
|
||||||
|
$size = (int)$_FILES['file']['size'];
|
||||||
|
$mimeType = mime_content_type($tmpPath) ?: ($_FILES['file']['type'] ?? null);
|
||||||
|
|
||||||
|
$dir = __DIR__ . '/../../files/employees/' . $employeeId . '/documents';
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
if (!mkdir($dir, 0775, true) && !is_dir($dir)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Impossibile creare la cartella di destinazione.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$safeOriginal = preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName);
|
||||||
|
$storedName = uniqid('doc_') . '_' . $safeOriginal;
|
||||||
|
$destPath = $dir . '/' . $storedName;
|
||||||
|
|
||||||
|
if (!move_uploaded_file($tmpPath, $destPath)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Impossibile salvare il file su disco.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
INSERT INTO employee_documents
|
||||||
|
(employee_id, category, original_name, stored_name, mime_type, size, notes, uploaded_by, created_at)
|
||||||
|
VALUES
|
||||||
|
(:employee_id, :category, :original_name, :stored_name, :mime_type, :size, :notes, :uploaded_by, NOW())
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
'employee_id' => $employeeId,
|
||||||
|
'category' => $category,
|
||||||
|
'original_name' => $originalName,
|
||||||
|
'stored_name' => $storedName,
|
||||||
|
'mime_type' => $mimeType,
|
||||||
|
'size' => $size,
|
||||||
|
'notes' => $notes !== '' ? $notes : null,
|
||||||
|
'uploaded_by' => $currentUserId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
@unlink($destPath);
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$trainingId = (int)($_POST['training_id'] ?? 0);
|
||||||
|
if ($trainingId <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID formazione non valido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tr = $pdo->prepare("SELECT employee_id FROM employee_trainings WHERE id = :id");
|
||||||
|
$tr->execute(['id' => $trainingId]);
|
||||||
|
$trainingRow = $tr->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$trainingRow) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Formazione non trovata.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$employeeId = (int)$trainingRow['employee_id'];
|
||||||
|
|
||||||
|
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
$errCode = $_FILES['file']['error'] ?? -1;
|
||||||
|
$msg = 'Errore nel caricamento del file.';
|
||||||
|
if ($errCode === UPLOAD_ERR_INI_SIZE || $errCode === UPLOAD_ERR_FORM_SIZE) {
|
||||||
|
$msg = 'Il file supera la dimensione massima consentita.';
|
||||||
|
}
|
||||||
|
echo json_encode(['success' => false, 'message' => $msg]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$originalName = $_FILES['file']['name'];
|
||||||
|
$tmpPath = $_FILES['file']['tmp_name'];
|
||||||
|
$size = (int)$_FILES['file']['size'];
|
||||||
|
$mimeType = mime_content_type($tmpPath) ?: ($_FILES['file']['type'] ?? null);
|
||||||
|
|
||||||
|
$dir = __DIR__ . '/../../files/employees/' . $employeeId . '/trainings';
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
if (!mkdir($dir, 0775, true) && !is_dir($dir)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Impossibile creare la cartella di destinazione.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$safeOriginal = preg_replace('/[^a-zA-Z0-9._-]/', '_', $originalName);
|
||||||
|
$storedName = uniqid('tr_') . '_' . $safeOriginal;
|
||||||
|
$destPath = $dir . '/' . $storedName;
|
||||||
|
|
||||||
|
if (!move_uploaded_file($tmpPath, $destPath)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Impossibile salvare il file su disco.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
$ins = $pdo->prepare("
|
||||||
|
INSERT INTO employee_training_attachments
|
||||||
|
(training_id, original_name, stored_name, mime_type, size, uploaded_by, created_at)
|
||||||
|
VALUES
|
||||||
|
(:tid, :original_name, :stored_name, :mime_type, :size, :uploaded_by, NOW())
|
||||||
|
");
|
||||||
|
$ins->execute([
|
||||||
|
'tid' => $trainingId,
|
||||||
|
'original_name' => $originalName,
|
||||||
|
'stored_name' => $storedName,
|
||||||
|
'mime_type' => $mimeType,
|
||||||
|
'size' => $size,
|
||||||
|
'uploaded_by' => $currentUserId,
|
||||||
|
]);
|
||||||
|
$attachmentId = (int)$pdo->lastInsertId();
|
||||||
|
|
||||||
|
$pdo->prepare("
|
||||||
|
INSERT INTO employee_training_log
|
||||||
|
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
|
||||||
|
VALUES
|
||||||
|
(:eid, :tid, 'attachment_added', 'attachment', NULL, :name, :cb, NOW())
|
||||||
|
")->execute([
|
||||||
|
'eid' => $employeeId,
|
||||||
|
'tid' => $trainingId,
|
||||||
|
'name' => $originalName,
|
||||||
|
'cb' => $currentUserId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
echo json_encode(['success' => true, 'id' => $attachmentId]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||||
|
@unlink($destPath);
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* HR auth check for AJAX endpoints that require HR-management permissions.
|
||||||
|
* Allowed roles: Admin, User, Superuser, employee-hr, manager.
|
||||||
|
* Sets $currentUserId and $currentUserRole, or returns 401/403 JSON.
|
||||||
|
*/
|
||||||
|
require_once(__DIR__ . '/auth_check.php');
|
||||||
|
require_once(__DIR__ . '/../class/db-functions.php');
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT r.name AS role_name
|
||||||
|
FROM auth_users u
|
||||||
|
LEFT JOIN auth_roles r ON r.id = u.role_id
|
||||||
|
WHERE u.id = :id
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$stmt->execute(['id' => $currentUserId]);
|
||||||
|
$currentUserRole = (string)$stmt->fetchColumn();
|
||||||
|
|
||||||
|
$allowedHrRoles = ['Admin', 'Superuser', 'employee-hr', 'manager'];
|
||||||
|
|
||||||
|
if (!in_array($currentUserRole, $allowedHrRoles, true)) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Permessi insufficienti per questa operazione.',
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../auth_check.php');
|
||||||
|
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID mansione non valido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$usage = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE job_role_id = :id");
|
||||||
|
$usage->execute(['id' => $id]);
|
||||||
|
if ((int)$usage->fetchColumn() > 0) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Questa mansione è associata a uno o più dipendenti e non può essere cancellata.',
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM job_roles WHERE id = :id");
|
||||||
|
$stmt->execute(['id' => $id]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../auth_check.php');
|
||||||
|
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$name = trim($_POST['name'] ?? '');
|
||||||
|
$description = trim($_POST['description'] ?? '');
|
||||||
|
$sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] !== '' ? (int)$_POST['sort_order'] : 999;
|
||||||
|
$is_active = isset($_POST['is_active']) ? ((int)$_POST['is_active'] === 1 ? 1 : 0) : 1;
|
||||||
|
|
||||||
|
if ($name === '') {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Il nome della mansione è obbligatorio.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($id > 0) {
|
||||||
|
$check = $pdo->prepare("SELECT COUNT(*) FROM job_roles WHERE name = :name AND id <> :id");
|
||||||
|
$check->execute(['name' => $name, 'id' => $id]);
|
||||||
|
if ((int)$check->fetchColumn() > 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Esiste già un\'altra mansione con questo nome.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE job_roles
|
||||||
|
SET name = :name,
|
||||||
|
description = :description,
|
||||||
|
sort_order = :sort_order,
|
||||||
|
is_active = :is_active,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = :id
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
'name' => $name,
|
||||||
|
'description' => $description !== '' ? $description : null,
|
||||||
|
'sort_order' => $sort_order,
|
||||||
|
'is_active' => $is_active,
|
||||||
|
'id' => $id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'id' => $id]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$check = $pdo->prepare("SELECT COUNT(*) FROM job_roles WHERE name = :name");
|
||||||
|
$check->execute(['name' => $name]);
|
||||||
|
if ((int)$check->fetchColumn() > 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Esiste già una mansione con questo nome.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
INSERT INTO job_roles (name, description, sort_order, is_active, created_at, updated_at)
|
||||||
|
VALUES (:name, :description, :sort_order, :is_active, NOW(), NOW())
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
'name' => $name,
|
||||||
|
'description' => $description !== '' ? $description : null,
|
||||||
|
'sort_order' => $sort_order,
|
||||||
|
'is_active' => $is_active,
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../auth_check.php');
|
||||||
|
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
if ($id <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'ID corso non valido.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$usage = $pdo->prepare("SELECT COUNT(*) FROM employee_trainings WHERE training_topic_id = :id");
|
||||||
|
$usage->execute(['id' => $id]);
|
||||||
|
if ((int)$usage->fetchColumn() > 0) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Questo corso ha già delle registrazioni di formazione e non può essere cancellato.',
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM training_topics WHERE id = :id");
|
||||||
|
$stmt->execute(['id' => $id]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . '/../auth_check.php');
|
||||||
|
require_once(__DIR__ . '/../../class/db-functions.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$name = trim($_POST['name'] ?? '');
|
||||||
|
$description = trim($_POST['description'] ?? '');
|
||||||
|
$freqRaw = $_POST['default_frequency_months'] ?? '';
|
||||||
|
$remRaw = $_POST['default_reminder_days'] ?? '';
|
||||||
|
$sort_order = isset($_POST['sort_order']) && $_POST['sort_order'] !== '' ? (int)$_POST['sort_order'] : 999;
|
||||||
|
$is_active = isset($_POST['is_active']) ? ((int)$_POST['is_active'] === 1 ? 1 : 0) : 1;
|
||||||
|
$is_mandatory = isset($_POST['is_mandatory']) && (int)$_POST['is_mandatory'] === 1 ? 1 : 0;
|
||||||
|
|
||||||
|
$freq = ($freqRaw === '' || $freqRaw === null) ? null : max(0, (int)$freqRaw);
|
||||||
|
$rem = ($remRaw === '' || $remRaw === null) ? 30 : max(0, (int)$remRaw);
|
||||||
|
|
||||||
|
if ($name === '') {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Il nome del corso è obbligatorio.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($id > 0) {
|
||||||
|
$check = $pdo->prepare("SELECT COUNT(*) FROM training_topics WHERE name = :name AND id <> :id");
|
||||||
|
$check->execute(['name' => $name, 'id' => $id]);
|
||||||
|
if ((int)$check->fetchColumn() > 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Esiste già un altro corso con questo nome.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE training_topics
|
||||||
|
SET name = :name,
|
||||||
|
description = :description,
|
||||||
|
default_frequency_months = :freq,
|
||||||
|
default_reminder_days = :rem,
|
||||||
|
sort_order = :sort_order,
|
||||||
|
is_active = :is_active,
|
||||||
|
is_mandatory = :is_mandatory,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = :id
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
'name' => $name,
|
||||||
|
'description' => $description !== '' ? $description : null,
|
||||||
|
'freq' => $freq,
|
||||||
|
'rem' => $rem,
|
||||||
|
'sort_order' => $sort_order,
|
||||||
|
'is_active' => $is_active,
|
||||||
|
'is_mandatory' => $is_mandatory,
|
||||||
|
'id' => $id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'id' => $id]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$check = $pdo->prepare("SELECT COUNT(*) FROM training_topics WHERE name = :name");
|
||||||
|
$check->execute(['name' => $name]);
|
||||||
|
if ((int)$check->fetchColumn() > 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Esiste già un corso con questo nome.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
INSERT INTO training_topics
|
||||||
|
(name, description, default_frequency_months, default_reminder_days, sort_order, is_active, is_mandatory, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(:name, :description, :freq, :rem, :sort_order, :is_active, :is_mandatory, NOW(), NOW())
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
'name' => $name,
|
||||||
|
'description' => $description !== '' ? $description : null,
|
||||||
|
'freq' => $freq,
|
||||||
|
'rem' => $rem,
|
||||||
|
'sort_order' => $sort_order,
|
||||||
|
'is_active' => $is_active,
|
||||||
|
'is_mandatory' => $is_mandatory,
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Bulk "renew": set a common completed_date on the selected training records
|
||||||
|
*/
|
||||||
|
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// $pdo and $currentUserId from hr_auth_check.php
|
||||||
|
|
||||||
|
$completedDate = trim($_POST['completed_date'] ?? '');
|
||||||
|
$ids = $_POST['training_ids'] ?? [];
|
||||||
|
|
||||||
|
if (!is_array($ids)) {
|
||||||
|
$ids = [];
|
||||||
|
}
|
||||||
|
$ids = array_values(array_unique(array_filter(array_map('intval', $ids), fn($v) => $v > 0)));
|
||||||
|
|
||||||
|
if ($completedDate === '' || !DateTime::createFromFormat('Y-m-d', $completedDate)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Indicare una data valida.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if (empty($ids)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Selezionare almeno un record.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
// Load each record with its topic default frequency
|
||||||
|
$rowStmt = $pdo->prepare("
|
||||||
|
SELECT et.id, et.employee_id, et.completed_date, et.next_due_date,
|
||||||
|
et.update_frequency_months, tt.default_frequency_months
|
||||||
|
FROM employee_trainings et
|
||||||
|
JOIN training_topics tt ON tt.id = et.training_topic_id
|
||||||
|
WHERE et.id = :id
|
||||||
|
");
|
||||||
|
$upd = $pdo->prepare("
|
||||||
|
UPDATE employee_trainings
|
||||||
|
SET completed_date = :cd, next_due_date = :nd, updated_at = NOW()
|
||||||
|
WHERE id = :id
|
||||||
|
");
|
||||||
|
$logStmt = $pdo->prepare("
|
||||||
|
INSERT INTO employee_training_log
|
||||||
|
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
|
||||||
|
VALUES
|
||||||
|
(:eid, :tid, 'updated', :field, :old_v, :new_v, :cb, NOW())
|
||||||
|
");
|
||||||
|
|
||||||
|
$updated = 0;
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
$rowStmt->execute(['id' => $id]);
|
||||||
|
$row = $rowStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$row) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Effective frequency: per-record override, else topic default
|
||||||
|
$effFreq = $row['update_frequency_months'] !== null
|
||||||
|
? (int)$row['update_frequency_months']
|
||||||
|
: ($row['default_frequency_months'] !== null ? (int)$row['default_frequency_months'] : null);
|
||||||
|
|
||||||
|
$nextDue = null;
|
||||||
|
if ($effFreq !== null && $effFreq > 0) {
|
||||||
|
$d = DateTime::createFromFormat('Y-m-d', $completedDate);
|
||||||
|
if ($d) {
|
||||||
|
$d->modify('+' . $effFreq . ' months');
|
||||||
|
$nextDue = $d->format('Y-m-d');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$upd->execute(['cd' => $completedDate, 'nd' => $nextDue, 'id' => $id]);
|
||||||
|
|
||||||
|
if ((string)$row['completed_date'] !== (string)$completedDate) {
|
||||||
|
$logStmt->execute([
|
||||||
|
'eid' => $row['employee_id'], 'tid' => $id, 'field' => 'completed_date',
|
||||||
|
'old_v' => $row['completed_date'], 'new_v' => $completedDate, 'cb' => $currentUserId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if ((string)$row['next_due_date'] !== (string)$nextDue) {
|
||||||
|
$logStmt->execute([
|
||||||
|
'eid' => $row['employee_id'], 'tid' => $id, 'field' => 'next_due_date',
|
||||||
|
'old_v' => $row['next_due_date'], 'new_v' => $nextDue, 'cb' => $currentUserId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
$updated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'updated' => $updated,
|
||||||
|
'message' => $updated . ' record aggiornat' . ($updated === 1 ? 'o' : 'i') . '.',
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Calendar events for the training calendar (training_calendar.php).
|
||||||
|
* Returns FullCalendar event objects for the *current* training record per
|
||||||
|
* (employee, topic) that has a next_due_date, colored by computed status.
|
||||||
|
* HR-only.
|
||||||
|
*/
|
||||||
|
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// $pdo and $currentUserId provided by hr_auth_check.php
|
||||||
|
|
||||||
|
$start = $_GET['start'] ?? null;
|
||||||
|
$end = $_GET['end'] ?? null;
|
||||||
|
$fStatus = isset($_GET['status']) ? trim($_GET['status']) : '';
|
||||||
|
$fDept = isset($_GET['department_id']) && $_GET['department_id'] !== '' ? (int)$_GET['department_id'] : 0;
|
||||||
|
$fTopic = isset($_GET['topic_id']) && $_GET['topic_id'] !== '' ? (int)$_GET['topic_id'] : 0;
|
||||||
|
$fEmp = isset($_GET['employee_id']) && $_GET['employee_id'] !== '' ? (int)$_GET['employee_id'] : 0;
|
||||||
|
|
||||||
|
$where = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
// Deadlines only (one-time trainings have no next_due_date)
|
||||||
|
$where[] = "et.next_due_date IS NOT NULL";
|
||||||
|
|
||||||
|
// Only the most recent record per (employee, topic)
|
||||||
|
$where[] = "NOT EXISTS (
|
||||||
|
SELECT 1 FROM employee_trainings et2
|
||||||
|
WHERE et2.employee_id = et.employee_id
|
||||||
|
AND et2.training_topic_id = et.training_topic_id
|
||||||
|
AND (et2.completed_date > et.completed_date
|
||||||
|
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
|
||||||
|
)";
|
||||||
|
|
||||||
|
if ($start && $end) {
|
||||||
|
$where[] = "et.next_due_date >= :start AND et.next_due_date <= :end";
|
||||||
|
$params['start'] = $start;
|
||||||
|
$params['end'] = $end;
|
||||||
|
}
|
||||||
|
if ($fDept > 0) { $where[] = "e.department_id = :did"; $params['did'] = $fDept; }
|
||||||
|
if ($fTopic > 0) { $where[] = "et.training_topic_id = :tid"; $params['tid'] = $fTopic; }
|
||||||
|
if ($fEmp > 0) { $where[] = "et.employee_id = :eid"; $params['eid'] = $fEmp; }
|
||||||
|
|
||||||
|
$whereSql = 'WHERE ' . implode(' AND ', $where);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT et.id, et.employee_id, et.next_due_date, et.reminder_days,
|
||||||
|
tt.name AS topic_name, tt.default_reminder_days AS topic_default_rem,
|
||||||
|
e.first_name, e.last_name
|
||||||
|
FROM employee_trainings et
|
||||||
|
JOIN training_topics tt ON tt.id = et.training_topic_id
|
||||||
|
JOIN employees e ON e.id = et.employee_id
|
||||||
|
$whereSql
|
||||||
|
");
|
||||||
|
$stmt->execute($params);
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$today = new DateTime('today');
|
||||||
|
$events = [];
|
||||||
|
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$rem = $r['reminder_days'] !== null
|
||||||
|
? (int)$r['reminder_days']
|
||||||
|
: ($r['topic_default_rem'] !== null ? (int)$r['topic_default_rem'] : 30);
|
||||||
|
|
||||||
|
$due = DateTime::createFromFormat('Y-m-d', $r['next_due_date']);
|
||||||
|
if (!$due) continue;
|
||||||
|
$daysLeft = (int)$today->diff($due)->format('%r%a');
|
||||||
|
|
||||||
|
if ($daysLeft < 0) { $code = 'expired'; $color = '#dc3545'; }
|
||||||
|
elseif ($daysLeft <= $rem){ $code = 'due_soon'; $color = '#e8930c'; }
|
||||||
|
else { $code = 'compliant'; $color = '#198754'; }
|
||||||
|
|
||||||
|
if ($fStatus !== '' && $fStatus !== $code) continue;
|
||||||
|
|
||||||
|
$name = trim($r['first_name'] . ' ' . $r['last_name']);
|
||||||
|
$events[] = [
|
||||||
|
'id' => (int)$r['id'],
|
||||||
|
'title' => $name . ' — ' . $r['topic_name'],
|
||||||
|
'start' => $r['next_due_date'],
|
||||||
|
'allDay' => true,
|
||||||
|
'backgroundColor' => $color,
|
||||||
|
'borderColor' => $color,
|
||||||
|
'url' => 'employee-profile.php?id=' . (int)$r['employee_id'] . '#tab-training',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($events);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode([]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Bulk-create training records: one employee_trainings row per selected employee,
|
||||||
|
* all sharing the same course + parameters (a single training "session").
|
||||||
|
* Mirrors the next_due_date logic of ajax/employee_profile/save_training.php.
|
||||||
|
* HR-only.
|
||||||
|
*/
|
||||||
|
require_once(__DIR__ . '/../hr_auth_check.php');
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// $pdo and $currentUserId from hr_auth_check.php
|
||||||
|
|
||||||
|
$topicId = (int)($_POST['training_topic_id'] ?? 0);
|
||||||
|
$completedDate = trim($_POST['completed_date'] ?? '');
|
||||||
|
$deliveredBy = trim($_POST['delivered_by'] ?? '');
|
||||||
|
$description = trim($_POST['description'] ?? '');
|
||||||
|
$trainingType = trim($_POST['training_type'] ?? 'initial');
|
||||||
|
$freqRaw = $_POST['update_frequency_months'] ?? '';
|
||||||
|
$remRaw = $_POST['reminder_days'] ?? '';
|
||||||
|
$employeeIds = $_POST['employee_ids'] ?? [];
|
||||||
|
|
||||||
|
if (!is_array($employeeIds)) {
|
||||||
|
$employeeIds = [];
|
||||||
|
}
|
||||||
|
$employeeIds = array_values(array_unique(array_filter(array_map('intval', $employeeIds), fn($v) => $v > 0)));
|
||||||
|
|
||||||
|
if ($topicId <= 0) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Selezionare un corso.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($completedDate === '' || !DateTime::createFromFormat('Y-m-d', $completedDate)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'La data di completamento è obbligatoria.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if (empty($employeeIds)) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Selezionare almeno un dipendente.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if (!in_array($trainingType, ['initial', 'refresher'], true)) {
|
||||||
|
$trainingType = 'initial';
|
||||||
|
}
|
||||||
|
|
||||||
|
$topicStmt = $pdo->prepare("SELECT default_frequency_months, default_reminder_days FROM training_topics WHERE id = :id");
|
||||||
|
$topicStmt->execute(['id' => $topicId]);
|
||||||
|
$topic = $topicStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$topic) {
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Corso non trovato.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$freq = ($freqRaw === '' || $freqRaw === null) ? null : max(0, (int)$freqRaw);
|
||||||
|
$rem = ($remRaw === '' || $remRaw === null) ? null : max(0, (int)$remRaw);
|
||||||
|
|
||||||
|
/* Effective frequency → next_due_date (same for every employee: same date + same frequency) */
|
||||||
|
$effFreq = $freq !== null ? $freq : ($topic['default_frequency_months'] !== null ? (int)$topic['default_frequency_months'] : null);
|
||||||
|
$nextDue = null;
|
||||||
|
if ($effFreq !== null && $effFreq > 0) {
|
||||||
|
$d = DateTime::createFromFormat('Y-m-d', $completedDate);
|
||||||
|
if ($d) {
|
||||||
|
$d->modify('+' . (int)$effFreq . ' months');
|
||||||
|
$nextDue = $d->format('Y-m-d');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null;
|
||||||
|
$description = $description !== '' ? $description : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
// Only insert for employees that actually exist
|
||||||
|
$checkEmp = $pdo->prepare("SELECT id FROM employees WHERE id = :id");
|
||||||
|
|
||||||
|
$ins = $pdo->prepare("
|
||||||
|
INSERT INTO employee_trainings
|
||||||
|
(employee_id, training_topic_id, completed_date,
|
||||||
|
delivered_by, description,
|
||||||
|
training_type, update_frequency_months, reminder_days, next_due_date,
|
||||||
|
created_by, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(:eid, :tid, :completed_date,
|
||||||
|
:delivered_by, :description,
|
||||||
|
:training_type, :freq, :rem, :next_due,
|
||||||
|
:cb, NOW(), NOW())
|
||||||
|
");
|
||||||
|
$logStmt = $pdo->prepare("
|
||||||
|
INSERT INTO employee_training_log
|
||||||
|
(employee_id, training_id, action, field, old_value, new_value, changed_by, changed_at)
|
||||||
|
VALUES
|
||||||
|
(:eid, :tid, 'created', NULL, NULL, NULL, :cb, NOW())
|
||||||
|
");
|
||||||
|
|
||||||
|
$created = 0;
|
||||||
|
foreach ($employeeIds as $eid) {
|
||||||
|
$checkEmp->execute(['id' => $eid]);
|
||||||
|
if (!$checkEmp->fetchColumn()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$ins->execute([
|
||||||
|
'eid' => $eid,
|
||||||
|
'tid' => $topicId,
|
||||||
|
'completed_date' => $completedDate,
|
||||||
|
'delivered_by' => $deliveredBy,
|
||||||
|
'description' => $description,
|
||||||
|
'training_type' => $trainingType,
|
||||||
|
'freq' => $freq,
|
||||||
|
'rem' => $rem,
|
||||||
|
'next_due' => $nextDue,
|
||||||
|
'cb' => $currentUserId,
|
||||||
|
]);
|
||||||
|
$newId = (int)$pdo->lastInsertId();
|
||||||
|
$logStmt->execute(['eid' => $eid, 'tid' => $newId, 'cb' => $currentUserId]);
|
||||||
|
$created++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'created' => $created,
|
||||||
|
'message' => $created . ' formazion' . ($created === 1 ? 'e registrata' : 'i registrate') . '.',
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
require_once(__DIR__ . '/include/headscript.php');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
|
$iduser = $iduserlogin ?? null;
|
||||||
|
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$id = (int)($input['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
throw new Exception('ID non valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($iduser === null) {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT *
|
||||||
|
FROM cad_area_jobs
|
||||||
|
WHERE id = :id
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
':id' => $id
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT *
|
||||||
|
FROM cad_area_jobs
|
||||||
|
WHERE id = :id
|
||||||
|
AND iduser = :iduser
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
':id' => $id,
|
||||||
|
':iduser' => $iduser
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$job = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$job) {
|
||||||
|
throw new Exception('Record non trovato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($job['file_path']) && file_exists($job['file_path'])) {
|
||||||
|
unlink($job['file_path']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmtDelete = $pdo->prepare("
|
||||||
|
DELETE FROM cad_area_jobs
|
||||||
|
WHERE id = :id
|
||||||
|
");
|
||||||
|
$stmtDelete->execute([':id' => $id]);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true
|
||||||
|
]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log('CAD area delete error: ' . $e->getMessage());
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
require_once(__DIR__ . '/include/headscript.php');
|
||||||
|
|
||||||
|
function jsonResponse(array $data): void
|
||||||
|
{
|
||||||
|
echo json_encode($data);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateJobProcessing(PDO $pdo, int $id): void
|
||||||
|
{
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE cad_area_jobs
|
||||||
|
SET
|
||||||
|
status = 'processing',
|
||||||
|
message = 'Elaborazione in corso...',
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateJobError(PDO $pdo, int $id, string $message, ?array $pythonResponse = null): void
|
||||||
|
{
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE cad_area_jobs
|
||||||
|
SET
|
||||||
|
status = 'error',
|
||||||
|
message = ?,
|
||||||
|
python_response = ?,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$message,
|
||||||
|
$pythonResponse ? json_encode($pythonResponse) : null,
|
||||||
|
$id
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateJobCompleted(PDO $pdo, int $id, array $response): void
|
||||||
|
{
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE cad_area_jobs
|
||||||
|
SET
|
||||||
|
status = 'completed',
|
||||||
|
message = ?,
|
||||||
|
area_mm2 = ?,
|
||||||
|
area_cm2 = ?,
|
||||||
|
area_m2 = ?,
|
||||||
|
width_mm = ?,
|
||||||
|
height_mm = ?,
|
||||||
|
scale_detected = ?,
|
||||||
|
scale_used = ?,
|
||||||
|
confidence = ?,
|
||||||
|
python_response = ?,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$response['message'] ?? 'Area calcolata correttamente.',
|
||||||
|
$response['area_mm2'] ?? null,
|
||||||
|
$response['area_cm2'] ?? null,
|
||||||
|
$response['area_m2'] ?? null,
|
||||||
|
$response['width_mm'] ?? null,
|
||||||
|
$response['height_mm'] ?? null,
|
||||||
|
$response['scale_detected'] ?? null,
|
||||||
|
$response['scale_used'] ?? null,
|
||||||
|
$response['confidence'] ?? null,
|
||||||
|
json_encode($response),
|
||||||
|
$id
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCalculationMode(?string $mode): string
|
||||||
|
{
|
||||||
|
$allowed = [
|
||||||
|
'auto_roi',
|
||||||
|
'stitch_contour',
|
||||||
|
'filled_union',
|
||||||
|
'closed_path'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!$mode || !in_array($mode, $allowed, true)) {
|
||||||
|
return 'auto_roi';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasValidRoi(array $job): bool
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
array_key_exists('roi_x', $job) &&
|
||||||
|
array_key_exists('roi_y', $job) &&
|
||||||
|
array_key_exists('roi_width', $job) &&
|
||||||
|
array_key_exists('roi_height', $job) &&
|
||||||
|
$job['roi_x'] !== null &&
|
||||||
|
$job['roi_y'] !== null &&
|
||||||
|
$job['roi_width'] !== null &&
|
||||||
|
$job['roi_height'] !== null &&
|
||||||
|
(float)$job['roi_width'] > 0 &&
|
||||||
|
(float)$job['roi_height'] > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function callPythonAreaService(string $url, array $job): array
|
||||||
|
{
|
||||||
|
$filePath = $job['file_path'] ?? '';
|
||||||
|
$originalFilename = $job['original_filename'] ?? basename($filePath);
|
||||||
|
|
||||||
|
if (!$filePath || !file_exists($filePath)) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'File PDF non trovato sul server: ' . $filePath
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$mode = normalizeCalculationMode($job['calculation_mode'] ?? 'auto_roi');
|
||||||
|
|
||||||
|
$scaleRatio = $job['scale_used'] ?? null;
|
||||||
|
|
||||||
|
if ($scaleRatio === null || $scaleRatio === '' || (float)$scaleRatio <= 0) {
|
||||||
|
$scaleRatio = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
$curlFile = new CURLFile(
|
||||||
|
$filePath,
|
||||||
|
'application/pdf',
|
||||||
|
$originalFilename
|
||||||
|
);
|
||||||
|
|
||||||
|
$postFields = [
|
||||||
|
'file' => $curlFile,
|
||||||
|
'mode' => $mode,
|
||||||
|
'scale_ratio' => (string)$scaleRatio,
|
||||||
|
'roi_x' => (string)$job['roi_x'],
|
||||||
|
'roi_y' => (string)$job['roi_y'],
|
||||||
|
'roi_width' => (string)$job['roi_width'],
|
||||||
|
'roi_height' => (string)$job['roi_height'],
|
||||||
|
'roi_page' => (string)($job['roi_page'] ?? 1)
|
||||||
|
];
|
||||||
|
|
||||||
|
$ch = curl_init();
|
||||||
|
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_URL => $url,
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => $postFields,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => 180,
|
||||||
|
CURLOPT_CONNECTTIMEOUT => 10,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Accept: application/json'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$rawResponse = curl_exec($ch);
|
||||||
|
$curlError = curl_error($ch);
|
||||||
|
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($rawResponse === false) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Errore cURL verso Python: ' . $curlError
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode($rawResponse, true);
|
||||||
|
|
||||||
|
if (!is_array($decoded)) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Risposta Python non JSON valida.',
|
||||||
|
'http_code' => $httpCode,
|
||||||
|
'raw_response' => $rawResponse
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($httpCode < 200 || $httpCode >= 300) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => $decoded['message'] ?? ('Servizio Python HTTP ' . $httpCode),
|
||||||
|
'http_code' => $httpCode,
|
||||||
|
'python_response' => $decoded,
|
||||||
|
'raw_response' => $rawResponse
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
|
$iduser = $iduserlogin ?? null;
|
||||||
|
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (!is_array($input)) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Payload JSON non valido.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ids = $input['ids'] ?? [];
|
||||||
|
|
||||||
|
if (!is_array($ids) || count($ids) === 0) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Nessun ID ricevuto.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ids = array_values(array_unique(array_map('intval', $ids)));
|
||||||
|
$ids = array_filter($ids, fn($id) => $id > 0);
|
||||||
|
|
||||||
|
if (count($ids) === 0) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Nessun ID valido ricevuto.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pythonServiceUrl = 'http://127.0.0.1:5055/calculate';
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
if ($iduser === null || $iduser === '') {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT *
|
||||||
|
FROM cad_area_jobs
|
||||||
|
WHERE id = ?
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT *
|
||||||
|
FROM cad_area_jobs
|
||||||
|
WHERE id = ?
|
||||||
|
AND iduser = ?
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([$id, $iduser]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$job = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$job) {
|
||||||
|
$results[] = [
|
||||||
|
'id' => $id,
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Record non trovato.'
|
||||||
|
];
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasValidRoi($job)) {
|
||||||
|
$message = 'Prima devi definire la sezione da misurare tramite il pulsante Sezione.';
|
||||||
|
|
||||||
|
updateJobError($pdo, $id, $message, [
|
||||||
|
'success' => false,
|
||||||
|
'message' => $message,
|
||||||
|
'job_roi_debug' => [
|
||||||
|
'roi_x' => $job['roi_x'] ?? null,
|
||||||
|
'roi_y' => $job['roi_y'] ?? null,
|
||||||
|
'roi_width' => $job['roi_width'] ?? null,
|
||||||
|
'roi_height' => $job['roi_height'] ?? null,
|
||||||
|
'roi_page' => $job['roi_page'] ?? null,
|
||||||
|
'calculation_mode' => $job['calculation_mode'] ?? null
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$results[] = [
|
||||||
|
'id' => $id,
|
||||||
|
'success' => false,
|
||||||
|
'message' => $message
|
||||||
|
];
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateJobProcessing($pdo, $id);
|
||||||
|
|
||||||
|
$pythonResponse = callPythonAreaService($pythonServiceUrl, $job);
|
||||||
|
|
||||||
|
if (!($pythonResponse['success'] ?? false)) {
|
||||||
|
$message = $pythonResponse['message'] ?? 'Errore durante il calcolo Python.';
|
||||||
|
|
||||||
|
updateJobError($pdo, $id, $message, $pythonResponse);
|
||||||
|
|
||||||
|
$results[] = [
|
||||||
|
'id' => $id,
|
||||||
|
'success' => false,
|
||||||
|
'message' => $message,
|
||||||
|
'python_response' => $pythonResponse
|
||||||
|
];
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateJobCompleted($pdo, $id, $pythonResponse);
|
||||||
|
|
||||||
|
$results[] = [
|
||||||
|
'id' => $id,
|
||||||
|
'success' => true,
|
||||||
|
'message' => $pythonResponse['message'] ?? 'Area calcolata.',
|
||||||
|
'area_mm2' => $pythonResponse['area_mm2'] ?? null,
|
||||||
|
'area_cm2' => $pythonResponse['area_cm2'] ?? null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'results' => $results
|
||||||
|
]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log('CAD area process error: ' . $e->getMessage());
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
require_once(__DIR__ . '/include/headscript.php');
|
||||||
|
|
||||||
|
function jsonResponse(array $data): void
|
||||||
|
{
|
||||||
|
echo json_encode($data);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
|
$iduser = $iduserlogin ?? null;
|
||||||
|
|
||||||
|
$rawInput = file_get_contents('php://input');
|
||||||
|
$input = json_decode($rawInput, true);
|
||||||
|
|
||||||
|
if (!is_array($input)) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Payload JSON non valido.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = (int)($input['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'ID non valido.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$areaMm2 = isset($input['area_mm2']) ? (float)$input['area_mm2'] : 0;
|
||||||
|
$areaCm2 = isset($input['area_cm2']) ? (float)$input['area_cm2'] : 0;
|
||||||
|
|
||||||
|
$outerAreaMm2 = isset($input['manual_outer_area_mm2']) ? (float)$input['manual_outer_area_mm2'] : $areaMm2;
|
||||||
|
$holesAreaMm2 = isset($input['manual_holes_area_mm2']) ? (float)$input['manual_holes_area_mm2'] : 0;
|
||||||
|
|
||||||
|
$widthMm = isset($input['width_mm']) ? (float)$input['width_mm'] : null;
|
||||||
|
$heightMm = isset($input['height_mm']) ? (float)$input['height_mm'] : null;
|
||||||
|
|
||||||
|
$calibrationPx = isset($input['manual_calibration_px']) ? (float)$input['manual_calibration_px'] : 0;
|
||||||
|
$calibrationMm = isset($input['manual_calibration_mm']) ? (float)$input['manual_calibration_mm'] : 0;
|
||||||
|
$mmPerPx = isset($input['manual_mm_per_px']) ? (float)$input['manual_mm_per_px'] : 0;
|
||||||
|
|
||||||
|
$outerPolygon = $input['manual_polygon'] ?? null;
|
||||||
|
$holes = $input['manual_holes'] ?? [];
|
||||||
|
$roi = $input['roi'] ?? null;
|
||||||
|
|
||||||
|
if ($areaMm2 <= 0) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Area finale non valida.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($outerAreaMm2 <= 0) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Area esterna non valida.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($holesAreaMm2 < 0) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Area fori non valida.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($calibrationPx <= 0 || $calibrationMm <= 0 || $mmPerPx <= 0) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Calibrazione non valida.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($outerPolygon) || count($outerPolygon) < 3) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Poligono esterno non valido. Servono almeno 3 punti.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($holes)) {
|
||||||
|
$holes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$manualPolygonJson = json_encode([
|
||||||
|
'outer_polygon' => $outerPolygon,
|
||||||
|
'holes' => $holes,
|
||||||
|
'roi' => $roi,
|
||||||
|
'calibration' => $input['calibration'] ?? null,
|
||||||
|
'canvas' => $input['canvas'] ?? null,
|
||||||
|
'areas' => [
|
||||||
|
'outer_area_mm2' => $outerAreaMm2,
|
||||||
|
'holes_area_mm2' => $holesAreaMm2,
|
||||||
|
'final_area_mm2' => $areaMm2,
|
||||||
|
'final_area_cm2' => $areaCm2
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$manualHolesJson = json_encode($holes);
|
||||||
|
|
||||||
|
$roiX = null;
|
||||||
|
$roiY = null;
|
||||||
|
$roiW = null;
|
||||||
|
$roiH = null;
|
||||||
|
|
||||||
|
if (is_array($roi)) {
|
||||||
|
$roiX = isset($roi['x']) ? (float)$roi['x'] : null;
|
||||||
|
$roiY = isset($roi['y']) ? (float)$roi['y'] : null;
|
||||||
|
$roiW = isset($roi['width']) ? (float)$roi['width'] : null;
|
||||||
|
$roiH = isset($roi['height']) ? (float)$roi['height'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($iduser === null || $iduser === '') {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE cad_area_jobs
|
||||||
|
SET
|
||||||
|
roi_x = COALESCE(?, roi_x),
|
||||||
|
roi_y = COALESCE(?, roi_y),
|
||||||
|
roi_width = COALESCE(?, roi_width),
|
||||||
|
roi_height = COALESCE(?, roi_height),
|
||||||
|
roi_page = 1,
|
||||||
|
|
||||||
|
status = 'completed',
|
||||||
|
message = 'Area calcolata tramite tracciamento manuale calibrato.',
|
||||||
|
|
||||||
|
area_mm2 = ?,
|
||||||
|
area_cm2 = ?,
|
||||||
|
area_m2 = ?,
|
||||||
|
|
||||||
|
manual_area_mm2 = ?,
|
||||||
|
manual_area_cm2 = ?,
|
||||||
|
manual_outer_area_mm2 = ?,
|
||||||
|
manual_holes_area_mm2 = ?,
|
||||||
|
manual_width_mm = ?,
|
||||||
|
manual_height_mm = ?,
|
||||||
|
|
||||||
|
width_mm = ?,
|
||||||
|
height_mm = ?,
|
||||||
|
|
||||||
|
manual_calibration_px = ?,
|
||||||
|
manual_calibration_mm = ?,
|
||||||
|
manual_mm_per_px = ?,
|
||||||
|
manual_polygon_json = ?,
|
||||||
|
manual_holes_json = ?,
|
||||||
|
manual_status = 'completed',
|
||||||
|
|
||||||
|
scale_used = ?,
|
||||||
|
scale_detected = ?,
|
||||||
|
confidence = 'manual_validated',
|
||||||
|
strategy_used = 'manual_tracing_with_exclusions',
|
||||||
|
|
||||||
|
python_response = NULL,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$roiX,
|
||||||
|
$roiY,
|
||||||
|
$roiW,
|
||||||
|
$roiH,
|
||||||
|
|
||||||
|
$areaMm2,
|
||||||
|
$areaCm2,
|
||||||
|
$areaMm2 / 1000000,
|
||||||
|
|
||||||
|
$areaMm2,
|
||||||
|
$areaCm2,
|
||||||
|
$outerAreaMm2,
|
||||||
|
$holesAreaMm2,
|
||||||
|
$widthMm,
|
||||||
|
$heightMm,
|
||||||
|
|
||||||
|
$widthMm,
|
||||||
|
$heightMm,
|
||||||
|
|
||||||
|
$calibrationPx,
|
||||||
|
$calibrationMm,
|
||||||
|
$mmPerPx,
|
||||||
|
$manualPolygonJson,
|
||||||
|
$manualHolesJson,
|
||||||
|
|
||||||
|
$mmPerPx,
|
||||||
|
'manual',
|
||||||
|
|
||||||
|
$id
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE cad_area_jobs
|
||||||
|
SET
|
||||||
|
roi_x = COALESCE(?, roi_x),
|
||||||
|
roi_y = COALESCE(?, roi_y),
|
||||||
|
roi_width = COALESCE(?, roi_width),
|
||||||
|
roi_height = COALESCE(?, roi_height),
|
||||||
|
roi_page = 1,
|
||||||
|
|
||||||
|
status = 'completed',
|
||||||
|
message = 'Area calcolata tramite tracciamento manuale calibrato.',
|
||||||
|
|
||||||
|
area_mm2 = ?,
|
||||||
|
area_cm2 = ?,
|
||||||
|
area_m2 = ?,
|
||||||
|
|
||||||
|
manual_area_mm2 = ?,
|
||||||
|
manual_area_cm2 = ?,
|
||||||
|
manual_outer_area_mm2 = ?,
|
||||||
|
manual_holes_area_mm2 = ?,
|
||||||
|
manual_width_mm = ?,
|
||||||
|
manual_height_mm = ?,
|
||||||
|
|
||||||
|
width_mm = ?,
|
||||||
|
height_mm = ?,
|
||||||
|
|
||||||
|
manual_calibration_px = ?,
|
||||||
|
manual_calibration_mm = ?,
|
||||||
|
manual_mm_per_px = ?,
|
||||||
|
manual_polygon_json = ?,
|
||||||
|
manual_holes_json = ?,
|
||||||
|
manual_status = 'completed',
|
||||||
|
|
||||||
|
scale_used = ?,
|
||||||
|
scale_detected = ?,
|
||||||
|
confidence = 'manual_validated',
|
||||||
|
strategy_used = 'manual_tracing_with_exclusions',
|
||||||
|
|
||||||
|
python_response = NULL,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = ?
|
||||||
|
AND iduser = ?
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$roiX,
|
||||||
|
$roiY,
|
||||||
|
$roiW,
|
||||||
|
$roiH,
|
||||||
|
|
||||||
|
$areaMm2,
|
||||||
|
$areaCm2,
|
||||||
|
$areaMm2 / 1000000,
|
||||||
|
|
||||||
|
$areaMm2,
|
||||||
|
$areaCm2,
|
||||||
|
$outerAreaMm2,
|
||||||
|
$holesAreaMm2,
|
||||||
|
$widthMm,
|
||||||
|
$heightMm,
|
||||||
|
|
||||||
|
$widthMm,
|
||||||
|
$heightMm,
|
||||||
|
|
||||||
|
$calibrationPx,
|
||||||
|
$calibrationMm,
|
||||||
|
$mmPerPx,
|
||||||
|
$manualPolygonJson,
|
||||||
|
$manualHolesJson,
|
||||||
|
|
||||||
|
$mmPerPx,
|
||||||
|
'manual',
|
||||||
|
|
||||||
|
$id,
|
||||||
|
$iduser
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($stmt->rowCount() === 0) {
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Nessun record aggiornato. Controlla ID o utente.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Area manuale salvata correttamente.',
|
||||||
|
'area_mm2' => $areaMm2,
|
||||||
|
'area_cm2' => $areaCm2,
|
||||||
|
'outer_area_mm2' => $outerAreaMm2,
|
||||||
|
'holes_area_mm2' => $holesAreaMm2
|
||||||
|
]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log('CAD manual area save error: ' . $e->getMessage());
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
require_once(__DIR__ . '/include/headscript.php');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
|
$iduser = $iduserlogin ?? null;
|
||||||
|
|
||||||
|
$rawInput = file_get_contents('php://input');
|
||||||
|
$input = json_decode($rawInput, true);
|
||||||
|
|
||||||
|
if (!is_array($input)) {
|
||||||
|
throw new Exception('Payload JSON non valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = (int)($input['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
throw new Exception('ID non valido.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$roiX = isset($input['roi_x']) ? (float)$input['roi_x'] : null;
|
||||||
|
$roiY = isset($input['roi_y']) ? (float)$input['roi_y'] : null;
|
||||||
|
$roiW = isset($input['roi_width']) ? (float)$input['roi_width'] : null;
|
||||||
|
$roiH = isset($input['roi_height']) ? (float)$input['roi_height'] : null;
|
||||||
|
$roiPage = isset($input['roi_page']) ? (int)$input['roi_page'] : 1;
|
||||||
|
$mode = $input['calculation_mode'] ?? 'auto_roi';
|
||||||
|
|
||||||
|
if ($roiX === null || $roiY === null || $roiW === null || $roiH === null) {
|
||||||
|
throw new Exception('ROI non valida.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($roiW <= 0 || $roiH <= 0) {
|
||||||
|
throw new Exception('Dimensioni ROI non valide.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($roiX < 0 || $roiY < 0 || $roiX > 1 || $roiY > 1 || $roiW > 1 || $roiH > 1) {
|
||||||
|
throw new Exception('Coordinate ROI fuori scala.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowedModes = [
|
||||||
|
'auto_roi',
|
||||||
|
'stitch_contour',
|
||||||
|
'filled_union',
|
||||||
|
'closed_path'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!in_array($mode, $allowedModes, true)) {
|
||||||
|
$mode = 'auto_roi';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($iduser === null || $iduser === '') {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE cad_area_jobs
|
||||||
|
SET
|
||||||
|
roi_x = ?,
|
||||||
|
roi_y = ?,
|
||||||
|
roi_width = ?,
|
||||||
|
roi_height = ?,
|
||||||
|
roi_page = ?,
|
||||||
|
calculation_mode = ?,
|
||||||
|
status = 'uploaded',
|
||||||
|
message = NULL
|
||||||
|
WHERE id = ?
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$roiX,
|
||||||
|
$roiY,
|
||||||
|
$roiW,
|
||||||
|
$roiH,
|
||||||
|
$roiPage,
|
||||||
|
$mode,
|
||||||
|
$id
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
UPDATE cad_area_jobs
|
||||||
|
SET
|
||||||
|
roi_x = ?,
|
||||||
|
roi_y = ?,
|
||||||
|
roi_width = ?,
|
||||||
|
roi_height = ?,
|
||||||
|
roi_page = ?,
|
||||||
|
calculation_mode = ?,
|
||||||
|
status = 'uploaded',
|
||||||
|
message = NULL
|
||||||
|
WHERE id = ?
|
||||||
|
AND iduser = ?
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
$roiX,
|
||||||
|
$roiY,
|
||||||
|
$roiW,
|
||||||
|
$roiH,
|
||||||
|
$roiPage,
|
||||||
|
$mode,
|
||||||
|
$id,
|
||||||
|
$iduser
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($stmt->rowCount() === 0) {
|
||||||
|
throw new Exception('Nessun record aggiornato. Controlla ID o utente.');
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'ROI salvata correttamente.'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log('CAD area save ROI error: ' . $e->getMessage());
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
require_once(__DIR__ . '/include/headscript.php');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
|
$iduser = $iduserlogin ?? null;
|
||||||
|
|
||||||
|
$uploadDir = __DIR__ . '/uploads/cad_area/originals/';
|
||||||
|
$publicBaseUrl = 'uploads/cad_area/originals/';
|
||||||
|
|
||||||
|
if (!is_dir($uploadDir)) {
|
||||||
|
mkdir($uploadDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($_FILES['pdf_files'])) {
|
||||||
|
throw new Exception('Nessun file ricevuto.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = $_FILES['pdf_files'];
|
||||||
|
$insertedIds = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i < count($files['name']); $i++) {
|
||||||
|
if ($files['error'][$i] !== UPLOAD_ERR_OK) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$originalName = $files['name'][$i];
|
||||||
|
$tmpName = $files['tmp_name'][$i];
|
||||||
|
$size = (int)$files['size'][$i];
|
||||||
|
|
||||||
|
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
if ($extension !== 'pdf') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($size > 25 * 1024 * 1024) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$safeBaseName = preg_replace('/[^a-zA-Z0-9_\-]/', '_', pathinfo($originalName, PATHINFO_FILENAME));
|
||||||
|
$storedName = date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '_' . $safeBaseName . '.pdf';
|
||||||
|
|
||||||
|
$targetPath = $uploadDir . $storedName;
|
||||||
|
|
||||||
|
if (!move_uploaded_file($tmpName, $targetPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$relativeUrl = $publicBaseUrl . $storedName;
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
INSERT INTO cad_area_jobs
|
||||||
|
(
|
||||||
|
iduser,
|
||||||
|
original_filename,
|
||||||
|
stored_filename,
|
||||||
|
file_path,
|
||||||
|
file_url,
|
||||||
|
file_size,
|
||||||
|
status
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
:iduser,
|
||||||
|
:original_filename,
|
||||||
|
:stored_filename,
|
||||||
|
:file_path,
|
||||||
|
:file_url,
|
||||||
|
:file_size,
|
||||||
|
'uploaded'
|
||||||
|
)
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
':iduser' => $iduser,
|
||||||
|
':original_filename' => $originalName,
|
||||||
|
':stored_filename' => $storedName,
|
||||||
|
':file_path' => $targetPath,
|
||||||
|
':file_url' => $relativeUrl,
|
||||||
|
':file_size' => $size
|
||||||
|
]);
|
||||||
|
|
||||||
|
$insertedIds[] = (int)$pdo->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($insertedIds)) {
|
||||||
|
throw new Exception('Nessun PDF valido caricato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'ids' => $insertedIds
|
||||||
|
]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log('CAD area upload error: ' . $e->getMessage());
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,617 @@
|
|||||||
|
<?php
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
include('include/headscript.php');
|
||||||
|
|
||||||
|
$db = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
|
function jsonResponse(array $data): void
|
||||||
|
{
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
echo json_encode($data);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeNullableInt($value): ?int
|
||||||
|
{
|
||||||
|
return (isset($value) && $value !== '') ? (int)$value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeBoolValue($value): int
|
||||||
|
{
|
||||||
|
return ((string)$value === '0') ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanString(?string $value): string
|
||||||
|
{
|
||||||
|
return trim((string)$value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================
|
||||||
|
AJAX HANDLERS
|
||||||
|
========================================== */
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['ajax'] == '1') {
|
||||||
|
$action = $_POST['action'] ?? '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($action === 'add') {
|
||||||
|
$functionName = cleanString($_POST['function_name'] ?? '');
|
||||||
|
$personFullName = cleanString($_POST['person_full_name'] ?? '');
|
||||||
|
$phone = cleanString($_POST['phone'] ?? '');
|
||||||
|
$email = cleanString($_POST['email'] ?? '');
|
||||||
|
$notes = cleanString($_POST['notes'] ?? '');
|
||||||
|
$sortOrder = normalizeNullableInt($_POST['sort_order'] ?? '0') ?? 0;
|
||||||
|
$isActive = normalizeBoolValue($_POST['is_active'] ?? '1');
|
||||||
|
|
||||||
|
if ($functionName === '') {
|
||||||
|
jsonResponse(['success' => false, 'message' => 'Il nome funzione è obbligatorio.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
jsonResponse(['success' => false, 'message' => 'Email non valida.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("\n INSERT INTO company_functions\n (function_name, person_full_name, phone, email, notes, sort_order, is_active, created_at, updated_at)\n VALUES\n (:function_name, :person_full_name, :phone, :email, :notes, :sort_order, :is_active, NOW(), NOW())\n ");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
'function_name' => $functionName,
|
||||||
|
'person_full_name' => $personFullName !== '' ? $personFullName : '',
|
||||||
|
'phone' => $phone !== '' ? $phone : null,
|
||||||
|
'email' => $email !== '' ? $email : null,
|
||||||
|
'notes' => $notes !== '' ? $notes : null,
|
||||||
|
'sort_order' => $sortOrder,
|
||||||
|
'is_active' => $isActive,
|
||||||
|
]);
|
||||||
|
|
||||||
|
jsonResponse(['success' => true, 'message' => 'Funzione salvata correttamente.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'edit') {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$functionName = cleanString($_POST['function_name'] ?? '');
|
||||||
|
$personFullName = cleanString($_POST['person_full_name'] ?? '');
|
||||||
|
$phone = cleanString($_POST['phone'] ?? '');
|
||||||
|
$email = cleanString($_POST['email'] ?? '');
|
||||||
|
$notes = cleanString($_POST['notes'] ?? '');
|
||||||
|
$sortOrder = normalizeNullableInt($_POST['sort_order'] ?? '0') ?? 0;
|
||||||
|
$isActive = normalizeBoolValue($_POST['is_active'] ?? '1');
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
jsonResponse(['success' => false, 'message' => 'ID funzione non valido.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($functionName === '') {
|
||||||
|
jsonResponse(['success' => false, 'message' => 'Il nome funzione è obbligatorio.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
jsonResponse(['success' => false, 'message' => 'Email non valida.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("\n UPDATE company_functions\n SET function_name = :function_name,\n person_full_name = :person_full_name,\n phone = :phone,\n email = :email,\n notes = :notes,\n sort_order = :sort_order,\n is_active = :is_active,\n updated_at = NOW()\n WHERE id = :id\n ");
|
||||||
|
|
||||||
|
$stmt->execute([
|
||||||
|
'function_name' => $functionName,
|
||||||
|
'person_full_name' => $personFullName !== '' ? $personFullName : '',
|
||||||
|
'phone' => $phone !== '' ? $phone : null,
|
||||||
|
'email' => $email !== '' ? $email : null,
|
||||||
|
'notes' => $notes !== '' ? $notes : null,
|
||||||
|
'sort_order' => $sortOrder,
|
||||||
|
'is_active' => $isActive,
|
||||||
|
'id' => $id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
jsonResponse(['success' => true, 'message' => 'Funzione aggiornata correttamente.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'delete') {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
jsonResponse(['success' => false, 'message' => 'ID funzione non valido.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM company_functions WHERE id = :id");
|
||||||
|
$stmt->execute(['id' => $id]);
|
||||||
|
|
||||||
|
jsonResponse(['success' => true, 'message' => 'Funzione cancellata correttamente.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonResponse(['success' => false, 'message' => 'Azione non riconosciuta.']);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
jsonResponse(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================
|
||||||
|
PAGE DATA
|
||||||
|
========================================== */
|
||||||
|
$stmtFunctions = $pdo->query("\n SELECT id, function_name, person_full_name, phone, email, notes, sort_order, is_active, created_at, updated_at\n FROM company_functions\n ORDER BY is_active DESC, sort_order ASC, function_name ASC, person_full_name ASC\n");
|
||||||
|
$functions = $stmtFunctions->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="it">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||||
|
<?php include('cssinclude.php'); ?>
|
||||||
|
<title>Funzioni Aziendali - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
||||||
|
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||||
|
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-dashboard {
|
||||||
|
background-color: #cfe3ff !important;
|
||||||
|
color: #1f2d3d !important;
|
||||||
|
border: 1px solid #bcd4f4 !important;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 10px 18px;
|
||||||
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-dashboard:hover {
|
||||||
|
background-color: #b9d3ff !important;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-main-action,
|
||||||
|
.btn-add {
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-main-action:hover,
|
||||||
|
.btn-add:hover {
|
||||||
|
background-color: #0b5ed7;
|
||||||
|
color: #fff;
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead {
|
||||||
|
background-color: #cfe3ff;
|
||||||
|
color: #1f2d3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tabCompanyFunctions thead th {
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-name {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.person-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-line {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes-small {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
max-width: 420px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-status {
|
||||||
|
padding: 0.25rem 0.65rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-status.active {
|
||||||
|
background-color: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-status.inactive {
|
||||||
|
background-color: #e5e7eb;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.card-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start !important;
|
||||||
|
gap: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-dashboard,
|
||||||
|
.btn-main-action {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="wrapper" id="appWrapper">
|
||||||
|
<?php include('include/navbar.php'); ?>
|
||||||
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
<div class="page-wrapper">
|
||||||
|
<div class="page-content">
|
||||||
|
<div class="card p-3">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||||
|
<h5 class="mb-0">Funzioni Aziendali</h5>
|
||||||
|
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
|
||||||
|
↩️ Torna alla Dashboard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
||||||
|
<div>
|
||||||
|
<h6 class="fw-semibold mb-1">Elenco Funzioni</h6>
|
||||||
|
<div class="text-muted small">Gestione di RSPP, medico del lavoro, RLS e altre funzioni aziendali.</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-main-action" data-bs-toggle="modal" data-bs-target="#companyFunctionModal" onclick="openCompanyFunctionModal()">
|
||||||
|
➕ Aggiungi Funzione
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table id="tabCompanyFunctions" class="table table-striped align-middle text-center" style="width:100%;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Funzione</th>
|
||||||
|
<th>Nominativo</th>
|
||||||
|
<th>Contatti</th>
|
||||||
|
<th>Note</th>
|
||||||
|
<th>Ordine</th>
|
||||||
|
<th>Stato</th>
|
||||||
|
<th>Azioni</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($functions as $row): ?>
|
||||||
|
<?php
|
||||||
|
$id = (int)$row['id'];
|
||||||
|
$functionName = (string)($row['function_name'] ?? '');
|
||||||
|
$personFullName = (string)($row['person_full_name'] ?? '');
|
||||||
|
$phone = (string)($row['phone'] ?? '');
|
||||||
|
$email = (string)($row['email'] ?? '');
|
||||||
|
$notes = (string)($row['notes'] ?? '');
|
||||||
|
$sortOrder = (int)($row['sort_order'] ?? 0);
|
||||||
|
$isActive = (int)($row['is_active'] ?? 1);
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td class="text-start">
|
||||||
|
<div class="function-name"><?= htmlspecialchars($functionName, ENT_QUOTES, 'UTF-8') ?></div>
|
||||||
|
</td>
|
||||||
|
<td class="text-start">
|
||||||
|
<?php if ($personFullName !== ''): ?>
|
||||||
|
<div class="person-name"><?= htmlspecialchars($personFullName, ENT_QUOTES, 'UTF-8') ?></div>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="empty-text">Da definire</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td class="text-start">
|
||||||
|
<?php if ($phone !== ''): ?>
|
||||||
|
<a class="contact-line" href="tel:<?= htmlspecialchars($phone, ENT_QUOTES, 'UTF-8') ?>">
|
||||||
|
📞 <?= htmlspecialchars($phone, ENT_QUOTES, 'UTF-8') ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($email !== ''): ?>
|
||||||
|
<a class="contact-line" href="mailto:<?= htmlspecialchars($email, ENT_QUOTES, 'UTF-8') ?>">
|
||||||
|
✉️ <?= htmlspecialchars($email, ENT_QUOTES, 'UTF-8') ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($phone === '' && $email === ''): ?>
|
||||||
|
<span class="empty-text">Nessun contatto</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td class="text-start">
|
||||||
|
<?php if ($notes !== ''): ?>
|
||||||
|
<div class="notes-small" title="<?= htmlspecialchars($notes, ENT_QUOTES, 'UTF-8') ?>">
|
||||||
|
<?= htmlspecialchars($notes, ENT_QUOTES, 'UTF-8') ?>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="empty-text">—</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td><?= $sortOrder ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if ($isActive === 1): ?>
|
||||||
|
<span class="badge-status active">Attiva</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="badge-status inactive">Non attiva</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-outline-secondary edit-function mb-1"
|
||||||
|
data-id="<?= $id ?>"
|
||||||
|
data-function_name="<?= htmlspecialchars($functionName, ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-person_full_name="<?= htmlspecialchars($personFullName, ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-phone="<?= htmlspecialchars($phone, ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-email="<?= htmlspecialchars($email, ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-notes="<?= htmlspecialchars($notes, ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-sort_order="<?= $sortOrder ?>"
|
||||||
|
data-is_active="<?= $isActive ?>">
|
||||||
|
✏️ Modifica
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-outline-danger delete-function mb-1"
|
||||||
|
data-id="<?= $id ?>"
|
||||||
|
data-name="<?= htmlspecialchars($functionName, ENT_QUOTES, 'UTF-8') ?>">
|
||||||
|
🗑️ Cancella
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('include/footer.php'); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODALE ADD / EDIT FUNZIONE -->
|
||||||
|
<div class="modal fade" id="companyFunctionModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header" style="background-color:#cfe3ff;">
|
||||||
|
<h5 class="modal-title" id="companyFunctionModalTitle">Aggiungi Funzione</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="companyFunctionForm">
|
||||||
|
<input type="hidden" id="functionId">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Nome funzione <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control" id="functionName" placeholder="Es. RSPP, Medico del lavoro, RLS" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Nome e Cognome persona</label>
|
||||||
|
<input type="text" class="form-control" id="personFullName" placeholder="Es. Mario Rossi">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Telefono</label>
|
||||||
|
<input type="text" class="form-control" id="phone" placeholder="Es. +39 333 1234567">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Email</label>
|
||||||
|
<input type="email" class="form-control" id="email" placeholder="nome@azienda.it">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Ordine</label>
|
||||||
|
<input type="number" class="form-control" id="sortOrder" value="0" min="0" step="1">
|
||||||
|
<small class="text-muted">Serve solo per ordinare la visualizzazione.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Stato</label>
|
||||||
|
<select class="form-select" id="isActive">
|
||||||
|
<option value="1">Attiva</option>
|
||||||
|
<option value="0">Non attiva</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Note</label>
|
||||||
|
<textarea class="form-control" id="notes" rows="3" placeholder="Note interne, riferimenti, disponibilità, ecc."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<button type="submit" class="btn btn-add">💾 Salva</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('jsinclude.php'); ?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return $('<div>').text(value || '').html();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCompanyFunctionModal() {
|
||||||
|
$('#functionId').val('');
|
||||||
|
$('#functionName').val('');
|
||||||
|
$('#personFullName').val('');
|
||||||
|
$('#phone').val('');
|
||||||
|
$('#email').val('');
|
||||||
|
$('#notes').val('');
|
||||||
|
$('#sortOrder').val('0');
|
||||||
|
$('#isActive').val('1');
|
||||||
|
$('#companyFunctionModalTitle').text('Aggiungi Funzione');
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#tabCompanyFunctions').DataTable({
|
||||||
|
order: [
|
||||||
|
[5, 'asc'],
|
||||||
|
[4, 'asc'],
|
||||||
|
[0, 'asc']
|
||||||
|
],
|
||||||
|
pageLength: 25,
|
||||||
|
language: {
|
||||||
|
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json',
|
||||||
|
emptyTable: 'Nessuna funzione presente'
|
||||||
|
},
|
||||||
|
columnDefs: [{
|
||||||
|
targets: -1,
|
||||||
|
orderable: false,
|
||||||
|
searchable: false
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.edit-function', function() {
|
||||||
|
const btn = $(this);
|
||||||
|
|
||||||
|
$('#functionId').val(btn.data('id'));
|
||||||
|
$('#functionName').val(btn.data('function_name'));
|
||||||
|
$('#personFullName').val(btn.data('person_full_name'));
|
||||||
|
$('#phone').val(btn.data('phone'));
|
||||||
|
$('#email').val(btn.data('email'));
|
||||||
|
$('#notes').val(btn.data('notes'));
|
||||||
|
$('#sortOrder').val(btn.data('sort_order'));
|
||||||
|
$('#isActive').val(String(btn.data('is_active')));
|
||||||
|
$('#companyFunctionModalTitle').text('Modifica Funzione');
|
||||||
|
|
||||||
|
$('#companyFunctionModal').modal('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#companyFunctionForm').on('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const id = $('#functionId').val();
|
||||||
|
const payload = new URLSearchParams();
|
||||||
|
payload.append('ajax', '1');
|
||||||
|
payload.append('action', id ? 'edit' : 'add');
|
||||||
|
payload.append('id', id);
|
||||||
|
payload.append('function_name', $('#functionName').val().trim());
|
||||||
|
payload.append('person_full_name', $('#personFullName').val().trim());
|
||||||
|
payload.append('phone', $('#phone').val().trim());
|
||||||
|
payload.append('email', $('#email').val().trim());
|
||||||
|
payload.append('notes', $('#notes').val().trim());
|
||||||
|
payload.append('sort_order', $('#sortOrder').val() || '0');
|
||||||
|
payload.append('is_active', $('#isActive').val());
|
||||||
|
|
||||||
|
fetch('', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body: payload.toString()
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Salvato!',
|
||||||
|
text: data.message || 'Operazione completata.',
|
||||||
|
confirmButtonColor: '#3085d6'
|
||||||
|
}).then(() => location.reload());
|
||||||
|
} else {
|
||||||
|
Swal.fire('Errore', data.message || 'Impossibile salvare.', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
Swal.fire('Errore', 'Errore di comunicazione.', 'error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.delete-function', function() {
|
||||||
|
const id = $(this).data('id');
|
||||||
|
const name = $(this).data('name');
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Confermi la cancellazione?',
|
||||||
|
text: name ? ('Funzione: ' + name) : 'La funzione verrà cancellata.',
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: '#d33',
|
||||||
|
cancelButtonColor: '#6c757d',
|
||||||
|
confirmButtonText: 'Sì, cancella',
|
||||||
|
cancelButtonText: 'Annulla'
|
||||||
|
}).then((result) => {
|
||||||
|
if (!result.isConfirmed) return;
|
||||||
|
|
||||||
|
const payload = new URLSearchParams();
|
||||||
|
payload.append('ajax', '1');
|
||||||
|
payload.append('action', 'delete');
|
||||||
|
payload.append('id', id);
|
||||||
|
|
||||||
|
fetch('', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body: payload.toString()
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Cancellato!',
|
||||||
|
text: data.message || 'Funzione cancellata.',
|
||||||
|
confirmButtonColor: '#3085d6'
|
||||||
|
}).then(() => location.reload());
|
||||||
|
} else {
|
||||||
|
Swal.fire('Errore', data.message || 'Impossibile cancellare.', 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
Swal.fire('Errore', 'Errore di comunicazione.', 'error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Formazione — Email reminder cron script
|
||||||
|
* Run daily: 0 7 * * * php /var/www/html/public/userarea/cron/send_training_reminders.php
|
||||||
|
*
|
||||||
|
* Sends "due_soon" emails when next_due_date is within the reminder window
|
||||||
|
* (override reminder_days > topic default > 30 days).
|
||||||
|
* Sends "expired" emails when next_due_date is in the past.
|
||||||
|
* Skips rows with next_due_date IS NULL (one-off trainings).
|
||||||
|
* Skips already-sent notifications (same training + addressee + next_due_date).
|
||||||
|
* Recipients: the employee (employees.email or auth_users.email) + every HR user
|
||||||
|
* with role Admin / Superuser / employee-hr / manager.
|
||||||
|
*
|
||||||
|
* Optional CLI flags:
|
||||||
|
* --dry-run — log only, no SMTP, no DB write
|
||||||
|
* --only-email=foo@bar — restrict to a single addressee (for testing)
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../class/db-functions.php';
|
||||||
|
require_once __DIR__ . '/../../../vendor/autoload.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
use PHPMailer\PHPMailer\PHPMailer;
|
||||||
|
use PHPMailer\PHPMailer\Exception;
|
||||||
|
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/../../../');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
$db = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
|
$today = date('Y-m-d');
|
||||||
|
$appUrl = rtrim($_ENV['APP_URL'] ?? 'http://localhost:8001', '/');
|
||||||
|
|
||||||
|
/* CLI flags */
|
||||||
|
$dryRun = false;
|
||||||
|
$onlyEmail = null;
|
||||||
|
foreach (array_slice($argv ?? [], 1) as $a) {
|
||||||
|
if ($a === '--dry-run' || $a === '-n') {
|
||||||
|
$dryRun = true;
|
||||||
|
} elseif (strpos($a, '--only-email=') === 0) {
|
||||||
|
$onlyEmail = substr($a, strlen('--only-email='));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sent = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
$errors = 0;
|
||||||
|
|
||||||
|
/* Candidate trainings (with optional override reminder + topic default).
|
||||||
|
Only the most recent record per (employee, topic) — older history rows skipped. */
|
||||||
|
$stmt = $pdo->query("
|
||||||
|
SELECT et.id, et.employee_id, et.completed_date, et.next_due_date,
|
||||||
|
et.reminder_days, et.delivered_by,
|
||||||
|
tt.name AS topic_name, tt.default_reminder_days AS topic_default_rem,
|
||||||
|
e.first_name, e.last_name, e.employee_code,
|
||||||
|
e.email AS employee_email_direct,
|
||||||
|
au.email AS employee_email_auth
|
||||||
|
FROM employee_trainings et
|
||||||
|
JOIN training_topics tt ON tt.id = et.training_topic_id
|
||||||
|
JOIN employees e ON e.id = et.employee_id
|
||||||
|
LEFT JOIN auth_users au ON au.id = e.auth_user_id
|
||||||
|
WHERE et.next_due_date IS NOT NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM employee_trainings et2
|
||||||
|
WHERE et2.employee_id = et.employee_id
|
||||||
|
AND et2.training_topic_id = et.training_topic_id
|
||||||
|
AND (et2.completed_date > et.completed_date
|
||||||
|
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
|
||||||
|
)
|
||||||
|
");
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (empty($rows)) {
|
||||||
|
echo date('Y-m-d H:i:s') . " — Nessuna formazione da notificare.\n";
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HR addressees (one query, reused per training) */
|
||||||
|
$hrUsers = $pdo->query("
|
||||||
|
SELECT u.id, u.email, TRIM(CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,''))) AS name
|
||||||
|
FROM auth_users u
|
||||||
|
JOIN auth_roles r ON r.id = u.role_id
|
||||||
|
WHERE r.name IN ('Admin','Superuser','employee-hr','manager')
|
||||||
|
AND u.email IS NOT NULL AND u.email <> ''
|
||||||
|
")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$checkSent = $pdo->prepare("
|
||||||
|
SELECT COUNT(*) FROM training_reminder_log
|
||||||
|
WHERE training_id = ? AND addressee_email = ? AND next_due_date = ?
|
||||||
|
");
|
||||||
|
$insertLog = $pdo->prepare("
|
||||||
|
INSERT INTO training_reminder_log
|
||||||
|
(training_id, addressee_email, next_due_date, status_at_send, sent_at)
|
||||||
|
VALUES (?, ?, ?, ?, NOW())
|
||||||
|
");
|
||||||
|
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$rem = $r['reminder_days'] !== null
|
||||||
|
? (int)$r['reminder_days']
|
||||||
|
: ($r['topic_default_rem'] !== null ? (int)$r['topic_default_rem'] : 30);
|
||||||
|
$isOverdue = $r['next_due_date'] < $today;
|
||||||
|
$daysLeft = (int)((strtotime($r['next_due_date']) - strtotime($today)) / 86400);
|
||||||
|
|
||||||
|
if (!$isOverdue && $daysLeft > $rem) {
|
||||||
|
continue; // not yet in the reminder window
|
||||||
|
}
|
||||||
|
$type = $isOverdue ? 'expired' : 'update_to_be_scheduled';
|
||||||
|
|
||||||
|
$employeeFullName = trim($r['first_name'] . ' ' . $r['last_name']);
|
||||||
|
$employeeEmail = !empty($r['employee_email_direct'])
|
||||||
|
? $r['employee_email_direct']
|
||||||
|
: (!empty($r['employee_email_auth']) ? $r['employee_email_auth'] : null);
|
||||||
|
|
||||||
|
/* Collect addressees (employee + HR), deduplicated by lowercased email */
|
||||||
|
$recipients = [];
|
||||||
|
if ($employeeEmail) {
|
||||||
|
$key = strtolower(trim($employeeEmail));
|
||||||
|
$recipients[$key] = ['email' => $employeeEmail, 'name' => $employeeFullName, 'is_hr' => false];
|
||||||
|
}
|
||||||
|
foreach ($hrUsers as $hr) {
|
||||||
|
$key = strtolower(trim((string)$hr['email']));
|
||||||
|
if ($key === '' || isset($recipients[$key])) continue;
|
||||||
|
$recipients[$key] = ['email' => $hr['email'], 'name' => trim((string)$hr['name']), 'is_hr' => true];
|
||||||
|
}
|
||||||
|
if (empty($recipients)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($recipients as $email => $rec) {
|
||||||
|
if ($onlyEmail !== null && strcasecmp($rec['email'], $onlyEmail) !== 0) continue;
|
||||||
|
|
||||||
|
$checkSent->execute([$r['id'], $rec['email'], $r['next_due_date']]);
|
||||||
|
if ($checkSent->fetchColumn() > 0) {
|
||||||
|
$skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$mail = new PHPMailer(true);
|
||||||
|
|
||||||
|
// SMTP config from .env
|
||||||
|
$mailer = $_ENV['MAIL_MAILER'] ?? 'mail';
|
||||||
|
if ($mailer === 'smtp') {
|
||||||
|
$mail->isSMTP();
|
||||||
|
$mail->Host = $_ENV['MAIL_HOST'] ?? 'localhost';
|
||||||
|
$mail->Port = (int)($_ENV['MAIL_PORT'] ?? 587);
|
||||||
|
if (!empty($_ENV['MAIL_USERNAME']) && $_ENV['MAIL_USERNAME'] !== 'null') {
|
||||||
|
$mail->SMTPAuth = true;
|
||||||
|
$mail->Username = $_ENV['MAIL_USERNAME'];
|
||||||
|
$mail->Password = $_ENV['MAIL_PASSWORD'] ?? '';
|
||||||
|
}
|
||||||
|
$enc = $_ENV['MAIL_ENCRYPTION'] ?? '';
|
||||||
|
if ($enc && $enc !== 'null') {
|
||||||
|
$mail->SMTPSecure = $enc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$mail->CharSet = 'UTF-8';
|
||||||
|
$mail->setFrom(
|
||||||
|
$_ENV['MAIL_FROM_ADDRESS'] ?? 'noreply@zibogomma.it',
|
||||||
|
$_ENV['MAIL_FROM_NAME'] ?? 'Formazione ZIBOGOMMA'
|
||||||
|
);
|
||||||
|
$mail->addAddress($rec['email'], $rec['name'] ?: $rec['email']);
|
||||||
|
|
||||||
|
$profileUrl = $appUrl . '/userarea/employee-profile.php?id=' . (int)$r['employee_id'] . '#tab-training';
|
||||||
|
$topicText = $r['topic_name'] . ' — ' . $employeeFullName
|
||||||
|
. (!empty($r['employee_code']) ? ' (' . $r['employee_code'] . ')' : '');
|
||||||
|
|
||||||
|
if ($isOverdue) {
|
||||||
|
$mail->Subject = '⚠️ Formazione scaduta: ' . $r['topic_name'];
|
||||||
|
$mail->Body = buildHtml(
|
||||||
|
'Formazione scaduta',
|
||||||
|
$topicText,
|
||||||
|
'Completata il <strong>' . date('d/m/Y', strtotime($r['completed_date'])) . '</strong>. '
|
||||||
|
. 'Il prossimo aggiornamento era previsto per <strong>' . date('d/m/Y', strtotime($r['next_due_date'])) . '</strong>'
|
||||||
|
. ' (scaduta da <strong>' . abs($daysLeft) . ' giorni</strong>).',
|
||||||
|
'#dc3545',
|
||||||
|
$profileUrl,
|
||||||
|
$rec['is_hr']
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$mail->Subject = '📚 Formazione in scadenza: ' . $r['topic_name'];
|
||||||
|
$daysText = $daysLeft === 0 ? 'oggi' : 'tra <strong>' . $daysLeft . ' giorni</strong>';
|
||||||
|
$mail->Body = buildHtml(
|
||||||
|
'Formazione in scadenza',
|
||||||
|
$topicText,
|
||||||
|
'Completata il <strong>' . date('d/m/Y', strtotime($r['completed_date'])) . '</strong>. '
|
||||||
|
. 'Prossimo aggiornamento previsto per <strong>' . date('d/m/Y', strtotime($r['next_due_date'])) . '</strong>'
|
||||||
|
. ' (' . $daysText . ').',
|
||||||
|
'#e8930c',
|
||||||
|
$profileUrl,
|
||||||
|
$rec['is_hr']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$mail->isHTML(true);
|
||||||
|
$mail->AltBody = strip_tags(str_replace('<br>', "\n", $mail->Body));
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
echo date('H:i:s') . " ◌ DRY {$type} → {$rec['email']} — {$r['topic_name']}\n";
|
||||||
|
$sent++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mail->send();
|
||||||
|
$insertLog->execute([$r['id'], $rec['email'], $r['next_due_date'], $type]);
|
||||||
|
$sent++;
|
||||||
|
echo date('H:i:s') . " ✓ {$type} → {$rec['email']} — {$r['topic_name']}\n";
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$errors++;
|
||||||
|
echo date('H:i:s') . " ✗ Errore {$rec['email']}: {$e->getMessage()}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
NOT-PRESENT reminders — mandatory topics with no record for an employee.
|
||||||
|
Notify HR only.
|
||||||
|
De-dup by (employee_id, training_topic_id, addressee_email).
|
||||||
|
============================================================================ */
|
||||||
|
$missingStmt = $pdo->query("
|
||||||
|
SELECT e.id AS employee_id, e.first_name, e.last_name, e.employee_code,
|
||||||
|
tt.id AS topic_id, tt.name AS topic_name
|
||||||
|
FROM employees e
|
||||||
|
CROSS JOIN training_topics tt
|
||||||
|
WHERE tt.is_active = 1 AND tt.is_mandatory = 1
|
||||||
|
AND (e.status IS NULL OR e.status = 'active')
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM employee_trainings et
|
||||||
|
WHERE et.employee_id = e.id AND et.training_topic_id = tt.id
|
||||||
|
)
|
||||||
|
ORDER BY e.last_name, e.first_name, tt.name
|
||||||
|
");
|
||||||
|
$missingRows = $missingStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$checkMissingSent = $pdo->prepare("
|
||||||
|
SELECT COUNT(*) FROM training_reminder_log
|
||||||
|
WHERE employee_id = ? AND training_topic_id = ? AND addressee_email = ?
|
||||||
|
AND status_at_send = 'not_present'
|
||||||
|
");
|
||||||
|
$insertMissingLog = $pdo->prepare("
|
||||||
|
INSERT INTO training_reminder_log
|
||||||
|
(training_id, employee_id, training_topic_id, addressee_email, next_due_date, status_at_send, sent_at)
|
||||||
|
VALUES (NULL, ?, ?, ?, NULL, 'not_present', NOW())
|
||||||
|
");
|
||||||
|
|
||||||
|
foreach ($missingRows as $m) {
|
||||||
|
$employeeFullName = trim($m['first_name'] . ' ' . $m['last_name']);
|
||||||
|
|
||||||
|
foreach ($hrUsers as $hr) {
|
||||||
|
$email = trim((string)$hr['email']);
|
||||||
|
if ($email === '') continue;
|
||||||
|
if ($onlyEmail !== null && strcasecmp($email, $onlyEmail) !== 0) continue;
|
||||||
|
|
||||||
|
$checkMissingSent->execute([$m['employee_id'], $m['topic_id'], $email]);
|
||||||
|
if ($checkMissingSent->fetchColumn() > 0) {
|
||||||
|
$skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$mail = new PHPMailer(true);
|
||||||
|
$mailer = $_ENV['MAIL_MAILER'] ?? 'mail';
|
||||||
|
if ($mailer === 'smtp') {
|
||||||
|
$mail->isSMTP();
|
||||||
|
$mail->Host = $_ENV['MAIL_HOST'] ?? 'localhost';
|
||||||
|
$mail->Port = (int)($_ENV['MAIL_PORT'] ?? 587);
|
||||||
|
if (!empty($_ENV['MAIL_USERNAME']) && $_ENV['MAIL_USERNAME'] !== 'null') {
|
||||||
|
$mail->SMTPAuth = true;
|
||||||
|
$mail->Username = $_ENV['MAIL_USERNAME'];
|
||||||
|
$mail->Password = $_ENV['MAIL_PASSWORD'] ?? '';
|
||||||
|
}
|
||||||
|
$enc = $_ENV['MAIL_ENCRYPTION'] ?? '';
|
||||||
|
if ($enc && $enc !== 'null') {
|
||||||
|
$mail->SMTPSecure = $enc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$mail->CharSet = 'UTF-8';
|
||||||
|
$mail->setFrom(
|
||||||
|
$_ENV['MAIL_FROM_ADDRESS'] ?? 'noreply@zibogomma.it',
|
||||||
|
$_ENV['MAIL_FROM_NAME'] ?? 'Formazione ZIBOGOMMA'
|
||||||
|
);
|
||||||
|
$mail->addAddress($email, trim((string)$hr['name']) ?: $email);
|
||||||
|
|
||||||
|
$profileUrl = $appUrl . '/userarea/employee-profile.php?id=' . (int)$m['employee_id'] . '#tab-training';
|
||||||
|
$topicText = $m['topic_name'] . ' — ' . $employeeFullName
|
||||||
|
. (!empty($m['employee_code']) ? ' (' . $m['employee_code'] . ')' : '');
|
||||||
|
|
||||||
|
$mail->Subject = '🔔 Formazione obbligatoria non presente: ' . $m['topic_name'];
|
||||||
|
$mail->Body = buildHtml(
|
||||||
|
'Formazione obbligatoria non presente',
|
||||||
|
$topicText,
|
||||||
|
'Il dipendente <strong>' . htmlspecialchars($employeeFullName) . '</strong> non ha nessuna registrazione per il corso obbligatorio <strong>' . htmlspecialchars($m['topic_name']) . '</strong>. Programma la prima erogazione.',
|
||||||
|
'#6b7280',
|
||||||
|
$profileUrl,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
$mail->isHTML(true);
|
||||||
|
$mail->AltBody = strip_tags(str_replace('<br>', "\n", $mail->Body));
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
echo date('H:i:s') . " ◌ DRY not_present → {$email} — {$m['topic_name']} / {$employeeFullName}\n";
|
||||||
|
$sent++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mail->send();
|
||||||
|
$insertMissingLog->execute([$m['employee_id'], $m['topic_id'], $email]);
|
||||||
|
$sent++;
|
||||||
|
echo date('H:i:s') . " ✓ not_present → {$email} — {$m['topic_name']} / {$employeeFullName}\n";
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$errors++;
|
||||||
|
echo date('H:i:s') . " ✗ Errore {$email}: {$e->getMessage()}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n" . date('Y-m-d H:i:s') . " — Completato. Inviate: {$sent}, Saltate: {$skipped}, Errori: {$errors}\n";
|
||||||
|
|
||||||
|
// --- HTML email template ---
|
||||||
|
function buildHtml(string $title, string $topic, string $message, string $accentColor, string $url, bool $isForHr): string
|
||||||
|
{
|
||||||
|
$greeting = $isForHr
|
||||||
|
? 'Una formazione richiede attenzione.'
|
||||||
|
: 'Una delle tue formazioni richiede attenzione.';
|
||||||
|
return '
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head><meta charset="UTF-8"></head>
|
||||||
|
<body style="margin:0;padding:0;background:#f4f6f9;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="padding:30px 0">
|
||||||
|
<tr><td align="center">
|
||||||
|
<table width="560" cellpadding="0" cellspacing="0" style="background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.06)">
|
||||||
|
<tr><td style="background:' . $accentColor . ';padding:20px 30px">
|
||||||
|
<h1 style="margin:0;color:#fff;font-size:18px">' . htmlspecialchars($title) . '</h1>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="padding:30px">
|
||||||
|
<p style="margin:0 0 12px;color:#444;font-size:14px">' . htmlspecialchars($greeting) . '</p>
|
||||||
|
<h2 style="margin:0 0 15px;color:#2c3e6b;font-size:16px">' . htmlspecialchars($topic) . '</h2>
|
||||||
|
<p style="margin:0 0 20px;color:#444;font-size:14px;line-height:1.6">' . $message . '</p>
|
||||||
|
<a href="' . htmlspecialchars($url) . '" style="display:inline-block;background:#5a8fd8;color:#fff;padding:10px 24px;border-radius:6px;text-decoration:none;font-weight:600;font-size:14px">Apri profilo</a>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="padding:15px 30px;background:#f8f9fb;border-top:1px solid #eee">
|
||||||
|
<p style="margin:0;color:#999;font-size:11px">ZIBOGOMMA — Formazione</p>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>';
|
||||||
|
}
|
||||||
@@ -256,7 +256,6 @@ $departments = $stmtDepartments->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
|
|
||||||
<!-- jQuery and Bootstrap -->
|
<!-- jQuery and Bootstrap -->
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
<!-- DataTables -->
|
<!-- DataTables -->
|
||||||
@@ -367,7 +366,7 @@ $departments = $stmtDepartments->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+1658
-424
File diff suppressed because it is too large
Load Diff
@@ -40,8 +40,7 @@ $kindofrole = $user->present()->role_id;
|
|||||||
//$iduserlogin="1";
|
//$iduserlogin="1";
|
||||||
//$nameuser="Claudio";
|
//$nameuser="Claudio";
|
||||||
//$emailuser="info@claudiosironi.com";
|
//$emailuser="info@claudiosironi.com";
|
||||||
?>
|
|
||||||
<?php
|
|
||||||
if (session_status() == PHP_SESSION_NONE) {
|
if (session_status() == PHP_SESSION_NONE) {
|
||||||
session_start();
|
session_start();
|
||||||
}
|
}
|
||||||
@@ -54,10 +53,8 @@ $_SESSION["emailuser"] = $emailuser;
|
|||||||
$_SESSION["photouser"] = $avatar;
|
$_SESSION["photouser"] = $avatar;
|
||||||
$photouser = $_SESSION["photouser"];
|
$photouser = $_SESSION["photouser"];
|
||||||
$photousername = basename($avatar);
|
$photousername = basename($avatar);
|
||||||
?>
|
|
||||||
|
|
||||||
|
//include files
|
||||||
<?php //include files
|
|
||||||
|
|
||||||
require_once(__DIR__ . '/../../languages/en/general.php');
|
require_once(__DIR__ . '/../../languages/en/general.php');
|
||||||
|
|
||||||
|
|||||||
@@ -289,6 +289,16 @@
|
|||||||
<i class='bx bx-radio-circle'></i>Dipendenti
|
<i class='bx bx-radio-circle'></i>Dipendenti
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="job-roles.php">
|
||||||
|
<i class='bx bx-radio-circle'></i>Mansioni
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="ppe-items.php">
|
||||||
|
<i class='bx bx-radio-circle'></i>DPI
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (userCan('hr.departments.view')) : ?>
|
<?php if (userCan('hr.departments.view')) : ?>
|
||||||
@@ -299,28 +309,15 @@
|
|||||||
</li>
|
</li>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (userCan('hr.job_roles.view')) : ?>
|
|
||||||
<li>
|
|
||||||
<a href="job_roles.php">
|
|
||||||
<i class='bx bx-radio-circle'></i>Mansioni
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if (userCan('hr.training_topics.view')) : ?>
|
|
||||||
<li>
|
|
||||||
<a href="training_topics.php">
|
|
||||||
<i class='bx bx-radio-circle'></i>Corsi di Formazione
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if (userCan('hr.trainings.view')) : ?>
|
<?php if (userCan('hr.trainings.view')) : ?>
|
||||||
<li>
|
<li>
|
||||||
<a href="trainings.php">
|
<a href="trainings.php">
|
||||||
<i class='bx bx-radio-circle'></i>Storico Formazione
|
<i class='bx bx-radio-circle'></i>Gestione Formazione
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (userCan('hr.skills.view')) : ?>
|
<?php if (userCan('hr.skills.view')) : ?>
|
||||||
@@ -356,6 +353,11 @@
|
|||||||
<i class='bx bx-radio-circle'></i>Calendario
|
<i class='bx bx-radio-circle'></i>Calendario
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="scadenzario/functions/index.php">
|
||||||
|
<i class='bx bx-radio-circle'></i>Funzioni Aziendali
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
// Build an absolute URL to employee-profile.php so it works from any depth
|
||||||
|
// (e.g. /userarea/index.php, /userarea/scadenzario/index.php).
|
||||||
|
$__scriptName = $_SERVER['SCRIPT_NAME'] ?? '';
|
||||||
|
$__pos = strpos($__scriptName, '/userarea/');
|
||||||
|
$__base = $__pos !== false ? substr($__scriptName, 0, $__pos) : '';
|
||||||
|
$__myProfileHref = $__base . '/userarea/employee-profile.php';
|
||||||
|
?>
|
||||||
<header>
|
<header>
|
||||||
<div class="topbar d-flex align-items-center">
|
<div class="topbar d-flex align-items-center">
|
||||||
<nav class="navbar navbar-expand gap-3">
|
<nav class="navbar navbar-expand gap-3">
|
||||||
@@ -85,6 +93,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item d-flex align-items-center" href="<?= htmlspecialchars($__myProfileHref) ?>"
|
||||||
|
onclick="event.preventDefault(); window.location.assign(this.href);">
|
||||||
|
<i class="bx bx-id-card fs-5"></i><span>Il Mio Profilo</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item d-flex align-items-center" href="user_settings.php">
|
<a class="dropdown-item d-flex align-items-center" href="user_settings.php">
|
||||||
<i class="bx bx-user fs-5"></i><span>Utente</span>
|
<i class="bx bx-user fs-5"></i><span>Utente</span>
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Training reminders widget for the production dashboard.
|
||||||
|
* Visible to HR / manager / Admin / User / Superuser.
|
||||||
|
*
|
||||||
|
* Expects $pdo to be set (DBHandlerSelect connection).
|
||||||
|
*/
|
||||||
|
if (!isset($pdo)) {
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
$__trWidgetHr = isset($user)
|
||||||
|
&& ( $user->hasRole('Admin')
|
||||||
|
|| $user->hasRole('Superuser')
|
||||||
|
|| $user->hasRole('employee-hr')
|
||||||
|
|| $user->hasRole('manager'));
|
||||||
|
|
||||||
|
if (!$__trWidgetHr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Only the most recent record per (employee, topic) — older history rows ignored. */
|
||||||
|
$__trRows = $pdo->query("
|
||||||
|
SELECT et.id,
|
||||||
|
et.next_due_date,
|
||||||
|
et.reminder_days,
|
||||||
|
tt.default_reminder_days
|
||||||
|
FROM employee_trainings et
|
||||||
|
JOIN training_topics tt ON tt.id = et.training_topic_id
|
||||||
|
WHERE et.next_due_date IS NOT NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM employee_trainings et2
|
||||||
|
WHERE et2.employee_id = et.employee_id
|
||||||
|
AND et2.training_topic_id = et.training_topic_id
|
||||||
|
AND (et2.completed_date > et.completed_date
|
||||||
|
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
|
||||||
|
)
|
||||||
|
")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$__expiredCount = 0;
|
||||||
|
$__dueSoonCount = 0;
|
||||||
|
$__today = new DateTime('today');
|
||||||
|
foreach ($__trRows as $__r) {
|
||||||
|
$__rem = $__r['reminder_days'] !== null
|
||||||
|
? (int)$__r['reminder_days']
|
||||||
|
: ($__r['default_reminder_days'] !== null ? (int)$__r['default_reminder_days'] : 30);
|
||||||
|
$__due = DateTime::createFromFormat('Y-m-d', $__r['next_due_date']);
|
||||||
|
if (!$__due) continue;
|
||||||
|
$__days = (int)$__today->diff($__due)->format('%r%a');
|
||||||
|
if ($__days < 0) { $__expiredCount++; }
|
||||||
|
elseif ($__days <= $__rem) { $__dueSoonCount++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Missing mandatory trainings (status = not_present) */
|
||||||
|
$__notPresentCount = (int)$pdo->query("
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM employees e
|
||||||
|
CROSS JOIN training_topics tt
|
||||||
|
WHERE tt.is_active = 1 AND tt.is_mandatory = 1
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM employee_trainings et
|
||||||
|
WHERE et.employee_id = e.id AND et.training_topic_id = tt.id
|
||||||
|
)
|
||||||
|
")->fetchColumn();
|
||||||
|
|
||||||
|
if ($__expiredCount === 0 && $__dueSoonCount === 0 && $__notPresentCount === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="my-deadlines-widgets">
|
||||||
|
<?php if ($__expiredCount > 0): ?>
|
||||||
|
<a class="mdw mdw-red" href="trainings.php?status=expired">
|
||||||
|
<span class="mdw-icon"><i class="fa-solid fa-graduation-cap"></i></span>
|
||||||
|
<span class="mdw-body">
|
||||||
|
<span class="mdw-count"><?= (int)$__expiredCount ?></span>
|
||||||
|
<span class="mdw-label d-block">Formazion<?= $__expiredCount === 1 ? 'e scaduta' : 'i scadute' ?></span>
|
||||||
|
</span>
|
||||||
|
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($__dueSoonCount > 0): ?>
|
||||||
|
<a class="mdw mdw-orange" href="trainings.php?status=due_soon">
|
||||||
|
<span class="mdw-icon"><i class="fa-solid fa-hourglass-half"></i></span>
|
||||||
|
<span class="mdw-body">
|
||||||
|
<span class="mdw-count"><?= (int)$__dueSoonCount ?></span>
|
||||||
|
<span class="mdw-label d-block">Formazion<?= $__dueSoonCount === 1 ? 'e da aggiornare' : 'i da aggiornare' ?></span>
|
||||||
|
</span>
|
||||||
|
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($__notPresentCount > 0): ?>
|
||||||
|
<a class="mdw mdw-gray" href="trainings.php?status=not_present">
|
||||||
|
<span class="mdw-icon"><i class="fa-solid fa-circle-question"></i></span>
|
||||||
|
<span class="mdw-body">
|
||||||
|
<span class="mdw-count"><?= (int)$__notPresentCount ?></span>
|
||||||
|
<span class="mdw-label d-block">Obbligator<?= $__notPresentCount === 1 ? 'ia non presente' : 'ie non presenti' ?></span>
|
||||||
|
</span>
|
||||||
|
<span class="mdw-arrow"><i class="fa-solid fa-arrow-right"></i></span>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,428 @@
|
|||||||
|
<?php
|
||||||
|
include('include/headscript.php');
|
||||||
|
|
||||||
|
$db = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
|
/* ==========================================
|
||||||
|
PAGE DATA
|
||||||
|
========================================== */
|
||||||
|
$sql = "
|
||||||
|
SELECT jr.*,
|
||||||
|
(SELECT COUNT(*) FROM employees e WHERE e.job_role_id = jr.id) AS employees_count
|
||||||
|
FROM job_roles jr
|
||||||
|
ORDER BY jr.sort_order ASC, jr.name ASC
|
||||||
|
";
|
||||||
|
$jobRoles = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="it">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||||
|
<?php include('cssinclude.php'); ?>
|
||||||
|
<title>Gestione Mansioni - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
||||||
|
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||||
|
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body { font-size: 1.05rem; background: #f8fafc; }
|
||||||
|
.card { border-radius: 16px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); }
|
||||||
|
.back-dashboard {
|
||||||
|
background-color: #cfe3ff !important; color: #1f2d3d !important;
|
||||||
|
border: 1px solid #bcd4f4 !important; border-radius: 10px;
|
||||||
|
font-weight: 600; padding: 10px 18px;
|
||||||
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
.back-dashboard:hover { background-color: #b9d3ff !important; transform: translateY(-2px); }
|
||||||
|
.btn-add { background-color: #0d6efd; color: #fff; border-radius: 8px; padding: 10px 20px; font-weight: 500; }
|
||||||
|
.btn-add:hover { background-color: #0b5ed7; transform: scale(1.02); }
|
||||||
|
.table thead { background-color: #cfe3ff; color: #1f2d3d; }
|
||||||
|
.modal-content { border-radius: 16px; }
|
||||||
|
#tabellaJobRoles thead th { text-align: center; vertical-align: middle; }
|
||||||
|
.badge-status { padding: 0.25rem 0.6rem; border-radius: 999px; font-size: 0.8rem; font-weight: 600; }
|
||||||
|
.badge-status.active { background-color: #d1fae5; color: #065f46; }
|
||||||
|
.badge-status.inactive { background-color: #e5e7eb; color: #374151; }
|
||||||
|
.description-cell {
|
||||||
|
max-width: 320px; white-space: nowrap; overflow: hidden;
|
||||||
|
text-overflow: ellipsis; text-align: left;
|
||||||
|
}
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.card-header { flex-direction: column; align-items: flex-start !important; gap: .5rem; }
|
||||||
|
.back-dashboard { width: 100%; }
|
||||||
|
.btn-add { width: 100%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.jr-card {
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
.jr-card-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.jr-card-desc {
|
||||||
|
color: #475569;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.jr-card-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 14px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #64748b;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.jr-card-meta b { color: #1f2937; font-weight: 600; }
|
||||||
|
.jr-card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.jr-card-actions .btn {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.jr-empty {
|
||||||
|
text-align: center;
|
||||||
|
color: #94a3b8;
|
||||||
|
padding: 24px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="wrapper" id="appWrapper">
|
||||||
|
<?php include('include/navbar.php'); ?>
|
||||||
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
<div class="page-wrapper">
|
||||||
|
<div class="page-content">
|
||||||
|
<div class="card p-3">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||||
|
<h5 class="mb-0">Gestione Mansioni</h5>
|
||||||
|
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
|
||||||
|
↩️ Torna alla Dashboard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
||||||
|
<h6 class="fw-semibold mb-0">Elenco Mansioni / Job Roles</h6>
|
||||||
|
<button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#addJobRoleModal">
|
||||||
|
➕ Aggiungi Mansione
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DESKTOP / TABLET ≥768px: TABLE -->
|
||||||
|
<div class="table-responsive d-none d-md-block"><!-- hide on <md -->
|
||||||
|
<table id="tabellaJobRoles" class="table table-striped align-middle text-center" style="width:100%;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Nome</th>
|
||||||
|
<th>Descrizione</th>
|
||||||
|
<th>Ordine</th>
|
||||||
|
<th>Stato</th>
|
||||||
|
<th>Dipendenti</th>
|
||||||
|
<th>Creato</th>
|
||||||
|
<th>Azioni</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($jobRoles as $row): ?>
|
||||||
|
<?php
|
||||||
|
$id = (int)$row['id'];
|
||||||
|
$name = $row['name'] ?? '';
|
||||||
|
$description = $row['description'] ?? '';
|
||||||
|
$sortOrder = (int)($row['sort_order'] ?? 999);
|
||||||
|
$isActive = (int)($row['is_active'] ?? 1);
|
||||||
|
$cnt = (int)($row['employees_count'] ?? 0);
|
||||||
|
$statusClass = $isActive === 1 ? 'active' : 'inactive';
|
||||||
|
$statusLabel = $isActive === 1 ? 'Attivo' : 'Inattivo';
|
||||||
|
$createdAt = !empty($row['created_at'])
|
||||||
|
? date('d/m/Y H:i', strtotime($row['created_at']))
|
||||||
|
: '-';
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td><?= $id ?></td>
|
||||||
|
<td class="fw-semibold text-start"><?= htmlspecialchars($name) ?></td>
|
||||||
|
<td class="description-cell" title="<?= htmlspecialchars($description, ENT_QUOTES) ?>">
|
||||||
|
<?= $description !== '' ? htmlspecialchars($description) : '-' ?>
|
||||||
|
</td>
|
||||||
|
<td><?= $sortOrder ?></td>
|
||||||
|
<td>
|
||||||
|
<span class="badge-status <?= $statusClass ?>"><?= $statusLabel ?></span>
|
||||||
|
</td>
|
||||||
|
<td><?= $cnt ?></td>
|
||||||
|
<td><?= $createdAt ?></td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary edit-job-role"
|
||||||
|
data-id="<?= $id ?>"
|
||||||
|
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
|
||||||
|
data-description="<?= htmlspecialchars($description, ENT_QUOTES) ?>"
|
||||||
|
data-sort_order="<?= $sortOrder ?>"
|
||||||
|
data-is_active="<?= $isActive ?>">
|
||||||
|
✏️ Modifica
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger delete-job-role"
|
||||||
|
data-id="<?= $id ?>"
|
||||||
|
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
|
||||||
|
data-count="<?= $cnt ?>">
|
||||||
|
🗑️ Cancella
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MOBILE <768px: CARDS -->
|
||||||
|
<div class="d-block d-md-none">
|
||||||
|
<?php if (empty($jobRoles)): ?>
|
||||||
|
<div class="jr-empty">Nessuna mansione presente</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php foreach ($jobRoles as $row): ?>
|
||||||
|
<?php
|
||||||
|
$id = (int)$row['id'];
|
||||||
|
$name = $row['name'] ?? '';
|
||||||
|
$description = $row['description'] ?? '';
|
||||||
|
$sortOrder = (int)($row['sort_order'] ?? 999);
|
||||||
|
$isActive = (int)($row['is_active'] ?? 1);
|
||||||
|
$cnt = (int)($row['employees_count'] ?? 0);
|
||||||
|
$statusClass = $isActive === 1 ? 'active' : 'inactive';
|
||||||
|
$statusLabel = $isActive === 1 ? 'Attivo' : 'Inattivo';
|
||||||
|
?>
|
||||||
|
<div class="jr-card">
|
||||||
|
<h6 class="jr-card-title"><?= htmlspecialchars($name) ?></h6>
|
||||||
|
<?php if ($description !== ''): ?>
|
||||||
|
<p class="jr-card-desc"><?= htmlspecialchars($description) ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="jr-card-meta">
|
||||||
|
<span><span class="badge-status <?= $statusClass ?>"><?= $statusLabel ?></span></span>
|
||||||
|
<span><b>Dipendenti:</b> <?= $cnt ?></span>
|
||||||
|
<span><b>Ordine:</b> <?= $sortOrder ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="jr-card-actions">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary edit-job-role"
|
||||||
|
data-id="<?= $id ?>"
|
||||||
|
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
|
||||||
|
data-description="<?= htmlspecialchars($description, ENT_QUOTES) ?>"
|
||||||
|
data-sort_order="<?= $sortOrder ?>"
|
||||||
|
data-is_active="<?= $isActive ?>">
|
||||||
|
✏️ Modifica
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger delete-job-role"
|
||||||
|
data-id="<?= $id ?>"
|
||||||
|
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
|
||||||
|
data-count="<?= $cnt ?>">
|
||||||
|
🗑️ Cancella
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('include/footer.php'); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ADD MODAL -->
|
||||||
|
<div class="modal fade" id="addJobRoleModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered modal-fullscreen-sm-down">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header" style="background-color:#cfe3ff;">
|
||||||
|
<h5 class="modal-title">Aggiungi Mansione</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="addJobRoleForm">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Nome</label>
|
||||||
|
<input type="text" class="form-control" id="addName" name="name" placeholder="es. Saldatore" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Descrizione</label>
|
||||||
|
<textarea class="form-control" id="addDescription" name="description" rows="3" placeholder="Opzionale"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Ordine</label>
|
||||||
|
<input type="number" class="form-control" id="addSortOrder" name="sort_order" value="999" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Stato</label>
|
||||||
|
<select class="form-select" id="addIsActive" name="is_active">
|
||||||
|
<option value="1" selected>Attivo</option>
|
||||||
|
<option value="0">Inattivo</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<button type="submit" class="btn btn-add">💾 Salva</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- EDIT MODAL -->
|
||||||
|
<div class="modal fade" id="editJobRoleModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered modal-fullscreen-sm-down">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header" style="background-color:#cfe3ff;">
|
||||||
|
<h5 class="modal-title">Modifica Mansione</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="editJobRoleForm">
|
||||||
|
<input type="hidden" id="editJobRoleId">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Nome</label>
|
||||||
|
<input type="text" class="form-control" id="editName" name="name" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Descrizione</label>
|
||||||
|
<textarea class="form-control" id="editDescription" name="description" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Ordine</label>
|
||||||
|
<input type="number" class="form-control" id="editSortOrder" name="sort_order" value="999" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Stato</label>
|
||||||
|
<select class="form-select" id="editIsActive" name="is_active">
|
||||||
|
<option value="1">Attivo</option>
|
||||||
|
<option value="0">Inattivo</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<button type="submit" class="btn btn-add">💾 Salva Modifiche</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('jsinclude.php'); ?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#tabellaJobRoles').DataTable({
|
||||||
|
order: [[3, 'asc'], [1, 'asc']],
|
||||||
|
pageLength: 25,
|
||||||
|
language: {
|
||||||
|
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json',
|
||||||
|
emptyTable: 'Nessuna mansione presente'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function ajaxPost(url, payload, successTitle, errorFallback) {
|
||||||
|
return fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
|
body: payload.toString()
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
Swal.fire({ icon: "success", title: successTitle, confirmButtonColor: "#3085d6" })
|
||||||
|
.then(() => location.reload());
|
||||||
|
} else {
|
||||||
|
Swal.fire({ icon: "error", title: "Errore", text: data.message || errorFallback });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
Swal.fire({ icon: "error", title: "Errore", text: "Errore di comunicazione." });
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#addJobRoleForm").on("submit", function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const p = new URLSearchParams();
|
||||||
|
p.append('name', $("#addName").val().trim());
|
||||||
|
p.append('description', $("#addDescription").val().trim());
|
||||||
|
p.append('sort_order', $("#addSortOrder").val());
|
||||||
|
p.append('is_active', $("#addIsActive").val());
|
||||||
|
ajaxPost("ajax/job_roles/save.php", p, "Salvato!", "Impossibile salvare la mansione.");
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on("click", ".edit-job-role", function() {
|
||||||
|
const b = $(this);
|
||||||
|
$("#editJobRoleId").val(b.data("id"));
|
||||||
|
$("#editName").val(b.data("name"));
|
||||||
|
$("#editDescription").val(b.data("description"));
|
||||||
|
$("#editSortOrder").val(b.data("sort_order"));
|
||||||
|
$("#editIsActive").val(String(b.data("is_active")));
|
||||||
|
$("#editJobRoleModal").modal("show");
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#editJobRoleForm").on("submit", function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const p = new URLSearchParams();
|
||||||
|
p.append('id', $("#editJobRoleId").val());
|
||||||
|
p.append('name', $("#editName").val().trim());
|
||||||
|
p.append('description', $("#editDescription").val().trim());
|
||||||
|
p.append('sort_order', $("#editSortOrder").val());
|
||||||
|
p.append('is_active', $("#editIsActive").val());
|
||||||
|
ajaxPost("ajax/job_roles/save.php", p, "Aggiornato!", "Impossibile aggiornare la mansione.");
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on("click", ".delete-job-role", function() {
|
||||||
|
const id = $(this).data("id");
|
||||||
|
const name = $(this).data("name");
|
||||||
|
const cnt = parseInt($(this).data("count")) || 0;
|
||||||
|
|
||||||
|
if (cnt > 0) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: "warning",
|
||||||
|
title: "Impossibile cancellare",
|
||||||
|
text: "La mansione \"" + name + "\" è assegnata a " + cnt + " dipendente/i. Rimuovi prima l'associazione."
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: "Confermi la cancellazione?",
|
||||||
|
text: name ? ("Mansione: " + name) : "La mansione verrà cancellata.",
|
||||||
|
icon: "warning",
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: "#d33",
|
||||||
|
cancelButtonColor: "#6c757d",
|
||||||
|
confirmButtonText: "Sì, cancella",
|
||||||
|
cancelButtonText: "Annulla"
|
||||||
|
}).then((result) => {
|
||||||
|
if (!result.isConfirmed) return;
|
||||||
|
const p = new URLSearchParams();
|
||||||
|
p.append('id', id);
|
||||||
|
ajaxPost("ajax/job_roles/delete.php", p, "Cancellato!", "Impossibile cancellare la mansione.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -42,7 +42,6 @@ $params = $stmtParams->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
|
|
||||||
<!-- jQuery / Bootstrap / SweetAlert -->
|
<!-- jQuery / Bootstrap / SweetAlert -->
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
<!-- DataTables -->
|
<!-- DataTables -->
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
|
|
||||||
<!-- jQuery e Bootstrap -->
|
<!-- jQuery e Bootstrap -->
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
<!-- DataTables -->
|
<!-- DataTables -->
|
||||||
@@ -118,7 +117,7 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ function h($v)
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
@@ -874,7 +874,7 @@ $isEdit = ($worksheet_id > 0);
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ $rows_special = array_filter($rows, function ($r) {
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
@@ -551,7 +551,7 @@ function revisionLabel($rev)
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
|
|
||||||
<!-- jQuery e Bootstrap -->
|
<!-- jQuery e Bootstrap -->
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
<!-- DataTables -->
|
<!-- DataTables -->
|
||||||
@@ -138,7 +137,7 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
@@ -231,7 +231,7 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
|
|
||||||
<!-- jQuery + Bootstrap -->
|
<!-- jQuery + Bootstrap -->
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
<!-- DataTables -->
|
<!-- DataTables -->
|
||||||
@@ -133,7 +132,7 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,6 @@
|
|||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
|
||||||
<!-- Bootstrap (se già incluso puoi rimuoverlo) -->
|
<!-- Bootstrap (se già incluso puoi rimuoverlo) -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
|
|
||||||
<!-- SweetAlert2 -->
|
<!-- SweetAlert2 -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
|
||||||
<!-- Bootstrap -->
|
<!-- Bootstrap -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
|
|
||||||
<!-- DataTables -->
|
<!-- DataTables -->
|
||||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
||||||
|
|||||||
@@ -150,17 +150,24 @@ $dashboardSections = [
|
|||||||
[
|
[
|
||||||
'id' => 'secPersonale',
|
'id' => 'secPersonale',
|
||||||
'title' => 'Personale',
|
'title' => 'Personale',
|
||||||
'subtitle' => 'Dipendenti, skill',
|
'subtitle' => 'Dipendenti, formazione, skill',
|
||||||
'icon' => '👥',
|
'icon' => '👥',
|
||||||
'open' => false,
|
'open' => false,
|
||||||
'buttons' => [
|
'buttons' => [
|
||||||
[
|
[
|
||||||
'label' => 'Employees',
|
'label' => 'Dipendenti',
|
||||||
'icon' => '👥',
|
'icon' => '👥',
|
||||||
'class' => 'btn-employees',
|
'class' => 'btn-employees',
|
||||||
'url' => 'employees.php',
|
'url' => 'employees.php',
|
||||||
'permission' => 'hr.employees.view',
|
'permission' => 'hr.employees.view',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'label' => 'Mansioni',
|
||||||
|
'icon' => '🧩',
|
||||||
|
'class' => 'btn-setup',
|
||||||
|
'url' => 'job-roles.php',
|
||||||
|
'permission' => 'hr.employees.view',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'label' => 'Departments',
|
'label' => 'Departments',
|
||||||
'icon' => '🏢',
|
'icon' => '🏢',
|
||||||
@@ -168,6 +175,20 @@ $dashboardSections = [
|
|||||||
'url' => 'departments.php',
|
'url' => 'departments.php',
|
||||||
'permission' => 'hr.departments.view',
|
'permission' => 'hr.departments.view',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'label' => 'DPI',
|
||||||
|
'icon' => '🦺',
|
||||||
|
'class' => 'btn-setup',
|
||||||
|
'url' => 'ppe-items.php',
|
||||||
|
'permission' => 'hr.employees.view',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => 'Gestione Formazione',
|
||||||
|
'icon' => '🎓',
|
||||||
|
'class' => 'btn-setup',
|
||||||
|
'url' => 'trainings.php',
|
||||||
|
'permission' => 'hr.trainings.view',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'label' => 'Skills',
|
'label' => 'Skills',
|
||||||
'icon' => '🧠',
|
'icon' => '🧠',
|
||||||
@@ -478,17 +499,118 @@ $dashboardSections = [
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
|
|
||||||
<?php
|
<?php $pdo = DBHandlerSelect::getInstance()->getConnection(); ?>
|
||||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
<style>
|
||||||
include(__DIR__ . '/scadenzario/include/my_deadlines_widget.php');
|
.my-deadlines-widgets {
|
||||||
?>
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-deadlines-widgets:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Each widget wraps itself in .my-deadlines-widgets; collapse the nested
|
||||||
|
wrapper so all cards flow into the outer flex (single row). */
|
||||||
|
.my-deadlines-widgets .my-deadlines-widgets {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-deadlines-widgets .mdw {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.8rem 0.9rem;
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||||
|
transition: transform 0.15s, box-shadow 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
.my-deadlines-widgets .mdw {
|
||||||
|
flex: 1 1 calc(50% - 0.375rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 575.98px) {
|
||||||
|
.my-deadlines-widgets .mdw {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-deadlines-widgets .mdw:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-deadlines-widgets .mdw-red {
|
||||||
|
background: linear-gradient(135deg, #dc3545 0%, #b02a37 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-deadlines-widgets .mdw-orange {
|
||||||
|
background: linear-gradient(135deg, #e8930c 0%, #c77a00 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-deadlines-widgets .mdw-gray {
|
||||||
|
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-deadlines-widgets .mdw-icon {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(255, 255, 255, 0.22);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-deadlines-widgets .mdw-body {
|
||||||
|
flex: 1;
|
||||||
|
line-height: 1.2;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-deadlines-widgets .mdw-count {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-deadlines-widgets .mdw-label {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
opacity: 0.95;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-deadlines-widgets .mdw-arrow {
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="my-deadlines-widgets">
|
||||||
|
<?php include(__DIR__ . '/scadenzario/include/my_deadlines_widget.php'); ?>
|
||||||
|
<?php include(__DIR__ . '/include/training_widget.php'); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3 class="dashboard-title">Dashboard</h3>
|
<h3 class="dashboard-title">Dashboard</h3>
|
||||||
|
|
||||||
|
|||||||
@@ -1114,7 +1114,7 @@ if (!empty($_GET['ajax'])) {
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
@@ -363,7 +363,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['action'])) {
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php');
|
<?php include('include/navbar.php');
|
||||||
include('include/topbar.php'); ?>
|
include('include/topbar.php'); ?>
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
<?php
|
||||||
|
include('../../include/headscript.php');
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
function jsonResponse(array $data): void
|
||||||
|
{
|
||||||
|
echo json_encode($data);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeNullableInt($value): ?int
|
||||||
|
{
|
||||||
|
return (isset($value) && $value !== '') ? (int)$value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$isHrManager = Auth::user()->hasRole('Admin')
|
||||||
|
|| Auth::user()->hasRole('Superuser')
|
||||||
|
|| Auth::user()->hasRole('employee-hr')
|
||||||
|
|| Auth::user()->hasRole('manager');
|
||||||
|
|
||||||
|
if (!$isHrManager) {
|
||||||
|
jsonResponse(['success' => false, 'message' => 'Non autorizzato.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$employeeId = (int)($_POST['employee_id'] ?? 0);
|
||||||
|
$firstName = trim($_POST['first_name'] ?? '');
|
||||||
|
$lastName = trim($_POST['last_name'] ?? '');
|
||||||
|
$employeeCode = trim($_POST['employee_code'] ?? '');
|
||||||
|
$hireDate = trim($_POST['hire_date'] ?? '');
|
||||||
|
$address = trim($_POST['address'] ?? '');
|
||||||
|
$phone = trim($_POST['phone'] ?? '');
|
||||||
|
$email = trim($_POST['email'] ?? '');
|
||||||
|
$departmentId = normalizeNullableInt($_POST['department_id'] ?? '');
|
||||||
|
$status = trim($_POST['status'] ?? 'active');
|
||||||
|
$authUserId = normalizeNullableInt($_POST['auth_user_id'] ?? '');
|
||||||
|
$roleId = normalizeNullableInt($_POST['role_id'] ?? '');
|
||||||
|
|
||||||
|
$jobSubRoleIds = $_POST['job_sub_role_ids'] ?? [];
|
||||||
|
if (!is_array($jobSubRoleIds)) {
|
||||||
|
$jobSubRoleIds = [$jobSubRoleIds];
|
||||||
|
}
|
||||||
|
|
||||||
|
$jobSubRoleIds = array_values(array_unique(array_filter(array_map('intval', $jobSubRoleIds))));
|
||||||
|
|
||||||
|
if ($employeeId <= 0) {
|
||||||
|
jsonResponse(['success' => false, 'message' => 'ID dipendente non valido.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($firstName === '' || $lastName === '') {
|
||||||
|
jsonResponse(['success' => false, 'message' => 'Nome e cognome sono obbligatori.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
jsonResponse(['success' => false, 'message' => 'Email non valida.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($status, ['active', 'inactive', 'suspended'], true)) {
|
||||||
|
$status = 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmtEmployee = $pdo->prepare('SELECT id FROM employees WHERE id = ? LIMIT 1');
|
||||||
|
$stmtEmployee->execute([$employeeId]);
|
||||||
|
if (!$stmtEmployee->fetchColumn()) {
|
||||||
|
jsonResponse(['success' => false, 'message' => 'Dipendente non trovato.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$primaryJobRoleId = null;
|
||||||
|
$primaryJobSubRoleId = null;
|
||||||
|
|
||||||
|
if ($jobSubRoleIds) {
|
||||||
|
$placeholders = implode(',', array_fill(0, count($jobSubRoleIds), '?'));
|
||||||
|
$stmtSubRoles = $pdo->prepare("\n SELECT id, job_role_id\n FROM job_sub_roles\n WHERE id IN ($placeholders)\n AND is_active = 1\n ");
|
||||||
|
$stmtSubRoles->execute($jobSubRoleIds);
|
||||||
|
$validRows = $stmtSubRoles->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$validMap = [];
|
||||||
|
foreach ($validRows as $row) {
|
||||||
|
$validMap[(int)$row['id']] = (int)$row['job_role_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$jobSubRoleIds = array_values(array_filter($jobSubRoleIds, static function ($id) use ($validMap) {
|
||||||
|
return isset($validMap[(int)$id]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
if ($jobSubRoleIds) {
|
||||||
|
$primaryJobSubRoleId = (int)$jobSubRoleIds[0];
|
||||||
|
$primaryJobRoleId = $validMap[$primaryJobSubRoleId] ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("\n UPDATE employees\n SET first_name = :first_name,\n last_name = :last_name,\n employee_code = :employee_code,\n hire_date = :hire_date,\n address = :address,\n phone = :phone,\n email = :email,\n department_id = :department_id,\n job_role_id = :job_role_id,\n job_sub_role_id = :job_sub_role_id,\n status = :status,\n auth_user_id = :auth_user_id,\n updated_at = NOW()\n WHERE id = :employee_id\n ");
|
||||||
|
$stmt->execute([
|
||||||
|
'first_name' => $firstName,
|
||||||
|
'last_name' => $lastName,
|
||||||
|
'employee_code' => $employeeCode !== '' ? $employeeCode : null,
|
||||||
|
'hire_date' => $hireDate !== '' ? $hireDate : null,
|
||||||
|
'address' => $address !== '' ? $address : null,
|
||||||
|
'phone' => $phone !== '' ? $phone : null,
|
||||||
|
'email' => $email !== '' ? $email : null,
|
||||||
|
'department_id' => $departmentId,
|
||||||
|
'job_role_id' => $primaryJobRoleId,
|
||||||
|
'job_sub_role_id' => $primaryJobSubRoleId,
|
||||||
|
'status' => $status,
|
||||||
|
'auth_user_id' => $authUserId,
|
||||||
|
'employee_id' => $employeeId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$stmtDelete = $pdo->prepare('DELETE FROM employee_job_sub_roles WHERE employee_id = ?');
|
||||||
|
$stmtDelete->execute([$employeeId]);
|
||||||
|
|
||||||
|
if ($jobSubRoleIds) {
|
||||||
|
$stmtInsert = $pdo->prepare("\n INSERT INTO employee_job_sub_roles\n (employee_id, job_sub_role_id, is_primary, created_at)\n VALUES\n (:employee_id, :job_sub_role_id, :is_primary, NOW())\n ");
|
||||||
|
|
||||||
|
foreach ($jobSubRoleIds as $index => $jobSubRoleId) {
|
||||||
|
$stmtInsert->execute([
|
||||||
|
'employee_id' => $employeeId,
|
||||||
|
'job_sub_role_id' => (int)$jobSubRoleId,
|
||||||
|
'is_primary' => $index === 0 ? 1 : 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($authUserId !== null && $roleId !== null) {
|
||||||
|
$checkRole = $pdo->prepare('SELECT COUNT(*) FROM auth_roles WHERE id = ?');
|
||||||
|
$checkRole->execute([$roleId]);
|
||||||
|
|
||||||
|
if ((int)$checkRole->fetchColumn() > 0) {
|
||||||
|
$stmtRole = $pdo->prepare('UPDATE auth_users SET role_id = :role_id, updated_at = NOW() WHERE id = :auth_user_id');
|
||||||
|
$stmtRole->execute([
|
||||||
|
'role_id' => $roleId,
|
||||||
|
'auth_user_id' => $authUserId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
jsonResponse(['success' => true]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if (isset($pdo) && $pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ try {
|
|||||||
$id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : null;
|
$id = isset($_POST['id']) && is_numeric($_POST['id']) ? (int)$_POST['id'] : null;
|
||||||
$subject_id = isset($_POST['subject_id']) && is_numeric($_POST['subject_id']) && (int)$_POST['subject_id'] > 0 ? (int)$_POST['subject_id'] : null;
|
$subject_id = isset($_POST['subject_id']) && is_numeric($_POST['subject_id']) && (int)$_POST['subject_id'] > 0 ? (int)$_POST['subject_id'] : null;
|
||||||
$function_id = isset($_POST['function_id']) && is_numeric($_POST['function_id']) && (int)$_POST['function_id'] > 0 ? (int)$_POST['function_id'] : null;
|
$function_id = isset($_POST['function_id']) && is_numeric($_POST['function_id']) && (int)$_POST['function_id'] > 0 ? (int)$_POST['function_id'] : null;
|
||||||
|
$notify_function = isset($_POST['notify_function']) && (int)$_POST['notify_function'] === 1 ? 1 : 0;
|
||||||
$topic = trim($_POST['topic'] ?? '');
|
$topic = trim($_POST['topic'] ?? '');
|
||||||
$law_regulation = trim($_POST['law_regulation'] ?? '') ?: null;
|
$law_regulation = trim($_POST['law_regulation'] ?? '') ?: null;
|
||||||
$recurrence_type = $_POST['recurrence_type'] ?? 'once';
|
$recurrence_type = $_POST['recurrence_type'] ?? 'once';
|
||||||
@@ -53,7 +54,7 @@ try {
|
|||||||
if ($id) {
|
if ($id) {
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
UPDATE scad_deadlines SET
|
UPDATE scad_deadlines SET
|
||||||
subject_id = ?, function_id = ?, topic = ?, law_regulation = ?, recurrence_type = ?,
|
subject_id = ?, function_id = ?, notify_function = ?, topic = ?, law_regulation = ?, recurrence_type = ?,
|
||||||
due_date = ?, check_date = ?, document_date = ?, notification_days = ?,
|
due_date = ?, check_date = ?, document_date = ?, notification_days = ?,
|
||||||
storage_location = ?, notes = ?, departments = ?
|
storage_location = ?, notes = ?, departments = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
@@ -61,6 +62,7 @@ try {
|
|||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
$subject_id,
|
$subject_id,
|
||||||
$function_id,
|
$function_id,
|
||||||
|
$notify_function,
|
||||||
$topic,
|
$topic,
|
||||||
$law_regulation,
|
$law_regulation,
|
||||||
$recurrence_type,
|
$recurrence_type,
|
||||||
@@ -86,13 +88,14 @@ try {
|
|||||||
// INSERT
|
// INSERT
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
INSERT INTO scad_deadlines
|
INSERT INTO scad_deadlines
|
||||||
(subject_id, function_id, topic, law_regulation, recurrence_type, due_date, check_date,
|
(subject_id, function_id, notify_function, topic, law_regulation, recurrence_type, due_date, check_date,
|
||||||
document_date, notification_days, storage_location, notes, created_by, departments)
|
document_date, notification_days, storage_location, notes, created_by, departments)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
");
|
");
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
$subject_id,
|
$subject_id,
|
||||||
$function_id,
|
$function_id,
|
||||||
|
$notify_function,
|
||||||
$topic,
|
$topic,
|
||||||
$law_regulation,
|
$law_regulation,
|
||||||
$recurrence_type,
|
$recurrence_type,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scadenzario — Email notification cron script
|
* Scadenzario — Email notification cron script
|
||||||
* Run daily: 0 7 * * * php /var/www/html/public/userarea/scadenzario/cron/send_notifications.php
|
* Run daily: 0 7 * * * php /var/www/html/public/userarea/scadenzario/cron/send_notifications.php
|
||||||
@@ -42,9 +43,19 @@ $errors = 0;
|
|||||||
|
|
||||||
// Get active deadlines that are approaching or overdue
|
// Get active deadlines that are approaching or overdue
|
||||||
$stmt = $pdo->query("
|
$stmt = $pdo->query("
|
||||||
SELECT d.id, d.topic, s.name AS subject_name, d.due_date, d.notification_days
|
SELECT
|
||||||
|
d.id,
|
||||||
|
d.topic,
|
||||||
|
s.name AS subject_name,
|
||||||
|
d.due_date,
|
||||||
|
d.notification_days,
|
||||||
|
d.notify_function,
|
||||||
|
f.email AS function_email,
|
||||||
|
f.person_full_name AS function_person,
|
||||||
|
f.name AS function_name
|
||||||
FROM scad_deadlines d
|
FROM scad_deadlines d
|
||||||
LEFT JOIN scad_subjects s ON s.id = d.subject_id
|
LEFT JOIN scad_subjects s ON s.id = d.subject_id
|
||||||
|
LEFT JOIN scad_functions f ON f.id = d.function_id
|
||||||
WHERE d.status = 'active'
|
WHERE d.status = 'active'
|
||||||
AND d.due_date <= DATE_ADD(CURDATE(), INTERVAL d.notification_days DAY)
|
AND d.due_date <= DATE_ADD(CURDATE(), INTERVAL d.notification_days DAY)
|
||||||
");
|
");
|
||||||
@@ -101,20 +112,28 @@ foreach ($deadlines as $dl) {
|
|||||||
$type = $isOverdue ? 'overdue' : 'approaching';
|
$type = $isOverdue ? 'overdue' : 'approaching';
|
||||||
$daysLeft = (int)((strtotime($dl['due_date']) - strtotime($today)) / 86400);
|
$daysLeft = (int)((strtotime($dl['due_date']) - strtotime($today)) / 86400);
|
||||||
|
|
||||||
// Collect all recipients (direct + department)
|
// Collect all recipients (direct + department + optional function email)
|
||||||
$recipients = [];
|
$recipients = [];
|
||||||
|
$functionRecipient = null;
|
||||||
|
|
||||||
$getRecipients->execute([$dl['id']]);
|
$getRecipients->execute([$dl['id']]);
|
||||||
foreach ($getRecipients->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
foreach ($getRecipients->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
||||||
$recipients[$r['employee_id']] = $r;
|
$recipients[$r['employee_id']] = $r;
|
||||||
}
|
}
|
||||||
|
|
||||||
$getDeptRecipients->execute([$dl['id']]);
|
// Optional: also notify the linked function email if enabled on the deadline.
|
||||||
foreach ($getDeptRecipients->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
if (
|
||||||
$recipients[$r['employee_id']] = $r;
|
!empty($dl['notify_function'])
|
||||||
|
&& !empty($dl['function_email'])
|
||||||
|
&& filter_var($dl['function_email'], FILTER_VALIDATE_EMAIL)
|
||||||
|
) {
|
||||||
|
$functionRecipient = [
|
||||||
|
'email' => $dl['function_email'],
|
||||||
|
'name' => trim(($dl['function_person'] ?? '') !== '' ? $dl['function_person'] : ($dl['function_name'] ?? 'Funzione')),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($recipients)) {
|
if (empty($recipients) && empty($functionRecipient)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,15 +212,99 @@ foreach ($deadlines as $dl) {
|
|||||||
$sent++;
|
$sent++;
|
||||||
|
|
||||||
echo date('H:i:s') . " ✓ {$type} → {$emp['email']} — {$dl['topic']}\n";
|
echo date('H:i:s') . " ✓ {$type} → {$emp['email']} — {$dl['topic']}\n";
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$errors++;
|
$errors++;
|
||||||
echo date('H:i:s') . " ✗ Errore {$emp['email']}: {$e->getMessage()}\n";
|
echo date('H:i:s') . " ✗ Errore {$emp['email']}: {$e->getMessage()}\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Send notification to function email if enabled.
|
||||||
|
// It is tracked with employee_id = 0 to avoid duplicate daily sends.
|
||||||
|
if ($functionRecipient) {
|
||||||
|
$functionEmployeeId = 0;
|
||||||
|
|
||||||
|
$checkSent->execute([$dl['id'], $functionEmployeeId, $type]);
|
||||||
|
if ($checkSent->fetchColumn() > 0) {
|
||||||
|
$skipped++;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$mail = new PHPMailer(true);
|
||||||
|
|
||||||
|
$mailer = $_ENV['MAIL_MAILER'] ?? 'mail';
|
||||||
|
if ($mailer === 'smtp') {
|
||||||
|
$mail->isSMTP();
|
||||||
|
$mail->Host = $_ENV['MAIL_HOST'] ?? 'localhost';
|
||||||
|
$mail->Port = (int)($_ENV['MAIL_PORT'] ?? 587);
|
||||||
|
|
||||||
|
if (!empty($_ENV['MAIL_USERNAME']) && $_ENV['MAIL_USERNAME'] !== 'null') {
|
||||||
|
$mail->SMTPAuth = true;
|
||||||
|
$mail->Username = $_ENV['MAIL_USERNAME'];
|
||||||
|
$mail->Password = $_ENV['MAIL_PASSWORD'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$enc = $_ENV['MAIL_ENCRYPTION'] ?? '';
|
||||||
|
if ($enc && $enc !== 'null') {
|
||||||
|
$mail->SMTPSecure = $enc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$mail->CharSet = 'UTF-8';
|
||||||
|
$mail->setFrom(
|
||||||
|
$_ENV['MAIL_FROM_ADDRESS'] ?? 'noreply@zibogomma.it',
|
||||||
|
$_ENV['MAIL_FROM_NAME'] ?? 'Scadenzario ZIBOGOMMA'
|
||||||
|
);
|
||||||
|
|
||||||
|
$mail->addAddress($functionRecipient['email'], $functionRecipient['name']);
|
||||||
|
|
||||||
|
if ($managerCcEmail && strcasecmp($managerCcEmail, $functionRecipient['email']) !== 0) {
|
||||||
|
$mail->addCC($managerCcEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
$detailUrl = $appUrl . '/userarea/scadenzario/detail.php?id=' . $dl['id'];
|
||||||
|
$topicText = (!empty($dl['subject_name']) ? $dl['subject_name'] . ' — ' : '') . $dl['topic'];
|
||||||
|
|
||||||
|
if ($isOverdue) {
|
||||||
|
$mail->Subject = '⚠️ Scadenza superata: ' . $dl['topic'];
|
||||||
|
$mail->Body = buildHtml(
|
||||||
|
'Scadenza superata',
|
||||||
|
$topicText,
|
||||||
|
'La scadenza era prevista per il <strong>' . date('d/m/Y', strtotime($dl['due_date'])) . '</strong> ed è stata superata da <strong>' . abs($daysLeft) . ' giorni</strong>.',
|
||||||
|
'#dc3545',
|
||||||
|
$detailUrl
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$mail->Subject = '📅 Scadenza in arrivo: ' . $dl['topic'];
|
||||||
|
$daysText = $daysLeft === 0 ? 'oggi' : 'tra <strong>' . $daysLeft . ' giorni</strong>';
|
||||||
|
$mail->Body = buildHtml(
|
||||||
|
'Scadenza in arrivo',
|
||||||
|
$topicText,
|
||||||
|
'La scadenza è prevista per il <strong>' . date('d/m/Y', strtotime($dl['due_date'])) . '</strong> (' . $daysText . ').',
|
||||||
|
'#e8930c',
|
||||||
|
$detailUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$mail->isHTML(true);
|
||||||
|
$mail->AltBody = strip_tags(str_replace('<br>', "\n", $mail->Body));
|
||||||
|
|
||||||
|
$mail->send();
|
||||||
|
|
||||||
|
$insertNotif->execute([$dl['id'], $functionEmployeeId, $type]);
|
||||||
|
$sent++;
|
||||||
|
|
||||||
|
echo date('H:i:s') . " ✓ {$type} → funzione {$functionRecipient['email']} — {$dl['topic']}\n";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$errors++;
|
||||||
|
echo date('H:i:s') . " ✗ Errore funzione {$functionRecipient['email']}: {$e->getMessage()}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// History (one per deadline, not per recipient)
|
// History (one per deadline, not per recipient)
|
||||||
$recipientNames = implode(', ', array_map(fn($r) => trim($r['first_name'] . ' ' . $r['last_name']), $recipients));
|
$recipientNames = implode(', ', array_map(fn($r) => trim($r['first_name'] . ' ' . $r['last_name']), $recipients));
|
||||||
|
|
||||||
|
if ($functionRecipient) {
|
||||||
|
$recipientNames .= ($recipientNames !== '' ? ', ' : '') . 'Funzione: ' . $functionRecipient['name'] . ' <' . $functionRecipient['email'] . '>';
|
||||||
|
}
|
||||||
|
|
||||||
$insertHistory->execute([$dl['id'], "Notifica {$type} inviata a: {$recipientNames}"]);
|
$insertHistory->execute([$dl['id'], "Notifica {$type} inviata a: {$recipientNames}"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,122 @@
|
|||||||
<?php include('../../include/headscript.php'); ?>
|
<?php include('../../include/headscript.php'); ?>
|
||||||
<?php
|
<?php
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
$db = DBHandlerSelect::getInstance();
|
$db = DBHandlerSelect::getInstance();
|
||||||
$pdo = $db->getConnection();
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
$functions = $pdo->query("
|
function scadJsonResponse(array $data): void
|
||||||
SELECT f.*,
|
{
|
||||||
(SELECT COUNT(*) FROM scad_deadlines d WHERE d.function_id = f.id) AS deadline_count,
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
(SELECT COUNT(*) FROM scad_deadlines d WHERE d.function_id = f.id AND d.status <> 'completed') AS open_count
|
echo json_encode($data);
|
||||||
FROM scad_functions f
|
exit;
|
||||||
ORDER BY f.name ASC
|
}
|
||||||
")->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
function scadNullableString($value): ?string
|
||||||
|
{
|
||||||
|
$value = trim((string)($value ?? ''));
|
||||||
|
return $value !== '' ? $value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scadNormalizeStatus(string $status): string
|
||||||
|
{
|
||||||
|
return in_array($status, ['active', 'inactive'], true) ? $status : 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
function scadValidateEmail($email): ?string
|
||||||
|
{
|
||||||
|
$email = scadNullableString($email);
|
||||||
|
|
||||||
|
if ($email !== null && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
throw new Exception('Email non valida.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $email;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax']) && $_POST['ajax'] === '1') {
|
||||||
|
try {
|
||||||
|
$action = $_POST['action'] ?? '';
|
||||||
|
|
||||||
|
if ($action === 'save') {
|
||||||
|
$id = isset($_POST['id']) && $_POST['id'] !== '' ? (int)$_POST['id'] : 0;
|
||||||
|
$name = trim($_POST['name'] ?? '');
|
||||||
|
$description = scadNullableString($_POST['description'] ?? null);
|
||||||
|
$personFullName = scadNullableString($_POST['person_full_name'] ?? null);
|
||||||
|
$phone = scadNullableString($_POST['phone'] ?? null);
|
||||||
|
$email = scadValidateEmail($_POST['email'] ?? null);
|
||||||
|
$notes = scadNullableString($_POST['notes'] ?? null);
|
||||||
|
$sortOrder = isset($_POST['sort_order']) && $_POST['sort_order'] !== '' ? (int)$_POST['sort_order'] : 0;
|
||||||
|
$status = scadNormalizeStatus(trim($_POST['status'] ?? 'active'));
|
||||||
|
|
||||||
|
if ($name === '') {
|
||||||
|
scadJsonResponse(['success' => false, 'message' => 'Il nome funzione è obbligatorio.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($id > 0) {
|
||||||
|
$stmt = $pdo->prepare("\n UPDATE scad_functions\n SET name = :name,\n description = :description,\n person_full_name = :person_full_name,\n phone = :phone,\n email = :email,\n notes = :notes,\n sort_order = :sort_order,\n status = :status,\n updated_at = NOW()\n WHERE id = :id\n ");
|
||||||
|
$stmt->execute([
|
||||||
|
'name' => $name,
|
||||||
|
'description' => $description,
|
||||||
|
'person_full_name' => $personFullName,
|
||||||
|
'phone' => $phone,
|
||||||
|
'email' => $email,
|
||||||
|
'notes' => $notes,
|
||||||
|
'sort_order' => $sortOrder,
|
||||||
|
'status' => $status,
|
||||||
|
'id' => $id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
scadJsonResponse(['success' => true, 'message' => 'Funzione aggiornata correttamente.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("\n INSERT INTO scad_functions\n (name, description, person_full_name, phone, email, notes, sort_order, status, created_at, updated_at)\n VALUES\n (:name, :description, :person_full_name, :phone, :email, :notes, :sort_order, :status, NOW(), NOW())\n ");
|
||||||
|
$stmt->execute([
|
||||||
|
'name' => $name,
|
||||||
|
'description' => $description,
|
||||||
|
'person_full_name' => $personFullName,
|
||||||
|
'phone' => $phone,
|
||||||
|
'email' => $email,
|
||||||
|
'notes' => $notes,
|
||||||
|
'sort_order' => $sortOrder,
|
||||||
|
'status' => $status,
|
||||||
|
]);
|
||||||
|
|
||||||
|
scadJsonResponse(['success' => true, 'message' => 'Funzione creata correttamente.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'delete') {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
|
||||||
|
if ($id <= 0) {
|
||||||
|
scadJsonResponse(['success' => false, 'message' => 'ID funzione non valido.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmtUse = $pdo->prepare('SELECT COUNT(*) FROM scad_deadlines WHERE function_id = ?');
|
||||||
|
$stmtUse->execute([$id]);
|
||||||
|
$inUse = (int)$stmtUse->fetchColumn();
|
||||||
|
|
||||||
|
if ($inUse > 0) {
|
||||||
|
scadJsonResponse([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Impossibile eliminare: la funzione è utilizzata in ' . $inUse . ' scadenza/e.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('DELETE FROM scad_functions WHERE id = :id');
|
||||||
|
$stmt->execute(['id' => $id]);
|
||||||
|
|
||||||
|
scadJsonResponse(['success' => true, 'message' => 'Funzione eliminata correttamente.']);
|
||||||
|
}
|
||||||
|
|
||||||
|
scadJsonResponse(['success' => false, 'message' => 'Azione non riconosciuta.']);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
scadJsonResponse(['success' => false, 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$functions = $pdo->query("\n SELECT f.*,\n (SELECT COUNT(*) FROM scad_deadlines d WHERE d.function_id = f.id) AS deadline_count,\n (SELECT COUNT(*) FROM scad_deadlines d WHERE d.function_id = f.id AND d.status <> 'completed') AS open_count\n FROM scad_functions f\n ORDER BY COALESCE(f.sort_order, 0) ASC, f.name ASC\n")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="it">
|
<html lang="it">
|
||||||
@@ -24,11 +131,13 @@ $functions = $pdo->query("
|
|||||||
<base href="<?= $baseHref ?>">
|
<base href="<?= $baseHref ?>">
|
||||||
<?php include('../../cssinclude.php'); ?>
|
<?php include('../../cssinclude.php'); ?>
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
<title>Scadenzario - Funzioni</title>
|
<title>Scadenzario - Funzioni</title>
|
||||||
<script>
|
<script>
|
||||||
if (window.innerWidth > 1024) {
|
if (window.innerWidth > 1024) {
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
document.getElementById('appWrapper').classList.add('toggled');
|
const wrapper = document.getElementById('appWrapper');
|
||||||
|
if (wrapper) wrapper.classList.add('toggled');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -39,6 +148,7 @@ $functions = $pdo->query("
|
|||||||
--scad-heading: #2c3e6b;
|
--scad-heading: #2c3e6b;
|
||||||
--scad-card-bg: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%);
|
--scad-card-bg: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%);
|
||||||
--scad-card-border: #dde4f0;
|
--scad-card-border: #dde4f0;
|
||||||
|
--scad-muted: #6c757d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scad-card {
|
.scad-card {
|
||||||
@@ -127,6 +237,63 @@ $functions = $pdo->query("
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.function-table th {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--scad-heading);
|
||||||
|
background: #f8fafc;
|
||||||
|
border-bottom: 1px solid var(--scad-card-border);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-table td {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-name {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--scad-heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-description,
|
||||||
|
.function-notes {
|
||||||
|
color: var(--scad-muted);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
margin-top: 2px;
|
||||||
|
max-width: 280px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-line {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-line a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-function-status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-function-status.active {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-function-status.inactive {
|
||||||
|
background: #e5e7eb;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
.function-card {
|
.function-card {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid var(--scad-card-border);
|
border: 1px solid var(--scad-card-border);
|
||||||
@@ -141,12 +308,19 @@ $functions = $pdo->query("
|
|||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.function-card .fc-meta {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--scad-muted);
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
.function-card .fc-stats {
|
.function-card .fc-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: #6c757d;
|
color: var(--scad-muted);
|
||||||
margin: 0.5rem 0;
|
margin: 0.5rem 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.function-card .fc-stats strong {
|
.function-card .fc-stats strong {
|
||||||
@@ -156,7 +330,7 @@ $functions = $pdo->query("
|
|||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 3rem 1rem;
|
padding: 3rem 1rem;
|
||||||
color: #6c757d;
|
color: var(--scad-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state i {
|
.empty-state i {
|
||||||
@@ -165,6 +339,15 @@ $functions = $pdo->query("
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
background: var(--scad-card-bg);
|
||||||
|
border-bottom: 1px solid var(--scad-card-border);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 575.98px) {
|
@media (max-width: 575.98px) {
|
||||||
.scad-card .card-header {
|
.scad-card .card-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -223,25 +406,48 @@ $functions = $pdo->query("
|
|||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
|
|
||||||
<div id="functionsList">
|
<div id="functionsList">
|
||||||
|
|
||||||
<div class="d-md-none">
|
<div class="d-md-none">
|
||||||
<?php foreach ($functions as $f): ?>
|
<?php foreach ($functions as $f): ?>
|
||||||
|
<?php
|
||||||
|
$status = $f['status'] === 'inactive' ? 'inactive' : 'active';
|
||||||
|
$statusLabel = $status === 'active' ? 'Attiva' : 'Non attiva';
|
||||||
|
?>
|
||||||
<div class="function-card"
|
<div class="function-card"
|
||||||
data-id="<?= (int)$f['id'] ?>"
|
data-id="<?= (int)$f['id'] ?>"
|
||||||
data-name="<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>"
|
data-name="<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>"
|
||||||
data-description="<?= htmlspecialchars($f['description'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
data-description="<?= htmlspecialchars($f['description'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||||
data-status="<?= htmlspecialchars($f['status'], ENT_QUOTES, 'UTF-8') ?>"
|
data-person-full-name="<?= htmlspecialchars($f['person_full_name'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-phone="<?= htmlspecialchars($f['phone'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-email="<?= htmlspecialchars($f['email'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-notes="<?= htmlspecialchars($f['notes'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-sort-order="<?= (int)($f['sort_order'] ?? 0) ?>"
|
||||||
|
data-status="<?= htmlspecialchars($status, ENT_QUOTES, 'UTF-8') ?>"
|
||||||
data-in-use="<?= (int)$f['deadline_count'] ?>">
|
data-in-use="<?= (int)$f['deadline_count'] ?>">
|
||||||
|
|
||||||
<div class="fc-name"><?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?></div>
|
<div class="d-flex justify-content-between align-items-start gap-2">
|
||||||
|
<div class="fc-name"><?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||||
|
<span class="badge-function-status <?= $status ?>"><?= $statusLabel ?></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php if (!empty($f['description'])): ?>
|
<?php if (!empty($f['description'])): ?>
|
||||||
<div class="text-muted small mt-1"><?= htmlspecialchars($f['description'], ENT_QUOTES, 'UTF-8') ?></div>
|
<div class="fc-meta"><?= htmlspecialchars($f['description'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($f['person_full_name'])): ?>
|
||||||
|
<div class="fc-meta"><strong>Referente:</strong> <?= htmlspecialchars($f['person_full_name'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($f['phone']) || !empty($f['email'])): ?>
|
||||||
|
<div class="fc-meta">
|
||||||
|
<?php if (!empty($f['phone'])): ?>📞 <?= htmlspecialchars($f['phone'], ENT_QUOTES, 'UTF-8') ?><?php endif; ?>
|
||||||
|
<?php if (!empty($f['email'])): ?><?= !empty($f['phone']) ? '<br>' : '' ?>✉️ <?= htmlspecialchars($f['email'], ENT_QUOTES, 'UTF-8') ?><?php endif; ?>
|
||||||
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="fc-stats">
|
<div class="fc-stats">
|
||||||
<span>Scadenze: <strong><?= (int)$f['deadline_count'] ?></strong></span>
|
<span>Scadenze: <strong><?= (int)$f['deadline_count'] ?></strong></span>
|
||||||
<span>Aperte: <strong><?= (int)$f['open_count'] ?></strong></span>
|
<span>Aperte: <strong><?= (int)$f['open_count'] ?></strong></span>
|
||||||
|
<span>Ordine: <strong><?= (int)($f['sort_order'] ?? 0) ?></strong></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex gap-1 justify-content-end">
|
<div class="d-flex gap-1 justify-content-end">
|
||||||
@@ -258,30 +464,63 @@ $functions = $pdo->query("
|
|||||||
|
|
||||||
<div class="d-none d-md-block">
|
<div class="d-none d-md-block">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover align-middle mb-0">
|
<table class="table table-hover align-middle mb-0 function-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Nome</th>
|
<th style="width:70px" class="text-center">Ord.</th>
|
||||||
<th>Descrizione</th>
|
<th>Funzione</th>
|
||||||
<th class="text-center" style="width:120px">Scadenze</th>
|
<th style="width:220px">Referente</th>
|
||||||
<th class="text-center" style="width:120px">Aperte</th>
|
<th style="width:230px">Contatti</th>
|
||||||
<th class="text-center" style="width:120px">Azioni</th>
|
<th style="width:120px" class="text-center">Stato</th>
|
||||||
|
<th style="width:120px" class="text-center">Scadenze</th>
|
||||||
|
<th style="width:120px" class="text-center">Aperte</th>
|
||||||
|
<th style="width:120px" class="text-center">Azioni</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($functions as $f): ?>
|
<?php foreach ($functions as $f): ?>
|
||||||
|
<?php
|
||||||
|
$status = $f['status'] === 'inactive' ? 'inactive' : 'active';
|
||||||
|
$statusLabel = $status === 'active' ? 'Attiva' : 'Non attiva';
|
||||||
|
?>
|
||||||
<tr data-id="<?= (int)$f['id'] ?>"
|
<tr data-id="<?= (int)$f['id'] ?>"
|
||||||
data-name="<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>"
|
data-name="<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>"
|
||||||
data-description="<?= htmlspecialchars($f['description'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
data-description="<?= htmlspecialchars($f['description'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||||
data-status="<?= htmlspecialchars($f['status'], ENT_QUOTES, 'UTF-8') ?>"
|
data-person-full-name="<?= htmlspecialchars($f['person_full_name'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-phone="<?= htmlspecialchars($f['phone'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-email="<?= htmlspecialchars($f['email'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-notes="<?= htmlspecialchars($f['notes'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
|
||||||
|
data-sort-order="<?= (int)($f['sort_order'] ?? 0) ?>"
|
||||||
|
data-status="<?= htmlspecialchars($status, ENT_QUOTES, 'UTF-8') ?>"
|
||||||
data-in-use="<?= (int)$f['deadline_count'] ?>">
|
data-in-use="<?= (int)$f['deadline_count'] ?>">
|
||||||
|
|
||||||
<td class="fw-semibold" style="color:var(--scad-heading)">
|
<td class="text-center"><?= (int)($f['sort_order'] ?? 0) ?></td>
|
||||||
<?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?>
|
<td>
|
||||||
|
<div class="function-name"><?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?></div>
|
||||||
|
<?php if (!empty($f['description'])): ?>
|
||||||
|
<div class="function-description" title="<?= htmlspecialchars($f['description'], ENT_QUOTES, 'UTF-8') ?>">
|
||||||
|
<?= htmlspecialchars($f['description'], ENT_QUOTES, 'UTF-8') ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($f['notes'])): ?>
|
||||||
|
<div class="function-notes" title="<?= htmlspecialchars($f['notes'], ENT_QUOTES, 'UTF-8') ?>">
|
||||||
|
Note: <?= htmlspecialchars($f['notes'], ENT_QUOTES, 'UTF-8') ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-muted">
|
<td><?= !empty($f['person_full_name']) ? htmlspecialchars($f['person_full_name'], ENT_QUOTES, 'UTF-8') : '<span class="text-muted">—</span>' ?></td>
|
||||||
<?= htmlspecialchars($f['description'] ?? '—', ENT_QUOTES, 'UTF-8') ?>
|
<td>
|
||||||
|
<?php if (!empty($f['phone'])): ?>
|
||||||
|
<div class="contact-line">📞 <a href="tel:<?= htmlspecialchars($f['phone'], ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($f['phone'], ENT_QUOTES, 'UTF-8') ?></a></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($f['email'])): ?>
|
||||||
|
<div class="contact-line">✉️ <a href="mailto:<?= htmlspecialchars($f['email'], ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($f['email'], ENT_QUOTES, 'UTF-8') ?></a></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (empty($f['phone']) && empty($f['email'])): ?>
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="text-center"><span class="badge-function-status <?= $status ?>"><?= $statusLabel ?></span></td>
|
||||||
<td class="text-center"><?= (int)$f['deadline_count'] ?></td>
|
<td class="text-center"><?= (int)$f['deadline_count'] ?></td>
|
||||||
<td class="text-center"><?= (int)$f['open_count'] ?></td>
|
<td class="text-center"><?= (int)$f['open_count'] ?></td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
@@ -300,9 +539,7 @@ $functions = $pdo->query("
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -314,7 +551,7 @@ $functions = $pdo->query("
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal fade" id="functionModal" tabindex="-1" aria-hidden="true">
|
<div class="modal fade" id="functionModal" tabindex="-1" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered modal-lg modal-dialog-scrollable">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="functionModalTitle">Nuova Funzione</h5>
|
<h5 class="modal-title" id="functionModalTitle">Nuova Funzione</h5>
|
||||||
@@ -325,20 +562,57 @@ $functions = $pdo->query("
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<input type="hidden" id="functionId" name="id" value="">
|
<input type="hidden" id="functionId" name="id" value="">
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="row">
|
||||||
<label for="functionName" class="form-label fw-semibold">Nome <span class="text-danger">*</span></label>
|
<div class="col-12 col-md-8 mb-3">
|
||||||
<input type="text" class="form-control" id="functionName" name="name" maxlength="255" required>
|
<label for="functionName" class="form-label fw-semibold">Nome funzione <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control" id="functionName" name="name" maxlength="255" required placeholder="Es. RSPP, Medico del lavoro, RLS">
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-4 mb-3">
|
||||||
|
<label for="functionSortOrder" class="form-label fw-semibold">Ordine</label>
|
||||||
|
<input type="number" class="form-control" id="functionSortOrder" name="sort_order" min="0" step="1" value="0">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="functionDescription" class="form-label fw-semibold">Descrizione</label>
|
<label for="functionDescription" class="form-label fw-semibold">Descrizione</label>
|
||||||
<textarea class="form-control" id="functionDescription" name="description" rows="3"></textarea>
|
<textarea class="form-control" id="functionDescription" name="description" rows="2"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="functionPersonFullName" class="form-label fw-semibold">Nome e cognome referente</label>
|
||||||
|
<input type="text" class="form-control" id="functionPersonFullName" name="person_full_name" maxlength="200" placeholder="Es. Mario Rossi">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label for="functionPhone" class="form-label fw-semibold">Telefono</label>
|
||||||
|
<input type="text" class="form-control" id="functionPhone" name="phone" maxlength="80">
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label for="functionEmail" class="form-label fw-semibold">Email</label>
|
||||||
|
<input type="email" class="form-control" id="functionEmail" name="email" maxlength="190">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label for="functionStatus" class="form-label fw-semibold">Stato</label>
|
||||||
|
<select class="form-select" id="functionStatus" name="status">
|
||||||
|
<option value="active">Attiva</option>
|
||||||
|
<option value="inactive">Non attiva</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="functionNotes" class="form-label fw-semibold">Note operative</label>
|
||||||
|
<textarea class="form-control" id="functionNotes" name="notes" rows="3"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Annulla</button>
|
||||||
<button type="submit" class="btn btn-scad-primary">Salva</button>
|
<button type="submit" class="btn btn-scad-primary" id="functionSaveBtn">Salva</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -349,6 +623,33 @@ $functions = $pdo->query("
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(function() {
|
$(function() {
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value == null ? '' : value).replace(/[&<>'"]/g, function(c) {
|
||||||
|
return ({
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
"'": ''',
|
||||||
|
'"': '"'
|
||||||
|
})[c];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRowData($row) {
|
||||||
|
return {
|
||||||
|
id: $row.data('id') || '',
|
||||||
|
name: $row.data('name') || '',
|
||||||
|
description: $row.data('description') || '',
|
||||||
|
person_full_name: $row.data('person-full-name') || '',
|
||||||
|
phone: $row.data('phone') || '',
|
||||||
|
email: $row.data('email') || '',
|
||||||
|
notes: $row.data('notes') || '',
|
||||||
|
sort_order: $row.data('sort-order') || 0,
|
||||||
|
status: $row.data('status') || 'active',
|
||||||
|
in_use: parseInt($row.data('in-use') || 0, 10)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function openModal(data) {
|
function openModal(data) {
|
||||||
const isEdit = !!data;
|
const isEdit = !!data;
|
||||||
|
|
||||||
@@ -356,6 +657,13 @@ $functions = $pdo->query("
|
|||||||
$('#functionId').val(isEdit ? data.id : '');
|
$('#functionId').val(isEdit ? data.id : '');
|
||||||
$('#functionName').val(isEdit ? data.name : '');
|
$('#functionName').val(isEdit ? data.name : '');
|
||||||
$('#functionDescription').val(isEdit ? data.description : '');
|
$('#functionDescription').val(isEdit ? data.description : '');
|
||||||
|
$('#functionPersonFullName').val(isEdit ? data.person_full_name : '');
|
||||||
|
$('#functionPhone').val(isEdit ? data.phone : '');
|
||||||
|
$('#functionEmail').val(isEdit ? data.email : '');
|
||||||
|
$('#functionNotes').val(isEdit ? data.notes : '');
|
||||||
|
$('#functionSortOrder').val(isEdit ? data.sort_order : 0);
|
||||||
|
$('#functionStatus').val(isEdit ? data.status : 'active');
|
||||||
|
$('#functionSaveBtn').prop('disabled', false).html('Salva');
|
||||||
|
|
||||||
new bootstrap.Modal('#functionModal').show();
|
new bootstrap.Modal('#functionModal').show();
|
||||||
}
|
}
|
||||||
@@ -366,30 +674,24 @@ $functions = $pdo->query("
|
|||||||
|
|
||||||
$('#functionsList').on('click', '.btn-edit', function() {
|
$('#functionsList').on('click', '.btn-edit', function() {
|
||||||
const $row = $(this).closest('[data-id]');
|
const $row = $(this).closest('[data-id]');
|
||||||
|
openModal(getRowData($row));
|
||||||
openModal({
|
|
||||||
id: $row.data('id'),
|
|
||||||
name: $row.data('name'),
|
|
||||||
description: $row.data('description')
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#functionsList').on('click', '.btn-delete', function() {
|
$('#functionsList').on('click', '.btn-delete', function() {
|
||||||
const $row = $(this).closest('[data-id]');
|
const $row = $(this).closest('[data-id]');
|
||||||
const inUse = parseInt($row.data('in-use') || 0, 10);
|
const data = getRowData($row);
|
||||||
const name = $row.data('name');
|
|
||||||
|
|
||||||
if (inUse > 0) {
|
if (data.in_use > 0) {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
icon: 'warning',
|
icon: 'warning',
|
||||||
title: 'Impossibile eliminare',
|
title: 'Impossibile eliminare',
|
||||||
text: `La funzione "${name}" è utilizzata in ${inUse} scadenz${inUse === 1 ? 'a' : 'e'}.`
|
text: 'La funzione "' + data.name + '" è utilizzata in ' + data.in_use + ' scadenza/e.'
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: `Eliminare "${name}"?`,
|
title: 'Eliminare "' + data.name + '"?',
|
||||||
icon: 'warning',
|
icon: 'warning',
|
||||||
showCancelButton: true,
|
showCancelButton: true,
|
||||||
confirmButtonText: 'Elimina',
|
confirmButtonText: 'Elimina',
|
||||||
@@ -398,8 +700,10 @@ $functions = $pdo->query("
|
|||||||
}).then(function(result) {
|
}).then(function(result) {
|
||||||
if (!result.isConfirmed) return;
|
if (!result.isConfirmed) return;
|
||||||
|
|
||||||
$.post('scadenzario/functions/ajax/delete_function.php', {
|
$.post(window.location.href.split('#')[0], {
|
||||||
id: $row.data('id')
|
ajax: '1',
|
||||||
|
action: 'delete',
|
||||||
|
id: data.id
|
||||||
})
|
})
|
||||||
.done(function(res) {
|
.done(function(res) {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
@@ -408,7 +712,7 @@ $functions = $pdo->query("
|
|||||||
Swal.fire({
|
Swal.fire({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: 'Errore',
|
title: 'Errore',
|
||||||
text: res.message
|
text: res.message || 'Impossibile eliminare.'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -424,10 +728,19 @@ $functions = $pdo->query("
|
|||||||
$('#functionForm').on('submit', function(e) {
|
$('#functionForm').on('submit', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
const $btn = $('#functionSaveBtn');
|
||||||
const payload = {
|
const payload = {
|
||||||
|
ajax: '1',
|
||||||
|
action: 'save',
|
||||||
id: $('#functionId').val(),
|
id: $('#functionId').val(),
|
||||||
name: $('#functionName').val().trim(),
|
name: $('#functionName').val().trim(),
|
||||||
description: $('#functionDescription').val().trim()
|
description: $('#functionDescription').val().trim(),
|
||||||
|
person_full_name: $('#functionPersonFullName').val().trim(),
|
||||||
|
phone: $('#functionPhone').val().trim(),
|
||||||
|
email: $('#functionEmail').val().trim(),
|
||||||
|
notes: $('#functionNotes').val().trim(),
|
||||||
|
sort_order: $('#functionSortOrder').val(),
|
||||||
|
status: $('#functionStatus').val()
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!payload.name) {
|
if (!payload.name) {
|
||||||
@@ -438,19 +751,23 @@ $functions = $pdo->query("
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$.post('scadenzario/functions/ajax/save_function.php', payload)
|
$btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span>Salvataggio...');
|
||||||
|
|
||||||
|
$.post(window.location.href.split('#')[0], payload)
|
||||||
.done(function(res) {
|
.done(function(res) {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
location.reload();
|
location.reload();
|
||||||
} else {
|
} else {
|
||||||
|
$btn.prop('disabled', false).html('Salva');
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: 'Errore',
|
title: 'Errore',
|
||||||
text: res.message
|
text: res.message || 'Impossibile salvare.'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.fail(function() {
|
.fail(function() {
|
||||||
|
$btn.prop('disabled', false).html('Salva');
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
title: 'Errore di rete'
|
title: 'Errore di rete'
|
||||||
|
|||||||
@@ -51,6 +51,15 @@
|
|||||||
<i class="fa-solid fa-gear"></i>
|
<i class="fa-solid fa-gear"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-check mt-2">
|
||||||
|
<input class="form-check-input" type="checkbox" id="notify_function" name="notify_function" value="1">
|
||||||
|
<label class="form-check-label" for="notify_function">
|
||||||
|
Invia promemoria anche alla funzione selezionata
|
||||||
|
</label>
|
||||||
|
<div class="form-text">
|
||||||
|
Se attivo, la mail giornaliera verrà inviata anche all’email collegata alla funzione.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<label for="dlLaw" class="form-label fw-semibold">Legge / Articolo</label>
|
<label for="dlLaw" class="form-label fw-semibold">Legge / Articolo</label>
|
||||||
@@ -96,7 +105,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Group 3: Responsabili -->
|
<!-- Group 3: Responsabili -->
|
||||||
<div class="form-section-title">Responsabili</div>
|
<div class="form-section-title">Esecutore</div>
|
||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label for="dlDepartments" class="form-label fw-semibold">Reparti</label>
|
<label for="dlDepartments" class="form-label fw-semibold">Reparti</label>
|
||||||
|
|||||||
@@ -36,23 +36,51 @@
|
|||||||
language: 'it',
|
language: 'it',
|
||||||
width: '100%'
|
width: '100%'
|
||||||
};
|
};
|
||||||
$('#dlSubject').select2($.extend({}, s2Opts, { placeholder: 'Seleziona argomento...' }));
|
$('#dlSubject').select2($.extend({}, s2Opts, {
|
||||||
$('#dlDepartments').select2($.extend({}, s2Opts, { placeholder: 'Seleziona reparti...' }));
|
placeholder: 'Seleziona argomento...'
|
||||||
$('#dlEmployees').select2($.extend({}, s2Opts, { placeholder: 'Seleziona persone...' }));
|
}));
|
||||||
$('#dlFunction').select2($.extend({}, s2Opts, { placeholder: 'Seleziona funzione...' }));
|
$('#dlDepartments').select2($.extend({}, s2Opts, {
|
||||||
|
placeholder: 'Seleziona reparti...'
|
||||||
|
}));
|
||||||
|
$('#dlEmployees').select2($.extend({}, s2Opts, {
|
||||||
|
placeholder: 'Seleziona persone...'
|
||||||
|
}));
|
||||||
|
$('#dlFunction').select2($.extend({}, s2Opts, {
|
||||||
|
placeholder: 'Seleziona funzione...'
|
||||||
|
}));
|
||||||
|
|
||||||
// --- Auto-calc due_date from document_date + recurrence ---
|
// --- Auto-calc due_date from document_date + recurrence ---
|
||||||
var RECURRENCE_OFFSETS = {
|
var RECURRENCE_OFFSETS = {
|
||||||
monthly: { months: 1 },
|
monthly: {
|
||||||
quarterly: { months: 3 },
|
months: 1
|
||||||
semiannual: { months: 6 },
|
},
|
||||||
annual: { years: 1 },
|
quarterly: {
|
||||||
biennial: { years: 2 },
|
months: 3
|
||||||
triennial: { years: 3 },
|
},
|
||||||
quadriennial: { years: 4 },
|
semiannual: {
|
||||||
quinquennial: { years: 5 },
|
months: 6
|
||||||
decennial: { years: 10 },
|
},
|
||||||
quindecennial: { years: 15 }
|
annual: {
|
||||||
|
years: 1
|
||||||
|
},
|
||||||
|
biennial: {
|
||||||
|
years: 2
|
||||||
|
},
|
||||||
|
triennial: {
|
||||||
|
years: 3
|
||||||
|
},
|
||||||
|
quadriennial: {
|
||||||
|
years: 4
|
||||||
|
},
|
||||||
|
quinquennial: {
|
||||||
|
years: 5
|
||||||
|
},
|
||||||
|
decennial: {
|
||||||
|
years: 10
|
||||||
|
},
|
||||||
|
quindecennial: {
|
||||||
|
years: 15
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function computeDueDate() {
|
function computeDueDate() {
|
||||||
@@ -106,6 +134,7 @@
|
|||||||
$('#dlDepartments').val(null).trigger('change');
|
$('#dlDepartments').val(null).trigger('change');
|
||||||
$('#dlEmployees').val(null).trigger('change');
|
$('#dlEmployees').val(null).trigger('change');
|
||||||
$('#dlFunction').val('').trigger('change');
|
$('#dlFunction').val('').trigger('change');
|
||||||
|
$('#notify_function').prop('checked', false);
|
||||||
renderAttachments([]);
|
renderAttachments([]);
|
||||||
modal.show();
|
modal.show();
|
||||||
};
|
};
|
||||||
@@ -126,6 +155,7 @@
|
|||||||
$('#dlSubject').val(d.subject_id || '').trigger('change');
|
$('#dlSubject').val(d.subject_id || '').trigger('change');
|
||||||
document.getElementById('dlTopic').value = d.topic || '';
|
document.getElementById('dlTopic').value = d.topic || '';
|
||||||
$('#dlFunction').val(d.function_id || '').trigger('change');
|
$('#dlFunction').val(d.function_id || '').trigger('change');
|
||||||
|
$('#notify_function').prop('checked', Number(d.notify_function) === 1);
|
||||||
document.getElementById('dlLaw').value = d.law_regulation || '';
|
document.getElementById('dlLaw').value = d.law_regulation || '';
|
||||||
document.getElementById('dlRecurrence').value = d.recurrence_type || 'once';
|
document.getElementById('dlRecurrence').value = d.recurrence_type || 'once';
|
||||||
fpDocDate.setDate(d.document_date || null, false, 'Y-m-d');
|
fpDocDate.setDate(d.document_date || null, false, 'Y-m-d');
|
||||||
|
|||||||
@@ -967,7 +967,7 @@ function getContrastTextColor($hexColor)
|
|||||||
<th>Scadenza</th>
|
<th>Scadenza</th>
|
||||||
<th class="d-none d-lg-table-cell">Verifica</th>
|
<th class="d-none d-lg-table-cell">Verifica</th>
|
||||||
<th>Funzione</th>
|
<th>Funzione</th>
|
||||||
<th>Responsabili</th>
|
<th>Esecutore</th>
|
||||||
<th>Stato</th>
|
<th>Stato</th>
|
||||||
<th class="text-center" style="width:120px">Azioni</th>
|
<th class="text-center" style="width:120px">Azioni</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ while ($r = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,6 @@ $tools = $pdo->query("
|
|||||||
<title>Gestione Skills</title>
|
<title>Gestione Skills</title>
|
||||||
|
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
||||||
@@ -177,7 +176,7 @@ $tools = $pdo->query("
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
|
|
||||||
<!-- jQuery e Bootstrap -->
|
<!-- jQuery e Bootstrap -->
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
<!-- DataTables -->
|
<!-- DataTables -->
|
||||||
@@ -119,7 +118,7 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
|
||||||
<!-- Bootstrap (se già incluso puoi rimuoverlo) -->
|
<!-- Bootstrap (se già incluso puoi rimuoverlo) -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
|
|
||||||
<!-- SweetAlert2 -->
|
<!-- SweetAlert2 -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|||||||
@@ -0,0 +1,364 @@
|
|||||||
|
<?php
|
||||||
|
include('include/headscript.php');
|
||||||
|
|
||||||
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
|
/* ==========================================
|
||||||
|
PERMISSIONS (mirror trainings.php)
|
||||||
|
========================================== */
|
||||||
|
$isHrManager = Auth::user()->hasRole('Admin')
|
||||||
|
|| Auth::user()->hasRole('Superuser')
|
||||||
|
|| Auth::user()->hasRole('employee-hr')
|
||||||
|
|| Auth::user()->hasRole('manager');
|
||||||
|
|
||||||
|
if (!$isHrManager) {
|
||||||
|
header('Location: employee-profile.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown data */
|
||||||
|
$employees = $pdo->query("
|
||||||
|
SELECT id, first_name, last_name, employee_code
|
||||||
|
FROM employees
|
||||||
|
ORDER BY last_name, first_name
|
||||||
|
")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
$topics = $pdo->query("
|
||||||
|
SELECT id, name FROM training_topics WHERE is_active = 1 ORDER BY sort_order, name
|
||||||
|
")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
$departments = $pdo->query("
|
||||||
|
SELECT id, name FROM departments WHERE is_active = 1 ORDER BY sort_order, name
|
||||||
|
")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="it">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||||
|
<?php include('cssinclude.php'); ?>
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.9/index.global.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@fullcalendar/core@6.1.9/locales/it.global.min.js"></script>
|
||||||
|
<title>Calendario Formazione - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-dashboard {
|
||||||
|
background-color: #cfe3ff !important;
|
||||||
|
color: #1f2d3d !important;
|
||||||
|
border: 1px solid #bcd4f4 !important;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 10px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.training-header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-training-action {
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 10px 16px;
|
||||||
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
color: #fff !important;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-training-action:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-training-topics {
|
||||||
|
background-color: #0d6efd !important;
|
||||||
|
border: 1px solid #0b5ed7 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-training-topics:hover {
|
||||||
|
background-color: #0b5ed7 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-training-history {
|
||||||
|
background-color: #2563eb !important;
|
||||||
|
border: 1px solid #1d4ed8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-training-history:hover {
|
||||||
|
background-color: #1d4ed8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.training-header-actions {
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.training-header-actions .btn,
|
||||||
|
.training-header-actions a {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-dot {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FullCalendar overrides */
|
||||||
|
.fc {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-toolbar-title {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c3e6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-button-primary {
|
||||||
|
background: #5a8fd8;
|
||||||
|
border-color: #5a8fd8;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-button-primary:hover {
|
||||||
|
background: #4578c0;
|
||||||
|
border-color: #4578c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-button-primary:disabled {
|
||||||
|
background: #9bbce6;
|
||||||
|
border-color: #9bbce6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-button-primary:not(:disabled).fc-button-active {
|
||||||
|
background: #2c3e6b;
|
||||||
|
border-color: #2c3e6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-daygrid-day-number {
|
||||||
|
color: #2c3e6b;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-daygrid-day.fc-day-today {
|
||||||
|
background: #f0f4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-event {
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
padding: 2px 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-event:hover {
|
||||||
|
filter: brightness(0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-list-event:hover td {
|
||||||
|
background: #f0f4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.card-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start !important;
|
||||||
|
gap: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-toolbar {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-toolbar-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="wrapper" id="appWrapper">
|
||||||
|
<?php include('include/navbar.php'); ?>
|
||||||
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
<div class="page-wrapper">
|
||||||
|
<div class="page-content">
|
||||||
|
<div class="card p-3">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||||
|
<h5 class="mb-0">📅 Calendario Formazione</h5>
|
||||||
|
|
||||||
|
<div class="training-header-actions">
|
||||||
|
<?php if (userCan('hr.training_topics.view')): ?>
|
||||||
|
<a href="training_topics.php" class="btn btn-training-action btn-training-topics">
|
||||||
|
📘 Corsi Formazione
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (userCan('hr.trainings.view')): ?>
|
||||||
|
<a href="trainings.php" class="btn btn-training-action btn-training-history">
|
||||||
|
📚 Gestione Formazione
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
|
||||||
|
↩️ Torna alla Dashboard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- FILTERS -->
|
||||||
|
<div class="row g-2 mb-3">
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<label class="form-label small text-muted mb-1">Stato</label>
|
||||||
|
<select id="filterStatus" class="form-select">
|
||||||
|
<option value="">Tutti</option>
|
||||||
|
<option value="expired">Scaduti</option>
|
||||||
|
<option value="due_soon">Da aggiornare</option>
|
||||||
|
<option value="compliant">Conformi</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<label class="form-label small text-muted mb-1">Corso</label>
|
||||||
|
<select id="filterTopic" class="form-select">
|
||||||
|
<option value="">Tutti</option>
|
||||||
|
<?php foreach ($topics as $t): ?>
|
||||||
|
<option value="<?= (int)$t['id'] ?>"><?= htmlspecialchars($t['name'], ENT_QUOTES, 'UTF-8') ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<label class="form-label small text-muted mb-1">Reparto</label>
|
||||||
|
<select id="filterDepartment" class="form-select">
|
||||||
|
<option value="">Tutti</option>
|
||||||
|
<?php foreach ($departments as $d): ?>
|
||||||
|
<option value="<?= (int)$d['id'] ?>"><?= htmlspecialchars($d['name'], ENT_QUOTES, 'UTF-8') ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<label class="form-label small text-muted mb-1">Dipendente</label>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<select id="filterEmployee" class="form-select">
|
||||||
|
<option value="">Tutti</option>
|
||||||
|
<?php foreach ($employees as $e): ?>
|
||||||
|
<option value="<?= (int)$e['id'] ?>"><?= htmlspecialchars(trim($e['last_name'] . ' ' . $e['first_name']), ENT_QUOTES, 'UTF-8') ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<button id="btnResetFilters" type="button" class="btn btn-light border flex-shrink-0" title="Reset filtri">
|
||||||
|
<i class="fa-solid fa-rotate-left"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- LEGEND -->
|
||||||
|
<div class="legend">
|
||||||
|
<div class="legend-item"><span class="legend-dot" style="background:#dc3545"></span> Scaduto</div>
|
||||||
|
<div class="legend-item"><span class="legend-dot" style="background:#e8930c"></span> Da aggiornare</div>
|
||||||
|
<div class="legend-item"><span class="legend-dot" style="background:#198754"></span> Conforme</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="calendar"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('include/footer.php'); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('jsinclude.php'); ?>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
var isMobile = window.innerWidth < 768;
|
||||||
|
var calendarEl = document.getElementById('calendar');
|
||||||
|
|
||||||
|
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||||
|
locale: 'it',
|
||||||
|
initialView: isMobile ? 'listMonth' : 'dayGridMonth',
|
||||||
|
headerToolbar: {
|
||||||
|
left: 'prev,next today',
|
||||||
|
center: 'title',
|
||||||
|
right: isMobile ? 'listMonth,dayGridMonth' : 'dayGridMonth,listMonth'
|
||||||
|
},
|
||||||
|
height: 'auto',
|
||||||
|
navLinks: true,
|
||||||
|
eventSources: [{
|
||||||
|
url: 'ajax/trainings/calendar_events.php',
|
||||||
|
extraParams: function() {
|
||||||
|
return {
|
||||||
|
status: document.getElementById('filterStatus').value,
|
||||||
|
topic_id: document.getElementById('filterTopic').value,
|
||||||
|
department_id: document.getElementById('filterDepartment').value,
|
||||||
|
employee_id: document.getElementById('filterEmployee').value
|
||||||
|
};
|
||||||
|
},
|
||||||
|
failure: function() {
|
||||||
|
if (window.Swal) Swal.fire('Errore', 'Impossibile caricare gli eventi.', 'error');
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
eventClick: function(info) {
|
||||||
|
info.jsEvent.preventDefault();
|
||||||
|
if (info.event.url) window.location.href = info.event.url;
|
||||||
|
},
|
||||||
|
windowResize: function() {
|
||||||
|
calendar.changeView(window.innerWidth < 768 ? 'listMonth' : 'dayGridMonth');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
calendar.render();
|
||||||
|
|
||||||
|
document.querySelectorAll('#filterStatus, #filterTopic, #filterDepartment, #filterEmployee').forEach(function(el) {
|
||||||
|
el.addEventListener('change', function() {
|
||||||
|
calendar.refetchEvents();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btnResetFilters').addEventListener('click', function() {
|
||||||
|
['filterStatus', 'filterTopic', 'filterDepartment', 'filterEmployee'].forEach(function(id) {
|
||||||
|
document.getElementById(id).value = '';
|
||||||
|
});
|
||||||
|
calendar.refetchEvents();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,732 @@
|
|||||||
|
<?php
|
||||||
|
include('include/headscript.php');
|
||||||
|
|
||||||
|
$db = DBHandlerSelect::getInstance();
|
||||||
|
$pdo = $db->getConnection();
|
||||||
|
|
||||||
|
/* ==========================================
|
||||||
|
PAGE DATA
|
||||||
|
========================================== */
|
||||||
|
$sql = "
|
||||||
|
SELECT tt.*,
|
||||||
|
(SELECT COUNT(*) FROM employee_trainings et WHERE et.training_topic_id = tt.id) AS trainings_count
|
||||||
|
FROM training_topics tt
|
||||||
|
ORDER BY tt.sort_order ASC, tt.name ASC
|
||||||
|
";
|
||||||
|
$topics = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="it">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||||
|
<?php include('cssinclude.php'); ?>
|
||||||
|
<title>Gestione Corsi di Formazione - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/dataTables.bootstrap5.min.css">
|
||||||
|
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
||||||
|
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-dashboard {
|
||||||
|
background-color: #cfe3ff !important;
|
||||||
|
color: #1f2d3d !important;
|
||||||
|
border: 1px solid #bcd4f4 !important;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 10px 18px;
|
||||||
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-dashboard:hover {
|
||||||
|
background-color: #b9d3ff !important;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.training-header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-training-action {
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 10px 16px;
|
||||||
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-training-action:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-training-history {
|
||||||
|
background-color: #0d6efd !important;
|
||||||
|
border: 1px solid #0b5ed7 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-training-history:hover {
|
||||||
|
background-color: #0b5ed7 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-training-calendar {
|
||||||
|
background-color: #2563eb !important;
|
||||||
|
border: 1px solid #1d4ed8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-training-calendar:hover {
|
||||||
|
background-color: #1d4ed8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.training-header-actions {
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.training-header-actions .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add {
|
||||||
|
background-color: #0d6efd;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add:hover {
|
||||||
|
background-color: #0b5ed7;
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.training-shortcuts {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.training-shortcut-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid #bcd4f4;
|
||||||
|
background: #cfe3ff;
|
||||||
|
color: #1f2d3d;
|
||||||
|
box-shadow: 0 3px 8px rgba(0, 0, 0, .08);
|
||||||
|
transition: all .2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.training-shortcut-btn:hover {
|
||||||
|
background: #b9d3ff;
|
||||||
|
color: #1f2d3d;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.training-shortcut-btn {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead {
|
||||||
|
background-color: #cfe3ff;
|
||||||
|
color: #1f2d3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tabellaTopics thead th {
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-status {
|
||||||
|
padding: 0.25rem 0.6rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-status.active {
|
||||||
|
background-color: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-status.inactive {
|
||||||
|
background-color: #e5e7eb;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-cell {
|
||||||
|
max-width: 280px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.num-pill {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #eef2ff;
|
||||||
|
color: #3730a3;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.card-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start !important;
|
||||||
|
gap: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-dashboard {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-card {
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-card-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-card-desc {
|
||||||
|
color: #475569;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-card-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 14px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #64748b;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-card-meta b {
|
||||||
|
color: #1f2937;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-card-actions .btn {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-empty {
|
||||||
|
text-align: center;
|
||||||
|
color: #94a3b8;
|
||||||
|
padding: 24px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="wrapper" id="appWrapper">
|
||||||
|
<?php include('include/navbar.php'); ?>
|
||||||
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
<div class="page-wrapper">
|
||||||
|
<div class="page-content">
|
||||||
|
<div class="card p-3">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||||
|
<h5 class="mb-0">Corsi Formazione</h5>
|
||||||
|
|
||||||
|
<div class="training-header-actions">
|
||||||
|
<?php if (userCan('hr.trainings.view')): ?>
|
||||||
|
<a href="trainings.php" class="btn btn-training-action btn-training-history">
|
||||||
|
🎓 Gestione Formazione
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="training_calendar.php" class="btn btn-training-action btn-training-calendar">
|
||||||
|
📅 Calendario Formazione
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
|
||||||
|
↩️ Torna alla Dashboard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
||||||
|
<h6 class="fw-semibold mb-0">Elenco Corsi / Training Topics</h6>
|
||||||
|
<button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#addTopicModal">
|
||||||
|
➕ Aggiungi Corso
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DESKTOP / TABLET ≥768px: TABLE -->
|
||||||
|
<div class="table-responsive d-none d-md-block">
|
||||||
|
<table id="tabellaTopics" class="table table-striped align-middle text-center" style="width:100%;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Nome</th>
|
||||||
|
<th>Descrizione</th>
|
||||||
|
<th>Frequenza<br>(mesi)</th>
|
||||||
|
<th>Promemoria<br>(giorni)</th>
|
||||||
|
<th>Ordine</th>
|
||||||
|
<th>Stato</th>
|
||||||
|
<th>Formazioni</th>
|
||||||
|
<th>Azioni</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($topics as $row): ?>
|
||||||
|
<?php
|
||||||
|
$id = (int)$row['id'];
|
||||||
|
$name = $row['name'] ?? '';
|
||||||
|
$description = $row['description'] ?? '';
|
||||||
|
$freq = $row['default_frequency_months'];
|
||||||
|
$rem = (int)($row['default_reminder_days'] ?? 30);
|
||||||
|
$sortOrder = (int)($row['sort_order'] ?? 999);
|
||||||
|
$isActive = (int)($row['is_active'] ?? 1);
|
||||||
|
$isMandatory = (int)($row['is_mandatory'] ?? 0);
|
||||||
|
$cnt = (int)($row['trainings_count'] ?? 0);
|
||||||
|
$statusClass = $isActive === 1 ? 'active' : 'inactive';
|
||||||
|
$statusLabel = $isActive === 1 ? 'Attivo' : 'Inattivo';
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td><?= $id ?></td>
|
||||||
|
<td class="fw-semibold text-start">
|
||||||
|
<?= htmlspecialchars($name) ?>
|
||||||
|
<?php if ($isMandatory === 1): ?>
|
||||||
|
<span class="badge bg-warning text-dark ms-1" title="Obbligatorio per tutti">★ Obbl.</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td class="description-cell" title="<?= htmlspecialchars($description, ENT_QUOTES) ?>">
|
||||||
|
<?= $description !== '' ? htmlspecialchars($description) : '-' ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php if ($freq === null || $freq === ''): ?>
|
||||||
|
<span class="text-muted">una tantum</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="num-pill"><?= (int)$freq ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td><span class="num-pill"><?= $rem ?></span></td>
|
||||||
|
<td><?= $sortOrder ?></td>
|
||||||
|
<td><span class="badge-status <?= $statusClass ?>"><?= $statusLabel ?></span></td>
|
||||||
|
<td><?= $cnt ?></td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary edit-topic"
|
||||||
|
data-id="<?= $id ?>"
|
||||||
|
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
|
||||||
|
data-description="<?= htmlspecialchars($description, ENT_QUOTES) ?>"
|
||||||
|
data-freq="<?= $freq === null ? '' : (int)$freq ?>"
|
||||||
|
data-rem="<?= $rem ?>"
|
||||||
|
data-sort_order="<?= $sortOrder ?>"
|
||||||
|
data-is_active="<?= $isActive ?>"
|
||||||
|
data-is_mandatory="<?= $isMandatory ?>">
|
||||||
|
✏️ Modifica
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger delete-topic"
|
||||||
|
data-id="<?= $id ?>"
|
||||||
|
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
|
||||||
|
data-count="<?= $cnt ?>">
|
||||||
|
🗑️ Cancella
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MOBILE <768px: CARDS -->
|
||||||
|
<div class="d-block d-md-none">
|
||||||
|
<?php if (empty($topics)): ?>
|
||||||
|
<div class="tt-empty">Nessun corso presente</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php foreach ($topics as $row): ?>
|
||||||
|
<?php
|
||||||
|
$id = (int)$row['id'];
|
||||||
|
$name = $row['name'] ?? '';
|
||||||
|
$description = $row['description'] ?? '';
|
||||||
|
$freq = $row['default_frequency_months'];
|
||||||
|
$rem = (int)($row['default_reminder_days'] ?? 30);
|
||||||
|
$sortOrder = (int)($row['sort_order'] ?? 999);
|
||||||
|
$isActive = (int)($row['is_active'] ?? 1);
|
||||||
|
$isMandatory = (int)($row['is_mandatory'] ?? 0);
|
||||||
|
$cnt = (int)($row['trainings_count'] ?? 0);
|
||||||
|
$statusClass = $isActive === 1 ? 'active' : 'inactive';
|
||||||
|
$statusLabel = $isActive === 1 ? 'Attivo' : 'Inattivo';
|
||||||
|
$freqLabel = ($freq === null || $freq === '') ? 'una tantum' : ((int)$freq . ' mesi');
|
||||||
|
?>
|
||||||
|
<div class="tt-card">
|
||||||
|
<h6 class="tt-card-title">
|
||||||
|
<?= htmlspecialchars($name) ?>
|
||||||
|
<?php if ($isMandatory === 1): ?>
|
||||||
|
<span class="badge bg-warning text-dark ms-1" title="Obbligatorio per tutti">★ Obbl.</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</h6>
|
||||||
|
<?php if ($description !== ''): ?>
|
||||||
|
<p class="tt-card-desc"><?= htmlspecialchars($description) ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="tt-card-meta">
|
||||||
|
<span><span class="badge-status <?= $statusClass ?>"><?= $statusLabel ?></span></span>
|
||||||
|
<span><b>Frequenza:</b> <?= htmlspecialchars($freqLabel) ?></span>
|
||||||
|
<span><b>Promemoria:</b> <?= $rem ?> gg</span>
|
||||||
|
<span><b>Formazioni:</b> <?= $cnt ?></span>
|
||||||
|
<span><b>Ordine:</b> <?= $sortOrder ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="tt-card-actions">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary edit-topic"
|
||||||
|
data-id="<?= $id ?>"
|
||||||
|
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
|
||||||
|
data-description="<?= htmlspecialchars($description, ENT_QUOTES) ?>"
|
||||||
|
data-freq="<?= $freq === null ? '' : (int)$freq ?>"
|
||||||
|
data-rem="<?= $rem ?>"
|
||||||
|
data-sort_order="<?= $sortOrder ?>"
|
||||||
|
data-is_active="<?= $isActive ?>"
|
||||||
|
data-is_mandatory="<?= $isMandatory ?>">
|
||||||
|
✏️ Modifica
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger delete-topic"
|
||||||
|
data-id="<?= $id ?>"
|
||||||
|
data-name="<?= htmlspecialchars($name, ENT_QUOTES) ?>"
|
||||||
|
data-count="<?= $cnt ?>">
|
||||||
|
🗑️ Cancella
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('include/footer.php'); ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ADD -->
|
||||||
|
<div class="modal fade" id="addTopicModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered modal-lg modal-fullscreen-sm-down">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header" style="background-color:#cfe3ff;">
|
||||||
|
<h5 class="modal-title">Aggiungi Corso</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="addTopicForm">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Nome</label>
|
||||||
|
<input type="text" class="form-control" id="addName" name="name" placeholder="es. Sicurezza antincendio" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Descrizione</label>
|
||||||
|
<textarea class="form-control" id="addDescription" name="description" rows="3" placeholder="Opzionale"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Frequenza aggiornamento</label>
|
||||||
|
<select class="form-select" id="addFreq" name="default_frequency_months">
|
||||||
|
<option value="" selected>Una tantum (nessun aggiornamento)</option>
|
||||||
|
<option value="3">3 mesi</option>
|
||||||
|
<option value="6">6 mesi</option>
|
||||||
|
<option value="12">12 mesi (1 anno)</option>
|
||||||
|
<option value="18">18 mesi</option>
|
||||||
|
<option value="24">24 mesi (2 anni)</option>
|
||||||
|
<option value="36">36 mesi (3 anni)</option>
|
||||||
|
<option value="48">48 mesi (4 anni)</option>
|
||||||
|
<option value="60">60 mesi (5 anni)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Promemoria (giorni prima della scadenza)</label>
|
||||||
|
<input type="number" class="form-control" id="addRem" name="default_reminder_days" value="30" min="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Ordine</label>
|
||||||
|
<input type="number" class="form-control" id="addSortOrder" name="sort_order" value="999" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Stato</label>
|
||||||
|
<select class="form-select" id="addIsActive" name="is_active">
|
||||||
|
<option value="1" selected>Attivo</option>
|
||||||
|
<option value="0">Inattivo</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input class="form-check-input" type="checkbox" id="addIsMandatory" value="1">
|
||||||
|
<label class="form-check-label fw-semibold" for="addIsMandatory">
|
||||||
|
Obbligatorio per tutti i dipendenti
|
||||||
|
</label>
|
||||||
|
<div class="small text-muted">
|
||||||
|
Se attivo, i dipendenti senza registrazione di questo corso compaiono come "Non presente" nello storico.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<button type="submit" class="btn btn-add">💾 Salva</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- EDIT -->
|
||||||
|
<div class="modal fade" id="editTopicModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered modal-lg modal-fullscreen-sm-down">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header" style="background-color:#cfe3ff;">
|
||||||
|
<h5 class="modal-title">Modifica Corso</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="editTopicForm">
|
||||||
|
<input type="hidden" id="editTopicId">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Nome</label>
|
||||||
|
<input type="text" class="form-control" id="editName" name="name" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Descrizione</label>
|
||||||
|
<textarea class="form-control" id="editDescription" name="description" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Frequenza aggiornamento</label>
|
||||||
|
<select class="form-select" id="editFreq" name="default_frequency_months">
|
||||||
|
<option value="">Una tantum (nessun aggiornamento)</option>
|
||||||
|
<option value="3">3 mesi</option>
|
||||||
|
<option value="6">6 mesi</option>
|
||||||
|
<option value="12">12 mesi (1 anno)</option>
|
||||||
|
<option value="18">18 mesi</option>
|
||||||
|
<option value="24">24 mesi (2 anni)</option>
|
||||||
|
<option value="36">36 mesi (3 anni)</option>
|
||||||
|
<option value="48">48 mesi (4 anni)</option>
|
||||||
|
<option value="60">60 mesi (5 anni)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Promemoria (giorni prima della scadenza)</label>
|
||||||
|
<input type="number" class="form-control" id="editRem" name="default_reminder_days" min="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Ordine</label>
|
||||||
|
<input type="number" class="form-control" id="editSortOrder" name="sort_order" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Stato</label>
|
||||||
|
<select class="form-select" id="editIsActive" name="is_active">
|
||||||
|
<option value="1">Attivo</option>
|
||||||
|
<option value="0">Inattivo</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input class="form-check-input" type="checkbox" id="editIsMandatory" value="1">
|
||||||
|
<label class="form-check-label fw-semibold" for="editIsMandatory">
|
||||||
|
Obbligatorio per tutti i dipendenti
|
||||||
|
</label>
|
||||||
|
<div class="small text-muted">
|
||||||
|
Se attivo, i dipendenti senza registrazione di questo corso compaiono come "Non presente" nello storico.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<button type="submit" class="btn btn-add">💾 Salva Modifiche</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include('jsinclude.php'); ?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#tabellaTopics').DataTable({
|
||||||
|
order: [
|
||||||
|
[5, 'asc'],
|
||||||
|
[1, 'asc']
|
||||||
|
],
|
||||||
|
pageLength: 25,
|
||||||
|
language: {
|
||||||
|
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json',
|
||||||
|
emptyTable: 'Nessun corso presente'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function ajaxPost(url, payload, successTitle, errorFallback) {
|
||||||
|
return fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
},
|
||||||
|
body: payload.toString()
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: "success",
|
||||||
|
title: successTitle,
|
||||||
|
confirmButtonColor: "#3085d6"
|
||||||
|
})
|
||||||
|
.then(() => location.reload());
|
||||||
|
} else {
|
||||||
|
Swal.fire({
|
||||||
|
icon: "error",
|
||||||
|
title: "Errore",
|
||||||
|
text: data.message || errorFallback
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
Swal.fire({
|
||||||
|
icon: "error",
|
||||||
|
title: "Errore",
|
||||||
|
text: "Errore di comunicazione."
|
||||||
|
});
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#addTopicForm").on("submit", function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const p = new URLSearchParams();
|
||||||
|
p.append('name', $("#addName").val().trim());
|
||||||
|
p.append('description', $("#addDescription").val().trim());
|
||||||
|
p.append('default_frequency_months', $("#addFreq").val());
|
||||||
|
p.append('default_reminder_days', $("#addRem").val());
|
||||||
|
p.append('sort_order', $("#addSortOrder").val());
|
||||||
|
p.append('is_active', $("#addIsActive").val());
|
||||||
|
p.append('is_mandatory', $("#addIsMandatory").is(':checked') ? '1' : '0');
|
||||||
|
ajaxPost("ajax/training_topics/save.php", p, "Salvato!", "Impossibile salvare il corso.");
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on("click", ".edit-topic", function() {
|
||||||
|
const b = $(this);
|
||||||
|
const rawFreq = b.data("freq");
|
||||||
|
const freqStr = (rawFreq === '' || rawFreq === null || rawFreq === undefined) ? '' : String(rawFreq);
|
||||||
|
if (freqStr !== '' && $("#editFreq option[value='" + freqStr + "']").length === 0) {
|
||||||
|
$("#editFreq").append('<option value="' + freqStr + '">' + freqStr + ' mesi</option>');
|
||||||
|
}
|
||||||
|
$("#editTopicId").val(b.data("id"));
|
||||||
|
$("#editName").val(b.data("name"));
|
||||||
|
$("#editDescription").val(b.data("description"));
|
||||||
|
$("#editFreq").val(freqStr);
|
||||||
|
$("#editRem").val(b.data("rem"));
|
||||||
|
$("#editSortOrder").val(b.data("sort_order"));
|
||||||
|
$("#editIsActive").val(String(b.data("is_active")));
|
||||||
|
$("#editIsMandatory").prop('checked', String(b.data("is_mandatory")) === '1');
|
||||||
|
$("#editTopicModal").modal("show");
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#editTopicForm").on("submit", function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const p = new URLSearchParams();
|
||||||
|
p.append('id', $("#editTopicId").val());
|
||||||
|
p.append('name', $("#editName").val().trim());
|
||||||
|
p.append('description', $("#editDescription").val().trim());
|
||||||
|
p.append('default_frequency_months', $("#editFreq").val());
|
||||||
|
p.append('default_reminder_days', $("#editRem").val());
|
||||||
|
p.append('sort_order', $("#editSortOrder").val());
|
||||||
|
p.append('is_active', $("#editIsActive").val());
|
||||||
|
p.append('is_mandatory', $("#editIsMandatory").is(':checked') ? '1' : '0');
|
||||||
|
ajaxPost("ajax/training_topics/save.php", p, "Aggiornato!", "Impossibile aggiornare il corso.");
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on("click", ".delete-topic", function() {
|
||||||
|
const id = $(this).data("id");
|
||||||
|
const name = $(this).data("name");
|
||||||
|
const cnt = parseInt($(this).data("count")) || 0;
|
||||||
|
|
||||||
|
if (cnt > 0) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: "warning",
|
||||||
|
title: "Impossibile cancellare",
|
||||||
|
text: "Il corso \"" + name + "\" ha " + cnt + " registrazione/i di formazione. Cancella prima le registrazioni."
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: "Confermi la cancellazione?",
|
||||||
|
text: name ? ("Corso: " + name) : "Il corso verrà cancellato.",
|
||||||
|
icon: "warning",
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonColor: "#d33",
|
||||||
|
cancelButtonColor: "#6c757d",
|
||||||
|
confirmButtonText: "Sì, cancella",
|
||||||
|
cancelButtonText: "Annulla"
|
||||||
|
}).then((result) => {
|
||||||
|
if (!result.isConfirmed) return;
|
||||||
|
const p = new URLSearchParams();
|
||||||
|
p.append('id', id);
|
||||||
|
ajaxPost("ajax/training_topics/delete.php", p, "Cancellato!", "Impossibile cancellare il corso.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
@@ -139,7 +139,7 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
|
|
||||||
<!-- jQuery e Bootstrap -->
|
<!-- jQuery e Bootstrap -->
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
<!-- DataTables -->
|
<!-- DataTables -->
|
||||||
@@ -117,7 +116,7 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
|
|
||||||
<!-- jQuery e Bootstrap -->
|
<!-- jQuery e Bootstrap -->
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
|
||||||
<!-- DataTables -->
|
<!-- DataTables -->
|
||||||
@@ -111,7 +110,7 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="wrapper toggled">
|
<div class="wrapper" id="appWrapper">
|
||||||
<?php include('include/navbar.php'); ?>
|
<?php include('include/navbar.php'); ?>
|
||||||
<?php include('include/topbar.php'); ?>
|
<?php include('include/topbar.php'); ?>
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user