Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27cbc9f449 | |||
| 4c09a0dcb4 | |||
| 8bb23ee563 | |||
| 20571c9e4b | |||
| fdde16b113 | |||
| 33b627f328 | |||
| d96b4be9e0 | |||
| 088e518db1 | |||
| 789c547bc7 | |||
| e5bf546ae7 | |||
| 6dd13e5d7d | |||
| b1f2bb60e3 | |||
| f7e97f55e9 | |||
| 70b712ff3b |
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +1,38 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once(__DIR__ . '/../hr_auth_check.php');
|
include('../../include/headscript.php');
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
||||||
http_response_code(405);
|
|
||||||
echo json_encode(['success' => false, 'message' => 'Metodo non consentito.']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
try {
|
||||||
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
$pdo = DBHandlerSelect::getInstance()->getConnection();
|
||||||
|
|
||||||
$id = (int)($_POST['id'] ?? 0);
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
|
||||||
if ($id <= 0) {
|
if ($id <= 0) {
|
||||||
echo json_encode(['success' => false, 'message' => 'ID DPI non valido.']);
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'ID DPI non valido.'
|
||||||
|
]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
$stmt = $pdo->prepare("
|
||||||
$stmt = $pdo->prepare("DELETE FROM employee_ppe WHERE id = :id");
|
UPDATE employee_ppe_items
|
||||||
$stmt->execute(['id' => $id]);
|
SET status = 'returned',
|
||||||
echo json_encode(['success' => true]);
|
updated_at = NOW()
|
||||||
} catch (Exception $e) {
|
WHERE id = ?
|
||||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
");
|
||||||
|
$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,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()]);
|
||||||
|
}
|
||||||
@@ -1,82 +1,153 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once(__DIR__ . '/../hr_auth_check.php');
|
include('../../include/headscript.php');
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
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);
|
|
||||||
$itemName = trim($_POST['item_name'] ?? '');
|
|
||||||
$deliveryDate = trim($_POST['delivery_date'] ?? '');
|
|
||||||
$deliveredBy = trim($_POST['delivered_by'] ?? '');
|
|
||||||
$notes = trim($_POST['notes'] ?? '');
|
|
||||||
|
|
||||||
if ($employeeId <= 0) {
|
|
||||||
echo json_encode(['success' => false, 'message' => 'ID dipendente non valido.']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
if ($itemName === '') {
|
|
||||||
echo json_encode(['success' => false, 'message' => 'Il nome del DPI è obbligatorio.']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$deliveryDate = $deliveryDate === '' ? null : $deliveryDate;
|
|
||||||
$deliveredBy = $deliveredBy !== '' ? $deliveredBy : null;
|
|
||||||
$notes = $notes !== '' ? $notes : null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($id > 0) {
|
$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("
|
$stmt = $pdo->prepare("
|
||||||
UPDATE employee_ppe
|
UPDATE employee_ppe_items
|
||||||
SET item_name = :item_name,
|
SET ppe_item_id = :ppe_item_id,
|
||||||
delivery_date = :delivery_date,
|
assigned_date = :assigned_date,
|
||||||
|
expiry_date = :expiry_date,
|
||||||
delivered_by = :delivered_by,
|
delivered_by = :delivered_by,
|
||||||
|
status = :status,
|
||||||
notes = :notes,
|
notes = :notes,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = :id AND employee_id = :eid
|
WHERE id = :id
|
||||||
|
AND employee_id = :employee_id
|
||||||
");
|
");
|
||||||
$stmt->execute([
|
|
||||||
'item_name' => $itemName,
|
|
||||||
'delivery_date' => $deliveryDate,
|
|
||||||
'delivered_by' => $deliveredBy,
|
|
||||||
'notes' => $notes,
|
|
||||||
'id' => $id,
|
|
||||||
'eid' => $employeeId,
|
|
||||||
]);
|
|
||||||
echo json_encode(['success' => true, 'id' => $id]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$check = $pdo->prepare("SELECT COUNT(*) FROM employees WHERE id = :id");
|
$stmt->execute([
|
||||||
$check->execute(['id' => $employeeId]);
|
'ppe_item_id' => $ppeItemId,
|
||||||
if ((int)$check->fetchColumn() === 0) {
|
'assigned_date' => $assignedDate !== '' ? $assignedDate : null,
|
||||||
echo json_encode(['success' => false, 'message' => 'Dipendente non trovato.']);
|
'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;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
INSERT INTO employee_ppe
|
INSERT INTO employee_ppe_items
|
||||||
(employee_id, item_name, delivery_date, delivered_by, notes, created_by, created_at, updated_at)
|
(
|
||||||
|
employee_id,
|
||||||
|
ppe_item_id,
|
||||||
|
assigned_date,
|
||||||
|
expiry_date,
|
||||||
|
delivered_by,
|
||||||
|
quantity,
|
||||||
|
status,
|
||||||
|
notes,
|
||||||
|
created_by,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(:employee_id, :item_name, :delivery_date, :delivered_by, :notes, :created_by, NOW(), NOW())
|
(
|
||||||
|
:employee_id,
|
||||||
|
:ppe_item_id,
|
||||||
|
:assigned_date,
|
||||||
|
:expiry_date,
|
||||||
|
:delivered_by,
|
||||||
|
1,
|
||||||
|
:status,
|
||||||
|
:notes,
|
||||||
|
:created_by,
|
||||||
|
NOW(),
|
||||||
|
NOW()
|
||||||
|
)
|
||||||
");
|
");
|
||||||
|
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
'employee_id' => $employeeId,
|
'employee_id' => $employeeId,
|
||||||
'item_name' => $itemName,
|
'ppe_item_id' => $ppeItemId,
|
||||||
'delivery_date' => $deliveryDate,
|
'assigned_date' => $assignedDate !== '' ? $assignedDate : null,
|
||||||
'delivered_by' => $deliveredBy,
|
'expiry_date' => $expiryDate !== '' ? $expiryDate : null,
|
||||||
'notes' => $notes,
|
'delivered_by' => $deliveredBy !== '' ? $deliveredBy : null,
|
||||||
'created_by' => $currentUserId,
|
'status' => $status,
|
||||||
|
'notes' => $notes !== '' ? $notes : null,
|
||||||
|
'created_by' => isset($iduserlogin) ? (int)$iduserlogin : null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
echo json_encode(['success' => true, 'id' => (int)$pdo->lastInsertId()]);
|
echo json_encode([
|
||||||
} catch (Exception $e) {
|
'success' => true,
|
||||||
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
|
'message' => 'DPI assegnato.'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $e->getMessage()
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
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>
|
||||||
@@ -40,15 +40,20 @@ if ($employeeId > 0) {
|
|||||||
d.name AS department_name,
|
d.name AS department_name,
|
||||||
d.color AS department_color,
|
d.color AS department_color,
|
||||||
jr.name AS job_role_name,
|
jr.name AS job_role_name,
|
||||||
|
jsr.name AS job_sub_role_name,
|
||||||
au.first_name AS auth_first_name,
|
au.first_name AS auth_first_name,
|
||||||
au.last_name AS auth_last_name,
|
au.last_name AS auth_last_name,
|
||||||
au.email AS auth_email,
|
au.email AS auth_email,
|
||||||
au.username AS auth_username,
|
au.username AS auth_username,
|
||||||
au.avatar AS auth_avatar
|
au.avatar AS auth_avatar,
|
||||||
|
ar.name AS auth_role_name,
|
||||||
|
ar.display_name AS auth_role_display_name
|
||||||
FROM employees e
|
FROM employees e
|
||||||
LEFT JOIN departments d ON d.id = e.department_id
|
LEFT JOIN departments d ON d.id = e.department_id
|
||||||
LEFT JOIN job_roles jr ON jr.id = e.job_role_id
|
LEFT JOIN job_roles jr ON jr.id = e.job_role_id
|
||||||
|
LEFT JOIN job_sub_roles jsr ON jsr.id = e.job_sub_role_id
|
||||||
LEFT JOIN auth_users au ON au.id = e.auth_user_id
|
LEFT JOIN auth_users au ON au.id = e.auth_user_id
|
||||||
|
LEFT JOIN auth_roles ar ON ar.id = au.role_id
|
||||||
WHERE e.id = :id
|
WHERE e.id = :id
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
");
|
");
|
||||||
@@ -64,6 +69,75 @@ if (!$isHrManager && $employee && (int)$employee['auth_user_id'] !== (int)$iduse
|
|||||||
|
|
||||||
$canEdit = $isHrManager;
|
$canEdit = $isHrManager;
|
||||||
|
|
||||||
|
/* ==========================================
|
||||||
|
EMPLOYEE JOB ROLES / SUB ROLES (multi assignment)
|
||||||
|
========================================== */
|
||||||
|
$employeeSubRoles = [];
|
||||||
|
$employeeJobRoleNames = [];
|
||||||
|
$employeeSubRoleNames = [];
|
||||||
|
$employeeSubRoleIds = [];
|
||||||
|
$employeeSubRolesByRole = [];
|
||||||
|
|
||||||
|
if ($employee) {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT
|
||||||
|
ejsr.job_sub_role_id,
|
||||||
|
ejsr.is_primary,
|
||||||
|
jsr.name AS job_sub_role_name,
|
||||||
|
jsr.job_role_id,
|
||||||
|
jr.name AS job_role_name
|
||||||
|
FROM employee_job_sub_roles ejsr
|
||||||
|
INNER JOIN job_sub_roles jsr ON jsr.id = ejsr.job_sub_role_id
|
||||||
|
LEFT JOIN job_roles jr ON jr.id = jsr.job_role_id
|
||||||
|
WHERE ejsr.employee_id = :eid
|
||||||
|
ORDER BY ejsr.is_primary DESC, jr.sort_order ASC, jr.name ASC, jsr.sort_order ASC, jsr.name ASC
|
||||||
|
");
|
||||||
|
$stmt->execute(['eid' => $employeeId]);
|
||||||
|
$employeeSubRoles = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Fallback: if the bridge table is empty but legacy employees.job_sub_role_id is filled, show the legacy value.
|
||||||
|
if (!$employeeSubRoles && !empty($employee['job_sub_role_id'])) {
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT
|
||||||
|
jsr.id AS job_sub_role_id,
|
||||||
|
1 AS is_primary,
|
||||||
|
jsr.name AS job_sub_role_name,
|
||||||
|
jsr.job_role_id,
|
||||||
|
jr.name AS job_role_name
|
||||||
|
FROM job_sub_roles jsr
|
||||||
|
LEFT JOIN job_roles jr ON jr.id = jsr.job_role_id
|
||||||
|
WHERE jsr.id = :sid
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$stmt->execute(['sid' => (int)$employee['job_sub_role_id']]);
|
||||||
|
$legacySubRole = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if ($legacySubRole) {
|
||||||
|
$employeeSubRoles = [$legacySubRole];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($employeeSubRoles as $sr) {
|
||||||
|
$employeeSubRoleIds[] = (int)$sr['job_sub_role_id'];
|
||||||
|
|
||||||
|
if (!empty($sr['job_role_name'])) {
|
||||||
|
$employeeJobRoleNames[(int)$sr['job_role_id']] = $sr['job_role_name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($sr['job_sub_role_name'])) {
|
||||||
|
$employeeSubRoleNames[(int)$sr['job_sub_role_id']] = $sr['job_sub_role_name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$roleKey = (int)($sr['job_role_id'] ?? 0);
|
||||||
|
if (!isset($employeeSubRolesByRole[$roleKey])) {
|
||||||
|
$employeeSubRolesByRole[$roleKey] = [
|
||||||
|
'job_role_name' => $sr['job_role_name'] ?: 'Senza mansione',
|
||||||
|
'items' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$employeeSubRolesByRole[$roleKey]['items'][] = $sr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ==========================================
|
/* ==========================================
|
||||||
DOCUMENTS (File Repository)
|
DOCUMENTS (File Repository)
|
||||||
========================================== */
|
========================================== */
|
||||||
@@ -136,19 +210,83 @@ if ($employee) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================
|
/* ==========================================
|
||||||
PPE (Assigned)
|
PPE (Assigned + Required by sub role)
|
||||||
========================================== */
|
========================================== */
|
||||||
$ppeList = [];
|
$ppeList = [];
|
||||||
|
$ppeItemsAll = [];
|
||||||
|
$requiredPpeList = [];
|
||||||
|
$assignedPpeIds = [];
|
||||||
|
|
||||||
if ($employee) {
|
if ($employee) {
|
||||||
|
// Assigned PPE history from the normalized table.
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
SELECT *
|
SELECT
|
||||||
FROM employee_ppe
|
epi.*,
|
||||||
WHERE employee_id = :eid
|
pi.name AS ppe_name,
|
||||||
ORDER BY delivery_date DESC, created_at DESC
|
pi.category AS ppe_category,
|
||||||
|
pi.photo AS ppe_photo,
|
||||||
|
pi.standard_reference,
|
||||||
|
pi.validity_months
|
||||||
|
FROM employee_ppe_items epi
|
||||||
|
INNER JOIN ppe_items pi ON pi.id = epi.ppe_item_id
|
||||||
|
WHERE epi.employee_id = :eid
|
||||||
|
ORDER BY
|
||||||
|
CASE epi.status
|
||||||
|
WHEN 'assigned' THEN 1
|
||||||
|
WHEN 'expired' THEN 2
|
||||||
|
WHEN 'damaged' THEN 3
|
||||||
|
WHEN 'lost' THEN 4
|
||||||
|
WHEN 'returned' THEN 5
|
||||||
|
ELSE 9
|
||||||
|
END,
|
||||||
|
epi.assigned_date DESC,
|
||||||
|
epi.created_at DESC
|
||||||
");
|
");
|
||||||
$stmt->execute(['eid' => $employeeId]);
|
$stmt->execute(['eid' => $employeeId]);
|
||||||
$ppeList = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$ppeList = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
foreach ($ppeList as $p) {
|
||||||
|
if (($p['status'] ?? '') === 'assigned') {
|
||||||
|
$assignedPpeIds[(int)$p['ppe_item_id']] = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All active PPE for manual assignment dropdown.
|
||||||
|
if ($canEdit) {
|
||||||
|
$ppeItemsAll = $pdo->query("
|
||||||
|
SELECT id, name, category, standard_reference, validity_months
|
||||||
|
FROM ppe_items
|
||||||
|
WHERE is_active = 1
|
||||||
|
ORDER BY sort_order ASC, name ASC
|
||||||
|
")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required PPE based on all employee sub roles.
|
||||||
|
// DISTINCT avoids duplicated PPE when two sub roles require the same item.
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT
|
||||||
|
pi.id,
|
||||||
|
pi.name,
|
||||||
|
pi.category,
|
||||||
|
pi.photo,
|
||||||
|
pi.standard_reference,
|
||||||
|
pi.validity_months,
|
||||||
|
GROUP_CONCAT(DISTINCT CONCAT(COALESCE(jr.name, 'Senza mansione'), ' / ', jsr.name) ORDER BY jr.sort_order ASC, jr.name ASC, jsr.sort_order ASC, jsr.name ASC SEPARATOR ' | ') AS source_sub_roles
|
||||||
|
FROM employee_job_sub_roles ejsr
|
||||||
|
INNER JOIN job_sub_roles jsr ON jsr.id = ejsr.job_sub_role_id
|
||||||
|
LEFT JOIN job_roles jr ON jr.id = jsr.job_role_id
|
||||||
|
INNER JOIN job_sub_role_ppe_items jsp ON jsp.job_sub_role_id = ejsr.job_sub_role_id
|
||||||
|
INNER JOIN ppe_items pi ON pi.id = jsp.ppe_item_id
|
||||||
|
WHERE ejsr.employee_id = :eid
|
||||||
|
AND jsp.is_active = 1
|
||||||
|
AND pi.is_active = 1
|
||||||
|
GROUP BY pi.id, pi.name, pi.category, pi.photo, pi.standard_reference, pi.validity_months
|
||||||
|
ORDER BY pi.category ASC, pi.name ASC
|
||||||
|
");
|
||||||
|
$stmt->execute(['eid' => $employeeId]);
|
||||||
|
$requiredPpeList = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ==========================================
|
/* ==========================================
|
||||||
DROPDOWN DATA FOR EDIT MODAL
|
DROPDOWN DATA FOR EDIT MODAL
|
||||||
@@ -159,6 +297,23 @@ $departments = $isHrManager
|
|||||||
$jobRoles = $isHrManager
|
$jobRoles = $isHrManager
|
||||||
? $pdo->query("SELECT id, name FROM job_roles WHERE is_active = 1 ORDER BY sort_order, name")->fetchAll(PDO::FETCH_ASSOC)
|
? $pdo->query("SELECT id, name FROM job_roles WHERE is_active = 1 ORDER BY sort_order, name")->fetchAll(PDO::FETCH_ASSOC)
|
||||||
: [];
|
: [];
|
||||||
|
$jobSubRolesAll = $isHrManager
|
||||||
|
? $pdo->query("
|
||||||
|
SELECT
|
||||||
|
jsr.id,
|
||||||
|
jsr.job_role_id,
|
||||||
|
jsr.name,
|
||||||
|
jr.name AS job_role_name
|
||||||
|
FROM job_sub_roles jsr
|
||||||
|
LEFT JOIN job_roles jr ON jr.id = jsr.job_role_id
|
||||||
|
WHERE jsr.is_active = 1
|
||||||
|
ORDER BY jr.sort_order ASC, jr.name ASC, jsr.sort_order ASC, jsr.name ASC
|
||||||
|
")->fetchAll(PDO::FETCH_ASSOC)
|
||||||
|
: [];
|
||||||
|
$jobSubRoleToRoleMap = [];
|
||||||
|
foreach ($jobSubRolesAll as $sr) {
|
||||||
|
$jobSubRoleToRoleMap[(int)$sr['id']] = (int)$sr['job_role_id'];
|
||||||
|
}
|
||||||
$authUsers = $isHrManager
|
$authUsers = $isHrManager
|
||||||
? $pdo->query("SELECT id, username, first_name, last_name, email, role_id FROM auth_users ORDER BY first_name, last_name")->fetchAll(PDO::FETCH_ASSOC)
|
? $pdo->query("SELECT id, username, first_name, last_name, email, role_id FROM auth_users ORDER BY first_name, last_name")->fetchAll(PDO::FETCH_ASSOC)
|
||||||
: [];
|
: [];
|
||||||
@@ -240,6 +395,8 @@ function fmtFileSize(?int $bytes): string
|
|||||||
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
<link rel="icon" href="assets/images/favicon-32x32.png" type="image/png" />
|
||||||
<?php include('cssinclude.php'); ?>
|
<?php include('cssinclude.php'); ?>
|
||||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.8/css/dataTables.bootstrap5.min.css">
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.8/css/dataTables.bootstrap5.min.css">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
|
||||||
<title>Profilo Dipendente - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
<title>Profilo Dipendente - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></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>
|
||||||
@@ -335,6 +492,79 @@ function fmtFileSize(?int $bytes): string
|
|||||||
margin: 4px 0 8px 0;
|
margin: 4px 0 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-summary-card {
|
||||||
|
background: rgba(255, 255, 255, .72);
|
||||||
|
border: 1px solid rgba(148, 163, 184, .45);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
min-height: 68px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-summary-label {
|
||||||
|
font-size: .72rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .06em;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-summary-value {
|
||||||
|
color: #1f2937;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-summary-muted {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: .84rem;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-role-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-role-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-role-main {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #e0f2fe;
|
||||||
|
color: #075985;
|
||||||
|
border: 1px solid #bae6fd;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 3px 9px;
|
||||||
|
font-size: .78rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-subrole-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #334155;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 3px 9px;
|
||||||
|
font-size: .78rem;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
.profile-badges {
|
.profile-badges {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -530,6 +760,43 @@ function fmtFileSize(?int $bytes): string
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.job-role-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-role-group {
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-role-group-title {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-subrole-chip-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-subrole-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #1d4ed8;
|
||||||
|
border: 1px solid #bfdbfe;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.empty-profile {
|
.empty-profile {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 60px 20px;
|
padding: 60px 20px;
|
||||||
@@ -714,7 +981,10 @@ function fmtFileSize(?int $bytes): string
|
|||||||
$status = statusBadge((string)($employee['status'] ?? 'active'));
|
$status = statusBadge((string)($employee['status'] ?? 'active'));
|
||||||
$deptName = $employee['department_name'] ?? null;
|
$deptName = $employee['department_name'] ?? null;
|
||||||
$deptColor = $employee['department_color'] ?? null;
|
$deptColor = $employee['department_color'] ?? null;
|
||||||
$jobName = $employee['job_role_name'] ?? null;
|
$jobNames = array_values($employeeJobRoleNames);
|
||||||
|
$jobSubRoleNames = array_values($employeeSubRoleNames);
|
||||||
|
$jobName = $jobNames ? implode(', ', $jobNames) : ($employee['job_role_name'] ?? null);
|
||||||
|
$jobSubRoleName = $jobSubRoleNames ? implode(', ', $jobSubRoleNames) : ($employee['job_sub_role_name'] ?? null);
|
||||||
|
|
||||||
$avatar = trim((string)($employee['auth_avatar'] ?? ''));
|
$avatar = trim((string)($employee['auth_avatar'] ?? ''));
|
||||||
|
|
||||||
@@ -746,20 +1016,63 @@ function fmtFileSize(?int $bytes): string
|
|||||||
Codice: <code><?= htmlspecialchars($employee['employee_code']) ?></code>
|
Codice: <code><?= htmlspecialchars($employee['employee_code']) ?></code>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<div class="profile-badges">
|
<div class="profile-summary-grid">
|
||||||
<?php if ($jobName): ?>
|
<div class="profile-summary-card">
|
||||||
<span class="pill pill-role">💼 <?= htmlspecialchars($jobName) ?></span>
|
<div class="profile-summary-label">Reparto</div>
|
||||||
<?php endif; ?>
|
<div class="profile-summary-value">
|
||||||
<?php if ($deptName): ?>
|
<?php if ($deptName): ?>
|
||||||
<span class="pill pill-dept" style="<?= $deptColor ? 'background:' . htmlspecialchars($deptColor, ENT_QUOTES) . '20; color:' . htmlspecialchars($deptColor, ENT_QUOTES) . ';' : '' ?>">
|
<span class="pill pill-dept" style="<?= $deptColor ? 'background:' . htmlspecialchars($deptColor, ENT_QUOTES) . '20; color:' . htmlspecialchars($deptColor, ENT_QUOTES) . ';' : '' ?>">
|
||||||
🏢 <?= htmlspecialchars($deptName) ?>
|
🏢 <?= htmlspecialchars($deptName) ?>
|
||||||
</span>
|
</span>
|
||||||
|
<?php else: ?>
|
||||||
|
—
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="profile-summary-card">
|
||||||
|
<div class="profile-summary-label">Mansioni / Sottomansioni</div>
|
||||||
|
<?php if (!empty($employeeSubRolesByRole)): ?>
|
||||||
|
<div class="profile-role-stack">
|
||||||
|
<?php foreach ($employeeSubRolesByRole as $roleGroup): ?>
|
||||||
|
<div class="profile-role-group">
|
||||||
|
<span class="profile-role-main">💼 <?= htmlspecialchars($roleGroup['job_role_name']) ?></span>
|
||||||
|
<?php foreach ($roleGroup['items'] as $sr): ?>
|
||||||
|
<span class="profile-subrole-chip">🧩 <?= htmlspecialchars($sr['job_sub_role_name']) ?></span>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="profile-summary-value">—</div>
|
||||||
|
<div class="profile-summary-muted">Nessuna sottomansione associata</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="profile-summary-card">
|
||||||
|
<div class="profile-summary-label">Ruolo accesso</div>
|
||||||
|
<div class="profile-summary-value">
|
||||||
|
<?php if (!empty($employee['auth_role_display_name']) || !empty($employee['auth_role_name'])): ?>
|
||||||
|
🔐 <?= htmlspecialchars($employee['auth_role_display_name'] ?: $employee['auth_role_name']) ?>
|
||||||
|
<?php else: ?>
|
||||||
|
—
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php if (!empty($employee['auth_username'])): ?>
|
||||||
|
<div class="profile-summary-muted"><?= htmlspecialchars($employee['auth_username']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="profile-summary-card">
|
||||||
|
<div class="profile-summary-label">Stato</div>
|
||||||
|
<div class="profile-summary-value">
|
||||||
<span class="pill pill-status-<?= htmlspecialchars($status['class']) ?>">
|
<span class="pill pill-status-<?= htmlspecialchars($status['class']) ?>">
|
||||||
<?= htmlspecialchars($status['label']) ?>
|
<?= htmlspecialchars($status['label']) ?>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<?php if ($canEdit): ?>
|
<?php if ($canEdit): ?>
|
||||||
<button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#editPersonalModal">
|
<button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#editPersonalModal">
|
||||||
✏️ Modifica
|
✏️ Modifica
|
||||||
@@ -853,9 +1166,26 @@ function fmtFileSize(?int $bytes): string
|
|||||||
<div class="info-label">Reparto</div>
|
<div class="info-label">Reparto</div>
|
||||||
<div class="info-value"><?= valOrDash($deptName) ?></div>
|
<div class="info-value"><?= valOrDash($deptName) ?></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row" style="grid-column: 1 / -1;">
|
||||||
<div class="info-label">Mansione</div>
|
<div class="info-label">Mansioni / Sottomansioni</div>
|
||||||
<div class="info-value"><?= valOrDash($jobName) ?></div>
|
<div class="info-value">
|
||||||
|
<?php if (!empty($employeeSubRolesByRole)): ?>
|
||||||
|
<div class="job-role-list">
|
||||||
|
<?php foreach ($employeeSubRolesByRole as $roleGroup): ?>
|
||||||
|
<div class="job-role-group">
|
||||||
|
<div class="job-role-group-title">💼 <?= htmlspecialchars($roleGroup['job_role_name']) ?></div>
|
||||||
|
<div class="job-subrole-chip-list">
|
||||||
|
<?php foreach ($roleGroup['items'] as $sr): ?>
|
||||||
|
<span class="job-subrole-chip">🧩 <?= htmlspecialchars($sr['job_sub_role_name']) ?></span>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="text-muted">Nessuna mansione/sottomansione associata</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<div class="info-label">Stato</div>
|
<div class="info-label">Stato</div>
|
||||||
@@ -1001,6 +1331,54 @@ function fmtFileSize(?int $bytes): string
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($requiredPpeList)): ?>
|
||||||
|
<div class="ppe-required-box">
|
||||||
|
<div class="ppe-required-title">
|
||||||
|
🦺 DPI richiesti dalle sottomansioni associate
|
||||||
|
<?php if (!empty($employeeSubRoleNames)): ?>
|
||||||
|
<span class="text-muted fw-normal">— calcolati su <?= count($employeeSubRoleNames) ?> sottomansion<?= count($employeeSubRoleNames) === 1 ? 'e' : 'i' ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php foreach ($requiredPpeList as $rp): ?>
|
||||||
|
<?php
|
||||||
|
$requiredPpeId = (int)$rp['id'];
|
||||||
|
$isAssigned = isset($assignedPpeIds[$requiredPpeId]);
|
||||||
|
?>
|
||||||
|
<div class="ppe-required-grid">
|
||||||
|
<div>
|
||||||
|
<div class="ppe-name-main"><?= htmlspecialchars($rp['name']) ?></div>
|
||||||
|
<div class="ppe-meta-small">
|
||||||
|
<?= !empty($rp['category']) ? htmlspecialchars($rp['category']) : 'Senza categoria' ?>
|
||||||
|
<?php if (!empty($rp['standard_reference'])): ?>
|
||||||
|
· <?= htmlspecialchars($rp['standard_reference']) ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($rp['source_sub_roles'])): ?>
|
||||||
|
<br><span class="text-primary">Da: <?= htmlspecialchars($rp['source_sub_roles']) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<?php if ($isAssigned): ?>
|
||||||
|
<span class="ppe-status-assigned">Assegnato</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="ppe-status-missing">Mancante</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php elseif (!empty($employeeSubRoleIds)): ?>
|
||||||
|
<div class="alert alert-light border">
|
||||||
|
Nessun DPI obbligatorio configurato per le sottomansioni associate al dipendente.
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
Nessuna sottomansione associata al dipendente: non è possibile suggerire DPI obbligatori.
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (empty($ppeList)): ?>
|
<?php if (empty($ppeList)): ?>
|
||||||
<div class="placeholder-section">
|
<div class="placeholder-section">
|
||||||
<i class='bx bx-shield-quarter'></i>
|
<i class='bx bx-shield-quarter'></i>
|
||||||
@@ -1018,31 +1396,62 @@ function fmtFileSize(?int $bytes): string
|
|||||||
<thead style="background-color:#cfe3ff;">
|
<thead style="background-color:#cfe3ff;">
|
||||||
<tr>
|
<tr>
|
||||||
<th>DPI</th>
|
<th>DPI</th>
|
||||||
|
<th>Categoria</th>
|
||||||
<th>Data Consegna</th>
|
<th>Data Consegna</th>
|
||||||
|
<th>Scadenza</th>
|
||||||
<th>Consegnato da</th>
|
<th>Consegnato da</th>
|
||||||
|
<th>Stato</th>
|
||||||
<th>Note</th>
|
<th>Note</th>
|
||||||
<?php if ($canEdit): ?><th class="text-end">Azioni</th><?php endif; ?>
|
<?php if ($canEdit): ?><th class="text-end">Azioni</th><?php endif; ?>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($ppeList as $p): ?>
|
<?php foreach ($ppeList as $p): ?>
|
||||||
<?php $pid = (int)$p['id']; ?>
|
<?php
|
||||||
|
$pid = (int)$p['id'];
|
||||||
|
$ppeStatus = $p['status'] ?? 'assigned';
|
||||||
|
|
||||||
|
$statusClass = 'ppe-status-assigned';
|
||||||
|
$statusLabel = 'Assegnato';
|
||||||
|
|
||||||
|
if ($ppeStatus === 'returned') {
|
||||||
|
$statusClass = 'ppe-status-returned';
|
||||||
|
$statusLabel = 'Restituito';
|
||||||
|
} elseif ($ppeStatus === 'expired') {
|
||||||
|
$statusClass = 'ppe-status-problem';
|
||||||
|
$statusLabel = 'Scaduto';
|
||||||
|
} elseif ($ppeStatus === 'lost') {
|
||||||
|
$statusClass = 'ppe-status-problem';
|
||||||
|
$statusLabel = 'Perso';
|
||||||
|
} elseif ($ppeStatus === 'damaged') {
|
||||||
|
$statusClass = 'ppe-status-problem';
|
||||||
|
$statusLabel = 'Danneggiato';
|
||||||
|
}
|
||||||
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-semibold"><?= htmlspecialchars($p['item_name']) ?></td>
|
<td class="fw-semibold"><?= htmlspecialchars($p['ppe_name']) ?></td>
|
||||||
<td><?= fmtDate($p['delivery_date']) ?></td>
|
<td><?= valOrDash($p['ppe_category']) ?></td>
|
||||||
<td><?= valOrDash($p['delivered_by']) ?></td>
|
<td><?= fmtDate($p['assigned_date']) ?></td>
|
||||||
|
<td><?= fmtDate($p['expiry_date']) ?></td>
|
||||||
|
<td><?= valOrDash($p['delivered_by'] ?? null) ?></td>
|
||||||
|
<td><span class="<?= $statusClass ?>"><?= $statusLabel ?></span></td>
|
||||||
<td><?= valOrDash($p['notes']) ?></td>
|
<td><?= valOrDash($p['notes']) ?></td>
|
||||||
<?php if ($canEdit): ?>
|
<?php if ($canEdit): ?>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<button class="btn btn-sm btn-outline-secondary edit-ppe"
|
<button class="btn btn-sm btn-outline-secondary edit-ppe"
|
||||||
data-id="<?= $pid ?>"
|
data-id="<?= $pid ?>"
|
||||||
data-item_name="<?= htmlspecialchars($p['item_name'], ENT_QUOTES) ?>"
|
data-ppe_item_id="<?= (int)$p['ppe_item_id'] ?>"
|
||||||
data-delivery_date="<?= htmlspecialchars($p['delivery_date'] ?? '', ENT_QUOTES) ?>"
|
data-assigned_date="<?= htmlspecialchars($p['assigned_date'] ?? '', ENT_QUOTES) ?>"
|
||||||
|
data-expiry_date="<?= htmlspecialchars($p['expiry_date'] ?? '', ENT_QUOTES) ?>"
|
||||||
data-delivered_by="<?= htmlspecialchars($p['delivered_by'] ?? '', ENT_QUOTES) ?>"
|
data-delivered_by="<?= htmlspecialchars($p['delivered_by'] ?? '', ENT_QUOTES) ?>"
|
||||||
|
data-status="<?= htmlspecialchars($p['status'] ?? 'assigned', ENT_QUOTES) ?>"
|
||||||
data-notes="<?= htmlspecialchars($p['notes'] ?? '', ENT_QUOTES) ?>">✏️</button>
|
data-notes="<?= htmlspecialchars($p['notes'] ?? '', ENT_QUOTES) ?>">✏️</button>
|
||||||
|
|
||||||
|
<?php if (($p['status'] ?? '') === 'assigned'): ?>
|
||||||
<button class="btn btn-sm btn-outline-danger delete-ppe"
|
<button class="btn btn-sm btn-outline-danger delete-ppe"
|
||||||
data-id="<?= $pid ?>"
|
data-id="<?= $pid ?>"
|
||||||
data-name="<?= htmlspecialchars($p['item_name'], ENT_QUOTES) ?>">🗑️</button>
|
data-name="<?= htmlspecialchars($p['ppe_name'], ENT_QUOTES) ?>">Rimuovi</button>
|
||||||
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1054,13 +1463,41 @@ function fmtFileSize(?int $bytes): string
|
|||||||
<!-- MOBILE CARDS -->
|
<!-- MOBILE CARDS -->
|
||||||
<div class="d-block d-md-none">
|
<div class="d-block d-md-none">
|
||||||
<?php foreach ($ppeList as $p): ?>
|
<?php foreach ($ppeList as $p): ?>
|
||||||
<?php $pid = (int)$p['id']; ?>
|
<?php
|
||||||
|
$pid = (int)$p['id'];
|
||||||
|
$ppeStatus = $p['status'] ?? 'assigned';
|
||||||
|
|
||||||
|
$statusClass = 'ppe-status-assigned';
|
||||||
|
$statusLabel = 'Assegnato';
|
||||||
|
|
||||||
|
if ($ppeStatus === 'returned') {
|
||||||
|
$statusClass = 'ppe-status-returned';
|
||||||
|
$statusLabel = 'Restituito';
|
||||||
|
} elseif ($ppeStatus === 'expired') {
|
||||||
|
$statusClass = 'ppe-status-problem';
|
||||||
|
$statusLabel = 'Scaduto';
|
||||||
|
} elseif ($ppeStatus === 'lost') {
|
||||||
|
$statusClass = 'ppe-status-problem';
|
||||||
|
$statusLabel = 'Perso';
|
||||||
|
} elseif ($ppeStatus === 'damaged') {
|
||||||
|
$statusClass = 'ppe-status-problem';
|
||||||
|
$statusLabel = 'Danneggiato';
|
||||||
|
}
|
||||||
|
?>
|
||||||
<div class="doc-card">
|
<div class="doc-card">
|
||||||
<div class="d-flex justify-content-between align-items-start gap-2 mb-2">
|
<div class="d-flex justify-content-between align-items-start gap-2 mb-2">
|
||||||
<span class="doc-card-title">🦺 <?= htmlspecialchars($p['item_name']) ?></span>
|
<span class="doc-card-title">🦺 <?= htmlspecialchars($p['ppe_name']) ?></span>
|
||||||
<span class="small text-muted text-nowrap"><?= fmtDate($p['delivery_date']) ?></span>
|
<span class="<?= $statusClass ?>"><?= $statusLabel ?></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="doc-card-meta">
|
<div class="doc-card-meta">
|
||||||
|
<?php if (!empty($p['ppe_category'])): ?>
|
||||||
|
<span><b>Categoria:</b> <?= htmlspecialchars($p['ppe_category']) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<span><b>Consegna:</b> <?= fmtDate($p['assigned_date']) ?></span>
|
||||||
|
<?php if (!empty($p['expiry_date'])): ?>
|
||||||
|
<span><b>Scadenza:</b> <?= fmtDate($p['expiry_date']) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
<?php if (!empty($p['delivered_by'])): ?>
|
<?php if (!empty($p['delivered_by'])): ?>
|
||||||
<span><b>Consegnato da:</b> <?= htmlspecialchars($p['delivered_by']) ?></span>
|
<span><b>Consegnato da:</b> <?= htmlspecialchars($p['delivered_by']) ?></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -1068,17 +1505,23 @@ function fmtFileSize(?int $bytes): string
|
|||||||
<span><b>Note:</b> <?= htmlspecialchars($p['notes']) ?></span>
|
<span><b>Note:</b> <?= htmlspecialchars($p['notes']) ?></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if ($canEdit): ?>
|
<?php if ($canEdit): ?>
|
||||||
<div class="doc-card-actions">
|
<div class="doc-card-actions">
|
||||||
<button class="btn btn-sm btn-outline-secondary edit-ppe"
|
<button class="btn btn-sm btn-outline-secondary edit-ppe"
|
||||||
data-id="<?= $pid ?>"
|
data-id="<?= $pid ?>"
|
||||||
data-item_name="<?= htmlspecialchars($p['item_name'], ENT_QUOTES) ?>"
|
data-ppe_item_id="<?= (int)$p['ppe_item_id'] ?>"
|
||||||
data-delivery_date="<?= htmlspecialchars($p['delivery_date'] ?? '', ENT_QUOTES) ?>"
|
data-assigned_date="<?= htmlspecialchars($p['assigned_date'] ?? '', ENT_QUOTES) ?>"
|
||||||
|
data-expiry_date="<?= htmlspecialchars($p['expiry_date'] ?? '', ENT_QUOTES) ?>"
|
||||||
data-delivered_by="<?= htmlspecialchars($p['delivered_by'] ?? '', ENT_QUOTES) ?>"
|
data-delivered_by="<?= htmlspecialchars($p['delivered_by'] ?? '', ENT_QUOTES) ?>"
|
||||||
|
data-status="<?= htmlspecialchars($p['status'] ?? 'assigned', ENT_QUOTES) ?>"
|
||||||
data-notes="<?= htmlspecialchars($p['notes'] ?? '', ENT_QUOTES) ?>">✏️ Modifica</button>
|
data-notes="<?= htmlspecialchars($p['notes'] ?? '', ENT_QUOTES) ?>">✏️ Modifica</button>
|
||||||
|
|
||||||
|
<?php if (($p['status'] ?? '') === 'assigned'): ?>
|
||||||
<button class="btn btn-sm btn-outline-danger delete-ppe"
|
<button class="btn btn-sm btn-outline-danger delete-ppe"
|
||||||
data-id="<?= $pid ?>"
|
data-id="<?= $pid ?>"
|
||||||
data-name="<?= htmlspecialchars($p['item_name'], ENT_QUOTES) ?>">🗑️</button>
|
data-name="<?= htmlspecialchars($p['ppe_name'], ENT_QUOTES) ?>">Rimuovi</button>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
@@ -1363,6 +1806,80 @@ function fmtFileSize(?int $bytes): string
|
|||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ppe-required-box {
|
||||||
|
border: 1px solid #bfdbfe;
|
||||||
|
background: #eff6ff;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 14px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ppe-required-title {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e3a8a;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ppe-required-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
border-top: 1px solid #dbeafe;
|
||||||
|
padding-top: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ppe-name-main {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ppe-meta-small {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ppe-status-assigned {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ppe-status-missing {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ppe-status-returned {
|
||||||
|
background: #e5e7eb;
|
||||||
|
color: #374151;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ppe-status-problem {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- TRAINING ATTACHMENTS MODAL (visible to everyone with profile access) -->
|
<!-- TRAINING ATTACHMENTS MODAL (visible to everyone with profile access) -->
|
||||||
@@ -1537,28 +2054,60 @@ function fmtFileSize(?int $bytes): string
|
|||||||
<h5 class="modal-title" id="ppeModalTitle">Aggiungi DPI</h5>
|
<h5 class="modal-title" id="ppeModalTitle">Aggiungi DPI</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form id="ppeForm">
|
<form id="ppeForm">
|
||||||
<input type="hidden" id="ppeId">
|
<input type="hidden" id="ppeId">
|
||||||
<input type="hidden" name="employee_id" id="ppeEmployeeId" value="<?= (int)$employee['id'] ?>">
|
<input type="hidden" name="employee_id" id="ppeEmployeeId" value="<?= (int)$employee['id'] ?>">
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-semibold">DPI *</label>
|
<label class="form-label fw-semibold">DPI *</label>
|
||||||
<input type="text" class="form-control" id="ppeItemName" placeholder="es. Casco, Guanti, Scarpe antinfortunistiche" required>
|
<select class="form-select" id="ppeItemId" required>
|
||||||
|
<option value="">— Seleziona DPI —</option>
|
||||||
|
<?php foreach ($ppeItemsAll as $item): ?>
|
||||||
|
<option value="<?= (int)$item['id'] ?>"
|
||||||
|
data-validity_months="<?= $item['validity_months'] !== null ? (int)$item['validity_months'] : '' ?>">
|
||||||
|
<?= htmlspecialchars($item['name']) ?>
|
||||||
|
<?= !empty($item['category']) ? ' — ' . htmlspecialchars($item['category']) : '' ?>
|
||||||
|
<?= !empty($item['standard_reference']) ? ' — ' . htmlspecialchars($item['standard_reference']) : '' ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-6 mb-3">
|
<div class="col-12 col-md-6 mb-3">
|
||||||
<label class="form-label fw-semibold">Data Consegna</label>
|
<label class="form-label fw-semibold">Data Consegna</label>
|
||||||
<input type="date" class="form-control" id="ppeDeliveryDate">
|
<input type="date" class="form-control" id="ppeAssignedDate" value="<?= date('Y-m-d') ?>">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-md-6 mb-3">
|
<div class="col-12 col-md-6 mb-3">
|
||||||
|
<label class="form-label fw-semibold">Data Scadenza</label>
|
||||||
|
<input type="date" class="form-control" id="ppeExpiryDate">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
<label class="form-label fw-semibold">Consegnato da</label>
|
<label class="form-label fw-semibold">Consegnato da</label>
|
||||||
<input type="text" class="form-control" id="ppeDeliveredBy" placeholder="Nome o azienda">
|
<input type="text" class="form-control" id="ppeDeliveredBy" placeholder="Nome o azienda">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Stato</label>
|
||||||
|
<select class="form-select" id="ppeStatus">
|
||||||
|
<option value="assigned">Assegnato</option>
|
||||||
|
<option value="returned">Restituito</option>
|
||||||
|
<option value="expired">Scaduto</option>
|
||||||
|
<option value="lost">Perso</option>
|
||||||
|
<option value="damaged">Danneggiato</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-semibold">Note</label>
|
<label class="form-label fw-semibold">Note</label>
|
||||||
<textarea class="form-control" id="ppeNotes" rows="2" placeholder="Opzionale"></textarea>
|
<textarea class="form-control" id="ppeNotes" rows="2" placeholder="Opzionale"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<button type="submit" class="btn btn-add">💾 Salva</button>
|
<button type="submit" class="btn btn-add">💾 Salva</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1631,15 +2180,29 @@ function fmtFileSize(?int $bytes): string
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6 mb-3">
|
<div class="col-12 col-md-6 mb-3">
|
||||||
<label class="form-label fw-semibold">Mansione</label>
|
<label class="form-label fw-semibold">Sottomansioni</label>
|
||||||
<select class="form-select" id="editJobRoleId">
|
<select class="form-select" id="editJobSubRoleIds" multiple style="width:100%;">
|
||||||
<option value="">— Nessuna —</option>
|
<?php
|
||||||
<?php foreach ($jobRoles as $r): ?>
|
$currentGroup = null;
|
||||||
<option value="<?= (int)$r['id'] ?>" <?= ((int)($employee['job_role_id'] ?? 0) === (int)$r['id']) ? 'selected' : '' ?>>
|
foreach ($jobSubRolesAll as $sr):
|
||||||
<?= htmlspecialchars($r['name']) ?>
|
$groupName = $sr['job_role_name'] ?: 'Senza mansione';
|
||||||
|
if ($currentGroup !== $groupName):
|
||||||
|
if ($currentGroup !== null): ?>
|
||||||
|
</optgroup>
|
||||||
|
<?php endif; ?>
|
||||||
|
<optgroup label="<?= htmlspecialchars($groupName, ENT_QUOTES, 'UTF-8') ?>">
|
||||||
|
<?php $currentGroup = $groupName;
|
||||||
|
endif;
|
||||||
|
?>
|
||||||
|
<option value="<?= (int)$sr['id'] ?>" <?= in_array((int)$sr['id'], $employeeSubRoleIds, true) ? 'selected' : '' ?>>
|
||||||
|
<?= htmlspecialchars($sr['name']) ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
<?php if ($currentGroup !== null): ?>
|
||||||
|
</optgroup>
|
||||||
|
<?php endif; ?>
|
||||||
</select>
|
</select>
|
||||||
|
<small class="text-muted">Puoi selezionare più sottomansioni anche appartenenti a mansioni diverse.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1698,6 +2261,7 @@ function fmtFileSize(?int $bytes): string
|
|||||||
<?php include('jsinclude.php'); ?>
|
<?php include('jsinclude.php'); ?>
|
||||||
<script src="https://cdn.datatables.net/1.13.8/js/jquery.dataTables.min.js"></script>
|
<script src="https://cdn.datatables.net/1.13.8/js/jquery.dataTables.min.js"></script>
|
||||||
<script src="https://cdn.datatables.net/1.13.8/js/dataTables.bootstrap5.min.js"></script>
|
<script src="https://cdn.datatables.net/1.13.8/js/dataTables.bootstrap5.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.full.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
|
||||||
@@ -2111,7 +2675,19 @@ function fmtFileSize(?int $bytes): string
|
|||||||
|
|
||||||
<?php if ($employee && $canEdit): ?>
|
<?php if ($employee && $canEdit): ?>
|
||||||
<script>
|
<script>
|
||||||
|
const jobSubRoleToRoleMap = <?= json_encode($jobSubRoleToRoleMap, JSON_UNESCAPED_UNICODE) ?>;
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
if ($('#editJobSubRoleIds').length) {
|
||||||
|
$('#editJobSubRoleIds').select2({
|
||||||
|
theme: 'bootstrap-5',
|
||||||
|
dropdownParent: $('#editPersonalModal'),
|
||||||
|
placeholder: 'Seleziona una o più sottomansioni...',
|
||||||
|
closeOnSelect: false,
|
||||||
|
width: '100%'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ---- UPLOAD DOCUMENT ----
|
// ---- UPLOAD DOCUMENT ----
|
||||||
$("#uploadDocumentForm").on("submit", function(e) {
|
$("#uploadDocumentForm").on("submit", function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -2315,35 +2891,83 @@ function fmtFileSize(?int $bytes): string
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- PPE: open modal (add or edit) ----
|
// ---- PPE: Select2 ----
|
||||||
|
if ($('#ppeItemId').length) {
|
||||||
|
$('#ppeItemId').select2({
|
||||||
|
theme: 'bootstrap-5',
|
||||||
|
dropdownParent: $('#ppeModal'),
|
||||||
|
placeholder: 'Cerca DPI...',
|
||||||
|
width: '100%',
|
||||||
|
allowClear: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMonthsToDate(dateString, months) {
|
||||||
|
if (!dateString || !months) return '';
|
||||||
|
|
||||||
|
const date = new Date(dateString + 'T00:00:00');
|
||||||
|
if (isNaN(date.getTime())) return '';
|
||||||
|
|
||||||
|
date.setMonth(date.getMonth() + parseInt(months, 10));
|
||||||
|
|
||||||
|
const y = date.getFullYear();
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const d = String(date.getDate()).padStart(2, '0');
|
||||||
|
|
||||||
|
return `${y}-${m}-${d}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#ppeItemId, #ppeAssignedDate').on('change', function() {
|
||||||
|
const validityMonths = $('#ppeItemId option:selected').data('validity_months');
|
||||||
|
const assignedDate = $('#ppeAssignedDate').val();
|
||||||
|
|
||||||
|
if (validityMonths && assignedDate && !$('#ppeExpiryDate').val()) {
|
||||||
|
$('#ppeExpiryDate').val(addMonthsToDate(assignedDate, validityMonths));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- PPE: open modal add ----
|
||||||
window.openPpeModal = function() {
|
window.openPpeModal = function() {
|
||||||
$("#ppeId").val('');
|
$("#ppeId").val('');
|
||||||
$("#ppeItemName").val('');
|
$("#ppeItemId").val('').trigger('change');
|
||||||
$("#ppeDeliveryDate").val('');
|
$("#ppeAssignedDate").val('<?= date('Y-m-d') ?>');
|
||||||
|
$("#ppeExpiryDate").val('');
|
||||||
$("#ppeDeliveredBy").val('');
|
$("#ppeDeliveredBy").val('');
|
||||||
|
$("#ppeStatus").val('assigned');
|
||||||
$("#ppeNotes").val('');
|
$("#ppeNotes").val('');
|
||||||
$("#ppeModalTitle").text('Aggiungi DPI');
|
$("#ppeModalTitle").text('Aggiungi DPI');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---- PPE: open modal edit ----
|
||||||
$(document).on("click", ".edit-ppe", function() {
|
$(document).on("click", ".edit-ppe", function() {
|
||||||
const b = $(this);
|
const b = $(this);
|
||||||
|
|
||||||
$("#ppeId").val(b.data("id"));
|
$("#ppeId").val(b.data("id"));
|
||||||
$("#ppeItemName").val(b.data("item_name"));
|
$("#ppeItemId").val(String(b.data("ppe_item_id"))).trigger('change');
|
||||||
$("#ppeDeliveryDate").val(b.data("delivery_date"));
|
$("#ppeAssignedDate").val(b.data("assigned_date"));
|
||||||
|
$("#ppeExpiryDate").val(b.data("expiry_date"));
|
||||||
$("#ppeDeliveredBy").val(b.data("delivered_by"));
|
$("#ppeDeliveredBy").val(b.data("delivered_by"));
|
||||||
|
$("#ppeStatus").val(b.data("status") || 'assigned');
|
||||||
$("#ppeNotes").val(b.data("notes"));
|
$("#ppeNotes").val(b.data("notes"));
|
||||||
$("#ppeModalTitle").text('Modifica DPI');
|
$("#ppeModalTitle").text('Modifica DPI');
|
||||||
|
|
||||||
$("#ppeModal").modal("show");
|
$("#ppeModal").modal("show");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- PPE: save ----
|
||||||
$("#ppeForm").on("submit", function(e) {
|
$("#ppeForm").on("submit", function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const p = new URLSearchParams();
|
const p = new URLSearchParams();
|
||||||
p.append('id', $("#ppeId").val());
|
p.append('id', $("#ppeId").val());
|
||||||
p.append('employee_id', $("#ppeEmployeeId").val());
|
p.append('employee_id', $("#ppeEmployeeId").val());
|
||||||
p.append('item_name', $("#ppeItemName").val().trim());
|
p.append('ppe_item_id', $("#ppeItemId").val());
|
||||||
p.append('delivery_date', $("#ppeDeliveryDate").val());
|
p.append('assigned_date', $("#ppeAssignedDate").val());
|
||||||
|
p.append('expiry_date', $("#ppeExpiryDate").val());
|
||||||
p.append('delivered_by', $("#ppeDeliveredBy").val().trim());
|
p.append('delivered_by', $("#ppeDeliveredBy").val().trim());
|
||||||
|
p.append('status', $("#ppeStatus").val());
|
||||||
p.append('notes', $("#ppeNotes").val().trim());
|
p.append('notes', $("#ppeNotes").val().trim());
|
||||||
|
|
||||||
fetch("ajax/employee_profile/save_ppe.php", {
|
fetch("ajax/employee_profile/save_ppe.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -2378,22 +3002,26 @@ function fmtFileSize(?int $bytes): string
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---- PPE: remove / mark as returned ----
|
||||||
$(document).on("click", ".delete-ppe", function() {
|
$(document).on("click", ".delete-ppe", function() {
|
||||||
const id = $(this).data("id");
|
const id = $(this).data("id");
|
||||||
const name = $(this).data("name");
|
const name = $(this).data("name");
|
||||||
|
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: "Confermi la cancellazione?",
|
title: "Confermi la rimozione?",
|
||||||
text: name ? ("DPI: " + name) : "Il DPI verrà cancellato.",
|
text: name ? ("DPI: " + name) : "Il DPI verrà segnato come restituito.",
|
||||||
icon: "warning",
|
icon: "warning",
|
||||||
showCancelButton: true,
|
showCancelButton: true,
|
||||||
confirmButtonColor: "#d33",
|
confirmButtonColor: "#d33",
|
||||||
cancelButtonColor: "#6c757d",
|
cancelButtonColor: "#6c757d",
|
||||||
confirmButtonText: "Sì, cancella",
|
confirmButtonText: "Sì, rimuovi",
|
||||||
cancelButtonText: "Annulla"
|
cancelButtonText: "Annulla"
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
if (!result.isConfirmed) return;
|
if (!result.isConfirmed) return;
|
||||||
|
|
||||||
const p = new URLSearchParams();
|
const p = new URLSearchParams();
|
||||||
p.append('id', id);
|
p.append('id', id);
|
||||||
|
|
||||||
fetch("ajax/employee_profile/delete_ppe.php", {
|
fetch("ajax/employee_profile/delete_ppe.php", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -2406,7 +3034,7 @@ function fmtFileSize(?int $bytes): string
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
icon: "success",
|
icon: "success",
|
||||||
title: "Cancellato!",
|
title: "Rimosso!",
|
||||||
confirmButtonColor: "#3085d6"
|
confirmButtonColor: "#3085d6"
|
||||||
})
|
})
|
||||||
.then(() => location.reload());
|
.then(() => location.reload());
|
||||||
@@ -2414,7 +3042,7 @@ function fmtFileSize(?int $bytes): string
|
|||||||
Swal.fire({
|
Swal.fire({
|
||||||
icon: "error",
|
icon: "error",
|
||||||
title: "Errore",
|
title: "Errore",
|
||||||
text: data.message || "Impossibile cancellare."
|
text: data.message || "Impossibile rimuovere."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -2510,7 +3138,19 @@ function fmtFileSize(?int $bytes): string
|
|||||||
p.append('phone', $("#editPhone").val().trim());
|
p.append('phone', $("#editPhone").val().trim());
|
||||||
p.append('email', $("#editEmail").val().trim());
|
p.append('email', $("#editEmail").val().trim());
|
||||||
p.append('department_id', $("#editDepartmentId").val());
|
p.append('department_id', $("#editDepartmentId").val());
|
||||||
p.append('job_role_id', $("#editJobRoleId").val());
|
|
||||||
|
const selectedSubRoles = $("#editJobSubRoleIds").val() || [];
|
||||||
|
selectedSubRoles.forEach(function(subRoleId) {
|
||||||
|
p.append('job_sub_role_ids[]', subRoleId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Backward compatibility for ajax/employee_profile/save_personal.php:
|
||||||
|
// keep sending a legacy primary job_role_id/job_sub_role_id based on the first selected sub role.
|
||||||
|
const primarySubRoleId = selectedSubRoles.length ? selectedSubRoles[0] : '';
|
||||||
|
const primaryJobRoleId = primarySubRoleId && jobSubRoleToRoleMap[primarySubRoleId] ? jobSubRoleToRoleMap[primarySubRoleId] : '';
|
||||||
|
p.append('job_role_id', primaryJobRoleId);
|
||||||
|
p.append('job_sub_role_id', primarySubRoleId);
|
||||||
|
|
||||||
p.append('status', $("#editStatus").val());
|
p.append('status', $("#editStatus").val());
|
||||||
p.append('auth_user_id', $("#editAuthUserId").val());
|
p.append('auth_user_id', $("#editAuthUserId").val());
|
||||||
p.append('role_id', $("#editAuthUserId").val() ? ($("#editRoleId").val() || '') : '');
|
p.append('role_id', $("#editAuthUserId").val() ? ($("#editRoleId").val() || '') : '');
|
||||||
|
|||||||
+1412
-277
File diff suppressed because it is too large
Load Diff
@@ -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,33 +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>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="training_calendar.php">
|
|
||||||
<i class='bx bx-radio-circle'></i>Calendario Formazione
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (userCan('hr.skills.view')) : ?>
|
<?php if (userCan('hr.skills.view')) : ?>
|
||||||
@@ -361,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; ?>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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' => '🧠',
|
||||||
@@ -494,16 +515,23 @@ $dashboardSections = [
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.my-deadlines-widgets:empty { display: none; }
|
|
||||||
|
.my-deadlines-widgets:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Each widget wraps itself in .my-deadlines-widgets; collapse the nested
|
/* Each widget wraps itself in .my-deadlines-widgets; collapse the nested
|
||||||
wrapper so all cards flow into the outer flex (single row). */
|
wrapper so all cards flow into the outer flex (single row). */
|
||||||
.my-deadlines-widgets .my-deadlines-widgets {
|
.my-deadlines-widgets .my-deadlines-widgets {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
.my-deadlines-widgets .mdw {
|
.my-deadlines-widgets .mdw {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex; align-items: center; gap: 0.75rem;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
padding: 0.8rem 0.9rem;
|
padding: 0.8rem 0.9rem;
|
||||||
border-radius: 0.6rem;
|
border-radius: 0.6rem;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -511,28 +539,73 @@ $dashboardSections = [
|
|||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||||
transition: transform 0.15s, box-shadow 0.15s;
|
transition: transform 0.15s, box-shadow 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 991.98px) {
|
@media (max-width: 991.98px) {
|
||||||
.my-deadlines-widgets .mdw { flex: 1 1 calc(50% - 0.375rem); }
|
.my-deadlines-widgets .mdw {
|
||||||
|
flex: 1 1 calc(50% - 0.375rem);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 575.98px) {
|
@media (max-width: 575.98px) {
|
||||||
.my-deadlines-widgets .mdw { flex: 1 1 100%; }
|
.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:hover {
|
||||||
.my-deadlines-widgets .mdw-gray { background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%); }
|
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 {
|
.my-deadlines-widgets .mdw-icon {
|
||||||
width: 38px; height: 38px; border-radius: 50%;
|
width: 38px;
|
||||||
display: flex; align-items: center; justify-content: center;
|
height: 38px;
|
||||||
background: rgba(255,255,255,0.22); font-size: 1.05rem; flex-shrink: 0;
|
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-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 {
|
.my-deadlines-widgets .mdw-label {
|
||||||
font-size: 0.78rem; opacity: 0.95;
|
font-size: 0.78rem;
|
||||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
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;
|
||||||
}
|
}
|
||||||
.my-deadlines-widgets .mdw-arrow { opacity: 0.7; font-size: 0.85rem; flex-shrink: 0; }
|
|
||||||
</style>
|
</style>
|
||||||
<div class="my-deadlines-widgets">
|
<div class="my-deadlines-widgets">
|
||||||
<?php include(__DIR__ . '/scadenzario/include/my_deadlines_widget.php'); ?>
|
<?php include(__DIR__ . '/scadenzario/include/my_deadlines_widget.php'); ?>
|
||||||
|
|||||||
@@ -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="d-flex justify-content-between align-items-start gap-2">
|
||||||
<div class="fc-name"><?= htmlspecialchars($f['name'], ENT_QUOTES, 'UTF-8') ?></div>
|
<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>
|
||||||
|
|||||||
@@ -43,37 +43,172 @@ $departments = $pdo->query("
|
|||||||
<title>Calendario Formazione - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
<title>Calendario Formazione - <?= htmlspecialchars($titlewebsite, ENT_QUOTES, 'UTF-8'); ?></title>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body { font-size: 1.05rem; background: #f8fafc; }
|
body {
|
||||||
.card { border-radius: 16px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); }
|
font-size: 1.05rem;
|
||||||
.back-dashboard {
|
background: #f8fafc;
|
||||||
background-color: #cfe3ff !important; color: #1f2d3d !important;
|
|
||||||
border: 1px solid #bcd4f4 !important; border-radius: 10px;
|
|
||||||
font-weight: 600; padding: 10px 18px;
|
|
||||||
}
|
}
|
||||||
.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 */
|
.card {
|
||||||
.fc { font-size: 0.95rem; }
|
border-radius: 16px;
|
||||||
.fc .fc-toolbar-title { font-size: 1.15rem; font-weight: 700; color: #2c3e6b; }
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
.fc .fc-button-primary {
|
}
|
||||||
background: #5a8fd8; border-color: #5a8fd8;
|
|
||||||
font-weight: 600; font-size: 0.82rem; border-radius: 0.4rem;
|
.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;
|
||||||
}
|
}
|
||||||
.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) {
|
@media (max-width: 767.98px) {
|
||||||
.card-header { flex-direction: column; align-items: flex-start !important; gap: .5rem; }
|
.training-header-actions {
|
||||||
.fc .fc-toolbar { flex-direction: column; gap: 0.5rem; }
|
width: 100%;
|
||||||
.fc .fc-toolbar-title { font-size: 1rem; }
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -88,10 +223,20 @@ $departments = $pdo->query("
|
|||||||
<div class="card p-3">
|
<div class="card p-3">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||||
<h5 class="mb-0">📅 Calendario Formazione</h5>
|
<h5 class="mb-0">📅 Calendario Formazione</h5>
|
||||||
<div class="d-flex gap-2 flex-wrap">
|
|
||||||
<a href="trainings.php" class="btn btn-light border d-inline-flex align-items-center gap-2">
|
<div class="training-header-actions">
|
||||||
📚 <span>Storico Formazione</span>
|
<?php if (userCan('hr.training_topics.view')): ?>
|
||||||
|
<a href="training_topics.php" class="btn btn-training-action btn-training-topics">
|
||||||
|
📘 Corsi Formazione
|
||||||
</a>
|
</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'">
|
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
|
||||||
↩️ Torna alla Dashboard
|
↩️ Torna alla Dashboard
|
||||||
</button>
|
</button>
|
||||||
@@ -201,7 +346,9 @@ $departments = $pdo->query("
|
|||||||
calendar.render();
|
calendar.render();
|
||||||
|
|
||||||
document.querySelectorAll('#filterStatus, #filterTopic, #filterDepartment, #filterEmployee').forEach(function(el) {
|
document.querySelectorAll('#filterStatus, #filterTopic, #filterDepartment, #filterEmployee').forEach(function(el) {
|
||||||
el.addEventListener('change', function() { calendar.refetchEvents(); });
|
el.addEventListener('change', function() {
|
||||||
|
calendar.refetchEvents();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('btnResetFilters').addEventListener('click', function() {
|
document.getElementById('btnResetFilters').addEventListener('click', function() {
|
||||||
|
|||||||
@@ -33,36 +33,193 @@ $topics = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
<script src="https://cdn.datatables.net/1.13.6/js/dataTables.bootstrap5.min.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body { font-size: 1.05rem; background: #f8fafc; }
|
body {
|
||||||
.card { border-radius: 16px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); }
|
font-size: 1.05rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
.back-dashboard {
|
.back-dashboard {
|
||||||
background-color: #cfe3ff !important; color: #1f2d3d !important;
|
background-color: #cfe3ff !important;
|
||||||
border: 1px solid #bcd4f4 !important; border-radius: 10px;
|
color: #1f2d3d !important;
|
||||||
font-weight: 600; padding: 10px 18px;
|
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);
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
|
||||||
transition: all 0.2s ease-in-out;
|
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; }
|
.back-dashboard:hover {
|
||||||
.btn-add:hover { background-color: #0b5ed7; transform: scale(1.02); }
|
background-color: #b9d3ff !important;
|
||||||
.table thead { background-color: #cfe3ff; color: #1f2d3d; }
|
transform: translateY(-2px);
|
||||||
.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;
|
.training-header-actions {
|
||||||
background: #eef2ff; color: #3730a3; font-weight: 600; font-size: 0.85rem;
|
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) {
|
@media (max-width: 767.98px) {
|
||||||
.card-header { flex-direction: column; align-items: flex-start !important; gap: .5rem; }
|
.training-header-actions {
|
||||||
.back-dashboard { width: 100%; }
|
width: 100%;
|
||||||
.btn-add { 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 {
|
.tt-card {
|
||||||
@@ -73,6 +230,7 @@ $topics = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tt-card-title {
|
.tt-card-title {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -80,12 +238,14 @@ $topics = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
margin: 0 0 4px 0;
|
margin: 0 0 4px 0;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tt-card-desc {
|
.tt-card-desc {
|
||||||
color: #475569;
|
color: #475569;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 10px 0;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tt-card-meta {
|
.tt-card-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -94,12 +254,21 @@ $topics = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
color: #64748b;
|
color: #64748b;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
.tt-card-meta b { color: #1f2937; font-weight: 600; }
|
|
||||||
|
.tt-card-meta b {
|
||||||
|
color: #1f2937;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.tt-card-actions {
|
.tt-card-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.tt-card-actions .btn { flex: 1; }
|
|
||||||
|
.tt-card-actions .btn {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.tt-empty {
|
.tt-empty {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
@@ -117,13 +286,28 @@ $topics = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<div class="card p-3">
|
<div class="card p-3">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||||
<h5 class="mb-0">Gestione Corsi di Formazione</h5>
|
<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'">
|
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
|
||||||
↩️ Torna alla Dashboard
|
↩️ Torna alla Dashboard
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
<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>
|
<h6 class="fw-semibold mb-0">Elenco Corsi / Training Topics</h6>
|
||||||
<button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#addTopicModal">
|
<button class="btn btn-add" data-bs-toggle="modal" data-bs-target="#addTopicModal">
|
||||||
@@ -420,7 +604,10 @@ $topics = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
$('#tabellaTopics').DataTable({
|
$('#tabellaTopics').DataTable({
|
||||||
order: [[5, 'asc'], [1, 'asc']],
|
order: [
|
||||||
|
[5, 'asc'],
|
||||||
|
[1, 'asc']
|
||||||
|
],
|
||||||
pageLength: 25,
|
pageLength: 25,
|
||||||
language: {
|
language: {
|
||||||
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json',
|
url: 'https://cdn.datatables.net/plug-ins/1.13.6/i18n/it-IT.json',
|
||||||
@@ -431,20 +618,34 @@ $topics = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
function ajaxPost(url, payload, successTitle, errorFallback) {
|
function ajaxPost(url, payload, successTitle, errorFallback) {
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
},
|
||||||
body: payload.toString()
|
body: payload.toString()
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
Swal.fire({ icon: "success", title: successTitle, confirmButtonColor: "#3085d6" })
|
Swal.fire({
|
||||||
|
icon: "success",
|
||||||
|
title: successTitle,
|
||||||
|
confirmButtonColor: "#3085d6"
|
||||||
|
})
|
||||||
.then(() => location.reload());
|
.then(() => location.reload());
|
||||||
} else {
|
} else {
|
||||||
Swal.fire({ icon: "error", title: "Errore", text: data.message || errorFallback });
|
Swal.fire({
|
||||||
|
icon: "error",
|
||||||
|
title: "Errore",
|
||||||
|
text: data.message || errorFallback
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
Swal.fire({ icon: "error", title: "Errore", text: "Errore di comunicazione." });
|
Swal.fire({
|
||||||
|
icon: "error",
|
||||||
|
title: "Errore",
|
||||||
|
text: "Errore di comunicazione."
|
||||||
|
});
|
||||||
console.error(err);
|
console.error(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -527,4 +728,5 @@ $topics = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
+391
-76
@@ -39,13 +39,22 @@ $where[] = "NOT EXISTS (
|
|||||||
AND (et2.completed_date > et.completed_date
|
AND (et2.completed_date > et.completed_date
|
||||||
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
|
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
|
||||||
)";
|
)";
|
||||||
if ($fEmployeeId > 0) { $where[] = 'et.employee_id = :eid'; $params['eid'] = $fEmployeeId; }
|
if ($fEmployeeId > 0) {
|
||||||
if ($fTopicId > 0) { $where[] = 'et.training_topic_id = :tid'; $params['tid'] = $fTopicId; }
|
$where[] = 'et.employee_id = :eid';
|
||||||
|
$params['eid'] = $fEmployeeId;
|
||||||
|
}
|
||||||
|
if ($fTopicId > 0) {
|
||||||
|
$where[] = 'et.training_topic_id = :tid';
|
||||||
|
$params['tid'] = $fTopicId;
|
||||||
|
}
|
||||||
if ($fType !== '' && in_array($fType, ['initial', 'refresher'], true)) {
|
if ($fType !== '' && in_array($fType, ['initial', 'refresher'], true)) {
|
||||||
$where[] = 'et.training_type = :ty';
|
$where[] = 'et.training_type = :ty';
|
||||||
$params['ty'] = $fType;
|
$params['ty'] = $fType;
|
||||||
}
|
}
|
||||||
if ($fDepartmentId > 0) { $where[] = 'e.department_id = :did'; $params['did'] = $fDepartmentId; }
|
if ($fDepartmentId > 0) {
|
||||||
|
$where[] = 'e.department_id = :did';
|
||||||
|
$params['did'] = $fDepartmentId;
|
||||||
|
}
|
||||||
$whereSql = $where ? ('WHERE ' . implode(' AND ', $where)) : '';
|
$whereSql = $where ? ('WHERE ' . implode(' AND ', $where)) : '';
|
||||||
|
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
@@ -66,7 +75,8 @@ $stmt->execute($params);
|
|||||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
/* Filter by computed status */
|
/* Filter by computed status */
|
||||||
function trainingStatus(?string $nextDue, ?int $reminderDays, ?int $topicDefaultRem): array {
|
function trainingStatus(?string $nextDue, ?int $reminderDays, ?int $topicDefaultRem): array
|
||||||
|
{
|
||||||
if (!$nextDue) {
|
if (!$nextDue) {
|
||||||
return ['code' => 'compliant', 'label' => 'Conforme', 'class' => 'success'];
|
return ['code' => 'compliant', 'label' => 'Conforme', 'class' => 'success'];
|
||||||
}
|
}
|
||||||
@@ -83,9 +93,11 @@ function trainingStatus(?string $nextDue, ?int $reminderDays, ?int $topicDefault
|
|||||||
$filtered = [];
|
$filtered = [];
|
||||||
$counters = ['compliant' => 0, 'due_soon' => 0, 'expired' => 0, 'not_present' => 0, 'all' => 0];
|
$counters = ['compliant' => 0, 'due_soon' => 0, 'expired' => 0, 'not_present' => 0, 'all' => 0];
|
||||||
foreach ($rows as $r) {
|
foreach ($rows as $r) {
|
||||||
$s = trainingStatus($r['next_due_date'] ?: null,
|
$s = trainingStatus(
|
||||||
|
$r['next_due_date'] ?: null,
|
||||||
$r['reminder_days'] !== null ? (int)$r['reminder_days'] : null,
|
$r['reminder_days'] !== null ? (int)$r['reminder_days'] : null,
|
||||||
$r['topic_default_rem'] !== null ? (int)$r['topic_default_rem'] : null);
|
$r['topic_default_rem'] !== null ? (int)$r['topic_default_rem'] : null
|
||||||
|
);
|
||||||
$r['_status'] = $s;
|
$r['_status'] = $s;
|
||||||
$counters['all']++;
|
$counters['all']++;
|
||||||
$counters[$s['code']] = ($counters[$s['code']] ?? 0) + 1;
|
$counters[$s['code']] = ($counters[$s['code']] ?? 0) + 1;
|
||||||
@@ -101,9 +113,18 @@ foreach ($rows as $r) {
|
|||||||
if ($fType === '' || $fType === 'initial') {
|
if ($fType === '' || $fType === 'initial') {
|
||||||
$missingWhere = [];
|
$missingWhere = [];
|
||||||
$missingParams = [];
|
$missingParams = [];
|
||||||
if ($fEmployeeId > 0) { $missingWhere[] = 'e.id = :eid'; $missingParams['eid'] = $fEmployeeId; }
|
if ($fEmployeeId > 0) {
|
||||||
if ($fTopicId > 0) { $missingWhere[] = 'tt.id = :tid'; $missingParams['tid'] = $fTopicId; }
|
$missingWhere[] = 'e.id = :eid';
|
||||||
if ($fDepartmentId > 0) { $missingWhere[] = 'e.department_id = :did'; $missingParams['did'] = $fDepartmentId; }
|
$missingParams['eid'] = $fEmployeeId;
|
||||||
|
}
|
||||||
|
if ($fTopicId > 0) {
|
||||||
|
$missingWhere[] = 'tt.id = :tid';
|
||||||
|
$missingParams['tid'] = $fTopicId;
|
||||||
|
}
|
||||||
|
if ($fDepartmentId > 0) {
|
||||||
|
$missingWhere[] = 'e.department_id = :did';
|
||||||
|
$missingParams['did'] = $fDepartmentId;
|
||||||
|
}
|
||||||
$missingWhereSql = $missingWhere ? ('AND ' . implode(' AND ', $missingWhere)) : '';
|
$missingWhereSql = $missingWhere ? ('AND ' . implode(' AND ', $missingWhere)) : '';
|
||||||
|
|
||||||
$missingStmt = $pdo->prepare("
|
$missingStmt = $pdo->prepare("
|
||||||
@@ -163,7 +184,8 @@ $departments = $pdo->query("
|
|||||||
SELECT id, name, color FROM departments WHERE is_active = 1 ORDER BY sort_order, name
|
SELECT id, name, color FROM departments WHERE is_active = 1 ORDER BY sort_order, name
|
||||||
")->fetchAll(PDO::FETCH_ASSOC);
|
")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
function fmtDate(?string $d): string {
|
function fmtDate(?string $d): string
|
||||||
|
{
|
||||||
if (!$d || $d === '0000-00-00') return '—';
|
if (!$d || $d === '0000-00-00') return '—';
|
||||||
$ts = strtotime($d);
|
$ts = strtotime($d);
|
||||||
return $ts ? date('d/m/Y', $ts) : '—';
|
return $ts ? date('d/m/Y', $ts) : '—';
|
||||||
@@ -186,53 +208,266 @@ function fmtDate(?string $d): string {
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body { font-size: 1.05rem; background: #f8fafc; }
|
body {
|
||||||
.card { border-radius: 16px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); }
|
font-size: 1.05rem;
|
||||||
.back-dashboard {
|
background: #f8fafc;
|
||||||
background-color: #cfe3ff !important; color: #1f2d3d !important;
|
|
||||||
border: 1px solid #bcd4f4 !important; border-radius: 10px;
|
|
||||||
font-weight: 600; padding: 10px 18px;
|
|
||||||
}
|
}
|
||||||
.stat-row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; margin-bottom: 20px; }
|
|
||||||
@media (max-width: 991.98px) { .stat-row { grid-template-columns: repeat(3, 1fr); } }
|
|
||||||
@media (max-width: 575.98px) { .stat-row { grid-template-columns: repeat(2, 1fr); } }
|
|
||||||
.stat-card {
|
|
||||||
border-radius: 14px; padding: 14px 16px; text-align: center;
|
|
||||||
background: #fff; box-shadow: 0 2px 6px rgba(0,0,0,.05);
|
|
||||||
cursor: pointer; transition: transform .15s;
|
|
||||||
}
|
|
||||||
.stat-card:hover { transform: translateY(-2px); }
|
|
||||||
.stat-card.active { outline: 3px solid #0d6efd; }
|
|
||||||
.stat-card .stat-num { font-size: 1.8rem; font-weight: 700; line-height: 1; }
|
|
||||||
.stat-card .stat-label { font-size: 0.85rem; color: #64748b; margin-top: 4px; }
|
|
||||||
.stat-card.all .stat-num { color: #1f2937; }
|
|
||||||
.stat-card.compliant .stat-num { color: #16a34a; }
|
|
||||||
.stat-card.due_soon .stat-num { color: #d97706; }
|
|
||||||
.stat-card.expired .stat-num { color: #dc2626; }
|
|
||||||
.stat-card.not_present .stat-num { color: #6b7280; }
|
|
||||||
|
|
||||||
.pill { display: inline-block; padding: 3px 10px; border-radius: 999px; font-size: 0.85rem; font-weight: 600; }
|
.card {
|
||||||
.pill-success { background: #d1fae5; color: #065f46; }
|
border-radius: 16px;
|
||||||
.pill-warning { background: #fef3c7; color: #92400e; }
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
.pill-danger { background: #fee2e2; color: #991b1b; }
|
}
|
||||||
.pill-secondary { background: #e5e7eb; color: #374151; }
|
|
||||||
.pill-role { background: #fff; color: #334155; border: 1px solid #cbd5e1; }
|
.back-dashboard {
|
||||||
.pill-dept-inline { padding: 2px 8px; }
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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-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,
|
||||||
|
.training-header-actions a {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
.stat-row {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 575.98px) {
|
||||||
|
.stat-row {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
text-align: center;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, .05);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform .15s, box-shadow .15s, border-color .15s;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 14px rgba(0, 0, 0, .10);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.active {
|
||||||
|
outline: 3px solid #0d6efd;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .stat-num {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .stat-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Totale */
|
||||||
|
.stat-card.all {
|
||||||
|
background: #f8fafc;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.all .stat-num,
|
||||||
|
.stat-card.all .stat-label {
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Conforme - verde */
|
||||||
|
.stat-card.compliant {
|
||||||
|
background: #dcfce7;
|
||||||
|
border-color: #86efac;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.compliant .stat-num,
|
||||||
|
.stat-card.compliant .stat-label {
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Da aggiornare - arancio */
|
||||||
|
.stat-card.due_soon {
|
||||||
|
background: #ffedd5;
|
||||||
|
border-color: #fdba74;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.due_soon .stat-num,
|
||||||
|
.stat-card.due_soon .stat-label {
|
||||||
|
color: #9a3412;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scaduti - rosso */
|
||||||
|
.stat-card.expired {
|
||||||
|
background: #fee2e2;
|
||||||
|
border-color: #fca5a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.expired .stat-num,
|
||||||
|
.stat-card.expired .stat-label {
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Non presenti - rosso diverso / bordeaux */
|
||||||
|
.stat-card.not_present {
|
||||||
|
background: #fce7f3;
|
||||||
|
border-color: #f9a8d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.not_present .stat-num,
|
||||||
|
.stat-card.not_present .stat-label {
|
||||||
|
color: #9d174d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-success {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-warning {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-danger {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-secondary {
|
||||||
|
background: #e5e7eb;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-role {
|
||||||
|
background: #fff;
|
||||||
|
color: #334155;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill-dept-inline {
|
||||||
|
padding: 2px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.tr-card {
|
.tr-card {
|
||||||
border: 1px solid #e2e8f0; border-radius: 14px;
|
border: 1px solid #e2e8f0;
|
||||||
padding: 14px 16px; margin-bottom: 12px;
|
border-radius: 14px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
|
||||||
}
|
}
|
||||||
.tr-card .name a { color: #1f2937; font-weight: 600; text-decoration: none; }
|
|
||||||
.tr-card .topic { color: #475569; }
|
.tr-card .name a {
|
||||||
.tr-card .meta { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 0.85rem; color: #64748b; margin-top: 8px; }
|
color: #1f2937;
|
||||||
.tr-card .meta b { color: #1f2937; font-weight: 600; }
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-card .topic {
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-card .meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px 14px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #64748b;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-card .meta b {
|
||||||
|
color: #1f2937;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
.card-header { flex-direction: column; align-items: flex-start !important; gap: .5rem; }
|
.card-header {
|
||||||
.back-dashboard { width: 100%; }
|
flex-direction: column;
|
||||||
|
align-items: flex-start !important;
|
||||||
|
gap: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-dashboard {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -246,11 +481,25 @@ function fmtDate(?string $d): string {
|
|||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<div class="card p-3">
|
<div class="card p-3">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||||
<h5 class="mb-0">📚 Storico Formazione</h5>
|
<h5 class="mb-0">📚 Gestione Formazione</h5>
|
||||||
<div class="d-flex gap-2 flex-wrap">
|
|
||||||
|
<div class="training-header-actions">
|
||||||
<button type="button" class="btn btn-primary" id="btnBulkTraining">
|
<button type="button" class="btn btn-primary" id="btnBulkTraining">
|
||||||
➕ Aggiungi sessione
|
➕ Aggiungi sessione
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<?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="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'">
|
<button type="button" class="btn back-dashboard" onclick="location.href='production_dashboard.php'">
|
||||||
↩️ Torna alla Dashboard
|
↩️ Torna alla Dashboard
|
||||||
</button>
|
</button>
|
||||||
@@ -514,7 +763,9 @@ function fmtDate(?string $d): string {
|
|||||||
<textarea id="bulkDescription" class="form-control" rows="2"></textarea>
|
<textarea id="bulkDescription" class="form-control" rows="2"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12"><hr class="my-1"></div>
|
<div class="col-12">
|
||||||
|
<hr class="my-1">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label class="form-label fw-semibold">Dipendenti <span class="text-danger">*</span></label>
|
<label class="form-label fw-semibold">Dipendenti <span class="text-danger">*</span></label>
|
||||||
@@ -600,7 +851,9 @@ function fmtDate(?string $d): string {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('bulkSelectAll').addEventListener('click', function() {
|
document.getElementById('bulkSelectAll').addEventListener('click', function() {
|
||||||
var all = $emp.find('option').map(function() { return this.value; }).get();
|
var all = $emp.find('option').map(function() {
|
||||||
|
return this.value;
|
||||||
|
}).get();
|
||||||
$emp.val(all).trigger('change');
|
$emp.val(all).trigger('change');
|
||||||
});
|
});
|
||||||
document.getElementById('bulkClear').addEventListener('click', function() {
|
document.getElementById('bulkClear').addEventListener('click', function() {
|
||||||
@@ -613,9 +866,18 @@ function fmtDate(?string $d): string {
|
|||||||
var completed = document.getElementById('bulkCompletedDate').value;
|
var completed = document.getElementById('bulkCompletedDate').value;
|
||||||
var emps = $emp.val() || [];
|
var emps = $emp.val() || [];
|
||||||
|
|
||||||
if (!topicId) { Swal.fire('Attenzione', 'Selezionare un corso.', 'warning'); return; }
|
if (!topicId) {
|
||||||
if (!completed) { Swal.fire('Attenzione', 'Indicare la data di completamento.', 'warning'); return; }
|
Swal.fire('Attenzione', 'Selezionare un corso.', 'warning');
|
||||||
if (emps.length === 0) { Swal.fire('Attenzione', 'Selezionare almeno un dipendente.', 'warning'); return; }
|
return;
|
||||||
|
}
|
||||||
|
if (!completed) {
|
||||||
|
Swal.fire('Attenzione', 'Indicare la data di completamento.', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (emps.length === 0) {
|
||||||
|
Swal.fire('Attenzione', 'Selezionare almeno un dipendente.', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var btn = document.getElementById('bulkSaveBtn');
|
var btn = document.getElementById('bulkSaveBtn');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
@@ -630,22 +892,39 @@ function fmtDate(?string $d): string {
|
|||||||
fd.append('description', document.getElementById('bulkDescription').value);
|
fd.append('description', document.getElementById('bulkDescription').value);
|
||||||
fd.append('update_frequency_months', document.getElementById('bulkFreq').value);
|
fd.append('update_frequency_months', document.getElementById('bulkFreq').value);
|
||||||
fd.append('reminder_days', document.getElementById('bulkRem').value);
|
fd.append('reminder_days', document.getElementById('bulkRem').value);
|
||||||
emps.forEach(function(id) { fd.append('employee_ids[]', id); });
|
emps.forEach(function(id) {
|
||||||
|
fd.append('employee_ids[]', id);
|
||||||
|
});
|
||||||
|
|
||||||
fetch('ajax/trainings/save_bulk_training.php', { method: 'POST', body: fd })
|
fetch('ajax/trainings/save_bulk_training.php', {
|
||||||
.then(function(r) { return r.json(); })
|
method: 'POST',
|
||||||
|
body: fd
|
||||||
|
})
|
||||||
|
.then(function(r) {
|
||||||
|
return r.json();
|
||||||
|
})
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
bulkModal.hide();
|
bulkModal.hide();
|
||||||
Swal.fire({ icon: 'success', title: 'Fatto', text: data.message, timer: 1800, showConfirmButton: false })
|
Swal.fire({
|
||||||
.then(function() { location.reload(); });
|
icon: 'success',
|
||||||
|
title: 'Fatto',
|
||||||
|
text: data.message,
|
||||||
|
timer: 1800,
|
||||||
|
showConfirmButton: false
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
btn.disabled = false; btn.innerHTML = orig;
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = orig;
|
||||||
Swal.fire('Errore', data.message || 'Errore.', 'error');
|
Swal.fire('Errore', data.message || 'Errore.', 'error');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function() {
|
.catch(function() {
|
||||||
btn.disabled = false; btn.innerHTML = orig;
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = orig;
|
||||||
Swal.fire('Errore', 'Errore di connessione.', 'error');
|
Swal.fire('Errore', 'Errore di connessione.', 'error');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -682,14 +961,22 @@ function fmtDate(?string $d): string {
|
|||||||
|
|
||||||
function checkedIds() {
|
function checkedIds() {
|
||||||
return Array.prototype.slice.call(document.querySelectorAll('.row-check:checked'))
|
return Array.prototype.slice.call(document.querySelectorAll('.row-check:checked'))
|
||||||
.map(function(c) { return c.value; });
|
.map(function(c) {
|
||||||
|
return c.value;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshBulkBar() {
|
function refreshBulkBar() {
|
||||||
var ids = checkedIds();
|
var ids = checkedIds();
|
||||||
var bar = document.getElementById('bulkBar');
|
var bar = document.getElementById('bulkBar');
|
||||||
document.getElementById('bulkSelCount').textContent = ids.length;
|
document.getElementById('bulkSelCount').textContent = ids.length;
|
||||||
if (ids.length > 0) { bar.classList.remove('d-none'); bar.classList.add('d-flex'); }
|
if (ids.length > 0) {
|
||||||
else { bar.classList.add('d-none'); bar.classList.remove('d-flex'); }
|
bar.classList.remove('d-none');
|
||||||
|
bar.classList.add('d-flex');
|
||||||
|
} else {
|
||||||
|
bar.classList.add('d-none');
|
||||||
|
bar.classList.remove('d-flex');
|
||||||
|
}
|
||||||
var all = document.querySelectorAll('.row-check');
|
var all = document.querySelectorAll('.row-check');
|
||||||
if (checkAll) checkAll.checked = (all.length > 0 && ids.length === all.length);
|
if (checkAll) checkAll.checked = (all.length > 0 && ids.length === all.length);
|
||||||
}
|
}
|
||||||
@@ -698,11 +985,15 @@ function fmtDate(?string $d): string {
|
|||||||
if (e.target && e.target.classList && e.target.classList.contains('row-check')) refreshBulkBar();
|
if (e.target && e.target.classList && e.target.classList.contains('row-check')) refreshBulkBar();
|
||||||
});
|
});
|
||||||
if (checkAll) checkAll.addEventListener('change', function() {
|
if (checkAll) checkAll.addEventListener('change', function() {
|
||||||
document.querySelectorAll('.row-check').forEach(function(c) { c.checked = checkAll.checked; });
|
document.querySelectorAll('.row-check').forEach(function(c) {
|
||||||
|
c.checked = checkAll.checked;
|
||||||
|
});
|
||||||
refreshBulkBar();
|
refreshBulkBar();
|
||||||
});
|
});
|
||||||
document.getElementById('btnBulkDeselect').addEventListener('click', function() {
|
document.getElementById('btnBulkDeselect').addEventListener('click', function() {
|
||||||
document.querySelectorAll('.row-check').forEach(function(c) { c.checked = false; });
|
document.querySelectorAll('.row-check').forEach(function(c) {
|
||||||
|
c.checked = false;
|
||||||
|
});
|
||||||
if (checkAll) checkAll.checked = false;
|
if (checkAll) checkAll.checked = false;
|
||||||
refreshBulkBar();
|
refreshBulkBar();
|
||||||
});
|
});
|
||||||
@@ -718,8 +1009,14 @@ function fmtDate(?string $d): string {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var ids = checkedIds();
|
var ids = checkedIds();
|
||||||
var date = document.getElementById('renewDate').value;
|
var date = document.getElementById('renewDate').value;
|
||||||
if (ids.length === 0) { renewModal.hide(); return; }
|
if (ids.length === 0) {
|
||||||
if (!date) { Swal.fire('Attenzione', 'Indicare la data.', 'warning'); return; }
|
renewModal.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!date) {
|
||||||
|
Swal.fire('Attenzione', 'Indicare la data.', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var btn = document.getElementById('renewSaveBtn');
|
var btn = document.getElementById('renewSaveBtn');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
@@ -728,26 +1025,44 @@ function fmtDate(?string $d): string {
|
|||||||
|
|
||||||
var fd = new FormData();
|
var fd = new FormData();
|
||||||
fd.append('completed_date', date);
|
fd.append('completed_date', date);
|
||||||
ids.forEach(function(id) { fd.append('training_ids[]', id); });
|
ids.forEach(function(id) {
|
||||||
|
fd.append('training_ids[]', id);
|
||||||
|
});
|
||||||
|
|
||||||
fetch('ajax/trainings/bulk_update_deadline.php', { method: 'POST', body: fd })
|
fetch('ajax/trainings/bulk_update_deadline.php', {
|
||||||
.then(function(r) { return r.json(); })
|
method: 'POST',
|
||||||
|
body: fd
|
||||||
|
})
|
||||||
|
.then(function(r) {
|
||||||
|
return r.json();
|
||||||
|
})
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
renewModal.hide();
|
renewModal.hide();
|
||||||
Swal.fire({ icon: 'success', title: 'Fatto', text: data.message, timer: 1800, showConfirmButton: false })
|
Swal.fire({
|
||||||
.then(function() { location.reload(); });
|
icon: 'success',
|
||||||
|
title: 'Fatto',
|
||||||
|
text: data.message,
|
||||||
|
timer: 1800,
|
||||||
|
showConfirmButton: false
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
btn.disabled = false; btn.innerHTML = orig;
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = orig;
|
||||||
Swal.fire('Errore', data.message || 'Errore.', 'error');
|
Swal.fire('Errore', data.message || 'Errore.', 'error');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function() {
|
.catch(function() {
|
||||||
btn.disabled = false; btn.innerHTML = orig;
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = orig;
|
||||||
Swal.fire('Errore', 'Errore di connessione.', 'error');
|
Swal.fire('Errore', 'Errore di connessione.', 'error');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
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 |
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,157 @@
|
|||||||
|
from flask import Flask, request, jsonify
|
||||||
|
from flask_cors import CORS
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from cad_vector_area import calculate_pdf_vector_area
|
||||||
|
from auto_contour import propose_contour_from_image_bytes
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
CORS(app)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/health", methods=["GET"])
|
||||||
|
def health():
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"message": "Python CAD Area service is running"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def get_float_or_none(name):
|
||||||
|
value = request.form.get(name, "").strip()
|
||||||
|
|
||||||
|
if value == "":
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return float(value)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_int_or_default(name, default=1):
|
||||||
|
value = request.form.get(name, "").strip()
|
||||||
|
|
||||||
|
if value == "":
|
||||||
|
return default
|
||||||
|
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except ValueError:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/calculate", methods=["POST"])
|
||||||
|
def calculate():
|
||||||
|
try:
|
||||||
|
if "file" not in request.files:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": "No PDF file received"
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
uploaded_file = request.files["file"]
|
||||||
|
|
||||||
|
if uploaded_file.filename == "":
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": "Empty filename"
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
if not uploaded_file.filename.lower().endswith(".pdf"):
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": "Only PDF files are allowed"
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
pdf_bytes = uploaded_file.read()
|
||||||
|
|
||||||
|
scale_ratio = get_float_or_none("scale_ratio")
|
||||||
|
|
||||||
|
if scale_ratio is not None and scale_ratio <= 0:
|
||||||
|
scale_ratio = None
|
||||||
|
|
||||||
|
roi = {
|
||||||
|
"x": get_float_or_none("roi_x"),
|
||||||
|
"y": get_float_or_none("roi_y"),
|
||||||
|
"width": get_float_or_none("roi_width"),
|
||||||
|
"height": get_float_or_none("roi_height"),
|
||||||
|
"page": get_int_or_default("roi_page", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
has_roi = (
|
||||||
|
roi["x"] is not None and
|
||||||
|
roi["y"] is not None and
|
||||||
|
roi["width"] is not None and
|
||||||
|
roi["height"] is not None and
|
||||||
|
roi["width"] > 0 and
|
||||||
|
roi["height"] > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
mode = request.form.get("mode", "auto_roi")
|
||||||
|
|
||||||
|
result = calculate_pdf_vector_area(
|
||||||
|
pdf_bytes=pdf_bytes,
|
||||||
|
filename=uploaded_file.filename,
|
||||||
|
scale_ratio=scale_ratio,
|
||||||
|
profile_color=None,
|
||||||
|
roi=roi if has_roi else None,
|
||||||
|
mode=mode
|
||||||
|
)
|
||||||
|
|
||||||
|
status_code = 200 if result.get("success") else 422
|
||||||
|
|
||||||
|
return jsonify(result), status_code
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": str(e),
|
||||||
|
"trace": traceback.format_exc()
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/auto-contour-image", methods=["POST"])
|
||||||
|
def auto_contour_image():
|
||||||
|
try:
|
||||||
|
if "image" not in request.files:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": "No image received"
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
uploaded_image = request.files["image"]
|
||||||
|
image_bytes = uploaded_image.read()
|
||||||
|
|
||||||
|
max_points_raw = request.form.get("max_points", "90").strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
max_points = int(max_points_raw)
|
||||||
|
except ValueError:
|
||||||
|
max_points = 90
|
||||||
|
|
||||||
|
max_points = max(12, min(max_points, 250))
|
||||||
|
|
||||||
|
result = propose_contour_from_image_bytes(
|
||||||
|
image_bytes=image_bytes,
|
||||||
|
max_points=max_points
|
||||||
|
)
|
||||||
|
|
||||||
|
status_code = 200 if result.get("success") else 422
|
||||||
|
|
||||||
|
return jsonify(result), status_code
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": str(e),
|
||||||
|
"trace": traceback.format_exc()
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(
|
||||||
|
host="127.0.0.1",
|
||||||
|
port=5055,
|
||||||
|
debug=True
|
||||||
|
)
|
||||||
@@ -0,0 +1,404 @@
|
|||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_contour(contour, width, height):
|
||||||
|
points = contour.reshape(-1, 2)
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"x": round(float(x) / float(width), 8),
|
||||||
|
"y": round(float(y) / float(height), 8),
|
||||||
|
}
|
||||||
|
for x, y in points
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _simplify_contour(contour, max_points=120):
|
||||||
|
if contour is None or len(contour) < 3:
|
||||||
|
return contour
|
||||||
|
|
||||||
|
perimeter = cv2.arcLength(contour, True)
|
||||||
|
|
||||||
|
if perimeter <= 0:
|
||||||
|
return contour
|
||||||
|
|
||||||
|
epsilon = max(0.8, perimeter * 0.0025)
|
||||||
|
simplified = cv2.approxPolyDP(contour, epsilon, True)
|
||||||
|
|
||||||
|
while len(simplified) > max_points and epsilon < perimeter * 0.06:
|
||||||
|
epsilon *= 1.25
|
||||||
|
simplified = cv2.approxPolyDP(contour, epsilon, True)
|
||||||
|
|
||||||
|
if simplified is None or len(simplified) < 3:
|
||||||
|
return contour
|
||||||
|
|
||||||
|
return simplified
|
||||||
|
|
||||||
|
|
||||||
|
def _contour_score(contour, image_area, width, height):
|
||||||
|
area = abs(cv2.contourArea(contour))
|
||||||
|
|
||||||
|
if area <= 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
x, y, w, h = cv2.boundingRect(contour)
|
||||||
|
|
||||||
|
if w < 8 or h < 8:
|
||||||
|
return None
|
||||||
|
|
||||||
|
bbox_area = w * h
|
||||||
|
|
||||||
|
if bbox_area <= 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
area_ratio = area / image_area
|
||||||
|
bbox_ratio = bbox_area / image_area
|
||||||
|
fill_ratio = area / bbox_area
|
||||||
|
aspect = max(w, h) / max(1, min(w, h))
|
||||||
|
|
||||||
|
# Too small: usually dots/noise.
|
||||||
|
if area_ratio < 0.0004:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Too large: usually background / page.
|
||||||
|
if area_ratio > 0.96 or bbox_ratio > 0.98:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Very thin: usually dimensions/text lines.
|
||||||
|
if aspect > 40:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Prefer large compact-ish shapes, but allow irregular profiles.
|
||||||
|
score = area
|
||||||
|
|
||||||
|
if 0.08 <= fill_ratio <= 0.95:
|
||||||
|
score *= 1.25
|
||||||
|
|
||||||
|
# Penalize contours glued to the border because they are often crop/background artifacts.
|
||||||
|
border_touch = (
|
||||||
|
x <= 1 or
|
||||||
|
y <= 1 or
|
||||||
|
x + w >= width - 2 or
|
||||||
|
y + h >= height - 2
|
||||||
|
)
|
||||||
|
|
||||||
|
if border_touch:
|
||||||
|
score *= 0.65
|
||||||
|
|
||||||
|
return {
|
||||||
|
"score": score,
|
||||||
|
"area": area,
|
||||||
|
"bbox": (x, y, w, h),
|
||||||
|
"area_ratio": area_ratio,
|
||||||
|
"bbox_ratio": bbox_ratio,
|
||||||
|
"fill_ratio": fill_ratio,
|
||||||
|
"aspect": aspect,
|
||||||
|
"border_touch": border_touch,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _best_contour_from_mask(mask, image_area, width, height, mode_name):
|
||||||
|
contours, _hierarchy = cv2.findContours(
|
||||||
|
mask,
|
||||||
|
cv2.RETR_EXTERNAL,
|
||||||
|
cv2.CHAIN_APPROX_SIMPLE
|
||||||
|
)
|
||||||
|
|
||||||
|
candidates = []
|
||||||
|
|
||||||
|
for contour in contours:
|
||||||
|
info = _contour_score(contour, image_area, width, height)
|
||||||
|
|
||||||
|
if info is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
candidates.append((info["score"], contour, info))
|
||||||
|
|
||||||
|
if not candidates:
|
||||||
|
return None, {
|
||||||
|
"mode": mode_name,
|
||||||
|
"contours_total": len(contours),
|
||||||
|
"candidates": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates.sort(key=lambda item: item[0], reverse=True)
|
||||||
|
|
||||||
|
best_score, best_contour, best_info = candidates[0]
|
||||||
|
|
||||||
|
return best_contour, {
|
||||||
|
"mode": mode_name,
|
||||||
|
"contours_total": len(contours),
|
||||||
|
"candidates": len(candidates),
|
||||||
|
"selected": {
|
||||||
|
"score": round(float(best_score), 3),
|
||||||
|
"area": round(float(best_info["area"]), 3),
|
||||||
|
"bbox": {
|
||||||
|
"x": int(best_info["bbox"][0]),
|
||||||
|
"y": int(best_info["bbox"][1]),
|
||||||
|
"width": int(best_info["bbox"][2]),
|
||||||
|
"height": int(best_info["bbox"][3]),
|
||||||
|
},
|
||||||
|
"area_ratio": round(float(best_info["area_ratio"]), 5),
|
||||||
|
"bbox_ratio": round(float(best_info["bbox_ratio"]), 5),
|
||||||
|
"fill_ratio": round(float(best_info["fill_ratio"]), 5),
|
||||||
|
"aspect": round(float(best_info["aspect"]), 3),
|
||||||
|
"border_touch": bool(best_info["border_touch"]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_small_components(mask, min_area):
|
||||||
|
num_labels, labels, stats, _centroids = cv2.connectedComponentsWithStats(mask, connectivity=8)
|
||||||
|
|
||||||
|
output = np.zeros_like(mask)
|
||||||
|
|
||||||
|
for label in range(1, num_labels):
|
||||||
|
area = stats[label, cv2.CC_STAT_AREA]
|
||||||
|
|
||||||
|
if area >= min_area:
|
||||||
|
output[labels == label] = 255
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def _make_masks(image):
|
||||||
|
height, width = image.shape[:2]
|
||||||
|
image_area = width * height
|
||||||
|
|
||||||
|
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
||||||
|
|
||||||
|
# Slight blur to reduce antialias noise.
|
||||||
|
blurred = cv2.GaussianBlur(gray, (3, 3), 0)
|
||||||
|
|
||||||
|
# Dark ink mask: lines, hatches, dots, technical strokes.
|
||||||
|
mask_dark_245 = cv2.inRange(blurred, 0, 245)
|
||||||
|
mask_dark_235 = cv2.inRange(blurred, 0, 235)
|
||||||
|
mask_dark_220 = cv2.inRange(blurred, 0, 220)
|
||||||
|
|
||||||
|
# Otsu inverse.
|
||||||
|
_t, mask_otsu = cv2.threshold(
|
||||||
|
blurred,
|
||||||
|
0,
|
||||||
|
255,
|
||||||
|
cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU
|
||||||
|
)
|
||||||
|
|
||||||
|
# Adaptive threshold helps on scans with grey background.
|
||||||
|
mask_adaptive = cv2.adaptiveThreshold(
|
||||||
|
blurred,
|
||||||
|
255,
|
||||||
|
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
||||||
|
cv2.THRESH_BINARY_INV,
|
||||||
|
31,
|
||||||
|
7
|
||||||
|
)
|
||||||
|
|
||||||
|
base_mask = cv2.bitwise_or(mask_dark_235, mask_otsu)
|
||||||
|
base_mask = cv2.bitwise_or(base_mask, mask_adaptive)
|
||||||
|
|
||||||
|
min_component_area = max(6, int(image_area * 0.00002))
|
||||||
|
base_mask = _remove_small_components(base_mask, min_component_area)
|
||||||
|
|
||||||
|
kernel_3 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
|
||||||
|
kernel_5 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
|
||||||
|
kernel_9 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9, 9))
|
||||||
|
kernel_13 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (13, 13))
|
||||||
|
kernel_21 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (21, 21))
|
||||||
|
|
||||||
|
masks = []
|
||||||
|
|
||||||
|
# Strategy 1: normal dark mask, light close.
|
||||||
|
m1 = cv2.morphologyEx(base_mask, cv2.MORPH_CLOSE, kernel_5, iterations=1)
|
||||||
|
m1 = cv2.morphologyEx(m1, cv2.MORPH_OPEN, kernel_3, iterations=1)
|
||||||
|
masks.append(("dark_close_5", m1))
|
||||||
|
|
||||||
|
# Strategy 2: stronger close for broken profile lines / dotted hatches.
|
||||||
|
m2 = cv2.morphologyEx(base_mask, cv2.MORPH_CLOSE, kernel_9, iterations=2)
|
||||||
|
m2 = cv2.morphologyEx(m2, cv2.MORPH_OPEN, kernel_3, iterations=1)
|
||||||
|
masks.append(("dark_close_9x2", m2))
|
||||||
|
|
||||||
|
# Strategy 3: very strong close, useful when profile is made of dots/hatches.
|
||||||
|
m3 = cv2.morphologyEx(base_mask, cv2.MORPH_CLOSE, kernel_13, iterations=2)
|
||||||
|
m3 = cv2.morphologyEx(m3, cv2.MORPH_OPEN, kernel_5, iterations=1)
|
||||||
|
masks.append(("dark_close_13x2", m3))
|
||||||
|
|
||||||
|
# Strategy 4: Canny edges closed.
|
||||||
|
edges = cv2.Canny(blurred, 60, 180)
|
||||||
|
e1 = cv2.dilate(edges, kernel_3, iterations=1)
|
||||||
|
e1 = cv2.morphologyEx(e1, cv2.MORPH_CLOSE, kernel_9, iterations=2)
|
||||||
|
masks.append(("canny_close_9x2", e1))
|
||||||
|
|
||||||
|
# Strategy 5: flood fill from closed boundaries.
|
||||||
|
boundary = cv2.dilate(mask_dark_245, kernel_3, iterations=1)
|
||||||
|
boundary = cv2.morphologyEx(boundary, cv2.MORPH_CLOSE, kernel_9, iterations=2)
|
||||||
|
|
||||||
|
passable = cv2.bitwise_not(boundary)
|
||||||
|
flood = passable.copy()
|
||||||
|
flood_mask = np.zeros((height + 2, width + 2), dtype=np.uint8)
|
||||||
|
|
||||||
|
for x in range(width):
|
||||||
|
if flood[0, x] > 0:
|
||||||
|
cv2.floodFill(flood, flood_mask, (x, 0), 128)
|
||||||
|
if flood[height - 1, x] > 0:
|
||||||
|
cv2.floodFill(flood, flood_mask, (x, height - 1), 128)
|
||||||
|
|
||||||
|
for y in range(height):
|
||||||
|
if flood[y, 0] > 0:
|
||||||
|
cv2.floodFill(flood, flood_mask, (0, y), 128)
|
||||||
|
if flood[y, width - 1] > 0:
|
||||||
|
cv2.floodFill(flood, flood_mask, (width - 1, y), 128)
|
||||||
|
|
||||||
|
outside = (flood == 128).astype(np.uint8) * 255
|
||||||
|
enclosed = cv2.bitwise_not(outside)
|
||||||
|
|
||||||
|
enclosed[0, :] = 0
|
||||||
|
enclosed[-1, :] = 0
|
||||||
|
enclosed[:, 0] = 0
|
||||||
|
enclosed[:, -1] = 0
|
||||||
|
|
||||||
|
enclosed = cv2.morphologyEx(enclosed, cv2.MORPH_OPEN, kernel_5, iterations=1)
|
||||||
|
masks.append(("flood_enclosed", enclosed))
|
||||||
|
|
||||||
|
# Strategy 6: if everything is sparse, glue nearby strokes aggressively.
|
||||||
|
m6 = cv2.morphologyEx(mask_dark_220, cv2.MORPH_CLOSE, kernel_21, iterations=1)
|
||||||
|
m6 = cv2.morphologyEx(m6, cv2.MORPH_OPEN, kernel_5, iterations=1)
|
||||||
|
masks.append(("aggressive_close_21", m6))
|
||||||
|
|
||||||
|
return masks, {
|
||||||
|
"gray_mean": round(float(gray.mean()), 3),
|
||||||
|
"base_mask_pixels": int((base_mask > 0).sum()),
|
||||||
|
"image_width": width,
|
||||||
|
"image_height": height,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def propose_contour_from_image_bytes(image_bytes, max_points=120):
|
||||||
|
"""
|
||||||
|
Receives a PNG/JPG image of the currently visible ROI canvas and returns
|
||||||
|
a proposed outer contour as normalized x/y points.
|
||||||
|
|
||||||
|
This is a proposal only. The frontend must allow editing.
|
||||||
|
"""
|
||||||
|
np_buffer = np.frombuffer(image_bytes, dtype=np.uint8)
|
||||||
|
image = cv2.imdecode(np_buffer, cv2.IMREAD_COLOR)
|
||||||
|
|
||||||
|
if image is None:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "Immagine non valida o non decodificabile."
|
||||||
|
}
|
||||||
|
|
||||||
|
height, width = image.shape[:2]
|
||||||
|
|
||||||
|
if width < 20 or height < 20:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "Immagine troppo piccola per il riconoscimento del contorno."
|
||||||
|
}
|
||||||
|
|
||||||
|
image_area = width * height
|
||||||
|
|
||||||
|
masks, base_diag = _make_masks(image)
|
||||||
|
|
||||||
|
attempts = []
|
||||||
|
best_global = None
|
||||||
|
|
||||||
|
for mode_name, mask in masks:
|
||||||
|
contour, diag = _best_contour_from_mask(
|
||||||
|
mask=mask,
|
||||||
|
image_area=image_area,
|
||||||
|
width=width,
|
||||||
|
height=height,
|
||||||
|
mode_name=mode_name
|
||||||
|
)
|
||||||
|
|
||||||
|
diag["mask_pixels"] = int((mask > 0).sum())
|
||||||
|
attempts.append(diag)
|
||||||
|
|
||||||
|
if contour is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
info = _contour_score(contour, image_area, width, height)
|
||||||
|
|
||||||
|
if info is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
score = info["score"]
|
||||||
|
|
||||||
|
if best_global is None or score > best_global["score"]:
|
||||||
|
best_global = {
|
||||||
|
"score": score,
|
||||||
|
"contour": contour,
|
||||||
|
"mode": mode_name,
|
||||||
|
"info": info,
|
||||||
|
"mask_pixels": int((mask > 0).sum()),
|
||||||
|
}
|
||||||
|
|
||||||
|
if best_global is None:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "Nessun contorno plausibile trovato. Prova una ROI più stretta o procedi manualmente.",
|
||||||
|
"diagnostics": {
|
||||||
|
**base_diag,
|
||||||
|
"attempts": attempts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
simplified = _simplify_contour(best_global["contour"], max_points=max_points)
|
||||||
|
|
||||||
|
if simplified is None or len(simplified) < 3:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "Il contorno trovato non ha abbastanza punti validi.",
|
||||||
|
"diagnostics": {
|
||||||
|
**base_diag,
|
||||||
|
"selected_mode": best_global["mode"],
|
||||||
|
"attempts": attempts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
area_px2 = float(abs(cv2.contourArea(simplified)))
|
||||||
|
x, y, w, h = cv2.boundingRect(simplified)
|
||||||
|
|
||||||
|
# Defensive check.
|
||||||
|
if area_px2 <= 0:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "Il contorno trovato ha area nulla.",
|
||||||
|
"diagnostics": {
|
||||||
|
**base_diag,
|
||||||
|
"selected_mode": best_global["mode"],
|
||||||
|
"attempts": attempts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Contorno proposto correttamente.",
|
||||||
|
"outer_polygon": _normalize_contour(simplified, width, height),
|
||||||
|
"holes": [],
|
||||||
|
"diagnostics": {
|
||||||
|
**base_diag,
|
||||||
|
"selected_mode": best_global["mode"],
|
||||||
|
"points_count": int(len(simplified)),
|
||||||
|
"area_px2": round(area_px2, 3),
|
||||||
|
"bbox": {
|
||||||
|
"x": int(x),
|
||||||
|
"y": int(y),
|
||||||
|
"width": int(w),
|
||||||
|
"height": int(h)
|
||||||
|
},
|
||||||
|
"selected": {
|
||||||
|
"score": round(float(best_global["score"]), 3),
|
||||||
|
"area": round(float(best_global["info"]["area"]), 3),
|
||||||
|
"area_ratio": round(float(best_global["info"]["area_ratio"]), 5),
|
||||||
|
"bbox_ratio": round(float(best_global["info"]["bbox_ratio"]), 5),
|
||||||
|
"fill_ratio": round(float(best_global["info"]["fill_ratio"]), 5),
|
||||||
|
"aspect": round(float(best_global["info"]["aspect"]), 3),
|
||||||
|
"border_touch": bool(best_global["info"]["border_touch"]),
|
||||||
|
"mask_pixels": int(best_global["mask_pixels"]),
|
||||||
|
},
|
||||||
|
"attempts": attempts,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,776 @@
|
|||||||
|
import math
|
||||||
|
import re
|
||||||
|
from collections import Counter, deque
|
||||||
|
|
||||||
|
import fitz # PyMuPDF
|
||||||
|
import numpy as np
|
||||||
|
from shapely.geometry import Polygon
|
||||||
|
from shapely.ops import unary_union
|
||||||
|
from shapely.validation import make_valid
|
||||||
|
|
||||||
|
|
||||||
|
POINT_TO_MM = 25.4 / 72.0
|
||||||
|
DEFAULT_RENDER_ZOOM = 8.0
|
||||||
|
MAX_RENDER_SIDE_PX = 3200
|
||||||
|
STITCH_TOLERANCE = 1.2
|
||||||
|
MAX_ASPECT_RATIO = 80
|
||||||
|
|
||||||
|
|
||||||
|
_SCALE_PATTERN = re.compile(
|
||||||
|
r'(?:scale|echelle|échelle|scala|masstab|escala)?\s*'
|
||||||
|
r'(\d+(?:\.\d+)?)\s*[:/]\s*(\d+(?:\.\d+)?)',
|
||||||
|
re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _pt(point):
|
||||||
|
return float(point.x), float(point.y)
|
||||||
|
|
||||||
|
|
||||||
|
def _dist(a, b):
|
||||||
|
return math.hypot(a[0] - b[0], a[1] - b[1])
|
||||||
|
|
||||||
|
|
||||||
|
def _color_close(c1, c2, tol=0.05):
|
||||||
|
if c1 is None or c2 is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if len(c1) == 4:
|
||||||
|
c1 = c1[:3]
|
||||||
|
|
||||||
|
if len(c2) == 4:
|
||||||
|
c2 = c2[:3]
|
||||||
|
|
||||||
|
if len(c1) != len(c2):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return all(abs(float(a) - float(b)) <= tol for a, b in zip(c1, c2))
|
||||||
|
|
||||||
|
|
||||||
|
def _cubic_bezier(p0, p1, p2, p3, steps=24):
|
||||||
|
pts = []
|
||||||
|
|
||||||
|
for i in range(1, steps + 1):
|
||||||
|
t = i / steps
|
||||||
|
|
||||||
|
x = (
|
||||||
|
(1 - t) ** 3 * p0[0]
|
||||||
|
+ 3 * (1 - t) ** 2 * t * p1[0]
|
||||||
|
+ 3 * (1 - t) * t ** 2 * p2[0]
|
||||||
|
+ t ** 3 * p3[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
y = (
|
||||||
|
(1 - t) ** 3 * p0[1]
|
||||||
|
+ 3 * (1 - t) ** 2 * t * p1[1]
|
||||||
|
+ 3 * (1 - t) * t ** 2 * p2[1]
|
||||||
|
+ t ** 3 * p3[1]
|
||||||
|
)
|
||||||
|
|
||||||
|
pts.append((x, y))
|
||||||
|
|
||||||
|
return pts
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_polygon(points):
|
||||||
|
if len(points) < 3:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
polygon = Polygon(points)
|
||||||
|
|
||||||
|
if not polygon.is_valid:
|
||||||
|
polygon = make_valid(polygon)
|
||||||
|
|
||||||
|
if polygon.is_empty or polygon.area <= 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if polygon.geom_type == "MultiPolygon":
|
||||||
|
polygon = max(list(polygon.geoms), key=lambda g: g.area)
|
||||||
|
|
||||||
|
if polygon.geom_type != "Polygon":
|
||||||
|
return None
|
||||||
|
|
||||||
|
return polygon
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _area_mm2_from_polygon(polygon, scale_ratio):
|
||||||
|
return abs(float(polygon.area)) * (POINT_TO_MM ** 2) / (scale_ratio ** 2)
|
||||||
|
|
||||||
|
|
||||||
|
def _bounds_mm_from_polygon(polygon, scale_ratio):
|
||||||
|
minx, miny, maxx, maxy = polygon.bounds
|
||||||
|
|
||||||
|
width_mm = (maxx - minx) * POINT_TO_MM / scale_ratio
|
||||||
|
height_mm = (maxy - miny) * POINT_TO_MM / scale_ratio
|
||||||
|
|
||||||
|
return round(width_mm, 3), round(height_mm, 3)
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_scale_from_text(page):
|
||||||
|
raw = page.get_text("text")
|
||||||
|
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
|
||||||
|
candidates = []
|
||||||
|
|
||||||
|
for match in _SCALE_PATTERN.finditer(raw):
|
||||||
|
try:
|
||||||
|
a = float(match.group(1))
|
||||||
|
b = float(match.group(2))
|
||||||
|
|
||||||
|
if b <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ratio = a / b
|
||||||
|
|
||||||
|
if 0.01 <= ratio <= 100:
|
||||||
|
candidates.append(round(ratio, 4))
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not candidates:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return Counter(candidates).most_common(1)[0][0]
|
||||||
|
|
||||||
|
|
||||||
|
def _normalized_roi_to_page_rect(page_rect, roi):
|
||||||
|
if not roi:
|
||||||
|
return None
|
||||||
|
|
||||||
|
x = roi.get("x")
|
||||||
|
y = roi.get("y")
|
||||||
|
width = roi.get("width")
|
||||||
|
height = roi.get("height")
|
||||||
|
|
||||||
|
if x is None or y is None or width is None or height is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
x = float(x)
|
||||||
|
y = float(y)
|
||||||
|
width = float(width)
|
||||||
|
height = float(height)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if width <= 0 or height <= 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
x = max(0.0, min(1.0, x))
|
||||||
|
y = max(0.0, min(1.0, y))
|
||||||
|
width = max(0.0, min(1.0 - x, width))
|
||||||
|
height = max(0.0, min(1.0 - y, height))
|
||||||
|
|
||||||
|
x0 = page_rect.x0 + x * page_rect.width
|
||||||
|
y0 = page_rect.y0 + y * page_rect.height
|
||||||
|
x1 = x0 + width * page_rect.width
|
||||||
|
y1 = y0 + height * page_rect.height
|
||||||
|
|
||||||
|
return fitz.Rect(x0, y0, x1, y1)
|
||||||
|
|
||||||
|
|
||||||
|
def _drawing_intersects_rect(drawing, roi_rect):
|
||||||
|
if roi_rect is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
rect = drawing.get("rect")
|
||||||
|
|
||||||
|
if rect is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
return fitz.Rect(rect).intersects(roi_rect)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_drawings_by_roi(drawings, roi_rect):
|
||||||
|
if roi_rect is None:
|
||||||
|
return drawings
|
||||||
|
|
||||||
|
return [d for d in drawings if _drawing_intersects_rect(d, roi_rect)]
|
||||||
|
|
||||||
|
|
||||||
|
def _choose_render_zoom(rect):
|
||||||
|
zoom = DEFAULT_RENDER_ZOOM
|
||||||
|
|
||||||
|
max_side = max(rect.width, rect.height)
|
||||||
|
|
||||||
|
if max_side * zoom > MAX_RENDER_SIDE_PX:
|
||||||
|
zoom = MAX_RENDER_SIDE_PX / max_side
|
||||||
|
|
||||||
|
return max(3.0, min(DEFAULT_RENDER_ZOOM, zoom))
|
||||||
|
|
||||||
|
|
||||||
|
def _dilate(mask, iterations=1):
|
||||||
|
result = mask.astype(bool)
|
||||||
|
|
||||||
|
for _ in range(iterations):
|
||||||
|
padded = np.pad(result, 1, mode="constant", constant_values=False)
|
||||||
|
|
||||||
|
result = (
|
||||||
|
padded[1:-1, 1:-1]
|
||||||
|
| padded[:-2, 1:-1]
|
||||||
|
| padded[2:, 1:-1]
|
||||||
|
| padded[1:-1, :-2]
|
||||||
|
| padded[1:-1, 2:]
|
||||||
|
| padded[:-2, :-2]
|
||||||
|
| padded[:-2, 2:]
|
||||||
|
| padded[2:, :-2]
|
||||||
|
| padded[2:, 2:]
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _erode(mask, iterations=1):
|
||||||
|
result = mask.astype(bool)
|
||||||
|
|
||||||
|
for _ in range(iterations):
|
||||||
|
padded = np.pad(result, 1, mode="constant", constant_values=False)
|
||||||
|
|
||||||
|
result = (
|
||||||
|
padded[1:-1, 1:-1]
|
||||||
|
& padded[:-2, 1:-1]
|
||||||
|
& padded[2:, 1:-1]
|
||||||
|
& padded[1:-1, :-2]
|
||||||
|
& padded[1:-1, 2:]
|
||||||
|
& padded[:-2, :-2]
|
||||||
|
& padded[:-2, 2:]
|
||||||
|
& padded[2:, :-2]
|
||||||
|
& padded[2:, 2:]
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _close_mask(mask, iterations=2):
|
||||||
|
return _erode(_dilate(mask, iterations=iterations), iterations=iterations)
|
||||||
|
|
||||||
|
|
||||||
|
def _largest_component(mask):
|
||||||
|
h, w = mask.shape
|
||||||
|
visited = np.zeros_like(mask, dtype=bool)
|
||||||
|
|
||||||
|
best_pixels = []
|
||||||
|
best_count = 0
|
||||||
|
|
||||||
|
ys, xs = np.where(mask)
|
||||||
|
|
||||||
|
for start_y, start_x in zip(ys, xs):
|
||||||
|
if visited[start_y, start_x]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
q = deque()
|
||||||
|
q.append((start_y, start_x))
|
||||||
|
visited[start_y, start_x] = True
|
||||||
|
|
||||||
|
pixels = []
|
||||||
|
|
||||||
|
while q:
|
||||||
|
y, x = q.popleft()
|
||||||
|
pixels.append((y, x))
|
||||||
|
|
||||||
|
for ny in (y - 1, y, y + 1):
|
||||||
|
for nx in (x - 1, x, x + 1):
|
||||||
|
if ny == y and nx == x:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if ny < 0 or nx < 0 or ny >= h or nx >= w:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if visited[ny, nx]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not mask[ny, nx]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
visited[ny, nx] = True
|
||||||
|
q.append((ny, nx))
|
||||||
|
|
||||||
|
if len(pixels) > best_count:
|
||||||
|
best_count = len(pixels)
|
||||||
|
best_pixels = pixels
|
||||||
|
|
||||||
|
output = np.zeros_like(mask, dtype=bool)
|
||||||
|
|
||||||
|
for y, x in best_pixels:
|
||||||
|
output[y, x] = True
|
||||||
|
|
||||||
|
return output, best_count
|
||||||
|
|
||||||
|
|
||||||
|
def _flood_fill_outside(boundary_mask):
|
||||||
|
h, w = boundary_mask.shape
|
||||||
|
|
||||||
|
outside = np.zeros_like(boundary_mask, dtype=bool)
|
||||||
|
passable = ~boundary_mask
|
||||||
|
|
||||||
|
q = deque()
|
||||||
|
|
||||||
|
for x in range(w):
|
||||||
|
if passable[0, x]:
|
||||||
|
outside[0, x] = True
|
||||||
|
q.append((0, x))
|
||||||
|
|
||||||
|
if passable[h - 1, x]:
|
||||||
|
outside[h - 1, x] = True
|
||||||
|
q.append((h - 1, x))
|
||||||
|
|
||||||
|
for y in range(h):
|
||||||
|
if passable[y, 0]:
|
||||||
|
outside[y, 0] = True
|
||||||
|
q.append((y, 0))
|
||||||
|
|
||||||
|
if passable[y, w - 1]:
|
||||||
|
outside[y, w - 1] = True
|
||||||
|
q.append((y, w - 1))
|
||||||
|
|
||||||
|
while q:
|
||||||
|
y, x = q.popleft()
|
||||||
|
|
||||||
|
for ny, nx in (
|
||||||
|
(y - 1, x),
|
||||||
|
(y + 1, x),
|
||||||
|
(y, x - 1),
|
||||||
|
(y, x + 1),
|
||||||
|
):
|
||||||
|
if ny < 0 or nx < 0 or ny >= h or nx >= w:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if outside[ny, nx]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not passable[ny, nx]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
outside[ny, nx] = True
|
||||||
|
q.append((ny, nx))
|
||||||
|
|
||||||
|
return outside
|
||||||
|
|
||||||
|
|
||||||
|
def _bbox_from_mask(mask, padding=8):
|
||||||
|
ys, xs = np.where(mask)
|
||||||
|
|
||||||
|
if len(xs) == 0 or len(ys) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
h, w = mask.shape
|
||||||
|
|
||||||
|
x0 = max(0, int(xs.min()) - padding)
|
||||||
|
x1 = min(w - 1, int(xs.max()) + padding)
|
||||||
|
y0 = max(0, int(ys.min()) - padding)
|
||||||
|
y1 = min(h - 1, int(ys.max()) + padding)
|
||||||
|
|
||||||
|
return x0, y0, x1, y1
|
||||||
|
|
||||||
|
|
||||||
|
def _crop_mask(mask, bbox):
|
||||||
|
x0, y0, x1, y1 = bbox
|
||||||
|
return mask[y0:y1 + 1, x0:x1 + 1]
|
||||||
|
|
||||||
|
|
||||||
|
def _raster_roi_area(page, roi_rect, scale_ratio, mode="auto_roi"):
|
||||||
|
"""
|
||||||
|
Raster ROI method:
|
||||||
|
- render only the selected ROI
|
||||||
|
- detect technical ink / filled geometry
|
||||||
|
- try flood-fill to recover the enclosed section area
|
||||||
|
- fallback to filled dark pixels for filled profiles
|
||||||
|
"""
|
||||||
|
if roi_rect is None:
|
||||||
|
return None, "ROI mancante: definisci prima la sezione da misurare."
|
||||||
|
|
||||||
|
zoom = _choose_render_zoom(roi_rect)
|
||||||
|
|
||||||
|
pix = page.get_pixmap(
|
||||||
|
matrix=fitz.Matrix(zoom, zoom),
|
||||||
|
clip=roi_rect,
|
||||||
|
alpha=False
|
||||||
|
)
|
||||||
|
|
||||||
|
width = pix.width
|
||||||
|
height = pix.height
|
||||||
|
channels = pix.n
|
||||||
|
|
||||||
|
arr = np.frombuffer(pix.samples, dtype=np.uint8).reshape((height, width, channels))
|
||||||
|
|
||||||
|
if channels >= 3:
|
||||||
|
rgb = arr[:, :, :3].astype(np.int16)
|
||||||
|
else:
|
||||||
|
rgb = np.repeat(arr[:, :, :1], 3, axis=2).astype(np.int16)
|
||||||
|
|
||||||
|
brightness = rgb.mean(axis=2)
|
||||||
|
max_channel = rgb.max(axis=2)
|
||||||
|
min_channel = rgb.min(axis=2)
|
||||||
|
saturation = max_channel - min_channel
|
||||||
|
|
||||||
|
# Technical ink / profile geometry.
|
||||||
|
# Keep black, grey, colored CAD strokes, anti-aliased edges.
|
||||||
|
ink = (brightness < 245) | ((saturation > 25) & (brightness < 252))
|
||||||
|
|
||||||
|
# Remove very light background noise.
|
||||||
|
ink = _close_mask(ink, iterations=1)
|
||||||
|
|
||||||
|
ink_bbox = _bbox_from_mask(ink, padding=12)
|
||||||
|
|
||||||
|
if ink_bbox is None:
|
||||||
|
return None, "Nessuna geometria visibile trovata dentro la ROI."
|
||||||
|
|
||||||
|
cropped_ink = _crop_mask(ink, ink_bbox)
|
||||||
|
|
||||||
|
# Strengthen thin CAD lines to close small gaps.
|
||||||
|
boundary = _dilate(cropped_ink, iterations=2)
|
||||||
|
boundary = _close_mask(boundary, iterations=2)
|
||||||
|
|
||||||
|
outside = _flood_fill_outside(boundary)
|
||||||
|
filled_inside = ~outside
|
||||||
|
|
||||||
|
# Keep only the largest enclosed/filled component to avoid text or small debris.
|
||||||
|
largest_inside, largest_inside_pixels = _largest_component(filled_inside)
|
||||||
|
|
||||||
|
ink_pixels = int(cropped_ink.sum())
|
||||||
|
boundary_pixels = int(boundary.sum())
|
||||||
|
inside_pixels = int(largest_inside_pixels)
|
||||||
|
|
||||||
|
pixel_to_mm = POINT_TO_MM / zoom / scale_ratio
|
||||||
|
pixel_area_mm2 = pixel_to_mm ** 2
|
||||||
|
|
||||||
|
ink_area_mm2 = ink_pixels * pixel_area_mm2
|
||||||
|
flood_area_mm2 = inside_pixels * pixel_area_mm2
|
||||||
|
|
||||||
|
crop_h, crop_w = cropped_ink.shape
|
||||||
|
crop_area_pixels = crop_w * crop_h
|
||||||
|
|
||||||
|
flood_ratio = inside_pixels / crop_area_pixels if crop_area_pixels else 0
|
||||||
|
ink_ratio = ink_pixels / crop_area_pixels if crop_area_pixels else 0
|
||||||
|
|
||||||
|
# Decision logic:
|
||||||
|
# - If flood-fill finds a plausible closed section, use it.
|
||||||
|
# - If the ROI contains a filled/hatch mass and flood-fill is not useful, use ink area.
|
||||||
|
# - Never trust a flood area that almost fills the whole ROI crop.
|
||||||
|
selected_method = None
|
||||||
|
selected_area_mm2 = None
|
||||||
|
|
||||||
|
if mode in ("auto_roi", "stitch_contour", "closed_path"):
|
||||||
|
if flood_area_mm2 > max(ink_area_mm2 * 2.0, 5.0) and flood_ratio < 0.92:
|
||||||
|
selected_method = "raster_flood_fill"
|
||||||
|
selected_area_mm2 = flood_area_mm2
|
||||||
|
|
||||||
|
if selected_area_mm2 is None and mode in ("auto_roi", "filled_union"):
|
||||||
|
if ink_area_mm2 > 1.0:
|
||||||
|
selected_method = "raster_filled_ink"
|
||||||
|
selected_area_mm2 = ink_area_mm2
|
||||||
|
|
||||||
|
if selected_area_mm2 is None:
|
||||||
|
return None, "ROI analizzata, ma non è stata trovata un'area raster plausibile."
|
||||||
|
|
||||||
|
# Estimate width / height of detected ink bounding box.
|
||||||
|
width_mm = crop_w * pixel_to_mm
|
||||||
|
height_mm = crop_h * pixel_to_mm
|
||||||
|
|
||||||
|
diagnostics = {
|
||||||
|
"render_zoom": zoom,
|
||||||
|
"roi_rect_points": {
|
||||||
|
"x0": roi_rect.x0,
|
||||||
|
"y0": roi_rect.y0,
|
||||||
|
"x1": roi_rect.x1,
|
||||||
|
"y1": roi_rect.y1,
|
||||||
|
},
|
||||||
|
"render_width_px": width,
|
||||||
|
"render_height_px": height,
|
||||||
|
"ink_pixels": ink_pixels,
|
||||||
|
"boundary_pixels": boundary_pixels,
|
||||||
|
"inside_pixels": inside_pixels,
|
||||||
|
"ink_area_mm2": round(ink_area_mm2, 4),
|
||||||
|
"flood_area_mm2": round(flood_area_mm2, 4),
|
||||||
|
"ink_ratio": round(ink_ratio, 4),
|
||||||
|
"flood_ratio": round(flood_ratio, 4),
|
||||||
|
"selected_method": selected_method,
|
||||||
|
"crop_width_px": crop_w,
|
||||||
|
"crop_height_px": crop_h,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"area_mm2": selected_area_mm2,
|
||||||
|
"width_mm": round(width_mm, 3),
|
||||||
|
"height_mm": round(height_mm, 3),
|
||||||
|
"method": selected_method,
|
||||||
|
"diagnostics": diagnostics,
|
||||||
|
}, None
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_points_from_drawing(drawing):
|
||||||
|
points = []
|
||||||
|
source_type = "path"
|
||||||
|
|
||||||
|
for item in drawing.get("items", []):
|
||||||
|
cmd = item[0]
|
||||||
|
|
||||||
|
if cmd == "re":
|
||||||
|
rect = item[1]
|
||||||
|
source_type = "rectangle"
|
||||||
|
|
||||||
|
points = [
|
||||||
|
(float(rect.x0), float(rect.y0)),
|
||||||
|
(float(rect.x1), float(rect.y0)),
|
||||||
|
(float(rect.x1), float(rect.y1)),
|
||||||
|
(float(rect.x0), float(rect.y1)),
|
||||||
|
(float(rect.x0), float(rect.y0)),
|
||||||
|
]
|
||||||
|
|
||||||
|
return points, source_type
|
||||||
|
|
||||||
|
if cmd == "l":
|
||||||
|
p1 = _pt(item[1])
|
||||||
|
p2 = _pt(item[2])
|
||||||
|
|
||||||
|
if not points:
|
||||||
|
points.append(p1)
|
||||||
|
|
||||||
|
if _dist(points[-1], p1) > 0.01:
|
||||||
|
points.append(p1)
|
||||||
|
|
||||||
|
points.append(p2)
|
||||||
|
|
||||||
|
elif cmd == "c" and len(item) >= 5:
|
||||||
|
p0 = _pt(item[1])
|
||||||
|
c1 = _pt(item[2])
|
||||||
|
c2 = _pt(item[3])
|
||||||
|
p3 = _pt(item[4])
|
||||||
|
|
||||||
|
if not points:
|
||||||
|
points.append(p0)
|
||||||
|
elif _dist(points[-1], p0) > 0.01:
|
||||||
|
points.append(p0)
|
||||||
|
|
||||||
|
points.extend(_cubic_bezier(p0, c1, c2, p3, steps=24))
|
||||||
|
|
||||||
|
return points, source_type
|
||||||
|
|
||||||
|
|
||||||
|
def _vector_closed_path_area(drawings, scale_ratio):
|
||||||
|
candidates = []
|
||||||
|
|
||||||
|
for index, drawing in enumerate(drawings):
|
||||||
|
points, source_type = _extract_points_from_drawing(drawing)
|
||||||
|
|
||||||
|
if source_type == "rectangle":
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(points) < 6:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if _dist(points[0], points[-1]) > 1.5:
|
||||||
|
continue
|
||||||
|
|
||||||
|
polygon = _safe_polygon(points)
|
||||||
|
|
||||||
|
if polygon is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
area_mm2 = _area_mm2_from_polygon(polygon, scale_ratio)
|
||||||
|
|
||||||
|
if area_mm2 < 5:
|
||||||
|
continue
|
||||||
|
|
||||||
|
width_mm, height_mm = _bounds_mm_from_polygon(polygon, scale_ratio)
|
||||||
|
|
||||||
|
min_side = min(width_mm, height_mm)
|
||||||
|
max_side = max(width_mm, height_mm)
|
||||||
|
|
||||||
|
if min_side <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
aspect = max_side / min_side
|
||||||
|
|
||||||
|
if aspect > MAX_ASPECT_RATIO:
|
||||||
|
continue
|
||||||
|
|
||||||
|
candidates.append({
|
||||||
|
"drawing_index": index,
|
||||||
|
"area_mm2": area_mm2,
|
||||||
|
"width_mm": width_mm,
|
||||||
|
"height_mm": height_mm,
|
||||||
|
"points_count": len(points),
|
||||||
|
"aspect_ratio": round(aspect, 3),
|
||||||
|
})
|
||||||
|
|
||||||
|
if not candidates:
|
||||||
|
return None, "Nessun path vettoriale chiuso plausibile trovato."
|
||||||
|
|
||||||
|
candidates.sort(key=lambda x: x["area_mm2"], reverse=True)
|
||||||
|
best = candidates[0]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"area_mm2": best["area_mm2"],
|
||||||
|
"width_mm": best["width_mm"],
|
||||||
|
"height_mm": best["height_mm"],
|
||||||
|
"method": "vector_closed_path",
|
||||||
|
"diagnostics": {
|
||||||
|
"candidates_count": len(candidates),
|
||||||
|
"selected_candidate": best,
|
||||||
|
"candidates_preview": candidates[:20],
|
||||||
|
},
|
||||||
|
}, None
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_pdf_vector_area(
|
||||||
|
pdf_bytes,
|
||||||
|
filename="uploaded.pdf",
|
||||||
|
scale_ratio=None,
|
||||||
|
profile_color=None,
|
||||||
|
roi=None,
|
||||||
|
mode="auto_roi",
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
doc = fitz.open(stream=pdf_bytes, filetype="pdf")
|
||||||
|
except Exception as exc:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"Impossibile aprire il PDF: {exc}",
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(doc) == 0:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "Il PDF non ha pagine.",
|
||||||
|
}
|
||||||
|
|
||||||
|
page_index = 0
|
||||||
|
|
||||||
|
if roi and roi.get("page"):
|
||||||
|
try:
|
||||||
|
page_index = max(0, int(roi.get("page")) - 1)
|
||||||
|
except Exception:
|
||||||
|
page_index = 0
|
||||||
|
|
||||||
|
if page_index >= len(doc):
|
||||||
|
page_index = 0
|
||||||
|
|
||||||
|
page = doc[page_index]
|
||||||
|
all_drawings = page.get_drawings()
|
||||||
|
|
||||||
|
if scale_ratio is None:
|
||||||
|
detected_scale = _detect_scale_from_text(page)
|
||||||
|
|
||||||
|
if detected_scale is not None:
|
||||||
|
scale_ratio = detected_scale
|
||||||
|
scale_source = "text_detected"
|
||||||
|
else:
|
||||||
|
scale_ratio = 1.0
|
||||||
|
scale_source = "default_1:1"
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
scale_ratio = float(scale_ratio)
|
||||||
|
except Exception:
|
||||||
|
scale_ratio = 1.0
|
||||||
|
|
||||||
|
if scale_ratio <= 0:
|
||||||
|
scale_ratio = 1.0
|
||||||
|
|
||||||
|
scale_source = "manual"
|
||||||
|
|
||||||
|
roi_rect = _normalized_roi_to_page_rect(page.rect, roi)
|
||||||
|
drawings = _filter_drawings_by_roi(all_drawings, roi_rect)
|
||||||
|
|
||||||
|
diagnostics = {
|
||||||
|
"filename": filename,
|
||||||
|
"total_pages": len(doc),
|
||||||
|
"page_index_used": page_index,
|
||||||
|
"page_width_mm": round(page.rect.width * POINT_TO_MM / scale_ratio, 3),
|
||||||
|
"page_height_mm": round(page.rect.height * POINT_TO_MM / scale_ratio, 3),
|
||||||
|
"scale_ratio": scale_ratio,
|
||||||
|
"scale_source": scale_source,
|
||||||
|
"mode": mode,
|
||||||
|
"roi_used": roi_rect is not None,
|
||||||
|
"roi": roi,
|
||||||
|
"total_drawings_page": len(all_drawings),
|
||||||
|
"drawings_inside_roi": len(drawings),
|
||||||
|
}
|
||||||
|
|
||||||
|
# First choice: ROI raster method.
|
||||||
|
# This is safer for exploded CAD linework because it measures the selected section image.
|
||||||
|
if roi_rect is not None and mode in ("auto_roi", "stitch_contour", "filled_union", "closed_path"):
|
||||||
|
raster_result, raster_error = _raster_roi_area(
|
||||||
|
page=page,
|
||||||
|
roi_rect=roi_rect,
|
||||||
|
scale_ratio=scale_ratio,
|
||||||
|
mode=mode
|
||||||
|
)
|
||||||
|
|
||||||
|
if raster_result is not None:
|
||||||
|
area_mm2 = round(float(raster_result["area_mm2"]), 4)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Area calcolata sulla ROI selezionata.",
|
||||||
|
"area_mm2": area_mm2,
|
||||||
|
"area_cm2": round(area_mm2 / 100.0, 6),
|
||||||
|
"area_m2": round(area_mm2 / 1_000_000.0, 9),
|
||||||
|
"width_mm": raster_result["width_mm"],
|
||||||
|
"height_mm": raster_result["height_mm"],
|
||||||
|
"scale_detected": f"{scale_ratio}:1",
|
||||||
|
"scale_used": scale_ratio,
|
||||||
|
"scale_source": scale_source,
|
||||||
|
"strategy_used": raster_result["method"],
|
||||||
|
"confidence": "needs_validation",
|
||||||
|
"diagnostics": {
|
||||||
|
**diagnostics,
|
||||||
|
"raster": raster_result["diagnostics"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
diagnostics["raster_error"] = raster_error
|
||||||
|
|
||||||
|
# Fallback: closed vector path only.
|
||||||
|
vector_result, vector_error = _vector_closed_path_area(drawings, scale_ratio)
|
||||||
|
|
||||||
|
if vector_result is not None:
|
||||||
|
area_mm2 = round(float(vector_result["area_mm2"]), 4)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Area calcolata da path vettoriale chiuso.",
|
||||||
|
"area_mm2": area_mm2,
|
||||||
|
"area_cm2": round(area_mm2 / 100.0, 6),
|
||||||
|
"area_m2": round(area_mm2 / 1_000_000.0, 9),
|
||||||
|
"width_mm": vector_result["width_mm"],
|
||||||
|
"height_mm": vector_result["height_mm"],
|
||||||
|
"scale_detected": f"{scale_ratio}:1",
|
||||||
|
"scale_used": scale_ratio,
|
||||||
|
"scale_source": scale_source,
|
||||||
|
"strategy_used": vector_result["method"],
|
||||||
|
"confidence": "needs_validation",
|
||||||
|
"diagnostics": {
|
||||||
|
**diagnostics,
|
||||||
|
"vector": vector_result["diagnostics"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
diagnostics["vector_error"] = vector_error
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": (
|
||||||
|
"Nessuna area affidabile trovata. "
|
||||||
|
"Definisci una ROI più stretta intorno alla sola sezione del profilo, "
|
||||||
|
"oppure verifica la scala del disegno."
|
||||||
|
),
|
||||||
|
"area_mm2": None,
|
||||||
|
"area_cm2": None,
|
||||||
|
"area_m2": None,
|
||||||
|
"scale_used": scale_ratio,
|
||||||
|
"scale_source": scale_source,
|
||||||
|
"strategy_used": None,
|
||||||
|
"confidence": "low",
|
||||||
|
"diagnostics": diagnostics,
|
||||||
|
}
|
||||||
Binary file not shown.
Reference in New Issue
Block a user