Compare commits
16 Commits
bfdbbbfc8f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 27cbc9f449 | |||
| 4c09a0dcb4 | |||
| 8bb23ee563 | |||
| 20571c9e4b | |||
| fdde16b113 | |||
| 33b627f328 | |||
| d96b4be9e0 | |||
| 088e518db1 | |||
| 789c547bc7 | |||
| e5bf546ae7 | |||
| 6dd13e5d7d | |||
| b1f2bb60e3 | |||
| f7e97f55e9 | |||
| 70b712ff3b | |||
| fdc3af01f3 | |||
| 3d54140280 |
@@ -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>
|
||||||
+1406
-215
File diff suppressed because it is too large
Load Diff
+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